Cargo test output with indentation

It's not hard: https://www.rustexplorer.com/b/i058g3

Also you can use the following code (100~ lines in total) with https://rust-script.org/ to achieve your goal.

  • cargo install rust-script
  • execute the code
    • save the code in a file named pretty-test
    • chmod +x ./pretty-test
    • add it in your environment, like mv pretty-test ~/.cargo/bin
    • run pretty-test in your project
// src/lib.rs
#[test] fn works() {}
mod a {
    mod b {
        #[test] fn c() { panic!() }
    }
    #[test] fn d() {}
}

// output:
test
├── a
│   ├── b
│   │   └─ ❌ c
│   └─ ✅ d
└─ ✅ works
#!/usr/bin/env rust-script
//!
//! ```cargo
//! [dependencies]
//! termtree = "0.4.1"
//! regex-lite = "0.1"
//! ```

use regex_lite::Regex;
use std::{
    collections::{btree_map::Entry, BTreeMap},
    process::Command,
};
use termtree::{GlyphPalette, Tree};

fn main() {
    let output = Command::new("cargo").arg("test").output().unwrap();
    let text = String::from_utf8_lossy(&output.stdout);

    let re = Regex::new(r"(?m)^test \S+ \.\.\. \S+$").unwrap();
    println!(
        "{}",
        pretty_test(re.find_iter(&text).map(|m| m.as_str())).unwrap()
    );
}

#[derive(Debug)]
enum Node<'s> {
    Path(BTreeMap<&'s str, Node<'s>>),
    Status(&'s str),
}

fn pretty_test<'s>(lines: impl Iterator<Item = &'s str>) -> Option<String> {
    let mut path = BTreeMap::new();
    for line in lines {
        let mut iter = line.splitn(3, ' ');
        let mut split = iter.nth(1)?.split("::");
        let next = split.next();
        let status = iter.next()?;
        make_mods(split, status, &mut path, next);
    }
    let mut tree = Tree::new("test");
    for (root, child) in path {
        make_tree(root, &child, &mut tree);
    }
    Some(tree.to_string())
}

// Add paths to Node
fn make_mods<'s>(
    mut split: impl Iterator<Item = &'s str>,
    status: &'s str,
    path: &mut BTreeMap<&'s str, Node<'s>>,
    key: Option<&'s str>,
) {
    let Some(key) = key else { return };
    let next = split.next();
    match path.entry(key) {
        Entry::Vacant(empty) => {
            if next.is_some() {
                let mut btree = BTreeMap::new();
                make_mods(split, status, &mut btree, next);
                empty.insert(Node::Path(btree));
            } else {
                empty.insert(Node::Status(status));
            }
        }
        Entry::Occupied(mut btree) => {
            if let Node::Path(btree) = btree.get_mut() {
                make_mods(split, status, btree, next)
            }
        }
    }
}

// Add Node to Tree
fn make_tree<'s>(root: &'s str, node: &Node<'s>, parent: &mut Tree<&'s str>) {
    match node {
        Node::Path(btree) => {
            let mut t = Tree::new(root);
            for (path, child) in btree {
                make_tree(path, child, &mut t);
            }
            parent.push(t);
        }
        Node::Status(s) => {
            parent.push(Tree::new(root).with_glyphs(set_status(s)));
        }
    }
}

// Display with a status icon
fn set_status(status: &str) -> GlyphPalette {
    let mut glyph = GlyphPalette::new();
    glyph.item_indent = if status.ends_with("ok") {
        "─ ✅ "
    } else {
        "─ ❌ "
    };
    glyph
}
5 Likes