diff --git a/tests/testkit/README.md b/tests/testkit/README.md index 03550c5418..44be1f6767 100644 --- a/tests/testkit/README.md +++ b/tests/testkit/README.md @@ -185,7 +185,9 @@ A filter selects tests whose name contains it. `run` also takes: skipped. - `--timeout SECS` (default 900): a test that runs longer gets SIGTERM, then SIGKILL after `--grace SECS` (default 10). -- `--junit PATH`: writes a JUnit report with one test suite per family. +- `--junit PATH`: writes a JUnit report with one test suite per family. The + report is rewritten after each test, so a run that is stopped from outside + still leaves the results so far. Each test runs in its own process group. When the test's process exits, `run` kills anything left in the group, such as CLI processes the test started. diff --git a/tests/testkit/testkit/src/program/junit.rs b/tests/testkit/testkit/src/program/junit.rs index 0a1e66d648..5d64ef3499 100644 --- a/tests/testkit/testkit/src/program/junit.rs +++ b/tests/testkit/testkit/src/program/junit.rs @@ -74,7 +74,13 @@ pub fn write(path: &Path, report: &Report) -> std::io::Result<()> { std::fs::create_dir_all(parent)?; } let xml = report.to_string().map_err(std::io::Error::other)?; - std::fs::write(path, xml) + // Replace the file in one step, so a run that is killed while writing + // leaves the previous report, not a truncated one. + let mut partial = path.as_os_str().to_owned(); + partial.push(".partial"); + let partial = std::path::PathBuf::from(partial); + std::fs::write(&partial, xml)?; + std::fs::rename(&partial, path) } #[cfg(test)] @@ -125,4 +131,25 @@ mod tests { assert!(xml.contains(expected), "missing {expected} in\n{xml}"); } } + + #[test] + fn write_replaces_the_report_and_leaves_no_partial_file() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("results/junit.xml"); + let one = [TestRun { + test: &PASSED, + outcome: Outcome::Passed, + duration: None, + stdout: Vec::new(), + stderr: Vec::new(), + }]; + super::write(&path, &report("p", &one)).unwrap(); + super::write(&path, &report("p", &[])).unwrap(); + let xml = std::fs::read_to_string(&path).unwrap(); + assert!(xml.contains(r#"tests="0""#), "{xml}"); + assert_eq!( + std::fs::read_dir(path.parent().unwrap()).unwrap().count(), + 1 + ); + } } diff --git a/tests/testkit/testkit/src/program/supervisor.rs b/tests/testkit/testkit/src/program/supervisor.rs index 6dced12ae1..35dc9f82a8 100644 --- a/tests/testkit/testkit/src/program/supervisor.rs +++ b/tests/testkit/testkit/src/program/supervisor.rs @@ -132,6 +132,13 @@ pub fn run(program: &str, exe: &Path, tests: &[Test], options: &Options) -> Exit ), } runs.push(run); + // Keep the report current, so a run that is stopped from outside, such + // as by a run-level timeout, still leaves the results so far. + if let Some(path) = &options.junit + && let Err(error) = junit::write(path, &junit::report(program, &runs)) + { + eprintln!("{program}: write {}: {error}", path.display()); + } } let not_run = selected_count - runs.len();