├── .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 |
--------------------------------------------------------------------------------