Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracking Issue for libtest's JUnit reporter #85563

Open
1 of 3 tasks
andoriyu opened this issue May 21, 2021 · 9 comments
Open
1 of 3 tasks

Tracking Issue for libtest's JUnit reporter #85563

andoriyu opened this issue May 21, 2021 · 9 comments
Labels
A-libtest Area: `#[test]` / the `test` library C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@andoriyu
Copy link
Contributor

andoriyu commented May 21, 2021

This is a tracking issue for libtest's JUnit reporter.

Add an alternative formatter to libtest. The formatter produces valid xml that later can be interpreted as JUnit report. Report can be consumed by Continuous Integration tools like Jenkins.

Public API

Run test binary with --format=junit argument.

Steps / History

Unresolved Questions

  • timestamp is required by schema, but every viewer/parser ignores it. Attribute is not set to avoid depending on chrono;
  • Each test suite (doc tests, unit tests and each integration test) must be run separately. This due to a fact that from libtest perspective each one of them is a separate invocation.
  • libtest doesn't know what is the name of integration binary is being run, so in the report it just says integration for all of them.
  • No test cases in src/test or otherwise validating output format correctness.
@andoriyu andoriyu added C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels May 21, 2021
@jonas-schievink jonas-schievink added the A-libtest Area: `#[test]` / the `test` library label May 21, 2021
bors added a commit to rust-lang-ci/rust that referenced this issue May 27, 2021
…aahc

feat(libtest): Add JUnit formatter

tracking issue: rust-lang#85563

Add an alternative formatter to `libtest`. Formatter produces valid xml that later can be interpreted as JUnit report.

Caveats:

- `timestamp` is required by schema, but every viewer/parser ignores it. Attribute is not set to avoid depending on chrono;
- Running all "suits" (unit tests, doc-tests and integration tests) will produce a mess;
- I couldn't find a way to get integration test binary name, so it's just goes by "integration";

Sample output for unit tests (pretty printed by 3rd party tool):
```
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="test" package="test" id="0" errors="0" failures="0" tests="13" skipped="1">
    <testcase classname="results::tests" name="test_completed_bad" time="0"/>
    <testcase classname="results::tests" name="suite_started" time="0"/>
    <testcase classname="results::tests" name="suite_ended_ok" time="0"/>
    <testcase classname="results::tests" name="suite_ended_bad" time="0"/>
    <testcase classname="junit::tests" name="test_failed_output" time="0"/>
    <testcase classname="junit::tests" name="test_simple_output" time="0"/>
    <testcase classname="junit::tests" name="test_multiple_outputs" time="0"/>
    <testcase classname="results::tests" name="test_completed_ok" time="0"/>
    <testcase classname="results::tests" name="test_stared" time="0"/>
    <testcase classname="junit::tests" name="test_generate_xml_no_error_single_testsuite" time="0"/>
    <testcase classname="results::tests" name="test_simple_output" time="0"/>
    <testcase classname="test" name="should_panic" time="0"/>
    <system-out/>
    <system-err/>
  </testsuite>
</testsuites>
```

Sample output for integration tests (pretty printed by 3rd party tool):

```
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="test" package="test" id="0" errors="0" failures="0" tests="1" skipped="0">
    <testcase classname="integration" name="test_add" time="0"/>
    <system-out/>
    <system-err/>
  </testsuite>
</testsuites>
```

Sample output for Doc-tests (pretty printed by 3rd party tool):

```
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="test" package="test" id="0" errors="0" failures="0" tests="1" skipped="0">
    <testcase classname="src/lib.rs" name="(line 2)" time="0"/>
    <system-out/>
    <system-err/>
  </testsuite>
</testsuites>
```
@jonasbb
Copy link
Contributor

jonasbb commented May 27, 2021

self.write_message(&*format!(
"<testcase classname=\"{}\" \
name=\"{}\" time=\"{}\">",
class_name,
test_name,
duration.as_secs()
))?;

It would be nice if the output supports sub-second precision for the duration. This is what cargo2junit does. That way you don't lose the runtime information for fast tests.
I just repeat the comment here if my message on #84568 gets lost.

@spencergilbert
Copy link

Also commented on #49359 - but we'd love to help stabilize either of these, primarily to support Datadog CI

@andoriyu
Copy link
Contributor Author

andoriyu commented Jul 1, 2021

self.write_message(&*format!(
"<testcase classname=\"{}\" \
name=\"{}\" time=\"{}\">",
class_name,
test_name,
duration.as_secs()
))?;

It would be nice if the output supports sub-second precision for the duration. This is what cargo2junit does. That way you don't lose the runtime information for fast tests.
I just repeat the comment here if my message on #84568 gets lost.

Done via #86359

@cemoktra
Copy link

cemoktra commented Sep 3, 2021

Hey. What is timeline to stabilize this?

@last-partizan
Copy link
Contributor

Each test suite (doc tests, unit tests and each integration test) must be run separately. This due to a fact that from libtest perspective each one of them is a separate invocation.

Any ideas how to approach this?

@andoriyu
Copy link
Contributor Author

andoriyu commented Feb 6, 2022

Each test suite (doc tests, unit tests and each integration test) must be run separately. This due to a fact that from libtest perspective each one of them is a separate invocation.

Any ideas how to approach this?

Currently — just do multiple cargo invocations for each.

@last-partizan
Copy link
Contributor

I mean, in the long run, from architectural point of view. Looks like there is no way to combine all xml output into single file at libtest level, becouse each invocation is separate. So, maybe it's better to move formatters into cargo-test? Or maybe there is a way to signal global test start to libtest, so it can write header, and then after all test finished - write footer?

@detly
Copy link

detly commented Jun 8, 2023

Since both this and the JSON output is now gated as of 1.70, I just wanted to add a couple of notes from a user:

For anyone looking into how to get jUnit reporting for Rust projects now and finding this, we migrated our Gitlab CI to cargo-nextest instead of pinning to 1.69 or using RUSTC_BOOTSTRAP etc. We did not have any major issues with it, the docs are good and it took a couple of hours. YMMV of course. (Sorry if anyone sees this twice, I cross posted on another linked issue but it might help others finding this for the first time.)

Regarding this:

Each test suite (doc tests, unit tests and each integration test) must be run separately. This due to a fact that from libtest perspective each one of them is a separate invocation.

Any ideas how to approach this?

Currently — just do multiple cargo invocations for each.

I think this is a big reason that this feature has had less attention than the JSON output, and people seem more concerned with the JSON-output-via-cargo2unit workflow breaking than this needing completion/stabilisation. I myself am guilty of looking into CI integration a couple of years ago when we started with Rust, finding this very issue, thinking "this won't work" and then going off to suity/cargo2junit without leaving any feedback. So here it is.

It seems like a little thing - just run cargo test multiple times, upload multiple reports, etc. But there are a few problems with that, that add up to quite a maintenance burden:

  • Gitlab CI at least (I don't know about others) will treat and render separate reports differently to one report containing the invocations as testsuites. It is more nested, more annoying to navigate, more difficult to compare if names or (module) paths change etc. (I admit I have not checked in on this in a long time.)
  • It also means you have to write your CI config to "know" what cargo test invocations to do. We rely a lot on integration tests. It's the sort of thing I'd want a test harness (or the thing that calls it) to take care of automatically.
  • You have to write CI config/scripts to either know where are all the reports are (duplicating the information in the invocation step), or some script that combines them all into one (please, no).
  • Most annoyingly, there is no way to make this generic! If you have multiple projects, you can't just have a CI template/workflow that does cargo test in the project dir.

If it's an architectural issue, then maybe it is necessary to rely on JSON output for jUnit reporting, or see if it can go in Cargo instead.

@therealfrauholle
Copy link

Thank your for bringing this up @detly. I share your view and pain points. It makes me also very sad that I noticed a bug in the JUnit formatter (see #99813) but never cared enough to fix it. We now use gitlab-report, an alternative to cargo2junit to produce junit test reports in our pipeline.

I did not bother to fix #99813 because it feels really bad to implement XML generation by hand if technically good crates already exist for that. Generation might break for something else in the future and then we need to waste time again. gitlab-report gives me the freedom to have fancy macros and complex handling, being able to test functionality without going through the rust review process or complicating my test environment (e.g. building the rust toolchain myself).

This makes me wonder: Do we actually want to maintain a junit reporter after all? I have the feeling it would be better if the libtest component focuses on providing a rich API to implement custom reporters. I see that the json output format starts to be that default API, so from the view of the rust core language it might be smart to drop support for junit and other output formats in libtest and focus on how we can ease the work for tools like cargo nextest, gitlab-report or cargo2junit; e.g. stabilize the/a json output format.

aspotashev added a commit to aspotashev/rust that referenced this issue Mar 7, 2024
Rationale for adding this functionality:

* Bazel (build system) doesn't provide a way to process output from a
  binary (in this case, Rust test binary's output) other using a wrapper
  binary. However, using a wrapper binary potentially breaks debugging,
  because Bazel would suggest to debug the wrapper binary rather than
  the Rust test itself.
    * See bazelbuild/rules_rust#1303.
    * Cargo is not used in Rust Bazel rules.
    * Although we could wait for
      rust-lang#96290 and then modify Rust
      Bazel rules to pass `--logfile` on the command line to
      provisionally unblock
      bazelbuild/rules_rust#1303, that
      solution still wouldn't allow advanced test output postprocessing
      such as changing JSON/XML schema and injecting extra JUnit
      properties.
* Due to limitations of Rust libtest formatters, Rust developers often
  use a separate tool to postprocess the test results output (see
  comments to rust-lang#85563).
    * Examples of existing postprocessing tools:
	* https://crates.io/crates/cargo2junit
	* https://crates.io/crates/gitlab-report
	* https://crates.io/crates/cargo-suity
    * For these use cases, it might be helpful to use the new flags
      `--output_postprocess_executable`, `--output_postprocess_args`
      instead of piping the test output explicitly, e.g. to more
      reliably separate test results from other output.

Rationale for implementation details:

* Use platform-dependent scripts (.sh, .cmd) because it doesn't seem to
  be possible to enable unstable feature `bindeps`
  (https://rust-lang.github.io/rfcs/3028-cargo-binary-dependencies.html)
  in "x.py" by default.
    * Here's a preexisting test that also uses per-platform
      specialization: `library/std/src/process/tests.rs`.

How to use:

```
./testbinary --format=junit -Zunstable-options --output_postprocess_executable=/usr/bin/echo --output_postprocess_args=abc123
```
aspotashev added a commit to aspotashev/rust that referenced this issue Mar 7, 2024
Rationale for adding this functionality:

* Bazel (build system) doesn't provide a way to process output from a
  binary (in this case, Rust test binary's output) other using a wrapper
  binary. However, using a wrapper binary potentially breaks debugging,
  because Bazel would suggest to debug the wrapper binary rather than
  the Rust test itself.
    * See bazelbuild/rules_rust#1303.
    * Cargo is not used in Rust Bazel rules.
    * Although we could wait for
      rust-lang#96290 and then modify Rust
      Bazel rules to pass `--logfile` on the command line to
      provisionally unblock
      bazelbuild/rules_rust#1303, that
      solution still wouldn't allow advanced test output postprocessing
      such as changing JSON/XML schema and injecting extra JUnit
      properties.
* Due to limitations of Rust libtest formatters, Rust developers often
  use a separate tool to postprocess the test results output (see
  comments to rust-lang#85563).
    * Examples of existing postprocessing tools:
	* https://crates.io/crates/cargo2junit
	* https://crates.io/crates/gitlab-report
	* https://crates.io/crates/cargo-suity
    * For these use cases, it might be helpful to use the new flags
      `--output_postprocess_executable`, `--output_postprocess_args`
      instead of piping the test output explicitly, e.g. to more
      reliably separate test results from other output.

Rationale for implementation details:

* Use platform-dependent scripts (.sh, .cmd) because it doesn't seem to
  be possible to enable unstable feature `bindeps`
  (https://rust-lang.github.io/rfcs/3028-cargo-binary-dependencies.html)
  in "x.py" by default.
    * Here's a preexisting test that also uses per-platform
      specialization: `library/std/src/process/tests.rs`.

How to use:

```
./testbinary --format=junit -Zunstable-options --output_postprocess_executable=/usr/bin/echo --output_postprocess_args=abc123
```
aspotashev added a commit to aspotashev/rust that referenced this issue Mar 8, 2024
Add the following two optional flags to `libtest` (rustc's built-in unit-test
framework), in order to support postprocessing of the test results using a
separate executable:

*   `--output_postprocess_executable [PATH]`
*   `--output_postprocess_args [ARGUMENT]` (can be repeated.)

If you don't pass `--output_postprocess_executable [PATH]`, the behavior stays
the same as before this commit. That is, the test results are sent to stdout.

If you pass `--output_postprocess_executable [PATH]`, `libtest` will

1.  Spawn a child process from the executable binary (aka *postprocessor*) at the given path.
2.  Pass the arguments from the `--output_postprocess_args [ARGUMENT]` flags (if
    any) to the child process. If `--output_postprocess_args` was used multiple
    times, all listed arguments will be passed in the original order.
3.  Propagate the environment variables to the child process.

The *postprocessor* executable is expected to wait for the end of input (EOF) and then terminate.

Usage example #1: Filter lines of the test results

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 --output_postprocess_executable=/usr/bin/grep --output_postprocess_args="test result"
test result: ok. 59 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.31s
```

Usage example rust-lang#2: Save test results into a file

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 --output_postprocess_executable=/usr/bin/sh --output_postprocess_args=-c --output_postprocess_args="cat > /tmp/output.txt"
```

Usage example rust-lang#3: Save test results into a file while keeping the command line
output

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 --output_postprocess_executable=/usr/bin/tee --output_postprocess_args="/tmp/output.txt"

running 60 tests
...
```

Usage example rust-lang#4: Prepend every line of test results with the value of an environment variable (to demonstrate environment variable propagation)

```shell
$ LOG_PREFIX=">>>" LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 --output_postprocess_executable=/usr/bin/sh --output_postprocess_args=-c --output_postprocess_args="sed s/^/\$LOG_PREFIX/"
>>>
>>>running 60 tests
...
```

Usage example rust-lang#5: Change format of JSON output (using
https://jqlang.github.io/jq/)

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 -Zunstable-options --format=json --output_postprocess_executable=/usr/bin/jq
```

Usage example rust-lang#6: Print test execution time in machine-readable format

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 -Zunstable-options --format=json --output_postprocess_executable=/usr/bin/jq --output_postprocess_args=".exec_time | numbers"
0.234317996
```

Rationale for adding this functionality:

*   Bazel (build system) doesn't provide a way to process output from a binary
    (in this case, Rust test binary's output) other using a wrapper binary.
    However, using a wrapper binary potentially breaks debugging, because Bazel
    would suggest to debug the wrapper binary rather than the Rust test itself.
    *   See bazelbuild/rules_rust#1303.
    *   Cargo is not used in Rust Bazel rules.
    *   Although we could wait for rust-lang#96290
        and then modify Rust Bazel rules to pass `--logfile` on the command line
        to provisionally unblock
        bazelbuild/rules_rust#1303, that solution
        still wouldn't allow advanced test results postprocessing such as
        changing JSON/XML schema and injecting extra JUnit properties.
*   Due to limitations of Rust libtest formatters, Rust developers often use a
    separate tool to postprocess the test results output (see comments to
    rust-lang#85563).
    *   Examples of existing postprocessing tools:
        *   https://crates.io/crates/cargo2junit
        *   https://crates.io/crates/gitlab-report
        *   https://crates.io/crates/cargo-suity
    *   For these use cases, it might be helpful to use the new flags
        `--output_postprocess_executable`, `--output_postprocess_args` instead
        of piping the test results explicitly, e.g. to more reliably separate
        test results from other output.

Rationale for implementation details:

*   Use platform-dependent scripts (.sh, .cmd) because it doesn't seem to be
    possible to enable unstable feature `bindeps`
    (https://rust-lang.github.io/rfcs/3028-cargo-binary-dependencies.html) in
    "x.py" by default.
    *   Here's a preexisting test that also uses per-platform specialization:
        `library/std/src/process/tests.rs`.
aspotashev added a commit to aspotashev/rust that referenced this issue Mar 8, 2024
Add the following two optional flags to `libtest` (rustc's built-in unit-test
framework), in order to support postprocessing of the test results using a
separate executable:

*   `--output_postprocess_executable [PATH]`
*   `--output_postprocess_args [ARGUMENT]` (can be repeated.)

If you don't pass `--output_postprocess_executable [PATH]`, the behavior stays
the same as before this commit. That is, the test results are sent to stdout.

If you pass `--output_postprocess_executable [PATH]`, `libtest` will

1.  Spawn a child process from the executable binary (aka *postprocessor*) at
    the given path.
2.  Pass the arguments from the `--output_postprocess_args [ARGUMENT]` flags (if
    any) to the child process. If `--output_postprocess_args` was used multiple
    times, all listed arguments will be passed in the original order.
3.  Propagate the environment variables to the child process.

The *postprocessor* executable is expected to wait for the end of input (EOF)
and then terminate.

Usage example #1: Filter lines of the test results

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 --output_postprocess_executable=/usr/bin/grep --output_postprocess_args="test result"
test result: ok. 59 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.31s
```

Usage example rust-lang#2: Save test results into a file

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 --output_postprocess_executable=/usr/bin/sh --output_postprocess_args=-c --output_postprocess_args="cat > /tmp/output.txt"
```

Usage example rust-lang#3: Save test results into a file while keeping the command line
output

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 --output_postprocess_executable=/usr/bin/tee --output_postprocess_args="/tmp/output.txt"

running 60 tests
...
```

Usage example rust-lang#4: Prepend every line of test results with the value of an
environment variable (to demonstrate environment variable propagation)

```shell
$ LOG_PREFIX=">>>" LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 --output_postprocess_executable=/usr/bin/sh --output_postprocess_args=-c --output_postprocess_args="sed s/^/\$LOG_PREFIX/"
>>>
>>>running 60 tests
...
```

Usage example rust-lang#5: Change format of JSON output (using
https://jqlang.github.io/jq/)

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 -Zunstable-options --format=json --output_postprocess_executable=/usr/bin/jq
```

Usage example rust-lang#6: Print test execution time in machine-readable format

```shell
$ LD_LIBRARY_PATH=$(pwd) ./test-05daf44cb501aee6 -Zunstable-options --format=json --output_postprocess_executable=/usr/bin/jq --output_postprocess_args=".exec_time | numbers"
0.234317996
```

Rationale for adding this functionality:

*   Bazel (build system) doesn't provide a way to process output from a binary
    (in this case, Rust test binary's output) other using a wrapper binary.
    However, using a wrapper binary potentially breaks debugging, because Bazel
    would suggest to debug the wrapper binary rather than the Rust test itself.
    *   See bazelbuild/rules_rust#1303.
    *   Cargo is not used in Rust Bazel rules.
    *   Although we could wait for rust-lang#96290
        and then modify Rust Bazel rules to pass `--logfile` on the command line
        to provisionally unblock
        bazelbuild/rules_rust#1303, that solution
        still wouldn't allow advanced test results postprocessing such as
        changing JSON/XML schema and injecting extra JUnit properties.
*   Due to limitations of Rust libtest formatters, Rust developers often use a
    separate tool to postprocess the test results output (see comments to
    rust-lang#85563).
    *   Examples of existing postprocessing tools:
        *   https://crates.io/crates/cargo2junit
        *   https://crates.io/crates/gitlab-report
        *   https://crates.io/crates/cargo-suity
    *   For these use cases, it might be helpful to use the new flags
        `--output_postprocess_executable`, `--output_postprocess_args` instead
        of piping the test results explicitly, e.g. to more reliably separate
        test results from other output.

Rationale for implementation details:

*   Use platform-dependent scripts (.sh, .cmd) because it doesn't seem to be
    possible to enable unstable feature `bindeps`
    (https://rust-lang.github.io/rfcs/3028-cargo-binary-dependencies.html) in
    "x.py" by default.
    *   Here's a preexisting test that also uses per-platform specialization:
        `library/std/src/process/tests.rs`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-libtest Area: `#[test]` / the `test` library C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
Status: No status
Development

No branches or pull requests

8 participants