├── testdata └── testproj │ ├── .gitignore │ ├── Cargo.toml │ ├── Cargo.lock │ └── src │ └── lib.rs ├── .gitignore ├── img ├── demo.gif ├── logo.png └── cover.png ├── demo ├── Cargo.toml ├── Cargo.lock ├── demo.tape └── src │ └── lib.rs ├── .github └── workflows │ ├── nightly.yml │ ├── audit.yml │ └── ci.yml ├── src ├── main.rs └── lib.rs ├── tests └── cli.rs ├── Cargo.toml ├── LICENSE ├── README.md ├── Cargo.lock └── deny.toml /testdata/testproj/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | cargo-test*.profraw 3 | target 4 | -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/cargo-testdox/HEAD/img/demo.gif -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/cargo-testdox/HEAD/img/logo.png -------------------------------------------------------------------------------- /img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/cargo-testdox/HEAD/img/cover.png -------------------------------------------------------------------------------- /demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "demo" 3 | version = "0.1.0" 4 | edition = "2024" 5 | -------------------------------------------------------------------------------- /testdata/testproj/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "testproj" 3 | version = "0.1.0" 4 | edition = "2024" 5 | -------------------------------------------------------------------------------- /demo/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "demo" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /testdata/testproj/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "testproj" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /testdata/testproj/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | #[ignore] 5 | fn ignored_test() { 6 | assert_eq!(1 + 1, 2); 7 | } 8 | 9 | #[test] 10 | #[ignore = "expensive test"] 11 | fn pi_to_1m_digits() {} 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly 2 | on: [pull_request, workflow_dispatch] 3 | env: 4 | CARGO_TERM_COLOR: always 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: dtolnay/rust-toolchain@nightly 12 | - uses: Swatinem/rust-cache@v2 13 | - run: cargo test --all-features 14 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | push: 7 | paths: 8 | - "**/Cargo.toml" 9 | - "**/Cargo.lock" 10 | jobs: 11 | security_audit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: EmbarkStudios/cargo-deny-action@v2 16 | -------------------------------------------------------------------------------- /demo/demo.tape: -------------------------------------------------------------------------------- 1 | # https://github.com/charmbracelet/vhs 2 | 3 | Output img/demo.gif 4 | 5 | # Set Shell zsh 6 | Set FontSize 23 7 | Set Width 800 8 | Set Height 380 9 | Set Padding 5 10 | Set WindowBar Colorful 11 | Set FontFamily "Recursive Monospace" 12 | 13 | Type "cargo testdox" Sleep 500ms Enter 14 | 15 | Sleep 1s 16 | 17 | Type "cargo testdox -- --include-ignored" Sleep 500ms Enter 18 | 19 | Sleep 30s 20 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use cargo_testdox::{get_cargo_test_output, parse_test_results, Status}; 2 | 3 | fn main() { 4 | let output = get_cargo_test_output(std::env::args().skip(2).collect()); 5 | let results = parse_test_results(&output); 6 | let mut failed = false; 7 | for result in results { 8 | println!("{result}"); 9 | if result.status == Status::Fail { 10 | failed = true; 11 | } 12 | } 13 | if failed { 14 | std::process::exit(1); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn open_file_fn_returns_file_handle_on_success() { 5 | assert_eq!(1 + 1, 2); 6 | } 7 | 8 | fn open_file_fn_returns_error_on_failure() { 9 | assert_eq!(1 + 1, 2); 10 | } 11 | 12 | #[test] 13 | fn add_returns_2_for_1_plus_1() { 14 | assert_eq!(3, 2); 15 | } 16 | 17 | #[test] 18 | #[ignore] 19 | fn ignored_test() { 20 | assert_eq!(1 + 1, 2); 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod fancy_module { 26 | #[test] 27 | fn fancy_function_fn_returns_right_answer() { 28 | assert_eq!(1 + 1, 2); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | use predicates::prelude::*; 3 | 4 | #[test] 5 | fn passing_include_ignored_flag_runs_ignored_tests() { 6 | Command::cargo_bin("cargo-testdox") 7 | .unwrap() 8 | .current_dir("testdata/testproj") 9 | .arg("testdox") 10 | .arg("--") 11 | .arg("--include-ignored") 12 | .assert() 13 | .success() 14 | .stdout(predicate::str::contains("✔ ignored test\n")); 15 | } 16 | 17 | #[test] 18 | fn cli_reports_ignore_reason() { 19 | Command::cargo_bin("cargo-testdox") 20 | .unwrap() 21 | .current_dir("testdata/testproj") 22 | .arg("testdox") 23 | .assert() 24 | .success() 25 | .stdout(predicate::str::contains( 26 | "? [expensive test] pi to 1m digits\n", 27 | )); 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [pull_request, workflow_dispatch] 3 | env: 4 | CARGO_TERM_COLOR: always 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: dtolnay/rust-toolchain@stable 12 | with: 13 | components: "clippy, rustfmt" 14 | - uses: Swatinem/rust-cache@v2 15 | - run: cargo fmt --all -- --check 16 | - run: cargo clippy --all-targets --all-features -- -D clippy::pedantic -D warnings 17 | 18 | test: 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest, macos-latest, windows-latest] 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: dtolnay/rust-toolchain@stable 26 | - uses: Swatinem/rust-cache@v2 27 | - run: cargo test --all-features 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-testdox" 3 | version = "0.6.0" 4 | authors = ["John Arundel "] 5 | edition = "2021" 6 | description = """ 7 | A Cargo subcommand to print your test names as sentences. 8 | """ 9 | keywords = ["test", "testdox", "unit-tests"] 10 | categories = [ 11 | "command-line-utilities", 12 | "development-tools", 13 | "development-tools::cargo-plugins", 14 | "development-tools::testing", 15 | ] 16 | license = "MIT OR Apache-2.0" 17 | readme = "README.md" 18 | documentation = "https://docs.rs/cargo-testdox" 19 | homepage = "https://github.com/bitfield/cargo-testdox" 20 | repository = "https://github.com/bitfield/cargo-testdox" 21 | exclude = ["/.github/"] 22 | 23 | [badges] 24 | github = { repository = "bitfield/cargo-testdox", workflow = "CI" } 25 | maintenance = { status = "actively-developed" } 26 | 27 | [dependencies] 28 | anyhow = "1.0.86" 29 | colored = "2.1.0" 30 | 31 | [dev-dependencies] 32 | assert_cmd = "2.0.17" 33 | predicates = "3.1.3" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 John Arundel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crate](https://img.shields.io/crates/v/cargo-testdox.svg)](https://crates.io/crates/cargo-testdox) 2 | [![Docs](https://docs.rs/cargo-testdox/badge.svg)](https://docs.rs/cargo-testdox) 3 | ![CI](https://github.com/bitfield/cargo-testdox/actions/workflows/ci.yml/badge.svg) 4 | ![Audit](https://github.com/bitfield/cargo-testdox/actions/workflows/audit.yml/badge.svg) 5 | ![Maintenance](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg) 6 | 7 | [Subscribe to learn Rust with me!](https://bitfieldconsulting.com/subscribe) 8 | 9 | # cargo-testdox 10 | 11 | ![cargo-testdox logo](img/logo.png) 12 | 13 | A Cargo subcommand to print your Rust test names as sentences. 14 | 15 | ## Installation 16 | 17 | ```sh 18 | cargo install cargo-testdox 19 | ``` 20 | 21 | ## Usage 22 | 23 | In any Rust project with tests, run: 24 | 25 | ```sh 26 | cargo testdox 27 | ``` 28 | 29 | ![Animated demo](img/demo.gif) 30 | 31 | `cargo-testdox` will first invoke `cargo test` to run your tests, with any extra arguments that you give it. It will then show the result for each test (passed, failed, or ignored), with the test name formatted as a sentence. That is, with underscores replaced by spaces. 32 | 33 | For example, the following test: 34 | 35 | ```rust,ignore 36 | #[test] 37 | fn it_works() {} 38 | ``` 39 | 40 | will produce this output when run with `cargo-testdox`: 41 | 42 | ```txt 43 | ✔ it works 44 | ``` 45 | 46 | If the test were failing, it would produce: 47 | 48 | ```txt 49 | x it works 50 | ``` 51 | 52 | If the test were ignored, it would produce: 53 | 54 | ```txt 55 | ? it works 56 | ``` 57 | 58 | If the test were ignored with a reason (`[ignore = "expensive"]`), it would produce: 59 | 60 | ```txt 61 | ? [expensive] it works 62 | ``` 63 | 64 | If the test were in a module `foo::bar`, it would produce: 65 | 66 | ```txt 67 | ✔ foo::bar — it works 68 | ``` 69 | 70 | However, if the module path ends with `test` or `tests`, this part is omitted, and the name of the parent module (if there is one) is used instead. For example, if the module is `foo::tests`: 71 | 72 | ```txt 73 | ✔ foo — it works 74 | ``` 75 | 76 | Doctests are ignored, since they can't currently be named (pending [RFC #3311](https://github.com/rust-lang/rfcs/pull/3311)). 77 | 78 | ### Function names with underscores 79 | 80 | To avoid underscores in a snake-case function name from being replaced, put `_fn_` after the function name: 81 | 82 | ```rust,ignore 83 | #[test] 84 | fn print_hello_world_fn_prints_hello_world() {} 85 | ``` 86 | 87 | becomes: 88 | 89 | ```txt 90 | ✔ print_hello_world prints hello world 91 | ``` 92 | 93 | ## Why 94 | 95 | Because [test names should be sentences](https://bitfieldconsulting.com/posts/test-names). 96 | 97 | Compare [`gotestdox`](https://github.com/bitfield/gotestdox), a similar tool for Go tests. 98 | 99 | This is an example project from my book [The Secrets of Rust: Tools](https://bitfieldconsulting.com/books/rust-tools). 100 | 101 | [![Secrets of Rust: Tools cover image](img/cover.png)](https://bitfieldconsulting.com/books/rust-tools) 102 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstyle" 16 | version = "1.0.13" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.86" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 25 | 26 | [[package]] 27 | name = "assert_cmd" 28 | version = "2.0.17" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" 31 | dependencies = [ 32 | "anstyle", 33 | "bstr", 34 | "doc-comment", 35 | "libc", 36 | "predicates", 37 | "predicates-core", 38 | "predicates-tree", 39 | "wait-timeout", 40 | ] 41 | 42 | [[package]] 43 | name = "autocfg" 44 | version = "1.5.0" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 47 | 48 | [[package]] 49 | name = "bstr" 50 | version = "1.12.0" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" 53 | dependencies = [ 54 | "memchr", 55 | "regex-automata", 56 | "serde", 57 | ] 58 | 59 | [[package]] 60 | name = "cargo-testdox" 61 | version = "0.6.0" 62 | dependencies = [ 63 | "anyhow", 64 | "assert_cmd", 65 | "colored", 66 | "predicates", 67 | ] 68 | 69 | [[package]] 70 | name = "colored" 71 | version = "2.1.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 74 | dependencies = [ 75 | "lazy_static", 76 | "windows-sys", 77 | ] 78 | 79 | [[package]] 80 | name = "difflib" 81 | version = "0.4.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 84 | 85 | [[package]] 86 | name = "doc-comment" 87 | version = "0.3.3" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 90 | 91 | [[package]] 92 | name = "float-cmp" 93 | version = "0.10.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" 96 | dependencies = [ 97 | "num-traits", 98 | ] 99 | 100 | [[package]] 101 | name = "lazy_static" 102 | version = "1.5.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 105 | 106 | [[package]] 107 | name = "libc" 108 | version = "0.2.176" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 111 | 112 | [[package]] 113 | name = "memchr" 114 | version = "2.7.6" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 117 | 118 | [[package]] 119 | name = "normalize-line-endings" 120 | version = "0.3.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 123 | 124 | [[package]] 125 | name = "num-traits" 126 | version = "0.2.19" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 129 | dependencies = [ 130 | "autocfg", 131 | ] 132 | 133 | [[package]] 134 | name = "predicates" 135 | version = "3.1.3" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" 138 | dependencies = [ 139 | "anstyle", 140 | "difflib", 141 | "float-cmp", 142 | "normalize-line-endings", 143 | "predicates-core", 144 | "regex", 145 | ] 146 | 147 | [[package]] 148 | name = "predicates-core" 149 | version = "1.0.9" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" 152 | 153 | [[package]] 154 | name = "predicates-tree" 155 | version = "1.0.12" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" 158 | dependencies = [ 159 | "predicates-core", 160 | "termtree", 161 | ] 162 | 163 | [[package]] 164 | name = "proc-macro2" 165 | version = "1.0.101" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 168 | dependencies = [ 169 | "unicode-ident", 170 | ] 171 | 172 | [[package]] 173 | name = "quote" 174 | version = "1.0.41" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 177 | dependencies = [ 178 | "proc-macro2", 179 | ] 180 | 181 | [[package]] 182 | name = "regex" 183 | version = "1.11.3" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" 186 | dependencies = [ 187 | "aho-corasick", 188 | "memchr", 189 | "regex-automata", 190 | "regex-syntax", 191 | ] 192 | 193 | [[package]] 194 | name = "regex-automata" 195 | version = "0.4.11" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" 198 | dependencies = [ 199 | "aho-corasick", 200 | "memchr", 201 | "regex-syntax", 202 | ] 203 | 204 | [[package]] 205 | name = "regex-syntax" 206 | version = "0.8.6" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" 209 | 210 | [[package]] 211 | name = "serde" 212 | version = "1.0.228" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 215 | dependencies = [ 216 | "serde_core", 217 | ] 218 | 219 | [[package]] 220 | name = "serde_core" 221 | version = "1.0.228" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 224 | dependencies = [ 225 | "serde_derive", 226 | ] 227 | 228 | [[package]] 229 | name = "serde_derive" 230 | version = "1.0.228" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 233 | dependencies = [ 234 | "proc-macro2", 235 | "quote", 236 | "syn", 237 | ] 238 | 239 | [[package]] 240 | name = "syn" 241 | version = "2.0.106" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 244 | dependencies = [ 245 | "proc-macro2", 246 | "quote", 247 | "unicode-ident", 248 | ] 249 | 250 | [[package]] 251 | name = "termtree" 252 | version = "0.5.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" 255 | 256 | [[package]] 257 | name = "unicode-ident" 258 | version = "1.0.19" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 261 | 262 | [[package]] 263 | name = "wait-timeout" 264 | version = "0.2.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 267 | dependencies = [ 268 | "libc", 269 | ] 270 | 271 | [[package]] 272 | name = "windows-sys" 273 | version = "0.48.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 276 | dependencies = [ 277 | "windows-targets", 278 | ] 279 | 280 | [[package]] 281 | name = "windows-targets" 282 | version = "0.48.5" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 285 | dependencies = [ 286 | "windows_aarch64_gnullvm", 287 | "windows_aarch64_msvc", 288 | "windows_i686_gnu", 289 | "windows_i686_msvc", 290 | "windows_x86_64_gnu", 291 | "windows_x86_64_gnullvm", 292 | "windows_x86_64_msvc", 293 | ] 294 | 295 | [[package]] 296 | name = "windows_aarch64_gnullvm" 297 | version = "0.48.5" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 300 | 301 | [[package]] 302 | name = "windows_aarch64_msvc" 303 | version = "0.48.5" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 306 | 307 | [[package]] 308 | name = "windows_i686_gnu" 309 | version = "0.48.5" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 312 | 313 | [[package]] 314 | name = "windows_i686_msvc" 315 | version = "0.48.5" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 318 | 319 | [[package]] 320 | name = "windows_x86_64_gnu" 321 | version = "0.48.5" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 324 | 325 | [[package]] 326 | name = "windows_x86_64_gnullvm" 327 | version = "0.48.5" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 330 | 331 | [[package]] 332 | name = "windows_x86_64_msvc" 333 | version = "0.48.5" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 336 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # Root options 13 | 14 | # The graph table configures how the dependency graph is constructed and thus 15 | # which crates the checks are performed against 16 | [graph] 17 | # If 1 or more target triples (and optionally, target_features) are specified, 18 | # only the specified targets will be checked when running `cargo deny check`. 19 | # This means, if a particular package is only ever used as a target specific 20 | # dependency, such as, for example, the `nix` crate only being used via the 21 | # `target_family = "unix"` configuration, that only having windows targets in 22 | # this list would mean the nix crate, as well as any of its exclusive 23 | # dependencies not shared by any other crates, would be ignored, as the target 24 | # list here is effectively saying which targets you are building for. 25 | targets = [ 26 | # The triple can be any string, but only the target triples built in to 27 | # rustc (as of 1.40) can be checked against actual config expressions 28 | #"x86_64-unknown-linux-musl", 29 | # You can also specify which target_features you promise are enabled for a 30 | # particular target. target_features are currently not validated against 31 | # the actual valid features supported by the target architecture. 32 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 33 | ] 34 | # When creating the dependency graph used as the source of truth when checks are 35 | # executed, this field can be used to prune crates from the graph, removing them 36 | # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate 37 | # is pruned from the graph, all of its dependencies will also be pruned unless 38 | # they are connected to another crate in the graph that hasn't been pruned, 39 | # so it should be used with care. The identifiers are [Package ID Specifications] 40 | # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) 41 | #exclude = [] 42 | # If true, metadata will be collected with `--all-features`. Note that this can't 43 | # be toggled off if true, if you want to conditionally enable `--all-features` it 44 | # is recommended to pass `--all-features` on the cmd line instead 45 | all-features = false 46 | # If true, metadata will be collected with `--no-default-features`. The same 47 | # caveat with `all-features` applies 48 | no-default-features = false 49 | # If set, these feature will be enabled when collecting metadata. If `--features` 50 | # is specified on the cmd line they will take precedence over this option. 51 | #features = [] 52 | 53 | # The output table provides options for how/if diagnostics are outputted 54 | [output] 55 | # When outputting inclusion graphs in diagnostics that include features, this 56 | # option can be used to specify the depth at which feature edges will be added. 57 | # This option is included since the graphs can be quite large and the addition 58 | # of features from the crate(s) to all of the graph roots can be far too verbose. 59 | # This option can be overridden via `--feature-depth` on the cmd line 60 | feature-depth = 1 61 | 62 | # This section is considered when running `cargo deny check advisories` 63 | # More documentation for the advisories section can be found here: 64 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 65 | [advisories] 66 | # The path where the advisory databases are cloned/fetched into 67 | #db-path = "$CARGO_HOME/advisory-dbs" 68 | # The url(s) of the advisory databases to use 69 | #db-urls = ["https://github.com/rustsec/advisory-db"] 70 | # A list of advisory IDs to ignore. Note that ignored advisories will still 71 | # output a note when they are encountered. 72 | ignore = [ 73 | #"RUSTSEC-0000-0000", 74 | #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, 75 | #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish 76 | #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, 77 | ] 78 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 79 | # If this is false, then it uses a built-in git library. 80 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 81 | # See Git Authentication for more information about setting up git authentication. 82 | #git-fetch-with-cli = true 83 | 84 | # This section is considered when running `cargo deny check licenses` 85 | # More documentation for the licenses section can be found here: 86 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 87 | [licenses] 88 | # List of explicitly allowed licenses 89 | # See https://spdx.org/licenses/ for list of possible licenses 90 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 91 | allow = [ 92 | "MIT", 93 | "Apache-2.0", 94 | "MPL-2.0", 95 | #"Apache-2.0 WITH LLVM-exception", 96 | ] 97 | # The confidence threshold for detecting a license from license text. 98 | # The higher the value, the more closely the license text must be to the 99 | # canonical license text of a valid SPDX license file. 100 | # [possible values: any between 0.0 and 1.0]. 101 | confidence-threshold = 0.8 102 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 103 | # aren't accepted for every possible crate as with the normal allow list 104 | exceptions = [ 105 | # Each entry is the crate and version constraint, and its specific allow 106 | # list 107 | #{ allow = ["Zlib"], crate = "adler32" }, 108 | ] 109 | 110 | # Some crates don't have (easily) machine readable licensing information, 111 | # adding a clarification entry for it allows you to manually specify the 112 | # licensing information 113 | #[[licenses.clarify]] 114 | # The package spec the clarification applies to 115 | #crate = "ring" 116 | # The SPDX expression for the license requirements of the crate 117 | #expression = "MIT AND ISC AND OpenSSL" 118 | # One or more files in the crate's source used as the "source of truth" for 119 | # the license expression. If the contents match, the clarification will be used 120 | # when running the license check, otherwise the clarification will be ignored 121 | # and the crate will be checked normally, which may produce warnings or errors 122 | # depending on the rest of your configuration 123 | #license-files = [ 124 | # Each entry is a crate relative path, and the (opaque) hash of its contents 125 | #{ path = "LICENSE", hash = 0xbd0eed23 } 126 | #] 127 | 128 | [licenses.private] 129 | # If true, ignores workspace crates that aren't published, or are only 130 | # published to private registries. 131 | # To see how to mark a crate as unpublished (to the official registry), 132 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 133 | ignore = false 134 | # One or more private registries that you might publish crates to, if a crate 135 | # is only published to private registries, and ignore is true, the crate will 136 | # not have its license(s) checked 137 | registries = [ 138 | #"https://sekretz.com/registry 139 | ] 140 | 141 | # This section is considered when running `cargo deny check bans`. 142 | # More documentation about the 'bans' section can be found here: 143 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 144 | [bans] 145 | # Lint level for when multiple versions of the same crate are detected 146 | multiple-versions = "warn" 147 | # Lint level for when a crate version requirement is `*` 148 | wildcards = "allow" 149 | # The graph highlighting used when creating dotgraphs for crates 150 | # with multiple versions 151 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 152 | # * simplest-path - The path to the version with the fewest edges is highlighted 153 | # * all - Both lowest-version and simplest-path are used 154 | highlight = "all" 155 | # The default lint level for `default` features for crates that are members of 156 | # the workspace that is being checked. This can be overridden by allowing/denying 157 | # `default` on a crate-by-crate basis if desired. 158 | workspace-default-features = "allow" 159 | # The default lint level for `default` features for external crates that are not 160 | # members of the workspace. This can be overridden by allowing/denying `default` 161 | # on a crate-by-crate basis if desired. 162 | external-default-features = "allow" 163 | # List of crates that are allowed. Use with care! 164 | allow = [ 165 | #"ansi_term@0.11.0", 166 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, 167 | ] 168 | # List of crates to deny 169 | deny = [ 170 | #"ansi_term@0.11.0", 171 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, 172 | # Wrapper crates can optionally be specified to allow the crate when it 173 | # is a direct dependency of the otherwise banned crate 174 | #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, 175 | ] 176 | 177 | # List of features to allow/deny 178 | # Each entry the name of a crate and a version range. If version is 179 | # not specified, all versions will be matched. 180 | #[[bans.features]] 181 | #crate = "reqwest" 182 | # Features to not allow 183 | #deny = ["json"] 184 | # Features to allow 185 | #allow = [ 186 | # "rustls", 187 | # "__rustls", 188 | # "__tls", 189 | # "hyper-rustls", 190 | # "rustls", 191 | # "rustls-pemfile", 192 | # "rustls-tls-webpki-roots", 193 | # "tokio-rustls", 194 | # "webpki-roots", 195 | #] 196 | # If true, the allowed features must exactly match the enabled feature set. If 197 | # this is set there is no point setting `deny` 198 | #exact = true 199 | 200 | # Certain crates/versions that will be skipped when doing duplicate detection. 201 | skip = [ 202 | #"ansi_term@0.11.0", 203 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, 204 | ] 205 | # Similarly to `skip` allows you to skip certain crates during duplicate 206 | # detection. Unlike skip, it also includes the entire tree of transitive 207 | # dependencies starting at the specified crate, up to a certain depth, which is 208 | # by default infinite. 209 | skip-tree = [ 210 | #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies 211 | #{ crate = "ansi_term@0.11.0", depth = 20 }, 212 | ] 213 | 214 | # This section is considered when running `cargo deny check sources`. 215 | # More documentation about the 'sources' section can be found here: 216 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 217 | [sources] 218 | # Lint level for what to happen when a crate from a crate registry that is not 219 | # in the allow list is encountered 220 | unknown-registry = "warn" 221 | # Lint level for what to happen when a crate from a git repository that is not 222 | # in the allow list is encountered 223 | unknown-git = "warn" 224 | # List of URLs for allowed crate registries. Defaults to the crates.io index 225 | # if not specified. If it is specified but empty, no registries are allowed. 226 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 227 | # List of URLs for allowed Git repositories 228 | allow-git = [] 229 | 230 | [sources.allow-org] 231 | # 1 or more github.com organizations to allow git sources for 232 | #github = [""] 233 | # 1 or more gitlab.com organizations to allow git sources for 234 | #gitlab = [""] 235 | # 1 or more bitbucket.org organizations to allow git sources for 236 | #bitbucket = [""] 237 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | use anyhow::{anyhow, Context}; 3 | use colored::Colorize; 4 | use std::{fmt::Display, process::Command, str::FromStr}; 5 | 6 | #[must_use] 7 | /// Runs `cargo test` with any supplied extra arguments, and returns the 8 | /// resulting standard output. 9 | /// 10 | /// # Panics 11 | /// 12 | /// If executing the `cargo test` command fails. 13 | pub fn get_cargo_test_output(extra_args: Vec) -> String { 14 | let mut cargo = Command::new("cargo"); 15 | cargo.arg("test"); 16 | cargo.args(extra_args); 17 | let raw_output = cargo 18 | .output() 19 | .context(format!("{cargo:?}")) 20 | .expect("executing command should succeed") 21 | .stdout; 22 | String::from_utf8_lossy(&raw_output).to_string() 23 | } 24 | 25 | #[must_use] 26 | /// Parses the standard output of `cargo test` into a vec of `TestResult`. 27 | /// 28 | /// Results are returned sorted by module, name, and status for better readability. 29 | pub fn parse_test_results(test_output: &str) -> Vec { 30 | let mut results: Vec = test_output.lines().filter_map(parse_line).collect(); 31 | results.sort(); 32 | results 33 | } 34 | 35 | /// Parses a line from the standard output of `cargo test`. 36 | /// 37 | /// If the line represents the result of a test, returns `Some(TestResult)`, 38 | /// otherwise returns `None`. 39 | pub fn parse_line(line: impl AsRef) -> Option { 40 | let line = line.as_ref().strip_prefix("test ")?; 41 | if line.starts_with("result") || line.contains("(line ") { 42 | return None; 43 | } 44 | 45 | let (test, status) = line.split_once(" ... ")?; 46 | let (module, name) = match test.rsplit_once("::") { 47 | Some((module, name)) => (prettify_module(module), name), 48 | None => (None, test), 49 | }; 50 | Some(TestResult { 51 | module, 52 | name: prettify(name), 53 | status: status.parse().ok()?, 54 | }) 55 | } 56 | 57 | #[must_use] 58 | /// Formats the name of a test function as a sentence. 59 | /// 60 | /// Underscores are replaced with spaces. To retain the underscores in a function name, put `_fn_` after it. For example: 61 | /// 62 | /// ```text 63 | /// parse_line_fn_parses_a_line 64 | /// ``` 65 | /// 66 | /// becomes: 67 | /// 68 | /// ```text 69 | /// parse_line parses a line 70 | /// ``` 71 | pub fn prettify(input: impl AsRef) -> String { 72 | if let Some((fn_name, sentence)) = input.as_ref().split_once("_fn_") { 73 | format!("{} {}", fn_name, humanize(sentence)) 74 | } else { 75 | humanize(input) 76 | } 77 | } 78 | 79 | fn humanize(input: impl AsRef) -> String { 80 | input 81 | .as_ref() 82 | .replace('_', " ") 83 | .split_whitespace() 84 | .collect::>() 85 | .join(" ") 86 | } 87 | 88 | fn prettify_module(module: &str) -> Option { 89 | let mut parts = module.split("::").collect::>(); 90 | parts.pop_if(|&mut s| s == "tests" || s == "test"); 91 | if parts.is_empty() { 92 | return None; 93 | } 94 | Some(parts.join("::")) 95 | } 96 | 97 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 98 | /// The (prettified) name and pass/fail status of a given test. 99 | pub struct TestResult { 100 | pub module: Option, 101 | pub name: String, 102 | pub status: Status, 103 | } 104 | 105 | impl Display for TestResult { 106 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 107 | match &self.module { 108 | Some(module) => write!( 109 | f, 110 | "{} {} – {}", 111 | self.status, 112 | module.bright_blue(), 113 | self.name 114 | ), 115 | None => write!(f, "{} {}", self.status, self.name), 116 | } 117 | } 118 | } 119 | 120 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 121 | /// The status of a given test, as reported by `cargo test`. 122 | pub enum Status { 123 | Pass, 124 | Fail, 125 | Ignored(Option), 126 | } 127 | 128 | impl FromStr for Status { 129 | type Err = anyhow::Error; 130 | 131 | fn from_str(status: &str) -> Result { 132 | match status { 133 | "ok" => Ok(Status::Pass), 134 | "FAILED" => Ok(Status::Fail), 135 | "ignored" => Ok(Status::Ignored(None)), 136 | _ => { 137 | if let Some((_, reason)) = status.split_once(", ") { 138 | Ok(Status::Ignored(Some(reason.to_string()))) 139 | } else { 140 | Err(anyhow!("unhandled test status {status:?}")) 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | impl Display for Status { 148 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 149 | let status = match self { 150 | Status::Pass => "✔".bright_green(), 151 | Status::Fail => "x".bright_red(), 152 | Status::Ignored(None) => "?".bright_yellow(), 153 | Status::Ignored(Some(reason)) => format!("? [{reason}]").bright_yellow(), 154 | }; 155 | write!(f, "{status}") 156 | } 157 | } 158 | 159 | #[cfg(test)] 160 | mod tests { 161 | use super::*; 162 | 163 | #[test] 164 | fn prettify_returns_expected_results() { 165 | struct Case { 166 | input: &'static str, 167 | want: String, 168 | } 169 | let cases = Vec::from([ 170 | Case { 171 | input: "anagrams_must_use_all_letters_exactly_once", 172 | want: "anagrams must use all letters exactly once".into(), 173 | }, 174 | Case { 175 | input: "no_matches", 176 | want: "no matches".into(), 177 | }, 178 | Case { 179 | input: "single", 180 | want: "single".into(), 181 | }, 182 | Case { 183 | input: "parse_line_fn_does_stuff", 184 | want: "parse_line does stuff".into(), 185 | }, 186 | Case { 187 | input: "prettify__handles_multiple_underscores", 188 | want: "prettify handles multiple underscores".into(), 189 | }, 190 | Case { 191 | input: "prettify_fn__handles_multiple_underscores", 192 | want: "prettify handles multiple underscores".into(), 193 | }, 194 | ]); 195 | for case in cases { 196 | assert_eq!(case.want, prettify(case.input)); 197 | } 198 | } 199 | 200 | #[test] 201 | fn parse_line_fn_returns_expected_result() { 202 | struct Case { 203 | line: &'static str, 204 | want: Option, 205 | } 206 | let cases = Vec::from([ 207 | Case { 208 | line: " Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s", 209 | want: None, 210 | }, 211 | Case { 212 | line: "test foo ... ok", 213 | want: Some(TestResult { 214 | module: None, 215 | name: "foo".into(), 216 | status: Status::Pass, 217 | }), 218 | }, 219 | Case { 220 | line: "test foo::tests::does_foo_stuff ... ok", 221 | want: Some(TestResult { 222 | module: Some("foo".into()), 223 | name: "does foo stuff".into(), 224 | status: Status::Pass, 225 | }), 226 | }, 227 | Case { 228 | line: "test tests::urls_correctly_extracts_valid_urls ... FAILED", 229 | want: Some(TestResult { 230 | module: None, 231 | name: "urls correctly extracts valid urls".into(), 232 | status: Status::Fail, 233 | }), 234 | }, 235 | Case { 236 | line: "test files::test::files_can_be_sorted_in_descending_order ... ignored", 237 | want: Some(TestResult { 238 | module: Some("files".into()), 239 | name: "files can be sorted in descending order".into(), 240 | status: Status::Ignored(None), 241 | }), 242 | }, 243 | Case { 244 | line: "test files::test::foo::tests::files_can_be_sorted_in_descending_order ... ignored", 245 | want: Some(TestResult { 246 | module: Some("files::test::foo".into()), 247 | name: "files can be sorted in descending order".into(), 248 | status: Status::Ignored(None), 249 | }), 250 | }, 251 | Case { 252 | line: "test files::test_foo::files_can_be_sorted_in_descending_order ... ignored", 253 | want: Some(TestResult { 254 | module: Some("files::test_foo".into()), 255 | name: "files can be sorted in descending order".into(), 256 | status: Status::Ignored(None), 257 | }), 258 | }, 259 | Case { 260 | line: "test tests::pi_to_1m_digits ... ignored, expensive test", 261 | want: Some(TestResult { 262 | module: None, 263 | name: "pi to 1m digits".into(), 264 | status: Status::Ignored(Some("expensive test".into())), 265 | }), 266 | }, 267 | Case { 268 | line: "test src/lib.rs - find_top_n_largest_files (line 17) ... ok", 269 | want: None, 270 | }, 271 | Case { 272 | line: "test output_format::_concise_expects ... ok", 273 | want: Some(TestResult { 274 | module: Some("output_format".into()), 275 | name: "concise expects".into(), 276 | status: Status::Pass, 277 | }), 278 | }, 279 | ]); 280 | for case in cases { 281 | assert_eq!(case.want, parse_line(case.line)); 282 | } 283 | } 284 | 285 | // test results sort by module, name, and status. 286 | #[test] 287 | fn test_results_sort_by_module_name_and_status() { 288 | let mut results = vec![ 289 | TestResult { 290 | module: Some("zeta".into()), 291 | name: "zebra".into(), 292 | status: Status::Pass, 293 | }, 294 | TestResult { 295 | module: None, 296 | name: "alpha".into(), 297 | status: Status::Fail, 298 | }, 299 | TestResult { 300 | module: None, 301 | name: "alpha".into(), 302 | status: Status::Pass, 303 | }, 304 | TestResult { 305 | module: Some("alpha".into()), 306 | name: "beta".into(), 307 | status: Status::Ignored(None), 308 | }, 309 | TestResult { 310 | module: Some("alpha".into()), 311 | name: "alpha".into(), 312 | status: Status::Pass, 313 | }, 314 | ]; 315 | results.sort(); 316 | let expected = vec![ 317 | TestResult { 318 | module: None, 319 | name: "alpha".into(), 320 | status: Status::Pass, 321 | }, 322 | TestResult { 323 | module: None, 324 | name: "alpha".into(), 325 | status: Status::Fail, 326 | }, 327 | TestResult { 328 | module: Some("alpha".into()), 329 | name: "alpha".into(), 330 | status: Status::Pass, 331 | }, 332 | TestResult { 333 | module: Some("alpha".into()), 334 | name: "beta".into(), 335 | status: Status::Ignored(None), 336 | }, 337 | TestResult { 338 | module: Some("zeta".into()), 339 | name: "zebra".into(), 340 | status: Status::Pass, 341 | }, 342 | ]; 343 | assert_eq!(results, expected, "wrong order"); 344 | } 345 | } 346 | --------------------------------------------------------------------------------