kaizen/collect/
outcomes.rs1use regex::Regex;
5use std::io::Read;
6use std::path::Path;
7use std::process::{Command, Stdio};
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
12pub struct CargoTestCounts {
13 pub passed: i32,
14 pub failed: i32,
15 pub ignored: i32,
16}
17
18pub fn parse_cargo_test_summary(text: &str) -> Option<CargoTestCounts> {
20 let re =
21 Regex::new(r"test result:\s*\w+\.\s*(\d+)\s+passed;\s*(\d+)\s+failed;\s*(\d+)\s+ignored")
22 .ok()?;
23 let cap = re.captures(text)?;
24 Some(CargoTestCounts {
25 passed: cap.get(1)?.as_str().parse().ok()?,
26 failed: cap.get(2)?.as_str().parse().ok()?,
27 ignored: cap.get(3)?.as_str().parse().ok()?,
28 })
29}
30
31pub fn parse_clippy_error_count(text: &str) -> i32 {
33 text.lines()
34 .filter(|l| l.trim_start().starts_with("error:"))
35 .count() as i32
36}
37
38#[derive(Debug, Default)]
40pub struct OutcomeMeasureResult {
41 pub test_passed: Option<i32>,
42 pub test_failed: Option<i32>,
43 pub test_skipped: Option<i32>,
44 pub lint_errors: Option<i32>,
45 pub measure_error: Option<String>,
46}
47
48struct Captured {
49 combined: String,
50 exit_fail: bool,
51 timed_out: bool,
52}
53
54pub fn run_outcome_measure(
56 workspace: &Path,
57 test_cmd: &str,
58 lint_cmd: Option<&str>,
59 timeout: Duration,
60) -> OutcomeMeasureResult {
61 let test = shell_output(workspace, test_cmd, timeout);
62 let mut m = to_measure_result(&test);
63 if let Some(lc) = lint_cmd.filter(|s| !s.is_empty()) {
64 let lint = shell_output(workspace, lc, timeout);
65 m.lint_errors = Some(parse_clippy_error_count(&lint.combined));
66 if lint.timed_out {
67 m.measure_error = m.measure_error.or(Some("lint command timed out".into()));
68 } else if lint.exit_fail {
69 m.measure_error = m.measure_error.or(Some("lint command failed".into()));
70 }
71 }
72 m
73}
74
75fn to_measure_result(cap: &Captured) -> OutcomeMeasureResult {
76 let counts = parse_cargo_test_summary(&cap.combined);
77 let (tp, tf, tsk) = match counts {
78 Some(c) => (Some(c.passed), Some(c.failed), Some(c.ignored)),
79 None => (None, None, None),
80 };
81 let mut err = cap.then_error();
82 if err.is_none() && cap.exit_fail && counts.is_none() {
83 err = Some("command failed (no test result line)".into());
84 }
85 OutcomeMeasureResult {
86 test_passed: tp,
87 test_failed: tf,
88 test_skipped: tsk,
89 lint_errors: None,
90 measure_error: err,
91 }
92}
93
94impl Captured {
95 fn then_error(&self) -> Option<String> {
96 if self.timed_out {
97 Some("command timed out".into())
98 } else {
99 None
100 }
101 }
102}
103
104fn shell_output(workspace: &Path, cmd: &str, timeout: Duration) -> Captured {
105 let sh = if cfg!(unix) { "/bin/sh" } else { "sh" };
106 let mut c = match Command::new(sh)
107 .arg("-c")
108 .arg(cmd)
109 .current_dir(workspace)
110 .stdout(Stdio::piped())
111 .stderr(Stdio::piped())
112 .spawn()
113 {
114 Ok(ch) => ch,
115 Err(_) => {
116 return Captured {
117 combined: String::new(),
118 exit_fail: true,
119 timed_out: false,
120 };
121 }
122 };
123 let status = wait_limited(&mut c, timeout);
124 let timed_out = status.is_err();
125 let mut combined = String::new();
126 if let Some(mut stdout) = c.stdout.take() {
127 let _ = stdout.read_to_string(&mut combined);
128 }
129 if let Some(mut stderr) = c.stderr.take() {
130 let _ = stderr.read_to_string(&mut combined);
131 }
132 let exit_ok = status.as_ref().map(|s| s.success()).unwrap_or(false);
133 let exit_fail = timed_out || !exit_ok;
134 Captured {
135 combined,
136 exit_fail,
137 timed_out,
138 }
139}
140
141fn wait_limited(
142 child: &mut std::process::Child,
143 timeout: Duration,
144) -> Result<std::process::ExitStatus, ()> {
145 let start = Instant::now();
146 while start.elapsed() < timeout {
147 match child.try_wait() {
148 Ok(Some(s)) => return Ok(s),
149 Ok(None) => std::thread::sleep(Duration::from_millis(100)),
150 Err(_) => return child.wait().map_err(|_| ()),
151 }
152 }
153 let _ = child.kill();
154 child.wait().map_err(|_| ())
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn parse_test_result_line() {
163 let t = "foo\n\ntest result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; blah\n";
164 let c = parse_cargo_test_summary(t).unwrap();
165 assert_eq!(c.passed, 2);
166 assert_eq!(c.failed, 0);
167 assert_eq!(c.ignored, 1);
168 }
169
170 #[test]
171 fn clippy_errors_count() {
172 let t = "error: use of moved value\n\nerror: aborting";
173 assert_eq!(parse_clippy_error_count(t), 2);
174 }
175}