├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── audit.yml │ └── ci.yml ├── tests ├── README.md ├── generated.xml ├── reference.xml ├── integration_test.rs ├── JUnit.xsd └── JUnit-alternative.xsd ├── Cargo.toml ├── LICENSE ├── README.md ├── CHANGELOG.md ├── Cargo.lock └── src ├── reports.rs ├── collections.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | my-junit.xml 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | There seams to be no clear standard 2 | 3 | The best sources are: 4 | - https://stackoverflow.com/a/9410271/1045684 5 | - https://github.com/windyroad/JUnit-Schema 6 | 7 | 8 | Verify against Schema 9 | 10 | ``` 11 | xmllint --schema file://$(pwd)/test/JUnit2.xsd junit.xml --noout 12 | ``` -------------------------------------------------------------------------------- /tests/generated.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | tags: ["v*"] 7 | paths: ["**/Cargo.toml"] 8 | pull_request: 9 | branches: ["main"] 10 | paths: ["**/Cargo.toml"] 11 | schedule: 12 | - cron: "7 7 * * *" 13 | 14 | jobs: 15 | cargo-audit: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: stable 23 | - uses: actions-rs/audit-check@v1 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "junit-report" 3 | version = "0.8.4" 4 | authors = ["Pascal Bach "] 5 | description = "Create JUnit compatible XML reports." 6 | license = "MIT" 7 | repository = "https://github.com/bachp/junit-report-rs" 8 | keywords = ["junit", "xunit", "xml", "report"] 9 | readme = "README.md" 10 | edition = "2021" 11 | 12 | [dependencies] 13 | derive-getters = "0.5.0" 14 | quick-xml = "0.36.2" 15 | time = { version = "0.3.44", features = ["formatting", "macros"], default-features = false } 16 | 17 | [dev-dependencies] 18 | doc-comment = "0.3.4" 19 | once_cell = "1.21" 20 | pretty_assertions = "1.4.1" 21 | regex = "1.12" 22 | -------------------------------------------------------------------------------- /tests/reference.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Pascal Bach 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: # trigger on pull requests 5 | push: 6 | branches: # array of glob patterns matching against refs/heads. Optional; defaults to all 7 | - master # triggers on pushes that contain changes in master 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | build: [stable, nightly] 16 | include: 17 | - build: stable 18 | os: ubuntu-latest 19 | rust: stable 20 | - build: nightly 21 | os: ubuntu-latest 22 | rust: nightly 23 | 24 | steps: 25 | - name: Update package index 26 | run: sudo apt-get update 27 | 28 | - name: Install xmllint 29 | run: sudo apt-get install libxml2-utils 30 | 31 | - name: Checkout code 32 | uses: actions/checkout@v4 33 | 34 | - name: Setup rust toolchain 35 | uses: dtolnay/rust-toolchain@stable 36 | with: 37 | toolchain: ${{ matrix.rust }} 38 | 39 | - name: Run cargo test 40 | run: cargo test 41 | 42 | - name: Build release binary 43 | run: cargo build --release 44 | 45 | rustfmt: 46 | name: Rustfmt 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v4 51 | 52 | - name: Setup rust toolchain 53 | uses: dtolnay/rust-toolchain@stable 54 | with: 55 | toolchain: stable 56 | components: rustfmt 57 | 58 | - name: Check formatting 59 | run: cargo fmt -- --check 60 | 61 | clippy: 62 | name: Clippy 63 | runs-on: ubuntu-latest 64 | steps: 65 | - name: Checkout code 66 | uses: actions/checkout@v4 67 | 68 | - name: Setup rust toolchain 69 | uses: dtolnay/rust-toolchain@stable 70 | with: 71 | toolchain: stable 72 | components: clippy 73 | 74 | - name: Run cargo clippy checks 75 | run: cargo clippy -- -D warnings 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # JUnit Report in Rust 9 | 10 | Generate JUnit compatible XML reports in Rust. 11 | 12 | ## Example 13 | 14 | ```rust 15 | 16 | use junit_report::{datetime, Duration, ReportBuilder, TestCase, TestCaseBuilder, TestSuite, TestSuiteBuilder}; 17 | use std::fs::File; 18 | 19 | // Create a successful test case 20 | let test_success = TestCaseBuilder::success("good test", Duration::seconds(15)) 21 | .set_classname("MyClass") 22 | .set_filepath("MyFilePath") 23 | .build(); 24 | 25 | // Create a test case that encountered an unexpected error condition 26 | let test_error = TestCase::error( 27 | "error test", 28 | Duration::seconds(5), 29 | "git error", 30 | "unable to fetch", 31 | ); 32 | 33 | // Create a test case that failed because of a test failure 34 | let test_failure = TestCase::failure( 35 | "failure test", 36 | Duration::seconds(10), 37 | "assert_eq", 38 | "not equal", 39 | ); 40 | 41 | // Next we create a test suite named "ts1" with not test cases associated 42 | let ts1 = TestSuite::new("ts1"); 43 | 44 | // Then we create a second test suite called "ts2" and set an explicit time stamp 45 | // then we add all the test cases from above 46 | let timestamp = datetime!(1970-01-01 00:01:01 UTC); 47 | let ts2 = TestSuiteBuilder::new("ts2") 48 | .set_timestamp(timestamp) 49 | .add_testcase(test_success) 50 | .add_testcase(test_error) 51 | .add_testcase(test_failure) 52 | .build(); 53 | 54 | // Last we create a report and add all test suites to it 55 | let r = ReportBuilder::new() 56 | .add_testsuite(ts1) 57 | .add_testsuite(ts2) 58 | .build(); 59 | 60 | // The report can than be written in XML format to any writer 61 | let mut file = File::create("my-junit.xml").unwrap(); 62 | r.write_xml(&mut file).unwrap(); 63 | ``` 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | - Remove strip-ansi-escapes dependencies 10 | - Only enable required features for time 11 | 12 | ## [0.8.4] - 2023-12-07 13 | 14 | - Update dependencies 15 | - `system-err` and `system-out` are now independent of the test case result 16 | - Add `set_trace` function to add detailed trace to error or failure cases. 17 | 18 | ## [0.8.3] - 2023-10-23 19 | 20 | - Update dependencies 21 | 22 | ## [0.8.2] - 2022-12-16 23 | 24 | - Re-export `quick_xml::Error` as `junit_report::Error` 25 | 26 | ## [0.8.1] - 2022-09-10 27 | 28 | - Remove unsecure dev dependency from `failure` 29 | 30 | ## [0.8.0] - 2022-09-09 31 | 32 | - Bump Rust edition to 2021 33 | 34 | ### BREAKING CHANGES 35 | - Switch from `xml-rs` to `quick-xml` due to maintenance status 36 | - Change `Err` type of `Report::write_xml()` 37 | - Remove indentations and newlines from the generated `Report` 38 | 39 | ## [0.7.1] - 2022-04-27 40 | 41 | - Added support for an optional `file` attribute in test cases 42 | 43 | ## [0.7.0] - 2021-11-06 44 | 45 | ### BREAKING CHANGES 46 | - Switch from `chrono` to `time` 47 | - Switch timestamp formatting (still compliant with both `RFC3339` and `ISO8601`) 48 | 49 | ## [0.6.0] - 2021-07-20 50 | 51 | - Saparate builder types 52 | 53 | ### BREAKING CHANGES 54 | - Seprate types for the data types and the builders. This restores the old data based API from 0.3.0 and moves 55 | the builder API as introduced in 0.4.0 to their own *Builder types. 56 | - If you are migrating from 0.3.0 there should be no big changes required. 57 | - If you migrate from 0.4.0 or 0.5.0 you need the following renames: 58 | Report -> ReportBuilder 59 | TestSuite -> TestSuiteBuilder 60 | TestCase -> TestCaseBuilder 61 | 62 | ## [0.5.0] - 2021-06-15 63 | 64 | ### Added 65 | - Support for skipped or ignored testcases 66 | ### BREAKING CHANGES 67 | - Adding support for skipped and ignored testcases extends the `TestResult` struct by one more variant. 68 | 69 | ## [0.4.2] - 2021-05-28 70 | 71 | ### Fixed 72 | 73 | - Make Error Type public 74 | 75 | ## [0.4.1] - 2021-03-02 76 | 77 | ### Fixed 78 | 79 | - Output format compatible with GitLab and Jenkins. 80 | 81 | ## [0.4.0] - 2020-06-04 82 | 83 | ### Added 84 | - `system_out` and `system_err` fields added 85 | 86 | ### BREAKING CHANGES 87 | - Revamp the API to use the builder pattern. This makes the API more future proof and hopefully avoids breaking changes in the future when more optional fields are added. 88 | - Change error type to no longer expose the internals of the XML processing. 89 | 90 | ## [0.3.0] - 2020-05-12 91 | 92 | ### Added 93 | - `classname` attribute is now supported 94 | 95 | ## [0.2.1] - 2020-04-14 96 | ### Changed 97 | - Make sure all examples in the readme are run 98 | - Update dependencies 99 | 100 | ## [0.2.0] - 2019-08-19 101 | ### Fixed 102 | - Testsuite id is now properly set even when using `add_testsuites` 103 | - Unittests now work in Windows too 104 | 105 | ### Changed 106 | - Crate now uses the Rust 2018 edition 107 | - The batch methods (`add_testsuites`, `add_testcases`) now accept any iterators, not just `Vec` 108 | - Durations are now decimals as per spec 109 | 110 | ## [0.1.2] - 2018-11-22 111 | ### Changed 112 | - Change order to `system-out` and `system-err` to conform to new schema 113 | - Don't add an empty optional properties tag 114 | 115 | ## [0.1.1] - 2018-09-22 116 | ### Added 117 | - Add functions to add testcases and testsuites from a Vec 118 | 119 | ## [0.1.0] - 2018-09-21 120 | ### Added 121 | - Initial Release 122 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Pascal Bach 3 | * Copyright (c) 2021 Siemens Mobility GmbH 4 | * 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | 8 | use std::fs::{self, File}; 9 | use std::process::Command; 10 | 11 | use junit_report::{ 12 | datetime, Duration, ReportBuilder, TestCase, TestCaseBuilder, TestSuiteBuilder, 13 | }; 14 | use once_cell::sync::Lazy; 15 | use regex::{Regex, RegexBuilder}; 16 | 17 | static REGEX: Lazy = Lazy::new(|| { 18 | RegexBuilder::new("\\n|^\\s+") 19 | .multi_line(true) 20 | .build() 21 | .unwrap() 22 | }); 23 | 24 | #[test] 25 | fn reference_report() { 26 | let timestamp = datetime!(2018-04-21 12:02 UTC); 27 | 28 | let test_success = TestCaseBuilder::success("test1", Duration::seconds(15)) 29 | .set_classname("MyClass") 30 | .set_filepath("./foo.rs") 31 | .build(); 32 | let test_error = TestCase::error( 33 | "test3", 34 | Duration::seconds(5), 35 | "git error", 36 | "Could not clone", 37 | ); 38 | let test_failure = TestCase::failure( 39 | "test2", 40 | Duration::seconds(10), 41 | "assert_eq", 42 | "What was not true", 43 | ); 44 | let test_ignored = TestCase::skipped("test4"); 45 | 46 | let ts1 = TestSuiteBuilder::new("ts1") 47 | .set_timestamp(timestamp) 48 | .add_testcase(test_success) 49 | .add_testcase(test_failure) 50 | .add_testcase(test_error) 51 | .add_testcase(test_ignored) 52 | .build(); 53 | 54 | let r = ReportBuilder::new().add_testsuite(ts1).build(); 55 | 56 | let mut out: Vec = Vec::new(); 57 | 58 | r.write_xml(&mut out).unwrap(); 59 | 60 | let report = String::from_utf8(out).unwrap(); 61 | 62 | let reference = fs::read_to_string("tests/reference.xml").unwrap(); 63 | let reference = REGEX.replace_all(reference.as_str(), ""); 64 | 65 | assert_eq!(report, reference); 66 | } 67 | 68 | #[test] 69 | fn validate_reference_xml_schema() { 70 | let res = Command::new("xmllint") 71 | .arg("--schema") 72 | .arg("tests/JUnit.xsd") 73 | .arg("tests/reference.xml") 74 | .arg("--noout") 75 | .output() 76 | .expect("reference.xml does not validate against XML Schema"); 77 | print!("{}", String::from_utf8_lossy(&res.stdout)); 78 | eprint!("{}", String::from_utf8_lossy(&res.stderr)); 79 | assert!(res.status.success()); 80 | } 81 | 82 | #[test] 83 | fn validate_generated_xml_schema() { 84 | let timestamp = datetime!(2018-04-21 12:02 UTC); 85 | 86 | let test_success = TestCaseBuilder::success("MyTest3", Duration::seconds(15)) 87 | .set_classname("MyClass") 88 | .build(); 89 | let test_error = TestCase::error( 90 | "Blabla", 91 | Duration::seconds(5), 92 | "git error", 93 | "Could not clone", 94 | ); 95 | let test_failure = TestCase::failure("Burk", Duration::seconds(10), "asdfasf", "asdfajfhk"); 96 | let test_skipped = TestCase::skipped("Alpha"); 97 | 98 | let ts1 = TestSuiteBuilder::new("Some Testsuite") 99 | .set_timestamp(timestamp) 100 | .add_testcase(test_success) 101 | .add_testcase(test_failure) 102 | .add_testcase(test_error) 103 | .add_testcase(test_skipped) 104 | .build(); 105 | 106 | let r = ReportBuilder::new().add_testsuite(ts1).build(); 107 | 108 | let mut f = File::create("target/generated.xml").unwrap(); 109 | 110 | r.write_xml(&mut f).unwrap(); 111 | 112 | let res = Command::new("xmllint") 113 | .arg("--schema") 114 | .arg("tests/JUnit.xsd") 115 | .arg("target/generated.xml") 116 | .arg("--noout") 117 | .output() 118 | .expect("generated.xml does not validate against XML Schema"); 119 | print!("{}", String::from_utf8_lossy(&res.stdout)); 120 | eprint!("{}", String::from_utf8_lossy(&res.stderr)); 121 | assert!(res.status.success()); 122 | } 123 | -------------------------------------------------------------------------------- /tests/JUnit.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /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.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "deranged" 16 | version = "0.5.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "75d7cc94194b4dd0fa12845ef8c911101b7f37633cda14997a6e82099aa0b693" 19 | dependencies = [ 20 | "powerfmt", 21 | ] 22 | 23 | [[package]] 24 | name = "derive-getters" 25 | version = "0.5.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" 28 | dependencies = [ 29 | "proc-macro2", 30 | "quote", 31 | "syn", 32 | ] 33 | 34 | [[package]] 35 | name = "diff" 36 | version = "0.1.13" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 39 | 40 | [[package]] 41 | name = "doc-comment" 42 | version = "0.3.4" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" 45 | 46 | [[package]] 47 | name = "itoa" 48 | version = "1.0.15" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 51 | 52 | [[package]] 53 | name = "junit-report" 54 | version = "0.8.4" 55 | dependencies = [ 56 | "derive-getters", 57 | "doc-comment", 58 | "once_cell", 59 | "pretty_assertions", 60 | "quick-xml", 61 | "regex", 62 | "time", 63 | ] 64 | 65 | [[package]] 66 | name = "memchr" 67 | version = "2.6.4" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 70 | 71 | [[package]] 72 | name = "num-conv" 73 | version = "0.1.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 76 | 77 | [[package]] 78 | name = "once_cell" 79 | version = "1.21.3" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 82 | 83 | [[package]] 84 | name = "powerfmt" 85 | version = "0.2.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 88 | 89 | [[package]] 90 | name = "pretty_assertions" 91 | version = "1.4.1" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 94 | dependencies = [ 95 | "diff", 96 | "yansi", 97 | ] 98 | 99 | [[package]] 100 | name = "proc-macro2" 101 | version = "1.0.70" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 104 | dependencies = [ 105 | "unicode-ident", 106 | ] 107 | 108 | [[package]] 109 | name = "quick-xml" 110 | version = "0.36.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" 113 | dependencies = [ 114 | "memchr", 115 | ] 116 | 117 | [[package]] 118 | name = "quote" 119 | version = "1.0.33" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 122 | dependencies = [ 123 | "proc-macro2", 124 | ] 125 | 126 | [[package]] 127 | name = "regex" 128 | version = "1.12.2" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 131 | dependencies = [ 132 | "aho-corasick", 133 | "memchr", 134 | "regex-automata", 135 | "regex-syntax", 136 | ] 137 | 138 | [[package]] 139 | name = "regex-automata" 140 | version = "0.4.13" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 143 | dependencies = [ 144 | "aho-corasick", 145 | "memchr", 146 | "regex-syntax", 147 | ] 148 | 149 | [[package]] 150 | name = "regex-syntax" 151 | version = "0.8.5" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 154 | 155 | [[package]] 156 | name = "serde" 157 | version = "1.0.193" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 160 | dependencies = [ 161 | "serde_derive", 162 | ] 163 | 164 | [[package]] 165 | name = "serde_derive" 166 | version = "1.0.193" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 169 | dependencies = [ 170 | "proc-macro2", 171 | "quote", 172 | "syn", 173 | ] 174 | 175 | [[package]] 176 | name = "syn" 177 | version = "2.0.39" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 180 | dependencies = [ 181 | "proc-macro2", 182 | "quote", 183 | "unicode-ident", 184 | ] 185 | 186 | [[package]] 187 | name = "time" 188 | version = "0.3.44" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 191 | dependencies = [ 192 | "deranged", 193 | "itoa", 194 | "num-conv", 195 | "powerfmt", 196 | "serde", 197 | "time-core", 198 | "time-macros", 199 | ] 200 | 201 | [[package]] 202 | name = "time-core" 203 | version = "0.1.6" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 206 | 207 | [[package]] 208 | name = "time-macros" 209 | version = "0.2.24" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 212 | dependencies = [ 213 | "num-conv", 214 | "time-core", 215 | ] 216 | 217 | [[package]] 218 | name = "unicode-ident" 219 | version = "1.0.12" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 222 | 223 | [[package]] 224 | name = "yansi" 225 | version = "1.0.1" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 228 | -------------------------------------------------------------------------------- /tests/JUnit-alternative.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JUnit test result schema for the Apache Ant JUnit and JUnitReport tasks 6 | Copyright © 2011, Windy Road Technology Pty. Limited 7 | The Apache Ant JUnit XML Schema is distributed under the terms of the Apache License Version 2.0 http://www.apache.org/licenses/ 8 | Permission to waive conditions of this license may be requested from Windy Road Support (http://windyroad.org/support). 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Contains an aggregation of testsuite results 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Derived from testsuite/@name in the non-aggregated documents 29 | 30 | 31 | 32 | 33 | Starts at '0' for the first testsuite and is incremented by 1 for each following testsuite 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Contains the results of exexuting a testsuite 46 | 47 | 48 | 49 | 50 | Properties (e.g., environment settings) set during test execution 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Indicates that the test errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. Contains as a text node relevant data for the error, e.g., a stack trace 75 | 76 | 77 | 78 | 79 | 80 | 81 | The error message. e.g., if a java exception is thrown, the return value of getMessage() 82 | 83 | 84 | 85 | 86 | The type of error that occured. e.g., if a java execption is thrown the full class name of the exception. 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | Indicates that the test failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals. Contains as a text node relevant data for the failure, e.g., a stack trace 96 | 97 | 98 | 99 | 100 | 101 | 102 | The message specified in the assert 103 | 104 | 105 | 106 | 107 | The type of the assert. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Name of the test method 118 | 119 | 120 | 121 | 122 | Full class name for the class the test method is in. 123 | 124 | 125 | 126 | 127 | Time taken (in seconds) to execute the test 128 | 129 | 130 | 131 | 132 | 133 | 134 | Data that was written to standard out while the test was executed 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | Data that was written to standard error while the test was executed 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | Full class name of the test for non-aggregated testsuite documents. Class name without the package for aggregated testsuites documents 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | when the test was executed. Timezone may not be specified. 166 | 167 | 168 | 169 | 170 | Host on which the tests were executed. 'localhost' should be used if the hostname cannot be determined. 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | The total number of tests in the suite 181 | 182 | 183 | 184 | 185 | The total number of tests in the suite that failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals 186 | 187 | 188 | 189 | 190 | The total number of tests in the suite that errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. 191 | 192 | 193 | 194 | 195 | Time taken (in seconds) to execute the tests in the suite 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /src/reports.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Pascal Bach 3 | * Copyright (c) 2021 Siemens Mobility GmbH 4 | * 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | 8 | use std::io::Write; 9 | 10 | use derive_getters::Getters; 11 | use quick_xml::events::BytesDecl; 12 | use quick_xml::{ 13 | events::{BytesCData, Event}, 14 | ElementWriter, Result, Writer, 15 | }; 16 | use time::format_description::well_known::Rfc3339; 17 | 18 | use crate::{TestCase, TestResult, TestSuite}; 19 | 20 | /// Root element of a JUnit report 21 | #[derive(Default, Debug, Clone, Getters)] 22 | pub struct Report { 23 | testsuites: Vec, 24 | } 25 | 26 | impl Report { 27 | /// Create a new empty Report 28 | pub fn new() -> Report { 29 | Report { 30 | testsuites: Vec::new(), 31 | } 32 | } 33 | 34 | /// Add a [`TestSuite`](struct.TestSuite.html) to this report. 35 | /// 36 | /// The function takes ownership of the supplied [`TestSuite`](struct.TestSuite.html). 37 | pub fn add_testsuite(&mut self, testsuite: TestSuite) { 38 | self.testsuites.push(testsuite); 39 | } 40 | 41 | /// Add multiple[`TestSuite`s](struct.TestSuite.html) from an iterator. 42 | pub fn add_testsuites(&mut self, testsuites: impl IntoIterator) { 43 | self.testsuites.extend(testsuites); 44 | } 45 | 46 | /// Write the XML version of the Report to the given `Writer`. 47 | pub fn write_xml(&self, sink: W) -> Result<()> { 48 | let mut writer = Writer::new(sink); 49 | 50 | writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))?; 51 | 52 | writer 53 | .create_element("testsuites") 54 | .write_empty_or_inner( 55 | |_| self.testsuites.is_empty(), 56 | |w| { 57 | w.write_iter(self.testsuites.iter().enumerate(), |w, (id, ts)| { 58 | w.create_element("testsuite") 59 | .with_attributes([ 60 | ("id", id.to_string().as_str()), 61 | ("name", &ts.name), 62 | ("package", &ts.package), 63 | ("tests", &ts.tests().to_string()), 64 | ("errors", &ts.errors().to_string()), 65 | ("failures", &ts.failures().to_string()), 66 | ("hostname", &ts.hostname), 67 | ("timestamp", &ts.timestamp.format(&Rfc3339).unwrap()), 68 | ("time", &ts.time().as_seconds_f64().to_string()), 69 | ]) 70 | .write_empty_or_inner( 71 | |_| { 72 | ts.testcases.is_empty() 73 | && ts.system_out.is_none() 74 | && ts.system_err.is_none() 75 | }, 76 | |w| { 77 | w.write_iter(ts.testcases.iter(), |w, tc| tc.write_xml(w))? 78 | .write_opt(ts.system_out.as_ref(), |writer, out| { 79 | writer 80 | .create_element("system-out") 81 | .write_cdata_content(BytesCData::new(out)) 82 | })? 83 | .write_opt(ts.system_err.as_ref(), |writer, err| { 84 | writer 85 | .create_element("system-err") 86 | .write_cdata_content(BytesCData::new(err)) 87 | }) 88 | .map(drop) 89 | }, 90 | ) 91 | }) 92 | .map(drop) 93 | }, 94 | ) 95 | .map(drop) 96 | } 97 | } 98 | 99 | impl TestCase { 100 | /// Write the XML version of the [`TestCase`] to the given [`Writer`]. 101 | fn write_xml<'a, W: Write>(&self, w: &'a mut Writer) -> Result<&'a mut Writer> { 102 | let time = self.time.as_seconds_f64().to_string(); 103 | w.create_element("testcase") 104 | .with_attributes( 105 | [ 106 | Some(("name", self.name.as_str())), 107 | Some(("time", time.as_str())), 108 | self.classname.as_ref().map(|cl| ("classname", cl.as_str())), 109 | self.filepath.as_ref().map(|f| ("file", f.as_str())), 110 | ] 111 | .into_iter() 112 | .flatten(), 113 | ) 114 | .write_empty_or_inner( 115 | |_| { 116 | matches!(self.result, TestResult::Success) 117 | && self.system_out.is_none() 118 | && self.system_err.is_none() 119 | }, 120 | |w| { 121 | match self.result { 122 | TestResult::Success => Ok(w), 123 | TestResult::Error { 124 | ref type_, 125 | ref message, 126 | ref cause, 127 | } => w 128 | .create_element("error") 129 | .with_attributes([ 130 | ("type", type_.as_str()), 131 | ("message", message.as_str()), 132 | ]) 133 | .write_empty_or_inner( 134 | |_| cause.is_none(), 135 | |w| { 136 | w.write_opt(cause.as_ref(), |w, cause| { 137 | let data = BytesCData::new(cause.as_str()); 138 | w.write_event(Event::CData(BytesCData::new( 139 | String::from_utf8_lossy(&data), 140 | ))) 141 | .map(|_| w) 142 | }) 143 | .map(drop) 144 | }, 145 | ), 146 | TestResult::Failure { 147 | ref type_, 148 | ref message, 149 | ref cause, 150 | } => w 151 | .create_element("failure") 152 | .with_attributes([ 153 | ("type", type_.as_str()), 154 | ("message", message.as_str()), 155 | ]) 156 | .write_empty_or_inner( 157 | |_| cause.is_none(), 158 | |w| { 159 | w.write_opt(cause.as_ref(), |w, cause| { 160 | let data = BytesCData::new(cause.as_str()); 161 | w.write_event(Event::CData(BytesCData::new( 162 | String::from_utf8_lossy(&data), 163 | ))) 164 | .map(|_| w) 165 | }) 166 | .map(drop) 167 | }, 168 | ), 169 | TestResult::Skipped => w.create_element("skipped").write_empty(), 170 | }? 171 | .write_opt(self.system_out.as_ref(), |w, out| { 172 | w.create_element("system-out") 173 | .write_cdata_content(BytesCData::new(out.as_str())) 174 | })? 175 | .write_opt(self.system_err.as_ref(), |w, err| { 176 | w.create_element("system-err") 177 | .write_cdata_content(BytesCData::new(err.as_str())) 178 | }) 179 | .map(drop) 180 | }, 181 | ) 182 | } 183 | } 184 | 185 | /// Builder for JUnit [`Report`](struct.Report.html) objects 186 | #[derive(Default, Debug, Clone, Getters)] 187 | pub struct ReportBuilder { 188 | report: Report, 189 | } 190 | 191 | impl ReportBuilder { 192 | /// Create a new empty ReportBuilder 193 | pub fn new() -> ReportBuilder { 194 | ReportBuilder { 195 | report: Report::new(), 196 | } 197 | } 198 | 199 | /// Add a [`TestSuite`](struct.TestSuite.html) to this report builder. 200 | /// 201 | /// The function takes ownership of the supplied [`TestSuite`](struct.TestSuite.html). 202 | pub fn add_testsuite(&mut self, testsuite: TestSuite) -> &mut Self { 203 | self.report.testsuites.push(testsuite); 204 | self 205 | } 206 | 207 | /// Add multiple[`TestSuite`s](struct.TestSuite.html) from an iterator. 208 | pub fn add_testsuites(&mut self, testsuites: impl IntoIterator) -> &mut Self { 209 | self.report.testsuites.extend(testsuites); 210 | self 211 | } 212 | 213 | /// Build and return a [`Report`](struct.Report.html) object based on the data stored in this ReportBuilder object. 214 | pub fn build(&self) -> Report { 215 | self.report.clone() 216 | } 217 | } 218 | 219 | /// [`Writer`] extension. 220 | trait WriterExt { 221 | /// [`Write`]s in case `val` is [`Some`] or does nothing otherwise. 222 | fn write_opt( 223 | &mut self, 224 | val: Option, 225 | inner: impl FnOnce(&mut Self, T) -> Result<&mut Self>, 226 | ) -> Result<&mut Self>; 227 | 228 | /// [`Write`]s every item of the [`Iterator`]. 229 | fn write_iter( 230 | &mut self, 231 | val: I, 232 | inner: impl FnMut(&mut Self, T) -> Result<&mut Self>, 233 | ) -> Result<&mut Self> 234 | where 235 | I: IntoIterator; 236 | } 237 | 238 | impl WriterExt for Writer { 239 | fn write_opt( 240 | &mut self, 241 | val: Option, 242 | inner: impl FnOnce(&mut Self, T) -> Result<&mut Self>, 243 | ) -> Result<&mut Self> { 244 | if let Some(val) = val { 245 | inner(self, val) 246 | } else { 247 | Ok(self) 248 | } 249 | } 250 | 251 | fn write_iter( 252 | &mut self, 253 | iter: I, 254 | inner: impl FnMut(&mut Self, T) -> Result<&mut Self>, 255 | ) -> Result<&mut Self> 256 | where 257 | I: IntoIterator, 258 | { 259 | iter.into_iter().try_fold(self, inner) 260 | } 261 | } 262 | 263 | /// [`ElementWriter`] extension. 264 | trait ElementWriterExt<'a, W: Write> { 265 | /// [`Writes`] with `inner` in case `is_empty` resolves to [`false`] or 266 | /// [`Write`]s with [`ElementWriter::write_empty`] otherwise. 267 | fn write_empty_or_inner( 268 | self, 269 | is_empty: impl FnOnce(&mut Self) -> bool, 270 | inner: Inner, 271 | ) -> Result<&'a mut Writer> 272 | where 273 | Inner: Fn(&mut Writer) -> Result<()>; 274 | } 275 | 276 | impl<'a, W: Write> ElementWriterExt<'a, W> for ElementWriter<'a, W> { 277 | fn write_empty_or_inner( 278 | mut self, 279 | is_empty: impl FnOnce(&mut Self) -> bool, 280 | inner: Inner, 281 | ) -> Result<&'a mut Writer> 282 | where 283 | Inner: Fn(&mut Writer) -> Result<()>, 284 | { 285 | if is_empty(&mut self) { 286 | self.write_empty() 287 | } else { 288 | self.write_inner_content(inner) 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/collections.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Pascal Bach 3 | * Copyright (c) 2021 Siemens Mobility GmbH 4 | * 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | 8 | use derive_getters::Getters; 9 | use time::{Duration, OffsetDateTime}; 10 | 11 | /// A `TestSuite` groups together several [`TestCase`s](struct.TestCase.html). 12 | #[derive(Debug, Clone, Getters)] 13 | pub struct TestSuite { 14 | pub name: String, 15 | pub package: String, 16 | pub timestamp: OffsetDateTime, 17 | pub hostname: String, 18 | pub testcases: Vec, 19 | pub system_out: Option, 20 | pub system_err: Option, 21 | } 22 | 23 | impl TestSuite { 24 | /// Create a new `TestSuite` with a given name 25 | pub fn new(name: &str) -> Self { 26 | TestSuite { 27 | hostname: "localhost".into(), 28 | package: format!("testsuite/{}", &name), 29 | name: name.into(), 30 | timestamp: OffsetDateTime::now_utc(), 31 | testcases: Vec::new(), 32 | system_out: None, 33 | system_err: None, 34 | } 35 | } 36 | 37 | /// Add a [`TestCase`](struct.TestCase.html) to the `TestSuite`. 38 | pub fn add_testcase(&mut self, testcase: TestCase) { 39 | self.testcases.push(testcase); 40 | } 41 | 42 | /// Add several [`TestCase`s](struct.TestCase.html) from a Vec. 43 | pub fn add_testcases(&mut self, testcases: impl IntoIterator) { 44 | self.testcases.extend(testcases); 45 | } 46 | 47 | /// Set the timestamp of the given `TestSuite`. 48 | /// 49 | /// By default the timestamp is set to the time when the `TestSuite` was created. 50 | pub fn set_timestamp(&mut self, timestamp: OffsetDateTime) { 51 | self.timestamp = timestamp; 52 | } 53 | 54 | pub fn set_system_out(&mut self, system_out: &str) { 55 | self.system_out = Some(system_out.to_owned()); 56 | } 57 | 58 | pub fn set_system_err(&mut self, system_err: &str) { 59 | self.system_err = Some(system_err.to_owned()); 60 | } 61 | 62 | pub fn tests(&self) -> usize { 63 | self.testcases.len() 64 | } 65 | 66 | pub fn errors(&self) -> usize { 67 | self.testcases.iter().filter(|x| x.is_error()).count() 68 | } 69 | 70 | pub fn failures(&self) -> usize { 71 | self.testcases.iter().filter(|x| x.is_failure()).count() 72 | } 73 | 74 | pub fn skipped(&self) -> usize { 75 | self.testcases.iter().filter(|x| x.is_skipped()).count() 76 | } 77 | 78 | pub fn time(&self) -> Duration { 79 | self.testcases 80 | .iter() 81 | .fold(Duration::ZERO, |sum, d| sum + d.time) 82 | } 83 | } 84 | 85 | /// Builder for [`TestSuite`](struct.TestSuite.html) objects. 86 | #[derive(Debug, Clone, Getters)] 87 | pub struct TestSuiteBuilder { 88 | pub testsuite: TestSuite, 89 | } 90 | 91 | impl TestSuiteBuilder { 92 | /// Create a new `TestSuiteBuilder` with a given name 93 | pub fn new(name: &str) -> Self { 94 | TestSuiteBuilder { 95 | testsuite: TestSuite::new(name), 96 | } 97 | } 98 | 99 | /// Add a [`TestCase`](struct.TestCase.html) to the `TestSuiteBuilder`. 100 | pub fn add_testcase(&mut self, testcase: TestCase) -> &mut Self { 101 | self.testsuite.testcases.push(testcase); 102 | self 103 | } 104 | 105 | /// Add several [`TestCase`s](struct.TestCase.html) from a Vec. 106 | pub fn add_testcases(&mut self, testcases: impl IntoIterator) -> &mut Self { 107 | self.testsuite.testcases.extend(testcases); 108 | self 109 | } 110 | 111 | /// Set the timestamp of the `TestSuiteBuilder`. 112 | /// 113 | /// By default the timestamp is set to the time when the `TestSuiteBuilder` was created. 114 | pub fn set_timestamp(&mut self, timestamp: OffsetDateTime) -> &mut Self { 115 | self.testsuite.timestamp = timestamp; 116 | self 117 | } 118 | 119 | pub fn set_system_out(&mut self, system_out: &str) -> &mut Self { 120 | self.testsuite.system_out = Some(system_out.to_owned()); 121 | self 122 | } 123 | 124 | pub fn set_system_err(&mut self, system_err: &str) -> &mut Self { 125 | self.testsuite.system_err = Some(system_err.to_owned()); 126 | self 127 | } 128 | 129 | /// Build and return a [`TestSuite`](struct.TestSuite.html) object based on the data stored in this TestSuiteBuilder object. 130 | pub fn build(&self) -> TestSuite { 131 | self.testsuite.clone() 132 | } 133 | } 134 | 135 | /// One single test case 136 | #[derive(Debug, Clone, Getters)] 137 | pub struct TestCase { 138 | pub name: String, 139 | pub time: Duration, 140 | pub result: TestResult, 141 | pub classname: Option, 142 | pub filepath: Option, 143 | pub system_out: Option, 144 | pub system_err: Option, 145 | } 146 | 147 | /// Result of a test case 148 | #[derive(Debug, Clone)] 149 | pub enum TestResult { 150 | Success, 151 | Skipped, 152 | Error { 153 | type_: String, 154 | message: String, 155 | cause: Option, 156 | }, 157 | Failure { 158 | type_: String, 159 | message: String, 160 | cause: Option, 161 | }, 162 | } 163 | 164 | impl TestCase { 165 | /// Creates a new successful `TestCase` 166 | pub fn success(name: &str, time: Duration) -> Self { 167 | TestCase { 168 | name: name.into(), 169 | time, 170 | result: TestResult::Success, 171 | classname: None, 172 | filepath: None, 173 | system_out: None, 174 | system_err: None, 175 | } 176 | } 177 | 178 | /// Set the `classname` for the `TestCase` 179 | pub fn set_classname(&mut self, classname: &str) { 180 | self.classname = Some(classname.to_owned()); 181 | } 182 | 183 | /// Set the `file` for the `TestCase` 184 | pub fn set_filepath(&mut self, filepath: &str) { 185 | self.filepath = Some(filepath.to_owned()); 186 | } 187 | 188 | /// Set the `system_out` for the `TestCase` 189 | pub fn set_system_out(&mut self, system_out: &str) { 190 | self.system_out = Some(system_out.to_owned()); 191 | } 192 | 193 | /// Set the `system_err` for the `TestCase` 194 | pub fn set_system_err(&mut self, system_err: &str) { 195 | self.system_err = Some(system_err.to_owned()); 196 | } 197 | 198 | /// Check if a `TestCase` is successful 199 | pub fn is_success(&self) -> bool { 200 | matches!(self.result, TestResult::Success) 201 | } 202 | 203 | /// Creates a new erroneous `TestCase` 204 | /// 205 | /// An erroneous `TestCase` is one that encountered an unexpected error condition. 206 | pub fn error(name: &str, time: Duration, type_: &str, message: &str) -> Self { 207 | TestCase { 208 | name: name.into(), 209 | time, 210 | result: TestResult::Error { 211 | type_: type_.into(), 212 | message: message.into(), 213 | cause: None, 214 | }, 215 | classname: None, 216 | filepath: None, 217 | system_out: None, 218 | system_err: None, 219 | } 220 | } 221 | 222 | /// Check if a `TestCase` is erroneous 223 | pub fn is_error(&self) -> bool { 224 | matches!(self.result, TestResult::Error { .. }) 225 | } 226 | 227 | /// Creates a new failed `TestCase` 228 | /// 229 | /// A failed `TestCase` is one where an explicit assertion failed 230 | pub fn failure(name: &str, time: Duration, type_: &str, message: &str) -> Self { 231 | TestCase { 232 | name: name.into(), 233 | time, 234 | result: TestResult::Failure { 235 | type_: type_.into(), 236 | message: message.into(), 237 | cause: None, 238 | }, 239 | classname: None, 240 | filepath: None, 241 | system_out: None, 242 | system_err: None, 243 | } 244 | } 245 | 246 | /// Check if a `TestCase` failed 247 | pub fn is_failure(&self) -> bool { 248 | matches!(self.result, TestResult::Failure { .. }) 249 | } 250 | 251 | /// Create a new ignored `TestCase` 252 | /// 253 | /// An ignored `TestCase` is one where an ignored or skipped 254 | pub fn skipped(name: &str) -> Self { 255 | TestCase { 256 | name: name.into(), 257 | time: Duration::ZERO, 258 | result: TestResult::Skipped, 259 | classname: None, 260 | filepath: None, 261 | system_out: None, 262 | system_err: None, 263 | } 264 | } 265 | 266 | /// Check if a `TestCase` ignored 267 | pub fn is_skipped(&self) -> bool { 268 | matches!(self.result, TestResult::Skipped) 269 | } 270 | } 271 | 272 | /// Builder for [`TestCase`](struct.TestCase.html) objects. 273 | #[derive(Debug, Clone, Getters)] 274 | pub struct TestCaseBuilder { 275 | pub testcase: TestCase, 276 | } 277 | 278 | impl TestCaseBuilder { 279 | /// Creates a new TestCaseBuilder for a successful `TestCase` 280 | pub fn success(name: &str, time: Duration) -> Self { 281 | TestCaseBuilder { 282 | testcase: TestCase::success(name, time), 283 | } 284 | } 285 | 286 | /// Set the `classname` for the `TestCase` 287 | pub fn set_classname(&mut self, classname: &str) -> &mut Self { 288 | self.testcase.classname = Some(classname.to_owned()); 289 | self 290 | } 291 | 292 | /// Set the `file` for the `TestCase` 293 | pub fn set_filepath(&mut self, filepath: &str) -> &mut Self { 294 | self.testcase.filepath = Some(filepath.to_owned()); 295 | self 296 | } 297 | 298 | /// Set the `system_out` for the `TestCase` 299 | pub fn set_system_out(&mut self, system_out: &str) -> &mut Self { 300 | self.testcase.system_out = Some(system_out.to_owned()); 301 | self 302 | } 303 | 304 | /// Set the `system_err` for the `TestCase` 305 | pub fn set_system_err(&mut self, system_err: &str) -> &mut Self { 306 | self.testcase.system_err = Some(system_err.to_owned()); 307 | self 308 | } 309 | 310 | /// Set the `result.trace` for the `TestCase` 311 | /// 312 | /// It has no effect on successful `TestCase`s. 313 | pub fn set_trace(&mut self, trace: &str) -> &mut Self { 314 | match self.testcase.result { 315 | TestResult::Error { ref mut cause, .. } => *cause = Some(trace.to_owned()), 316 | TestResult::Failure { ref mut cause, .. } => *cause = Some(trace.to_owned()), 317 | _ => {} 318 | } 319 | self 320 | } 321 | 322 | /// Creates a new TestCaseBuilder for an erroneous `TestCase` 323 | /// 324 | /// An erroneous `TestCase` is one that encountered an unexpected error condition. 325 | pub fn error(name: &str, time: Duration, type_: &str, message: &str) -> Self { 326 | TestCaseBuilder { 327 | testcase: TestCase::error(name, time, type_, message), 328 | } 329 | } 330 | 331 | /// Creates a new TestCaseBuilder for a failed `TestCase` 332 | /// 333 | /// A failed `TestCase` is one where an explicit assertion failed 334 | pub fn failure(name: &str, time: Duration, type_: &str, message: &str) -> Self { 335 | TestCaseBuilder { 336 | testcase: TestCase::failure(name, time, type_, message), 337 | } 338 | } 339 | 340 | /// Creates a new TestCaseBuilder for an ignored `TestCase` 341 | /// 342 | /// An ignored `TestCase` is one where an ignored or skipped 343 | pub fn skipped(name: &str) -> Self { 344 | TestCaseBuilder { 345 | testcase: TestCase::skipped(name), 346 | } 347 | } 348 | 349 | /// Build and return a [`TestCase`](struct.TestCase.html) object based on the data stored in this TestCaseBuilder object. 350 | pub fn build(&self) -> TestCase { 351 | self.testcase.clone() 352 | } 353 | } 354 | 355 | // Make sure the readme is tested too 356 | #[cfg(doctest)] 357 | doc_comment::doctest!("../README.md"); 358 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Pascal Bach 3 | * Copyright (c) 2021 Siemens Mobility GmbH 4 | * 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | 8 | //! Create JUnit compatible XML reports. 9 | //! 10 | //! ## Example 11 | //! 12 | //! ```rust 13 | //! use junit_report::{datetime, Duration, ReportBuilder, TestCase, TestCaseBuilder, TestSuiteBuilder}; 14 | //! 15 | //! let timestamp = datetime!(1970-01-01 01:01 UTC); 16 | //! 17 | //! let test_success = TestCase::success("good test", Duration::seconds(15)); 18 | //! let test_error = TestCase::error( 19 | //! "error test", 20 | //! Duration::seconds(5), 21 | //! "git error", 22 | //! "unable to fetch", 23 | //! ); 24 | //! let test_failure = TestCaseBuilder::failure( 25 | //! "failure test", 26 | //! Duration::seconds(10), 27 | //! "assert_eq", 28 | //! "not equal", 29 | //! ).set_classname("classname").set_filepath("./foo.rs") 30 | //! .build(); 31 | //! 32 | //! let ts1 = TestSuiteBuilder::new("ts1").set_timestamp(timestamp).build(); 33 | //! 34 | //! let ts2 = TestSuiteBuilder::new("ts2").set_timestamp(timestamp) 35 | //! .add_testcase(test_success) 36 | //! .add_testcase(test_error) 37 | //! .add_testcase(test_failure) 38 | //! .build(); 39 | //! 40 | //! let r = ReportBuilder::new() 41 | //! .add_testsuite(ts1) 42 | //! .add_testsuite(ts2) 43 | //! .build(); 44 | //! 45 | //! let mut out: Vec = Vec::new(); 46 | //! 47 | //! r.write_xml(&mut out).unwrap(); 48 | //! ``` 49 | 50 | mod collections; 51 | mod reports; 52 | 53 | pub use quick_xml::Error; 54 | pub use time::{macros::datetime, Duration, OffsetDateTime}; 55 | 56 | pub use crate::{ 57 | collections::{TestCase, TestCaseBuilder, TestResult, TestSuite, TestSuiteBuilder}, 58 | reports::{Report, ReportBuilder}, 59 | }; 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use crate::{ 64 | datetime, Duration, Report, ReportBuilder, TestCase, TestCaseBuilder, TestSuite, 65 | TestSuiteBuilder, 66 | }; 67 | use pretty_assertions::assert_eq; 68 | 69 | #[test] 70 | fn empty_testsuites() { 71 | let r = Report::new(); 72 | 73 | let mut out: Vec = Vec::new(); 74 | 75 | r.write_xml(&mut out).unwrap(); 76 | 77 | // language=xml 78 | assert_eq!( 79 | String::from_utf8(out).unwrap(), 80 | "", 81 | ); 82 | } 83 | 84 | #[test] 85 | fn add_empty_testsuite_single() { 86 | let timestamp = datetime!(1970-01-01 01:01 UTC); 87 | 88 | let ts1 = TestSuiteBuilder::new("ts1") 89 | .set_timestamp(timestamp) 90 | .build(); 91 | let mut tsb = TestSuiteBuilder::new("ts2"); 92 | tsb.set_timestamp(timestamp); 93 | let ts2 = tsb.build(); 94 | 95 | let r = ReportBuilder::new() 96 | .add_testsuite(ts1) 97 | .add_testsuite(ts2) 98 | .build(); 99 | 100 | let mut out: Vec = Vec::new(); 101 | 102 | r.write_xml(&mut out).unwrap(); 103 | 104 | // language=xml 105 | assert_eq!( 106 | String::from_utf8(out).unwrap(), 107 | "\ 108 | \ 109 | \ 110 | \ 111 | \ 112 | ", 113 | ); 114 | } 115 | 116 | #[test] 117 | fn add_empty_testsuite_single_with_sysout() { 118 | let timestamp = datetime!(1970-01-01 01:01 UTC); 119 | 120 | let ts1 = TestSuiteBuilder::new("ts1") 121 | .set_system_out("Test sysout") 122 | .set_timestamp(timestamp) 123 | .build(); 124 | 125 | let r = ReportBuilder::new().add_testsuite(ts1).build(); 126 | 127 | let mut out: Vec = Vec::new(); 128 | 129 | r.write_xml(&mut out).unwrap(); 130 | 131 | // language=xml 132 | assert_eq!( 133 | String::from_utf8(out).unwrap(), 134 | "\ 135 | \ 136 | \ 137 | \ 138 | \ 139 | \ 140 | ", 141 | ); 142 | } 143 | 144 | #[test] 145 | fn add_empty_testsuite_single_with_syserror() { 146 | let timestamp = datetime!(1970-01-01 01:01 UTC); 147 | 148 | let ts1 = TestSuiteBuilder::new("ts1") 149 | .set_system_err("Test syserror") 150 | .set_timestamp(timestamp) 151 | .build(); 152 | 153 | let r = ReportBuilder::new().add_testsuite(ts1).build(); 154 | 155 | let mut out: Vec = Vec::new(); 156 | 157 | r.write_xml(&mut out).unwrap(); 158 | 159 | // language=xml 160 | assert_eq!( 161 | String::from_utf8(out).unwrap(), 162 | "\ 163 | \ 164 | \ 165 | \ 166 | \ 167 | \ 168 | ", 169 | ); 170 | } 171 | 172 | #[test] 173 | fn add_empty_testsuite_batch() { 174 | let timestamp = datetime!(1970-01-01 01:01 UTC); 175 | 176 | let ts1 = TestSuiteBuilder::new("ts1") 177 | .set_timestamp(timestamp) 178 | .build(); 179 | let ts2 = TestSuiteBuilder::new("ts2") 180 | .set_timestamp(timestamp) 181 | .build(); 182 | 183 | let v = vec![ts1, ts2]; 184 | 185 | let r = ReportBuilder::new().add_testsuites(v).build(); 186 | 187 | let mut out: Vec = Vec::new(); 188 | 189 | r.write_xml(&mut out).unwrap(); 190 | 191 | // language=xml 192 | assert_eq!( 193 | String::from_utf8(out).unwrap(), 194 | "\ 195 | \ 196 | \ 197 | \ 198 | \ 199 | ", 200 | ); 201 | } 202 | 203 | #[test] 204 | fn count_tests() { 205 | let mut ts = TestSuite::new("ts"); 206 | 207 | let tc1 = TestCase::success("mysuccess", Duration::milliseconds(6001)); 208 | let tc2 = TestCase::error( 209 | "myerror", 210 | Duration::seconds(6), 211 | "Some Error", 212 | "An Error happened", 213 | ); 214 | let tc3 = TestCase::failure( 215 | "myerror", 216 | Duration::seconds(6), 217 | "Some failure", 218 | "A Failure happened", 219 | ); 220 | 221 | assert_eq!(0, ts.tests()); 222 | assert_eq!(0, ts.errors()); 223 | assert_eq!(0, ts.failures()); 224 | 225 | ts.add_testcase(tc1); 226 | 227 | assert_eq!(1, ts.tests()); 228 | assert_eq!(0, ts.errors()); 229 | assert_eq!(0, ts.failures()); 230 | 231 | ts.add_testcase(tc2); 232 | 233 | assert_eq!(2, ts.tests()); 234 | assert_eq!(1, ts.errors()); 235 | assert_eq!(0, ts.failures()); 236 | 237 | ts.add_testcase(tc3); 238 | 239 | assert_eq!(3, ts.tests()); 240 | assert_eq!(1, ts.errors()); 241 | assert_eq!(1, ts.failures()); 242 | } 243 | 244 | #[test] 245 | fn testcases_no_stdout_stderr() { 246 | let timestamp = datetime!(1970-01-01 01:01 UTC); 247 | 248 | let test_success = TestCaseBuilder::success("good test", Duration::milliseconds(15001)) 249 | .set_classname("MyClass") 250 | .set_filepath("./foo.rs") 251 | .build(); 252 | let test_error = TestCaseBuilder::error( 253 | "error test", 254 | Duration::seconds(5), 255 | "git error", 256 | "unable to fetch", 257 | ) 258 | .build(); 259 | let test_failure = TestCaseBuilder::failure( 260 | "failure test", 261 | Duration::seconds(10), 262 | "assert_eq", 263 | "not equal", 264 | ) 265 | .build(); 266 | 267 | let ts1 = TestSuiteBuilder::new("ts1") 268 | .set_timestamp(timestamp) 269 | .build(); 270 | let ts2 = TestSuiteBuilder::new("ts2") 271 | .set_timestamp(timestamp) 272 | .add_testcase(test_success) 273 | .add_testcase(test_error) 274 | .add_testcase(test_failure) 275 | .build(); 276 | 277 | let r = ReportBuilder::new() 278 | .add_testsuite(ts1) 279 | .add_testsuite(ts2) 280 | .build(); 281 | 282 | let mut out: Vec = Vec::new(); 283 | 284 | r.write_xml(&mut out).unwrap(); 285 | 286 | // language=xml 287 | assert_eq!( 288 | String::from_utf8(out).unwrap(), 289 | "\ 290 | \ 291 | \ 292 | \ 293 | \ 294 | \ 295 | \ 296 | \ 297 | \ 298 | \ 299 | \ 300 | \ 301 | \ 302 | ", 303 | ); 304 | } 305 | 306 | #[test] 307 | fn test_cases_with_sysout_and_syserr() { 308 | let timestamp = datetime!(1970-01-01 01:01 UTC); 309 | 310 | let test_success = TestCaseBuilder::success("good test", Duration::milliseconds(15001)) 311 | .set_classname("MyClass") 312 | .set_filepath("./foo.rs") 313 | .set_system_out("Some sysout message") 314 | .build(); 315 | let test_error = TestCaseBuilder::error( 316 | "error test", 317 | Duration::seconds(5), 318 | "git error", 319 | "unable to fetch", 320 | ) 321 | .set_system_err("Some syserror message") 322 | .build(); 323 | let test_failure = TestCaseBuilder::failure( 324 | "failure test", 325 | Duration::seconds(10), 326 | "assert_eq", 327 | "not equal", 328 | ) 329 | .set_system_out("System out or error message") 330 | .set_system_err("Another system error message") 331 | .build(); 332 | 333 | let ts1 = TestSuiteBuilder::new("ts1") 334 | .set_timestamp(timestamp) 335 | .build(); 336 | let ts2 = TestSuiteBuilder::new("ts2") 337 | .set_timestamp(timestamp) 338 | .add_testcase(test_success) 339 | .add_testcase(test_error) 340 | .add_testcase(test_failure) 341 | .build(); 342 | 343 | let r = ReportBuilder::new() 344 | .add_testsuite(ts1) 345 | .add_testsuite(ts2) 346 | .build(); 347 | 348 | let mut out: Vec = Vec::new(); 349 | 350 | r.write_xml(&mut out).unwrap(); 351 | 352 | // language=xml 353 | assert_eq!( 354 | String::from_utf8(out).unwrap(), 355 | "\ 356 | \ 357 | \ 358 | \ 359 | \ 360 | \ 361 | \ 362 | \ 363 | \ 364 | \ 365 | \ 366 | \ 367 | \ 368 | \ 369 | \ 370 | \ 371 | \ 372 | \ 373 | ", 374 | ); 375 | } 376 | 377 | #[test] 378 | fn test_cases_with_trace() { 379 | let timestamp = datetime!(1970-01-01 01:01 UTC); 380 | 381 | let test_success = TestCaseBuilder::success("good test", Duration::milliseconds(15001)) 382 | .set_classname("MyClass") 383 | .set_filepath("./foo.rs") 384 | .set_trace("Some trace message") // This should be ignored 385 | .build(); 386 | let test_error = TestCaseBuilder::error( 387 | "error test", 388 | Duration::seconds(5), 389 | "git error", 390 | "unable to fetch", 391 | ) 392 | .set_trace("Some error trace") 393 | .build(); 394 | let test_failure = TestCaseBuilder::failure( 395 | "failure test", 396 | Duration::seconds(10), 397 | "assert_eq", 398 | "not equal", 399 | ) 400 | .set_trace("Some failure trace") 401 | .build(); 402 | 403 | let ts1 = TestSuiteBuilder::new("ts1") 404 | .set_timestamp(timestamp) 405 | .build(); 406 | let ts2 = TestSuiteBuilder::new("ts2") 407 | .set_timestamp(timestamp) 408 | .add_testcase(test_success) 409 | .add_testcase(test_error) 410 | .add_testcase(test_failure) 411 | .build(); 412 | 413 | let r = ReportBuilder::new() 414 | .add_testsuite(ts1) 415 | .add_testsuite(ts2) 416 | .build(); 417 | 418 | let mut out: Vec = Vec::new(); 419 | 420 | r.write_xml(&mut out).unwrap(); 421 | 422 | // language=xml 423 | assert_eq!( 424 | String::from_utf8(out).unwrap(), 425 | "\ 426 | \ 427 | \ 428 | \ 429 | \ 430 | \ 431 | \ 432 | \ 433 | \ 434 | \ 435 | \ 436 | \ 437 | \ 438 | ", 439 | ); 440 | } 441 | } 442 | --------------------------------------------------------------------------------