Skip to main content

max / makenotwork

7.4 KB · 250 lines History Blame Raw
1 use crate::types::{StepResult, TestDetail, TestSummary};
2
3 /// Parse run-ci.sh output into a structured TestSummary.
4 ///
5 /// Looks for:
6 /// - `PASS <step name>` / `FAIL <step name>` lines from the CI summary
7 /// - `test result: ok. N passed; M failed` lines from cargo test
8 /// - `test <name> ... ok` / `test <name> ... FAILED` individual test lines
9 pub fn parse_ci_output(output: &str) -> TestSummary {
10 let mut steps = Vec::new();
11 let mut total_passed: i64 = 0;
12 let mut total_failed: i64 = 0;
13 let mut found_test_results = false;
14
15 for line in output.lines() {
16 let trimmed = line.trim();
17
18 // Parse PASS/FAIL lines from run-ci.sh summary
19 if let Some(name) = trimmed.strip_prefix("PASS ") {
20 steps.push(StepResult {
21 name: name.trim().to_string(),
22 passed: true,
23 });
24 } else if let Some(name) = trimmed.strip_prefix("FAIL ") {
25 steps.push(StepResult {
26 name: name.trim().to_string(),
27 passed: false,
28 });
29 }
30
31 // Parse cargo test result lines
32 if trimmed.starts_with("test result:") {
33 found_test_results = true;
34 // "test result: ok. 42 passed; 0 failed; 0 ignored; ..."
35 if let Some(counts) = parse_test_result_line(trimmed) {
36 total_passed += counts.0;
37 total_failed += counts.1;
38 }
39 }
40 }
41
42 let details = parse_individual_tests(output);
43
44 TestSummary {
45 steps,
46 total_passed: if found_test_results { Some(total_passed) } else { None },
47 total_failed: if found_test_results { Some(total_failed) } else { None },
48 details,
49 }
50 }
51
52 /// Parse individual test result lines from cargo test output.
53 ///
54 /// Matches lines like:
55 /// - `test workflows::endorsements::toggle ... ok`
56 /// - `test markdown::tests::bold_and_italic ... FAILED`
57 pub fn parse_individual_tests(output: &str) -> Vec<TestDetail> {
58 let mut details = Vec::new();
59
60 for line in output.lines() {
61 let trimmed = line.trim();
62
63 // Match: "test <name> ... ok" or "test <name> ... FAILED"
64 if let Some(rest) = trimmed.strip_prefix("test ") {
65 if let Some(name) = rest.strip_suffix(" ... ok") {
66 details.push(TestDetail {
67 test_name: name.to_string(),
68 passed: true,
69 });
70 } else if let Some(name) = rest.strip_suffix(" ... FAILED") {
71 details.push(TestDetail {
72 test_name: name.to_string(),
73 passed: false,
74 });
75 }
76 }
77 }
78
79 details
80 }
81
82 fn parse_test_result_line(line: &str) -> Option<(i64, i64)> {
83 // "test result: ok. 42 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out"
84 let mut passed = 0i64;
85 let mut failed = 0i64;
86
87 for part in line.split(';') {
88 let part = part.trim();
89 if part.ends_with("passed") {
90 // "42 passed" or "ok. 42 passed"
91 let num_str = part
92 .rsplit_once(". ")
93 .map(|(_, r)| r)
94 .unwrap_or(part)
95 .trim()
96 .strip_suffix(" passed")?
97 .trim();
98 passed = num_str.parse().ok()?;
99 } else if part.ends_with("failed") {
100 let num_str = part.trim().strip_suffix(" failed")?.trim();
101 failed = num_str.parse().ok()?;
102 }
103 }
104
105 Some((passed, failed))
106 }
107
108 #[cfg(test)]
109 mod tests {
110 use super::*;
111
112 #[test]
113 fn parse_full_ci_output() {
114 let output = r#"
115 ========================================
116 cargo check
117 ========================================
118
119 Finished `dev` profile
120
121 ========================================
122 cargo test --lib
123 ========================================
124
125 running 45 tests
126 test result: ok. 45 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.3s
127
128 ========================================
129 cargo test --test integration
130 ========================================
131
132 running 714 tests
133 test result: ok. 714 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 89.2s
134
135 ========================================
136 CI Summary
137 ========================================
138
139 PASS cargo check
140 PASS cargo test --lib
141 PASS cargo test --test integration
142 PASS cargo clippy
143 PASS cargo audit
144
145 All steps passed.
146 "#;
147 let summary = parse_ci_output(output);
148 assert_eq!(summary.steps.len(), 5);
149 assert!(summary.steps.iter().all(|s| s.passed));
150 assert_eq!(summary.total_passed, Some(759));
151 assert_eq!(summary.total_failed, Some(0));
152 }
153
154 #[test]
155 fn parse_failed_ci_output() {
156 let output = r#"
157 ========================================
158 CI Summary
159 ========================================
160
161 PASS cargo check
162 FAIL cargo test --lib
163 PASS cargo clippy
164
165 1 step(s) failed.
166 "#;
167 let summary = parse_ci_output(output);
168 assert_eq!(summary.steps.len(), 3);
169 assert!(!summary.steps[1].passed);
170 assert_eq!(summary.steps[1].name, "cargo test --lib");
171 }
172
173 #[test]
174 fn parse_individual_tests_ok_and_failed() {
175 let output = r#"
176 running 4 tests
177 test workflows::endorsements::toggle_endorsement_removes ... ok
178 test workflows::endorsements::self_endorse_rejected ... ok
179 test markdown::tests::bold_and_italic ... ok
180 test workflows::endorsements::endorse_post_happy_path ... FAILED
181
182 test result: FAILED. 3 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
183 "#;
184 let details = parse_individual_tests(output);
185 assert_eq!(details.len(), 4);
186 assert!(details[0].passed);
187 assert_eq!(details[0].test_name, "workflows::endorsements::toggle_endorsement_removes");
188 assert!(details[1].passed);
189 assert!(!details[3].passed);
190 assert_eq!(details[3].test_name, "workflows::endorsements::endorse_post_happy_path");
191 }
192
193 #[test]
194 fn parse_individual_tests_empty_when_no_individual_lines() {
195 let output = r#"
196 ========================================
197 CI Summary
198 ========================================
199
200 PASS cargo check
201 PASS cargo test --lib
202 "#;
203 let details = parse_individual_tests(output);
204 assert!(details.is_empty());
205 }
206
207 #[test]
208 fn parse_ci_output_includes_details() {
209 let output = r#"
210 ========================================
211 cargo test --lib
212 ========================================
213
214 running 2 tests
215 test foo::bar ... ok
216 test foo::baz ... FAILED
217
218 test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
219
220 ========================================
221 CI Summary
222 ========================================
223
224 FAIL cargo test --lib
225 "#;
226 let summary = parse_ci_output(output);
227 assert_eq!(summary.details.len(), 2);
228 assert!(summary.details[0].passed);
229 assert!(!summary.details[1].passed);
230 assert_eq!(summary.total_passed, Some(1));
231 assert_eq!(summary.total_failed, Some(1));
232 }
233
234 #[test]
235 fn parse_test_result_line_ok() {
236 let line = "test result: ok. 42 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out";
237 let (p, f) = parse_test_result_line(line).unwrap();
238 assert_eq!(p, 42);
239 assert_eq!(f, 0);
240 }
241
242 #[test]
243 fn parse_test_result_line_failed() {
244 let line = "test result: FAILED. 40 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out";
245 let (p, f) = parse_test_result_line(line).unwrap();
246 assert_eq!(p, 40);
247 assert_eq!(f, 2);
248 }
249 }
250