ctrf_rs/
results.rs

1// crate import(s)
2use crate::{
3    environment::Environment,
4    impl_extra,
5    summary::Summary,
6    test::{Status, Test},
7    tool::Tool,
8};
9
10// std import(s)
11use std::{
12    collections::{HashMap, HashSet},
13    time::SystemTime,
14};
15
16// other import(s)
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19
20/// Results element for a CTRF report.
21/// Corresponds to the spec's ["Results"](https://www.ctrf.io/docs/specification/results) object.
22#[derive(Deserialize, Serialize, Debug, PartialEq)]
23#[serde(rename_all = "camelCase")]
24pub struct Results {
25    tool: Tool,
26    summary: Summary,
27    tests: Vec<Test>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    environment: Option<Environment>,
30    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
31    extra: HashMap<String, Value>,
32}
33
34/// Builder used to generate a `Results` struct instance
35pub struct ResultsBuilder {
36    tool: Tool,
37    tests: Vec<Test>,
38    environment: Option<Environment>,
39    extra: HashMap<String, Value>,
40}
41
42impl ResultsBuilder {
43    /// Creates a `ResultsBuilder` with the provided `tool` and defaults otherwise
44    pub fn new(tool: Tool) -> Self {
45        Self {
46            tool,
47            tests: vec![],
48            environment: None,
49            extra: HashMap::new(),
50        }
51    }
52
53    /// Appends a Test to the contained list
54    pub fn add_test(&mut self, test: Test) {
55        self.tests.push(test);
56    }
57
58    /// Sets the Environment, can be None
59    pub fn environment(mut self, environment: Option<Environment>) {
60        self.environment = environment;
61    }
62
63    /// Builds and returns the final Results instance
64    pub fn build(self, start: SystemTime, stop: SystemTime) -> Results {
65        let ResultsBuilder {
66            tool,
67            tests,
68            environment,
69            extra,
70        } = self;
71
72        let mut summary = Summary::new(start, stop);
73
74        summary.passed(
75            tests
76                .iter()
77                .filter(|t| t.status() == Status::Passed)
78                .count(),
79        );
80        summary.failed(
81            tests
82                .iter()
83                .filter(|t| t.status() == Status::Failed)
84                .count(),
85        );
86        summary.pending(
87            tests
88                .iter()
89                .filter(|t| t.status() == Status::Pending)
90                .count(),
91        );
92        summary.skipped(
93            tests
94                .iter()
95                .filter(|t| t.status() == Status::Skipped)
96                .count(),
97        );
98        summary.other(tests.iter().filter(|t| t.status() == Status::Other).count());
99
100        let mut suites = HashSet::new();
101        for t in &tests {
102            suites.extend(&t.suite);
103        }
104
105        let suite_count = suites.len();
106        if suite_count > 0 {
107            summary.suites(Some(suite_count));
108        }
109
110        Results {
111            tool,
112            summary,
113            tests,
114            environment,
115            extra,
116        }
117    }
118}
119
120impl_extra!(Results, ResultsBuilder);
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    use crate::tool::TOOL_NAME;
127
128    use std::time::Duration;
129
130    use serde_json::Result;
131
132    #[test]
133    fn add_passed() -> Result<()> {
134        const TEST_COUNT: usize = 2;
135
136        let tool = Tool::new();
137        let mut builder = ResultsBuilder::new(tool);
138
139        for t in 0..TEST_COUNT {
140            builder.add_test(Test::new(
141                format!("pass{t}"),
142                Status::Passed,
143                Duration::from_millis(0),
144            ));
145        }
146
147        let time = SystemTime::now();
148        let results = builder.build(time, time);
149
150        let tool_text = serde_json::to_string::<Tool>(&results.tool)?;
151        let summary_text = serde_json::to_string::<Summary>(&results.summary)?;
152        let tests_text = serde_json::to_string::<Vec<Test>>(&results.tests)?;
153        assert!(tool_text.contains(TOOL_NAME));
154        assert!(summary_text.contains(&format!(r#""tests":{TEST_COUNT}"#)));
155        assert!(summary_text.contains(&format!(r#""passed":{TEST_COUNT}"#)));
156        for t in 0..TEST_COUNT {
157            assert!(tests_text.contains(&format!(r#""name":"pass{t}""#)));
158        }
159
160        Ok(())
161    }
162
163    #[test]
164    fn add_failed() -> Result<()> {
165        const TEST_COUNT: usize = 4;
166
167        let tool = Tool::new();
168        let mut builder = ResultsBuilder::new(tool);
169
170        for t in 0..TEST_COUNT {
171            builder.add_test(Test::new(
172                format!("fail{t}"),
173                Status::Failed,
174                Duration::from_millis(0),
175            ));
176        }
177
178        let time = SystemTime::now();
179        let results = builder.build(time, time);
180
181        let tool_text = serde_json::to_string::<Tool>(&results.tool)?;
182        let summary_text = serde_json::to_string::<Summary>(&results.summary)?;
183        let tests_text = serde_json::to_string::<Vec<Test>>(&results.tests)?;
184        assert!(tool_text.contains(TOOL_NAME));
185        assert!(summary_text.contains(&format!(r#""tests":{TEST_COUNT}"#)));
186        assert!(summary_text.contains(&format!(r#""failed":{TEST_COUNT}"#)));
187        for t in 0..TEST_COUNT {
188            assert!(tests_text.contains(&format!(r#""name":"fail{t}""#)));
189        }
190
191        Ok(())
192    }
193
194    #[test]
195    fn add_pending() -> Result<()> {
196        const TEST_COUNT: usize = 6;
197
198        let tool = Tool::new();
199        let mut builder = ResultsBuilder::new(tool);
200
201        for t in 0..TEST_COUNT {
202            builder.add_test(Test::new(
203                format!("pending{t}"),
204                Status::Pending,
205                Duration::from_millis(0),
206            ));
207        }
208
209        let time = SystemTime::now();
210        let results = builder.build(time, time);
211
212        let tool_text = serde_json::to_string::<Tool>(&results.tool)?;
213        let summary_text = serde_json::to_string::<Summary>(&results.summary)?;
214        let tests_text = serde_json::to_string::<Vec<Test>>(&results.tests)?;
215        assert!(tool_text.contains(TOOL_NAME));
216        assert!(summary_text.contains(&format!(r#""tests":{TEST_COUNT}"#)));
217        assert!(summary_text.contains(&format!(r#""pending":{TEST_COUNT}"#)));
218        for t in 0..TEST_COUNT {
219            assert!(tests_text.contains(&format!(r#""name":"pending{t}""#)));
220        }
221
222        Ok(())
223    }
224
225    #[test]
226    fn add_skipped() -> Result<()> {
227        const TEST_COUNT: usize = 8;
228
229        let tool = Tool::new();
230        let mut builder = ResultsBuilder::new(tool);
231
232        for t in 0..TEST_COUNT {
233            builder.add_test(Test::new(
234                format!("skipped{t}"),
235                Status::Skipped,
236                Duration::from_millis(0),
237            ));
238        }
239
240        let time = SystemTime::now();
241        let results = builder.build(time, time);
242
243        let tool_text = serde_json::to_string::<Tool>(&results.tool)?;
244        let summary_text = serde_json::to_string::<Summary>(&results.summary)?;
245        let tests_text = serde_json::to_string::<Vec<Test>>(&results.tests)?;
246        assert!(tool_text.contains(TOOL_NAME));
247        assert!(summary_text.contains(&format!(r#""tests":{TEST_COUNT}"#)));
248        assert!(summary_text.contains(&format!(r#""skipped":{TEST_COUNT}"#)));
249        for t in 0..TEST_COUNT {
250            assert!(tests_text.contains(&format!(r#""name":"skipped{t}""#)));
251        }
252
253        Ok(())
254    }
255
256    #[test]
257    fn add_other() -> Result<()> {
258        const TEST_COUNT: usize = 10;
259
260        let tool = Tool::new();
261        let mut builder = ResultsBuilder::new(tool);
262
263        for t in 0..TEST_COUNT {
264            builder.add_test(Test::new(
265                format!("other{t}"),
266                Status::Other,
267                Duration::from_millis(0),
268            ));
269        }
270
271        let time = SystemTime::now();
272        let results = builder.build(time, time);
273
274        let tool_text = serde_json::to_string::<Tool>(&results.tool)?;
275        let summary_text = serde_json::to_string::<Summary>(&results.summary)?;
276        let tests_text = serde_json::to_string::<Vec<Test>>(&results.tests)?;
277        assert!(tool_text.contains(TOOL_NAME));
278        assert!(summary_text.contains(&format!(r#""tests":{TEST_COUNT}"#)));
279        assert!(summary_text.contains(&format!(r#""other":{TEST_COUNT}"#)));
280        for t in 0..TEST_COUNT {
281            assert!(tests_text.contains(&format!(r#""name":"other{t}""#)));
282        }
283
284        Ok(())
285    }
286
287    #[test]
288    fn add_many() -> Result<()> {
289        const PRESENT_SUITE: &str = "present";
290        const ABSENT_SUITE: &str = "absent";
291        const UNKNOWN_SUITE: &str = "unknown";
292
293        let tool = Tool::new();
294        let mut builder = ResultsBuilder::new(tool);
295
296        const PASS_COUNT: usize = 10;
297        for t in 0..PASS_COUNT {
298            let mut test = Test::new(format!("pass{t}"), Status::Passed, Duration::from_millis(0));
299            test.suite = vec![String::from(PRESENT_SUITE)];
300            builder.add_test(test);
301        }
302
303        const FAIL_COUNT: usize = 8;
304        for t in 0..FAIL_COUNT {
305            let mut test = Test::new(format!("fail{t}"), Status::Failed, Duration::from_millis(0));
306            test.suite = vec![String::from(PRESENT_SUITE)];
307            builder.add_test(test);
308        }
309
310        const PENDING_COUNT: usize = 6;
311        for t in 0..PENDING_COUNT {
312            let mut test = Test::new(
313                format!("pending{t}"),
314                Status::Pending,
315                Duration::from_millis(0),
316            );
317            test.suite = vec![String::from(ABSENT_SUITE)];
318            builder.add_test(test);
319        }
320
321        const SKIPPED_COUNT: usize = 4;
322        for t in 0..SKIPPED_COUNT {
323            let mut test = Test::new(
324                format!("skipped{t}"),
325                Status::Skipped,
326                Duration::from_millis(0),
327            );
328            test.suite = vec![String::from(ABSENT_SUITE)];
329            builder.add_test(test);
330        }
331
332        const OTHER_COUNT: usize = 2;
333        for t in 0..OTHER_COUNT {
334            let mut test = Test::new(format!("other{t}"), Status::Other, Duration::from_millis(0));
335            test.suite = vec![String::from(UNKNOWN_SUITE)];
336            builder.add_test(test);
337        }
338
339        const TOTAL_COUNT: usize =
340            PASS_COUNT + FAIL_COUNT + PENDING_COUNT + SKIPPED_COUNT + OTHER_COUNT;
341
342        let time = SystemTime::now();
343        let results = builder.build(time, time);
344
345        let tool_text = serde_json::to_string::<Tool>(&results.tool)?;
346        let summary_text = serde_json::to_string::<Summary>(&results.summary)?;
347        let tests_text = serde_json::to_string::<Vec<Test>>(&results.tests)?;
348        assert!(tool_text.contains(TOOL_NAME));
349
350        assert!(summary_text.contains(&format!(r#""tests":{TOTAL_COUNT}"#)));
351        assert!(summary_text.contains(&format!(r#""passed":{PASS_COUNT}"#)));
352        assert!(summary_text.contains(&format!(r#""failed":{FAIL_COUNT}"#)));
353        assert!(summary_text.contains(&format!(r#""pending":{PENDING_COUNT}"#)));
354        assert!(summary_text.contains(&format!(r#""skipped":{SKIPPED_COUNT}"#)));
355        assert!(summary_text.contains(&format!(r#""other":{OTHER_COUNT}"#)));
356        assert!(summary_text.contains(&format!(r#""suites":3"#)));
357
358        for t in 0..PASS_COUNT {
359            assert!(tests_text.contains(&format!(r#""name":"pass{t}""#)));
360        }
361        for t in 0..FAIL_COUNT {
362            assert!(tests_text.contains(&format!(r#""name":"fail{t}""#)));
363        }
364        for t in 0..PENDING_COUNT {
365            assert!(tests_text.contains(&format!(r#""name":"pending{t}""#)));
366        }
367        for t in 0..SKIPPED_COUNT {
368            assert!(tests_text.contains(&format!(r#""name":"skipped{t}""#)));
369        }
370        for t in 0..OTHER_COUNT {
371            assert!(tests_text.contains(&format!(r#""name":"other{t}""#)));
372        }
373
374        Ok(())
375    }
376}