Cargo test output with indentation

Is there a crate to show the cargo test output like this?

I want to print out ordered by mods and with indentation, removing duplicate prefixes with the function namespace.

For example, this output:

test e2e::web::api::v1::contexts::category::contract::it_should_not_allow_adding_duplicated_categories ... ok
test e2e::web::api::v1::contexts::tag::contract::it_should_not_allow_adding_duplicated_tags ... ok
test e2e::web::api::v1::contexts::category::contract::it_should_not_allow_non_admins_to_delete_categories ... ok
test e2e::web::api::v1::contexts::tag::contract::it_should_not_allow_adding_a_tag_with_an_empty_name ... ok
test e2e::web::api::v1::contexts::tag::contract::it_should_not_allow_guests_to_delete_tags ... ok
test e2e::web::api::v1::contexts::category::contract::it_should_allow_admins_to_delete_categories ... ok
test e2e::web::api::v1::contexts::user::contract::banned_user_list::it_should_allow_an_admin_to_ban_a_user ... ok
test e2e::web::api::v1::contexts::tag::contract::it_should_allow_admins_to_delete_tags ... ok
test e2e::web::api::v1::contexts::user::contract::banned_user_list::it_should_not_allow_a_non_admin_to_ban_a_user ... ok
test e2e::web::api::v1::contexts::tag::contract::it_should_not_allow_non_admins_to_delete_tags ... ok

should produce this output:

test 
  e2e
    web
      api
        v1
          contexts
            category
              contract
               it_should_not_allow_adding_duplicated_categories ... ok
               it_should_not_allow_non_admins_to_delete_categories ... ok
               it_should_allow_admins_to_delete_categories ... ok
            tag
              contract
                it_should_not_allow_adding_duplicated_tags ... ok
                it_should_not_allow_adding_a_tag_with_an_empty_name ... ok
                it_should_not_allow_guests_to_delete_tags ... ok
                it_should_allow_admins_to_delete_tags ... ok
                it_should_not_allow_non_admins_to_delete_tags ... ok
            user
              contract
                banned_user_list
                  it_should_allow_an_admin_to_ban_a_user ... ok
                  it_should_not_allow_a_non_admin_to_ban_a_user ... ok
1 Like

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

Awesome @vague you made my day! It's something I'd missed a lot. I knew I would need some kind of tree structure and that's why I was looking for a crate.

Hi @vague, I've created a repo:

We can move it to your account on GitHub, or I can make you an owner if you agree (and with whatever the LICENSE you want to use). I've made some changes, like getting the input from the standard input, but I do not like it because if there is no input, it waits until the user cancels it with Ctrl+D or Ctrl+Z.

1 Like

But it requires the user to copy & paste part of the output from cargo test to your cli, which is inconvenient. That's why I obtain and handle the cargo test output in the script.


In README, the usage with rust-script is wrong because there is no dependencies declaration in the script (or root file). You have already put it in the project, so it'd be better deleting the rust-script usage.

1 Like

FYI with RFC 3455 merged, there is now a t-testing-devex team that will be looking at improving things. We're a bit slow getting started up as we are trying to roll off of other projects. One big focus I expect from this effort is to at least make programmtic output more meaningful so its easier to generate these custom UIs even if we don't bake a UI like this into it.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.