diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b31ed3b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + target-branch: "main" + + - package-ecosystem: cargo + directory: / + schedule: + interval: daily + target-branch: "main" diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml new file mode 100644 index 0000000..7eb43fb --- /dev/null +++ b/.github/workflows/testing.yaml @@ -0,0 +1,105 @@ +name: Testing + +on: + push: + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + format: + name: Formatting + runs-on: ubuntu-latest + + steps: + - id: checkout + name: Checkout Repository + uses: actions/checkout@v4 + + - id: setup + name: Setup Toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly + components: rustfmt + + - id: cache + name: Enable Workflow Cache + uses: Swatinem/rust-cache@v2 + + - id: format + name: Run Formatting-Checks + run: cargo fmt --check + + check: + name: Static Analysis + runs-on: ubuntu-latest + needs: format + + strategy: + matrix: + toolchain: [stable, nightly] + + steps: + - id: checkout + name: Checkout Repository + uses: actions/checkout@v4 + + - id: setup + name: Setup Toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.toolchain }} + components: clippy + + - id: cache + name: Enable Workflow Cache + uses: Swatinem/rust-cache@v2 + + - id: check + name: Run Build Checks + run: cargo check --tests --benches --examples --workspace --all-targets --all-features + + - id: lint + name: Run Lint Checks + run: cargo clippy --tests --benches --examples --workspace --all-targets --all-features -- -D clippy::correctness -D clippy::suspicious -D clippy::complexity -D clippy::perf -D clippy::style -D clippy::pedantic + + - id: doc + name: Run Documentation Checks + run: cargo test --doc + + unit: + name: Units + runs-on: ubuntu-latest + needs: check + + strategy: + matrix: + toolchain: [stable, nightly] + + steps: + - id: checkout + name: Checkout Repository + uses: actions/checkout@v4 + + - id: setup + name: Setup Toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.toolchain }} + components: llvm-tools-preview + + - id: cache + name: Enable Job Cache + uses: Swatinem/rust-cache@v2 + + - id: tools + name: Install Tools + uses: taiki-e/install-action@v2 + with: + tool: cargo-llvm-cov, cargo-nextest + + - id: test + name: Run Unit Tests + run: cargo test --tests --benches --examples --workspace --all-targets --all-features diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b85e5f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.coverage/ +/.idea/ +/.vscode/launch.json +/target diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..934a43e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "streetsidesoftware.code-spell-checker", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..661243f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,28 @@ +{ + "[rust]": { + "editor.formatOnSave": true + }, + "rust-analyzer.checkOnSave": true, + "rust-analyzer.check.command": "clippy", + "rust-analyzer.check.allTargets": true, + "rust-analyzer.check.extraArgs": [ + "--", + "-D", + "clippy::correctness", + "-D", + "clippy::suspicious", + "-W", + "clippy::complexity", + "-W", + "clippy::perf", + "-W", + "clippy::style", + "-W", + "clippy::pedantic", + ], + "evenBetterToml.formatter.allowedBlankLines": 1, + "evenBetterToml.formatter.columnWidth": 130, + "evenBetterToml.formatter.trailingNewline": true, + "evenBetterToml.formatter.reorderKeys": true, + "evenBetterToml.formatter.reorderArrays": true, +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7350a6e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,39 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "pretty-test" +version = "0.1.0" +dependencies = [ + "pretty_assertions", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bed7df5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pretty-test" +version = "0.1.0" +edition = "2021" +license = "GPL-3.0" +authors = ["vague", "Jose Celano "] +description = "A console command to format cargo test output" +repository = "https://github.com/josecelano/pretty-test" + +[dependencies] +termtree = "0.4.1" + +[dev-dependencies] +pretty_assertions = "1.4.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..2cf2e67 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Pretty-test ✨ + +A Rust command that prettifies the ugly `cargo test` into a beautiful output. + +Input: + +```s +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 ... fail +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 +``` + +Output: + +```s +test +└── e2e + └── web + └── api + └── v1 + └── contexts + ├── category + │ └── contract + │ ├─ ✅ it_should_allow_admins_to_delete_categories + │ ├─ ✅ it_should_not_allow_adding_duplicated_categories + │ └─ ✅ it_should_not_allow_non_admins_to_delete_categories + ├── tag + │ └── contract + │ ├─ ❌ it_should_allow_admins_to_delete_tags + │ ├─ ✅ it_should_not_allow_adding_a_tag_with_an_empty_name + │ ├─ ✅ it_should_not_allow_adding_duplicated_tags + │ ├─ ✅ it_should_not_allow_guests_to_delete_tags + │ └─ ✅ it_should_not_allow_non_admins_to_delete_tags + └── user + └── contract + └── banned_user_list + ├─ ✅ it_should_allow_an_admin_to_ban_a_user + └─ ✅ it_should_not_allow_a_non_admin_to_ban_a_user +``` + +## Run + +```s +cat tests/fixtures/sample_cargo_test_output.txt | cargo run +``` + +You can also create a Rust script with : + +- `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. + +## Test + +```s +cargo test +``` + +## Credits + +- +- diff --git a/cSpell.json b/cSpell.json new file mode 100644 index 0000000..7553f94 --- /dev/null +++ b/cSpell.json @@ -0,0 +1,11 @@ +{ + "words": [ + "splitn", + "termtree" + ], + "enableFiletypes": [ + "dockerfile", + "shellscript", + "toml" + ] +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..d5e9bdb --- /dev/null +++ b/src/app.rs @@ -0,0 +1,80 @@ +use std::collections::{btree_map::Entry, BTreeMap}; +use termtree::{GlyphPalette, Tree}; + +/// Make the cargo test output pretty. +#[must_use] +pub fn make_pretty(output: &str) -> Option> { + let mut path = BTreeMap::new(); + for line in output.trim().lines() { + let mut iter = line.trim().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) +} + +#[derive(Debug)] +enum Node<'s> { + Path(BTreeMap<&'s str, Node<'s>>), + Status(&'s str), +} + +/// Add paths to Node. +fn make_mods<'s>( + mut split: impl Iterator, + 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 node) => { + if let Node::Path(btree) = node.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 +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..309be62 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod app; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0142278 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,20 @@ +use pretty_test::app::make_pretty; +use std::io::{self, Read}; +use std::process; + +fn main() { + let mut input = String::new(); + + io::stdin() + .read_to_string(&mut input) + .expect("Failed to read from stdin"); + + if input.is_empty() { + println!("No input provided. Exiting..."); + process::exit(1); + } + + if let Some(pretty_output) = make_pretty(&input) { + println!("{pretty_output}"); + } +} diff --git a/tests/fixtures/sample_cargo_test_output.txt b/tests/fixtures/sample_cargo_test_output.txt new file mode 100644 index 0000000..a179df7 --- /dev/null +++ b/tests/fixtures/sample_cargo_test_output.txt @@ -0,0 +1,10 @@ +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 ... fail +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 \ No newline at end of file diff --git a/tests/golden_master_test.rs b/tests/golden_master_test.rs new file mode 100644 index 0000000..36d12ad --- /dev/null +++ b/tests/golden_master_test.rs @@ -0,0 +1,52 @@ +#[cfg(test)] +use pretty_assertions::assert_eq; +use pretty_test::app::make_pretty; + +#[test] +fn golden_master_test() { + // Snapshot test for output after one generation + + const INPUT: &str =" + 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 ... fail + 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 + "; + + const OUTPUT: &str = " +test +└── e2e + └── web + └── api + └── v1 + └── contexts + ├── category + │ └── contract + │ ├─ ✅ it_should_allow_admins_to_delete_categories + │ ├─ ✅ it_should_not_allow_adding_duplicated_categories + │ └─ ✅ it_should_not_allow_non_admins_to_delete_categories + ├── tag + │ └── contract + │ ├─ ❌ it_should_allow_admins_to_delete_tags + │ ├─ ✅ it_should_not_allow_adding_a_tag_with_an_empty_name + │ ├─ ✅ it_should_not_allow_adding_duplicated_tags + │ ├─ ✅ it_should_not_allow_guests_to_delete_tags + │ └─ ✅ it_should_not_allow_non_admins_to_delete_tags + └── user + └── contract + └── banned_user_list + ├─ ✅ it_should_allow_an_admin_to_ban_a_user + └─ ✅ it_should_not_allow_a_non_admin_to_ban_a_user +"; + + assert_eq!( + format!("\n{}", make_pretty(INPUT).unwrap().to_string()), + OUTPUT + ); +}