cargo-pretty-test 0.2.2

A console command to format cargo test output
Documentation
use crate::{
    parsing::{parse_cargo_test, Stats},
    prettify::{make_pretty, TestTree, ICON_NOTATION},
    regex::re,
};
use colored::Colorize;
use std::process::{Command, ExitCode, Output};
use termtree::Tree;

/// Output from `cargo test`
pub struct Emit {
    /// Raw output.
    output: Output,
    /// Don't parse the output. Forward the output instead.
    no_parse: bool,
}

impl Emit {
    pub fn run(self) -> ExitCode {
        let Emit { output, no_parse } = self;
        let stderr = strip_ansi_escapes::strip(output.stderr);
        let stdout = strip_ansi_escapes::strip(output.stdout);
        let stderr = String::from_utf8_lossy(&stderr);
        let stdout = String::from_utf8_lossy(&stdout);
        if no_parse {
            println!(
                "{phelp}\n{ICON_NOTATION}\n{sep}\n\n{help}",
                phelp = "cargo pretty-test help:".blue().bold(),
                sep = re().separator,
                help = "cargo test help:".blue().bold()
            );
            eprintln!("{stderr}");
            println!("{stdout}");
        } else {
            let (tree, stats) = parse_cargo_test_output(&stderr, &stdout);
            println!("{tree}");
            eprintln!("{stats}");
            if !stats.ok {
                return ExitCode::FAILURE;
            }
        }
        ExitCode::SUCCESS
    }
}

/// entrypoint for main.rs
pub fn run() -> ExitCode {
    cargo_test().run()
}

/// Collect arguments and forward them to `cargo test`.
///
/// Note: This filters some arguments that mess up the output, like
/// `--nocapture` which prints in the status part and hinders parsing.
pub fn cargo_test() -> Emit {
    let passin: Vec<_> = std::env::args().collect();
    let forward = if passin
        .get(..2)
        .is_some_and(|v| v[0].ends_with("cargo-pretty-test") && v[1] == "pretty-test")
    {
        // `cargo pretty-test` yields ["path-to-cargo-pretty-test", "pretty-test", rest]
        &passin[2..]
    } else {
        // `cargo-pretty-test` yields ["path-to-cargo-pretty-test", rest]
        &passin[1..]
    };
    let no_parse = passin.iter().any(|arg| arg == "--help" || arg == "-h");
    let args = forward.iter().filter(|arg| *arg != "--nocapture");
    Emit {
        output: Command::new("cargo")
            .arg("test")
            .args(args)
            .output()
            .expect("`cargo test` failed"),
        no_parse,
    }
}

pub fn parse_cargo_test_output<'s>(stderr: &'s str, stdout: &'s str) -> (TestTree<'s>, Stats) {
    let mut tree = Tree::new("Generated by cargo-pretty-test".bold().to_string().into());
    let mut stats = Stats::default();
    for (pkg, data) in parse_cargo_test(stderr, stdout).pkgs {
        stats += &data.stats;
        let root = data.stats.root_string(pkg.unwrap_or("tests")).into();
        tree.push(
            Tree::new(root).with_leaves(data.inner.into_iter().filter_map(|data| {
                let parsed = data.info.parsed;
                let detail_without_stats = parsed.detail;
                if !detail_without_stats.is_empty() {
                    eprintln!("{detail_without_stats}\n\n{}\n", re().separator);
                }
                let root = data.info.stats.subroot_string(data.runner.src.src_path);
                make_pretty(root, parsed.tree.into_iter())
            })),
        );
    }
    (tree, stats)
}