1use crate::{regex::re, Result};
2use colored::{ColoredString, Colorize};
3use indexmap::IndexMap;
4use std::{
5 path::{Component, Path},
6 time::Duration,
7};
8
9pub fn parse_cargo_test<'s>(stderr: &'s str, stdout: &'s str) -> Result<TestRunners<'s>> {
12 use TestType::*;
13
14 let mut pkg = None;
15 Ok(TestRunners::new(
16 parse_cargo_test_with_empty_ones(stderr, stdout)?
17 .filter_map(|(runner, info)| {
18 match runner.ty {
19 UnitLib | UnitBin => pkg = Some(runner.src.bin_name),
20 Doc => pkg = Some("Doc Tests"),
21 _ => (),
22 }
23 if info.stats.total == 0 {
24 None
26 } else {
27 Some((pkg, runner, info))
28 }
29 })
30 .collect(),
31 ))
32}
33
34pub fn parse_cargo_test_with_empty_ones<'s>(
36 stderr: &'s str,
37 stdout: &'s str,
38) -> Result<impl Iterator<Item = (TestRunner<'s>, TestInfo<'s>)>> {
39 let parsed_stderr = parse_stderr(stderr)?;
40 let parsed_stdout = parse_stdout(stdout)?;
41 let err_len = parsed_stderr.len();
42 let out_len = parsed_stdout.len();
43 if err_len != out_len {
44 return Err(format!(
45 "{err_len} (the amount of test runners from stderr) should \
46 equal to {out_len} (that from stdout)\n\
47 stderr = {stderr:?}\nstdout = {stdout:?}"
48 ));
49 }
50 Ok(parsed_stderr.into_iter().zip(parsed_stdout))
51}
52
53pub type Pkg<'s> = Option<Text<'s>>;
58
59#[derive(Debug, Default)]
61pub struct TestRunners<'s> {
62 pub pkgs: IndexMap<Pkg<'s>, PkgTest<'s>>,
63}
64
65impl<'s> TestRunners<'s> {
66 pub fn new(v: Vec<(Pkg<'s>, TestRunner<'s>, TestInfo<'s>)>) -> TestRunners<'s> {
67 let mut runners = TestRunners::default();
68 for (pkg, runner, info) in v {
69 match runners.pkgs.entry(pkg) {
70 indexmap::map::Entry::Occupied(mut item) => {
71 item.get_mut().push(runner, info);
72 }
73 indexmap::map::Entry::Vacant(empty) => {
74 empty.insert(PkgTest::new(runner, info));
75 }
76 }
77 }
78 runners
79 }
80}
81
82pub type Text<'s> = &'s str;
84
85#[derive(Debug, Default)]
89pub struct PkgTest<'s> {
90 pub inner: Vec<Data<'s>>,
91 pub stats: Stats,
92}
93
94impl<'s> PkgTest<'s> {
95 pub fn new(runner: TestRunner<'s>, info: TestInfo<'s>) -> PkgTest<'s> {
96 let stats = info.stats.clone();
97 PkgTest {
98 inner: vec![Data { runner, info }],
99 stats,
100 }
101 }
102 pub fn push(&mut self, runner: TestRunner<'s>, info: TestInfo<'s>) {
103 self.stats += &info.stats;
104 self.inner.push(Data { runner, info });
105 }
106}
107
108#[derive(Debug)]
110pub struct Data<'s> {
111 pub runner: TestRunner<'s>,
112 pub info: TestInfo<'s>,
113}
114
115#[derive(Debug, Hash, PartialEq, Eq)]
117pub struct TestRunner<'s> {
118 pub ty: TestType,
119 pub src: Src<'s>,
120}
121
122#[derive(Debug)]
124pub struct TestInfo<'s> {
125 pub raw: Text<'s>,
127 pub stats: Stats,
128 pub parsed: ParsedCargoTestOutput<'s>,
129}
130
131#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
133pub enum TestType {
134 UnitLib,
135 UnitBin,
136 Doc,
137 Tests,
138 Examples,
139 Benches,
140}
141
142#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
144pub struct Src<'s> {
145 pub src_path: Text<'s>,
150 pub bin_name: Text<'s>,
155}
156
157#[derive(Debug, PartialEq, Eq, Clone)]
159pub struct Stats {
160 pub ok: bool,
161 pub total: u32,
162 pub passed: u32,
163 pub failed: u32,
164 pub ignored: u32,
165 pub measured: u32,
166 pub filtered_out: u32,
167 pub finished_in: Duration,
168}
169
170impl std::fmt::Display for Stats {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 let Stats {
174 ok,
175 total,
176 passed,
177 failed,
178 ignored,
179 measured,
180 filtered_out,
181 finished_in,
182 } = *self;
183 let time = finished_in.as_secs_f32();
184 let fail = if failed == 0 {
185 format!("{failed} failed")
186 } else {
187 format!("{failed} failed").red().bold().to_string()
188 };
189 write!(
190 f,
191 "Status: {}; total {total} tests in {time:.2}s: \
192 {passed} passed; {fail}; {ignored} ignored; \
193 {measured} measured; {filtered_out} filtered out",
194 status(ok)
195 )
196 }
197}
198
199fn status(ok: bool) -> ColoredString {
200 if ok {
201 "OK".green().bold()
202 } else {
203 "FAIL".red().bold()
204 }
205}
206
207impl Stats {
208 pub fn inlay_summary_string(&self) -> String {
211 let Stats {
212 total,
213 passed,
214 failed,
215 ignored,
216 filtered_out,
217 finished_in,
218 ..
219 } = *self;
220 let time = finished_in.as_secs_f32();
221 let mut metrics = Vec::with_capacity(4);
222 if passed != 0 {
223 metrics.push(format!("✅ {passed}"));
224 };
225 if failed != 0 {
226 metrics.push(format!("❌ {failed}").red().to_string());
227 };
228 if ignored != 0 {
229 metrics.push(format!("🔕 {ignored}"));
230 };
231 if filtered_out != 0 {
232 metrics.push(format!("✂️ {filtered_out}"));
233 };
234 format!("{total} tests in {time:.2}s: {}", metrics.join("; "))
235 }
236
237 pub fn root_string(&self, pkg_name: Text) -> String {
239 format!(
240 "({}) {:} ... ({})",
241 status(self.ok),
242 pkg_name.blue().bold(),
243 self.inlay_summary_string().bold()
244 )
245 }
246
247 pub fn subroot_string(&self, runner_name: Text) -> String {
250 format!(
251 "({}) {} ... ({})",
252 status(self.ok),
253 runner_name,
254 self.inlay_summary_string()
255 )
256 }
257}
258
259impl Default for Stats {
260 fn default() -> Self {
261 Stats {
262 ok: true,
263 total: 0,
264 passed: 0,
265 failed: 0,
266 ignored: 0,
267 measured: 0,
268 filtered_out: 0,
269 finished_in: Duration::from_secs(0),
270 }
271 }
272}
273
274impl std::ops::Add<&Stats> for &Stats {
275 type Output = Stats;
276
277 fn add(self, rhs: &Stats) -> Self::Output {
278 Stats {
279 ok: self.ok && rhs.ok,
280 total: self.total + rhs.total,
281 passed: self.passed + rhs.passed,
282 failed: self.failed + rhs.failed,
283 ignored: self.ignored + rhs.ignored,
284 measured: self.measured + rhs.measured,
285 filtered_out: self.filtered_out + rhs.filtered_out,
286 finished_in: self.finished_in + rhs.finished_in,
287 }
288 }
289}
290
291impl std::ops::AddAssign<&Stats> for Stats {
292 fn add_assign(&mut self, rhs: &Stats) {
293 *self = &*self + rhs;
294 }
295}
296
297#[derive(Debug)]
299pub struct ParsedCargoTestOutput<'s> {
300 pub head: Text<'s>,
301 pub tree: Vec<Text<'s>>,
302 pub detail: Text<'s>,
303}
304
305pub fn parse_stderr(stderr: &str) -> Result<Vec<TestRunner>> {
306 fn parse_stderr_inner<'s>(cap: ®ex_lite::Captures<'s>) -> Result<TestRunner<'s>> {
307 if let Some((path, pkg)) = cap.name("path").zip(cap.name("pkg")) {
308 let path = path.as_str();
309 let path_norm = Path::new(path);
310 let ty = if cap.name("is_unit").is_some() {
311 if path_norm
312 .components()
313 .take(2)
314 .map(Component::as_os_str)
315 .eq(["src", "lib.rs"])
316 {
317 TestType::UnitLib
318 } else {
319 TestType::UnitBin
320 }
321 } else {
322 let Some(base_dir) = path_norm
323 .components()
324 .next()
325 .and_then(|p| p.as_os_str().to_str())
326 else {
327 return Err(format!("failed to parse the type of test: {path:?}"));
328 };
329 match base_dir {
330 "tests" => TestType::Tests,
331 "examples" => TestType::Examples,
332 "benches" => TestType::Benches,
333 _ => return Err(format!("failed to parse the type of test: {path:?}")),
334 }
335 };
336
337 let mut pkg_comp = Path::new(pkg.as_str()).components();
339 match pkg_comp.next().map(|p| p.as_os_str() == "target") {
340 Some(true) => (),
341 _ => return Err(format!("failed to parse the location of test: {pkg:?}")),
342 }
343 let pkg = pkg_comp
344 .nth(2)
345 .ok_or_else(|| format!("can't get the third component in {pkg:?}"))?
346 .as_os_str()
347 .to_str()
348 .ok_or_else(|| format!("can't turn os_str into str in {pkg:?}"))?;
349 let pkg = &pkg[..pkg
350 .find('-')
351 .ok_or_else(|| format!("pkg `{pkg}` should be of `pkgname-hash` pattern"))?];
352 Ok(TestRunner {
353 ty,
354 src: Src {
355 src_path: path,
356 bin_name: pkg,
357 },
358 })
359 } else if let Some(s) = cap.name("doc").map(|m| m.as_str()) {
360 Ok(TestRunner {
361 ty: TestType::Doc,
362 src: Src {
363 src_path: s,
364 bin_name: s,
365 },
366 })
367 } else {
368 Err(format!("{cap:?} is not supported to be parsed"))
369 }
370 }
371 re().ty
372 .captures_iter(stderr)
373 .map(|cap| parse_stderr_inner(&cap))
374 .collect::<Result<Vec<_>>>()
375}
376
377#[allow(clippy::too_many_lines)]
378pub fn parse_stdout(stdout: &str) -> Result<Vec<TestInfo>> {
379 fn parse_stdout_except_head(raw: &str) -> Result<(Vec<Text>, Text, Stats, Text)> {
380 fn parse_tree_detail(text: &str) -> (Vec<Text>, Text) {
381 let line: Vec<_> = re().tree.find_iter(text).collect();
382 let tree_end = line.last().map_or(0, |cap| cap.end() + 1);
383 let mut tree: Vec<_> = line.into_iter().map(|cap| cap.as_str()).collect();
384 tree.sort_unstable();
385 (tree, text[tree_end..].trim())
386 }
387
388 if raw.is_empty() {
389 Err("raw stdout is empty".into())
390 } else {
391 let (tree, detail) = parse_tree_detail(raw);
392 let cap = re()
393 .stats
394 .captures(detail)
395 .ok_or_else(|| format!("`stats` is not found in {raw:?}"))?;
396 let stats = Stats {
397 ok: cap
398 .name("ok")
399 .ok_or_else(|| format!("`ok` is not found in {raw:?}"))?
400 .as_str()
401 == "ok",
402 total: u32::try_from(tree.len()).map_err(|err| err.to_string())?,
403 passed: cap
404 .name("passed")
405 .ok_or_else(|| format!("`passed` is not found in {raw:?}"))?
406 .as_str()
407 .parse::<u32>()
408 .map_err(|err| err.to_string())?,
409 failed: cap
410 .name("failed")
411 .ok_or_else(|| format!("`failed` is not found in {raw:?}"))?
412 .as_str()
413 .parse::<u32>()
414 .map_err(|err| err.to_string())?,
415 ignored: cap
416 .name("ignored")
417 .ok_or_else(|| format!("`ignored` is not found in {raw:?}"))?
418 .as_str()
419 .parse::<u32>()
420 .map_err(|err| err.to_string())?,
421 measured: cap
422 .name("measured")
423 .ok_or_else(|| format!("`measured` is not found in {raw:?}"))?
424 .as_str()
425 .parse::<u32>()
426 .map_err(|err| err.to_string())?,
427 filtered_out: cap
428 .name("filtered")
429 .ok_or_else(|| format!("`filtered` is not found in {raw:?}"))?
430 .as_str()
431 .parse::<u32>()
432 .map_err(|err| err.to_string())?,
433 finished_in: Duration::from_secs_f32(
434 cap.name("time")
435 .ok_or_else(|| format!("`time` is not found in {raw:?}"))?
436 .as_str()
437 .parse::<f32>()
438 .map_err(|err| err.to_string())?,
439 ),
440 };
441 let stats_start = cap
442 .get(0)
443 .ok_or_else(|| format!("can't get stats start in {raw:?}"))?
444 .start();
445 Ok((tree, detail[..stats_start].trim(), stats, raw))
446 }
447 }
448
449 let split: Vec<_> = re()
450 .head
451 .captures_iter(stdout)
452 .filter_map(|cap| {
453 let full = cap.get(0)?;
454 Some((
455 full.start(),
456 full.as_str(),
457 cap.name("amount")?.as_str().parse::<u32>().ok()?,
458 ))
459 })
460 .collect();
461 if split.is_empty() {
462 return Err(format!(
463 "{stdout:?} should contain `running (?P<amount>\\d+) tests?` pattern"
464 ));
465 }
466 let parsed_stdout = if split.len() == 1 {
467 vec![parse_stdout_except_head(stdout)?]
468 } else {
469 let start = split.iter().map(|v| v.0);
470 let end = start.clone().skip(1).chain([stdout.len()]);
471 start
472 .zip(end)
473 .map(|(a, b)| {
474 let src = &stdout[a..b];
475 parse_stdout_except_head(src)
476 })
477 .collect::<Result<Vec<_>>>()?
478 };
479
480 let parsed_amount_from_head: Vec<_> = split.iter().map(|v| v.2).collect();
482 let stats_total: Vec<_> = parsed_stdout.iter().map(|v| v.2.total).collect();
483 if parsed_amount_from_head != stats_total {
484 return Err(format!(
485 "the parsed amount of running tests {parsed_amount_from_head:?} \
486 should equal to the number in stats.total {stats_total:?}\n\
487 split = {split:#?}\nparsed_stdout = {parsed_stdout:#?}"
488 ));
489 }
490
491 Ok(split
492 .iter()
493 .zip(parsed_stdout)
494 .map(|(head_info, v)| TestInfo {
495 parsed: ParsedCargoTestOutput {
496 head: head_info.1,
497 tree: v.0,
498 detail: v.1,
499 },
500 stats: v.2,
501 raw: v.3,
502 })
503 .collect())
504}