use crate::regex::re;
use colored::{ColoredString, Colorize};
use indexmap::IndexMap;
use std::{
path::{Component, Path},
time::Duration,
};
pub fn parse_cargo_test<'s>(stderr: &'s str, stdout: &'s str) -> TestRunners<'s> {
use TestType::*;
let mut pkg = None;
TestRunners::new(
parse_cargo_test_with_empty_ones(stderr, stdout)
.filter_map(|(runner, info)| {
match runner.ty {
UnitLib | UnitBin => pkg = Some(runner.src.bin_name),
Doc => pkg = Some("Doc Tests"),
_ => (),
}
if info.stats.total == 0 {
None
} else {
Some((pkg, runner, info))
}
})
.collect(),
)
}
pub fn parse_cargo_test_with_empty_ones<'s>(
stderr: &'s str,
stdout: &'s str,
) -> impl Iterator<Item = (TestRunner<'s>, TestInfo<'s>)> {
let parsed_stderr = parse_stderr(stderr);
let parsed_stdout = parse_stdout(stdout);
assert_eq!(
parsed_stderr.len(),
parsed_stdout.len(),
"the amount of test runners from stderr should equal to that from stdout"
);
parsed_stderr.into_iter().zip(parsed_stdout)
}
pub type Pkg<'s> = Option<Text<'s>>;
#[derive(Debug, Default)]
pub struct TestRunners<'s> {
pub pkgs: IndexMap<Pkg<'s>, PkgTest<'s>>,
}
impl<'s> TestRunners<'s> {
pub fn new(v: Vec<(Pkg<'s>, TestRunner<'s>, TestInfo<'s>)>) -> TestRunners<'s> {
let mut runners = TestRunners::default();
for (pkg, runner, info) in v {
match runners.pkgs.entry(pkg) {
indexmap::map::Entry::Occupied(mut item) => {
item.get_mut().push(runner, info);
}
indexmap::map::Entry::Vacant(empty) => {
empty.insert(PkgTest::new(runner, info));
}
}
}
runners
}
}
pub type Text<'s> = &'s str;
#[derive(Debug, Default)]
pub struct PkgTest<'s> {
pub inner: Vec<Data<'s>>,
pub stats: Stats,
}
impl<'s> PkgTest<'s> {
pub fn new(runner: TestRunner<'s>, info: TestInfo<'s>) -> PkgTest<'s> {
let stats = info.stats.clone();
PkgTest {
inner: vec![Data { runner, info }],
stats,
}
}
pub fn push(&mut self, runner: TestRunner<'s>, info: TestInfo<'s>) {
self.stats += &info.stats;
self.inner.push(Data { runner, info });
}
}
#[derive(Debug)]
pub struct Data<'s> {
pub runner: TestRunner<'s>,
pub info: TestInfo<'s>,
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct TestRunner<'s> {
pub ty: TestType,
pub src: Src<'s>,
}
#[derive(Debug)]
pub struct TestInfo<'s> {
pub raw: Text<'s>,
pub stats: Stats,
pub parsed: ParsedCargoTestOutput<'s>,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub enum TestType {
UnitLib,
UnitBin,
Doc,
Tests,
Examples,
Benches,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub struct Src<'s> {
pub src_path: Text<'s>,
pub bin_name: Text<'s>,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Stats {
pub ok: bool,
pub total: u32,
pub passed: u32,
pub failed: u32,
pub ignored: u32,
pub measured: u32,
pub filtered_out: u32,
pub finished_in: Duration,
}
impl std::fmt::Display for Stats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Stats {
ok,
total,
passed,
failed,
ignored,
measured,
filtered_out,
finished_in,
} = *self;
let time = finished_in.as_secs_f32();
let fail = if failed == 0 {
format!("{failed} failed")
} else {
format!("{failed} failed").red().bold().to_string()
};
write!(
f,
"Status: {}; total {total} tests in {time:.2}s: \
{passed} passed; {fail}; {ignored} ignored; \
{measured} measured; {filtered_out} filtered out",
status(ok)
)
}
}
fn status(ok: bool) -> ColoredString {
if ok {
"OK".green().bold()
} else {
"FAIL".red().bold()
}
}
impl Stats {
pub fn inlay_string(&self) -> String {
let Stats {
total,
passed,
failed,
ignored,
filtered_out,
finished_in,
..
} = *self;
let time = finished_in.as_secs_f32();
let mut part = Vec::with_capacity(4);
if passed != 0 {
part.push(format!("✅ {passed}"));
};
if failed != 0 {
part.push(format!("❌ {failed}").red().to_string());
};
if ignored != 0 {
part.push(format!("🔕 {ignored}"));
};
if filtered_out != 0 {
part.push(format!("✂️ {filtered_out}"));
};
format!("{total} tests in {time:.2}s: {}", part.join("; "))
}
pub fn root_string(&self, pkg_name: Text) -> String {
format!(
"({}) {:} ... ({})",
status(self.ok),
pkg_name.blue().bold(),
self.inlay_string().bold()
)
}
pub fn subroot_string(&self, pkg_name: Text) -> String {
format!(
"({}) {} ... ({})",
status(self.ok),
pkg_name,
self.inlay_string()
)
}
}
impl Default for Stats {
fn default() -> Self {
Stats {
ok: true,
total: 0,
passed: 0,
failed: 0,
ignored: 0,
measured: 0,
filtered_out: 0,
finished_in: Duration::from_secs(0),
}
}
}
impl std::ops::Add<&Stats> for &Stats {
type Output = Stats;
fn add(self, rhs: &Stats) -> Self::Output {
Stats {
ok: self.ok && rhs.ok,
total: self.total + rhs.total,
passed: self.passed + rhs.passed,
failed: self.failed + rhs.failed,
ignored: self.ignored + rhs.ignored,
measured: self.measured + rhs.measured,
filtered_out: self.filtered_out + rhs.filtered_out,
finished_in: self.finished_in + rhs.finished_in,
}
}
}
impl std::ops::AddAssign<&Stats> for Stats {
fn add_assign(&mut self, rhs: &Stats) {
*self = &*self + rhs;
}
}
#[derive(Debug)]
pub struct ParsedCargoTestOutput<'s> {
pub head: Text<'s>,
pub tree: Vec<Text<'s>>,
pub detail: Text<'s>,
}
pub fn parse_stderr(stderr: &str) -> Vec<TestRunner> {
fn parse_stderr_inner<'s>(cap: ®ex_lite::Captures<'s>) -> TestRunner<'s> {
if let Some((path, pkg)) = cap.name("path").zip(cap.name("pkg")) {
let path = path.as_str();
let path_norm = Path::new(path);
let ty = if cap.name("is_unit").is_some() {
if path_norm
.components()
.take(2)
.map(Component::as_os_str)
.eq(["src", "lib.rs"])
{
TestType::UnitLib
} else {
TestType::UnitBin
}
} else {
let Some(base_dir) = path_norm
.components()
.next()
.and_then(|p| p.as_os_str().to_str())
else {
unimplemented!("failed to parse the type of test: {path:?}")
};
match base_dir {
"tests" => TestType::Tests,
"examples" => TestType::Examples,
"benches" => TestType::Benches,
_ => unimplemented!("failed to parse the type of test: {path:?}"),
}
};
let mut pkg_comp = Path::new(pkg.as_str()).components();
match pkg_comp.next().map(|p| p.as_os_str() == "target") {
Some(true) => (),
_ => unimplemented!("failed to parse the location of test: {pkg:?}"),
}
let pkg = pkg_comp.nth(2).unwrap().as_os_str().to_str().unwrap();
let pkg = &pkg[..pkg
.find('-')
.expect("pkg should be of `pkgname-hash` pattern")];
TestRunner {
ty,
src: Src {
src_path: path,
bin_name: pkg,
},
}
} else if let Some(s) = cap.name("doc").map(|m| m.as_str()) {
TestRunner {
ty: TestType::Doc,
src: Src {
src_path: s,
bin_name: s,
},
}
} else {
unimplemented!();
}
}
re().ty
.captures_iter(stderr)
.map(|cap| parse_stderr_inner(&cap))
.collect::<Vec<_>>()
}
pub fn parse_stdout(stdout: &str) -> Vec<TestInfo> {
fn parse_stdout_except_head(raw: &str) -> Option<(Vec<Text>, Text, Stats, Text)> {
fn parse_tree_detail(text: &str) -> (Vec<Text>, Text) {
let line: Vec<_> = re().tree.find_iter(text).collect();
let tree_end = line.last().map_or(0, |cap| cap.end() + 1);
let mut tree: Vec<_> = line.into_iter().map(|cap| cap.as_str()).collect();
tree.sort_unstable();
(tree, text[tree_end..].trim())
}
if raw.is_empty() {
None
} else {
let (tree, detail) = parse_tree_detail(raw);
let cap = re().stats.captures(detail)?;
let stats = Stats {
ok: cap.name("ok").map(|ok| ok.as_str() == "ok")?,
total: tree.len().try_into().ok()?,
passed: cap.name("passed")?.as_str().parse().ok()?,
failed: cap.name("failed")?.as_str().parse().ok()?,
ignored: cap.name("ignored")?.as_str().parse().ok()?,
measured: cap.name("measured")?.as_str().parse().ok()?,
filtered_out: cap.name("filtered")?.as_str().parse().ok()?,
finished_in: Duration::from_secs_f32(cap.name("time")?.as_str().parse().ok()?),
};
let stats_start = cap.get(0)?.start();
Some((tree, detail[..stats_start].trim(), stats, raw))
}
}
let split: Vec<_> = re()
.head
.captures_iter(stdout)
.filter_map(|cap| {
let full = cap.get(0)?;
Some((
full.start(),
full.as_str(),
cap.name("amount")?.as_str().parse::<u32>().ok()?,
))
})
.collect();
assert!(
!split.is_empty(),
"{stdout} should contain `running (?P<amount>\\d+) tests?` pattern"
);
let parsed_stdout = if split.len() == 1 {
vec![parse_stdout_except_head(stdout).unwrap()]
} else {
let start = split.iter().map(|v| v.0);
let end = start.clone().skip(1).chain([stdout.len()]);
start
.zip(end)
.filter_map(|(a, b)| {
let src = &stdout[a..b];
parse_stdout_except_head(src)
})
.collect::<Vec<_>>()
};
let parsed_amount_from_head: Vec<_> = split.iter().map(|v| v.2).collect();
let stats_total: Vec<_> = parsed_stdout.iter().map(|v| v.2.total).collect();
assert_eq!(
parsed_amount_from_head, stats_total,
"the parsed amount of running tests {parsed_amount_from_head:?} \
should equal to the number in stats.total {stats_total:?}"
);
split
.iter()
.zip(parsed_stdout)
.map(|(head_info, v)| TestInfo {
parsed: ParsedCargoTestOutput {
head: head_info.1,
tree: v.0,
detail: v.1,
},
stats: v.2,
raw: v.3,
})
.collect()
}