From e39e127bd0d2baa51b57671f1fa00d0c4e7afbbb Mon Sep 17 00:00:00 2001 From: Oleksii Lozovskyi Date: Sat, 13 Dec 2025 16:19:33 +0900 Subject: [PATCH 1/4] libtest: Stricter XML validation Expect the entire input to be a single XML document, instead of reading it line by line. This detects trailing junk better. --- tests/run-make/libtest-junit/validate_junit.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/run-make/libtest-junit/validate_junit.py b/tests/run-make/libtest-junit/validate_junit.py index f92473751b036..a9cb0a059563d 100755 --- a/tests/run-make/libtest-junit/validate_junit.py +++ b/tests/run-make/libtest-junit/validate_junit.py @@ -13,10 +13,10 @@ import sys import xml.etree.ElementTree as ET -# Try to decode line in order to ensure it is a valid XML document -for line in sys.stdin: - try: - ET.fromstring(line) - except ET.ParseError: - print("Invalid xml: %r" % line) - raise +# Read the entire output and try to decode it as XML. +junit = sys.stdin.read() +try: + ET.fromstring(junit) +except ET.ParseError: + print("Invalid xml: %r" % junit) + raise From 2cc434863c56a925d56e979cbfb1d824040d1cd7 Mon Sep 17 00:00:00 2001 From: Oleksii Lozovskyi Date: Sat, 13 Dec 2025 14:29:44 +0900 Subject: [PATCH 2/4] rustdoc: Test --format=junit Copied validate_junit.py from libtest-junit. JUnit format works well in edition 2021, but is currently broken in edition 2024 by the mergeable doctest report. --- .../run-make/doctests-junit/doctest-2021.xml | 1 + tests/run-make/doctests-junit/doctest.rs | 14 ++++ tests/run-make/doctests-junit/rmake.rs | 72 +++++++++++++++++++ .../run-make/doctests-junit/validate_junit.py | 22 ++++++ 4 files changed, 109 insertions(+) create mode 100644 tests/run-make/doctests-junit/doctest-2021.xml create mode 100644 tests/run-make/doctests-junit/doctest.rs create mode 100644 tests/run-make/doctests-junit/rmake.rs create mode 100755 tests/run-make/doctests-junit/validate_junit.py diff --git a/tests/run-make/doctests-junit/doctest-2021.xml b/tests/run-make/doctests-junit/doctest-2021.xml new file mode 100644 index 0000000000000..5facfb80ce622 --- /dev/null +++ b/tests/run-make/doctests-junit/doctest-2021.xml @@ -0,0 +1 @@ + diff --git a/tests/run-make/doctests-junit/doctest.rs b/tests/run-make/doctests-junit/doctest.rs new file mode 100644 index 0000000000000..1873d63a49c63 --- /dev/null +++ b/tests/run-make/doctests-junit/doctest.rs @@ -0,0 +1,14 @@ +/// ``` +/// assert_eq!(doctest::add(2, 2), 4); +/// ``` +/// +/// ```should_panic +/// assert_eq!(doctest::add(2, 2), 5); +/// ``` +/// +/// ```compile_fail +/// assert_eq!(doctest::add(2, 2), "banana"); +/// ``` +pub fn add(a: i32, b: i32) -> i32 { + a + b +} diff --git a/tests/run-make/doctests-junit/rmake.rs b/tests/run-make/doctests-junit/rmake.rs new file mode 100644 index 0000000000000..40dc541b5d2ce --- /dev/null +++ b/tests/run-make/doctests-junit/rmake.rs @@ -0,0 +1,72 @@ +// Check rustdoc's test JUnit (XML) output against snapshots. + +//@ ignore-cross-compile (running doctests) +//@ needs-unwind (test file contains `should_panic` test) + +use std::path::Path; + +use run_make_support::{cwd, diff, python_command, rustc, rustdoc}; + +fn main() { + let rlib = cwd().join("libdoctest.rlib"); + rustc().input("doctest.rs").crate_type("rlib").output(&rlib).run(); + + run_doctests(&rlib, "2021", "doctest-2021.xml"); + run_doctests_fail(&rlib, "2024"); +} + +#[track_caller] +fn run_doctests(rlib: &Path, edition: &str, expected_xml: &str) { + let rustdoc_out = rustdoc() + .input("doctest.rs") + .args(&[ + "--test", + "--test-args=-Zunstable-options", + "--test-args=--test-threads=1", + "--test-args=--format=junit", + ]) + .edition(edition) + .env("RUST_BACKTRACE", "0") + .extern_("doctest", rlib.display().to_string()) + .run(); + let rustdoc_stdout = &rustdoc_out.stdout_utf8(); + + python_command().arg("validate_junit.py").stdin_buf(rustdoc_stdout).run(); + + diff() + .expected_file(expected_xml) + .actual_text("output", rustdoc_stdout) + .normalize(r#"\btime="[0-9.]+""#, r#"time="$$TIME""#) + .run(); +} + +// FIXME: gone in the next patch +#[track_caller] +fn run_doctests_fail(rlib: &Path, edition: &str) { + let rustdoc_out = rustdoc() + .input("doctest.rs") + .args(&[ + "--test", + "--test-args=-Zunstable-options", + "--test-args=--test-threads=1", + "--test-args=--format=junit", + ]) + .edition(edition) + .env("RUST_BACKTRACE", "0") + .extern_("doctest", rlib.display().to_string()) + .run_fail(); + let rustdoc_stderr = &rustdoc_out.stderr_utf8(); + + diff() + .expected_text( + "expected", + r#" +thread 'main' ($TID) panicked at library/test/src/formatters/junit.rs:22:9: +assertion failed: !s.contains('\n') +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +"#, + ) + .actual_text("actual", rustdoc_stderr) + .normalize(r#"thread 'main' \([0-9]+\)"#, r#"thread 'main' ($$TID)"#) + .run(); +} diff --git a/tests/run-make/doctests-junit/validate_junit.py b/tests/run-make/doctests-junit/validate_junit.py new file mode 100755 index 0000000000000..a9cb0a059563d --- /dev/null +++ b/tests/run-make/doctests-junit/validate_junit.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +# Trivial Python script that reads lines from stdin, and checks that each line +# is a well-formed XML document. +# +# This takes advantage of the fact that Python has a built-in XML parser, +# whereas doing the same check in Rust would require us to pull in an XML +# crate just for this relatively-minor test. +# +# If you're trying to remove Python scripts from the test suite, think twice +# before removing this one. You could do so, but it's probably not worth it. + +import sys +import xml.etree.ElementTree as ET + +# Read the entire output and try to decode it as XML. +junit = sys.stdin.read() +try: + ET.fromstring(junit) +except ET.ParseError: + print("Invalid xml: %r" % junit) + raise From 069cf9dfc906452d90d4e2898eac568fb558d25d Mon Sep 17 00:00:00 2001 From: Oleksii Lozovskyi Date: Sat, 29 Nov 2025 09:46:48 +0900 Subject: [PATCH 3/4] rustdoc: Write newline differently Fix the panic in write_message() which expects messages to contain no embedded newlines. We still want a trailing newline at the end of the file though, so write it in different manner. Doctest runner no longer panics, but the output is kinda broken when `compile_fail` doctests are present. This is because they are not mergeable. --- library/test/src/formatters/junit.rs | 6 ++- .../run-make/doctests-junit/doctest-2024.xml | 3 ++ tests/run-make/doctests-junit/rmake.rs | 40 +++---------------- 3 files changed, 13 insertions(+), 36 deletions(-) create mode 100644 tests/run-make/doctests-junit/doctest-2024.xml diff --git a/library/test/src/formatters/junit.rs b/library/test/src/formatters/junit.rs index 1566f1cb1dac6..74d99e0f1270e 100644 --- a/library/test/src/formatters/junit.rs +++ b/library/test/src/formatters/junit.rs @@ -189,8 +189,10 @@ impl OutputFormatter for JunitFormatter { compilation_time: f64, ) -> io::Result<()> { self.write_message(&format!( - "\n", - )) + "", + ))?; + self.out.write_all(b"\n")?; + Ok(()) } } diff --git a/tests/run-make/doctests-junit/doctest-2024.xml b/tests/run-make/doctests-junit/doctest-2024.xml new file mode 100644 index 0000000000000..4f94f01c3e32c --- /dev/null +++ b/tests/run-make/doctests-junit/doctest-2024.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/run-make/doctests-junit/rmake.rs b/tests/run-make/doctests-junit/rmake.rs index 40dc541b5d2ce..4cc7ac7c31d7f 100644 --- a/tests/run-make/doctests-junit/rmake.rs +++ b/tests/run-make/doctests-junit/rmake.rs @@ -12,7 +12,7 @@ fn main() { rustc().input("doctest.rs").crate_type("rlib").output(&rlib).run(); run_doctests(&rlib, "2021", "doctest-2021.xml"); - run_doctests_fail(&rlib, "2024"); + run_doctests(&rlib, "2024", "doctest-2024.xml"); } #[track_caller] @@ -31,42 +31,14 @@ fn run_doctests(rlib: &Path, edition: &str, expected_xml: &str) { .run(); let rustdoc_stdout = &rustdoc_out.stdout_utf8(); - python_command().arg("validate_junit.py").stdin_buf(rustdoc_stdout).run(); + // FIXME: merged output of compile_fail tests is broken + if edition != "2024" { + python_command().arg("validate_junit.py").stdin_buf(rustdoc_stdout).run(); + } diff() .expected_file(expected_xml) .actual_text("output", rustdoc_stdout) - .normalize(r#"\btime="[0-9.]+""#, r#"time="$$TIME""#) - .run(); -} - -// FIXME: gone in the next patch -#[track_caller] -fn run_doctests_fail(rlib: &Path, edition: &str) { - let rustdoc_out = rustdoc() - .input("doctest.rs") - .args(&[ - "--test", - "--test-args=-Zunstable-options", - "--test-args=--test-threads=1", - "--test-args=--format=junit", - ]) - .edition(edition) - .env("RUST_BACKTRACE", "0") - .extern_("doctest", rlib.display().to_string()) - .run_fail(); - let rustdoc_stderr = &rustdoc_out.stderr_utf8(); - - diff() - .expected_text( - "expected", - r#" -thread 'main' ($TID) panicked at library/test/src/formatters/junit.rs:22:9: -assertion failed: !s.contains('\n') -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace -"#, - ) - .actual_text("actual", rustdoc_stderr) - .normalize(r#"thread 'main' \([0-9]+\)"#, r#"thread 'main' ($$TID)"#) + .normalize(r#"\b(time|total_time|compilation_time)="[0-9.]+""#, r#"$1="$$TIME""#) .run(); } From a8f4a6744c5927053e3415704d54daf463e5eec1 Mon Sep 17 00:00:00 2001 From: Oleksii Lozovskyi Date: Tue, 16 Dec 2025 21:10:29 +0900 Subject: [PATCH 4/4] rustdoc: Test only in stage2 stage1's rustdoc is using stage0's libtest which does not have a fix from 069cf9dfc90, making the test run fail. Ensure that this test is executed with everything recompiled in stage2. --- tests/run-make/doctests-junit/rmake.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/run-make/doctests-junit/rmake.rs b/tests/run-make/doctests-junit/rmake.rs index 4cc7ac7c31d7f..e3198502bb174 100644 --- a/tests/run-make/doctests-junit/rmake.rs +++ b/tests/run-make/doctests-junit/rmake.rs @@ -1,6 +1,7 @@ // Check rustdoc's test JUnit (XML) output against snapshots. //@ ignore-cross-compile (running doctests) +//@ ignore-stage1 (rustdoc depends on a fix in libtest) //@ needs-unwind (test file contains `should_panic` test) use std::path::Path;