Skip to content

Commit

Permalink
[nextest-runner] add setup scripts to JUnit output
Browse files Browse the repository at this point in the history
A coworker of mine flagged this issue, where a failing setup script resulted in
an empty JUnit report.

With this change, each setup script becomes its own test suite with a single
test case within it.
  • Loading branch information
sunshowers committed Dec 11, 2024
1 parent 4469857 commit 4024461
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 56 deletions.
34 changes: 34 additions & 0 deletions nextest-runner/src/config/scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,10 @@ pub struct ScriptConfig {
/// Whether to capture standard error for this command.
#[serde(default)]
pub capture_stderr: bool,

/// JUnit configuration for this script.
#[serde(default)]
pub junit: ScriptJunitConfig,
}

impl ScriptConfig {
Expand All @@ -529,6 +533,36 @@ impl ScriptConfig {
}
}

/// A JUnit override configuration.
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ScriptJunitConfig {
/// Whether to store successful output.
///
/// Defaults to false.
#[serde(default)]
pub store_success_output: bool,

/// Whether to store failing output.
///
/// Defaults to true.
#[serde(default = "default_true")]
pub store_failure_output: bool,
}

impl Default for ScriptJunitConfig {
fn default() -> Self {
Self {
store_success_output: false,
store_failure_output: true,
}
}
}

fn default_true() -> bool {
true
}

Check warning on line 564 in nextest-runner/src/config/scripts.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/config/scripts.rs#L562-L564

Added lines #L562 - L564 were not covered by tests

fn deserialize_script_ids<'de, D>(deserializer: D) -> Result<Vec<ScriptId>, D::Error>
where
D: serde::Deserializer<'de>,
Expand Down
162 changes: 106 additions & 56 deletions nextest-runner/src/reporter/aggregator/junit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
//! Code to generate JUnit XML reports from test events.
use crate::{
config::JunitConfig,
config::{JunitConfig, ScriptId},
errors::{DisplayErrorChain, WriteEventError},
list::TestInstanceId,
reporter::{
events::{
ExecuteStatus, ExecutionDescription, ExecutionResult, TestEvent, TestEventKind,
UnitKind,
},
events::{ExecutionDescription, ExecutionResult, TestEvent, TestEventKind, UnitKind},
UnitErrorDescription,
},
test_output::{ChildExecutionOutput, ChildOutput},
Expand All @@ -21,7 +18,7 @@ use nextest_metadata::RustBinaryId;
use quick_junit::{
NonSuccessKind, Report, TestCase, TestCaseStatus, TestRerun, TestSuite, XmlString,
};
use std::{borrow::Cow, collections::HashMap, fmt, fs::File};
use std::{collections::HashMap, fmt, fs::File};

#[derive(Clone, Debug)]
pub(super) struct MetadataJunit<'cfg> {
Expand All @@ -42,10 +39,56 @@ impl<'cfg> MetadataJunit<'cfg> {
TestEventKind::RunStarted { .. }
| TestEventKind::RunPaused { .. }
| TestEventKind::RunContinued { .. } => {}
TestEventKind::SetupScriptStarted { .. }
| TestEventKind::SetupScriptSlow { .. }
| TestEventKind::SetupScriptFinished { .. }
| TestEventKind::InfoStarted { .. }
TestEventKind::SetupScriptStarted { .. } | TestEventKind::SetupScriptSlow { .. } => {}

Check warning on line 42 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L41-L42

Added lines #L41 - L42 were not covered by tests
TestEventKind::SetupScriptFinished {
index: _,
total: _,
script_id,
command,
args,
junit_store_success_output,
junit_store_failure_output,
no_capture: _,
run_status,
} => {
let is_success = run_status.result.is_success();

let test_suite = self.testsuite_for_setup_script(script_id.clone());
let testcase_status = if is_success {
TestCaseStatus::success()

Check warning on line 58 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L46-L58

Added lines #L46 - L58 were not covered by tests
} else {
let (kind, ty) = non_success_kind_and_type(UnitKind::Script, run_status.result);
let mut testcase_status = TestCaseStatus::non_success(kind);
testcase_status.set_type(ty);
testcase_status

Check warning on line 63 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L60-L63

Added lines #L60 - L63 were not covered by tests
};

let mut testcase =
TestCase::new(script_id.as_identifier().as_str(), testcase_status);
// classname doesn't quite make sense for setup scripts, but it
// is required by the spec at https://llg.cubic.org/docs/junit/.
// We use the same name as the test suite.
testcase
.set_classname(test_suite.name.clone())
.set_timestamp(run_status.start_time)
.set_time(run_status.time_taken);

Check warning on line 74 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L66-L74

Added lines #L66 - L74 were not covered by tests

let store_stdout_stderr = (junit_store_success_output && is_success)
|| (junit_store_failure_output && !is_success);

Check warning on line 77 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L76-L77

Added lines #L76 - L77 were not covered by tests

set_execute_status_props(
&run_status.output,
store_stdout_stderr,
TestcaseOrRerun::Testcase(&mut testcase),
);

test_suite.add_test_case(testcase);

// Add properties corresponding to the setup script.
test_suite.add_property(("command", command));
test_suite.add_property(("args".to_owned(), shell_words::join(args)));

Check warning on line 89 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L79-L89

Added lines #L79 - L89 were not covered by tests
}
TestEventKind::InfoStarted { .. }
| TestEventKind::InfoResponse { .. }
| TestEventKind::InfoFinished { .. } => {}
TestEventKind::TestStarted { .. } => {}
Expand All @@ -61,47 +104,7 @@ impl<'cfg> MetadataJunit<'cfg> {
junit_store_failure_output,
..
} => {
fn kind_ty(run_status: &ExecuteStatus) -> (NonSuccessKind, Cow<'static, str>) {
match run_status.result {
ExecutionResult::Fail {
abort_status: Some(_),
leaked: true,
} => (
NonSuccessKind::Failure,
"test abort with leaked handles".into(),
),
ExecutionResult::Fail {
abort_status: Some(_),
leaked: false,
} => (NonSuccessKind::Failure, "test abort".into()),
ExecutionResult::Fail {
abort_status: None,
leaked: true,
} => (
NonSuccessKind::Failure,
"test failure with leaked handles".into(),
),
ExecutionResult::Fail {
abort_status: None,
leaked: false,
} => (NonSuccessKind::Failure, "test failure".into()),
ExecutionResult::Timeout => {
(NonSuccessKind::Failure, "test timeout".into())
}
ExecutionResult::ExecFail => {
(NonSuccessKind::Error, "execution failure".into())
}
ExecutionResult::Leak => (
NonSuccessKind::Error,
"test passed but leaked handles".into(),
),
ExecutionResult::Pass => {
unreachable!("this is a failure status")
}
}
}

let testsuite = self.testsuite_for(test_instance.id());
let testsuite = self.testsuite_for_test(test_instance.id());

Check warning on line 107 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L101-L107

Added lines #L101 - L107 were not covered by tests

let (mut testcase_status, main_status, reruns) = match run_statuses.describe() {
ExecutionDescription::Success { single_status } => {
Expand All @@ -116,15 +119,16 @@ impl<'cfg> MetadataJunit<'cfg> {
retries,
..
} => {
let (kind, ty) = kind_ty(first_status);
let (kind, ty) =
non_success_kind_and_type(UnitKind::Test, first_status.result);
let mut testcase_status = TestCaseStatus::non_success(kind);
testcase_status.set_type(ty);
(testcase_status, first_status, retries)

Check warning on line 126 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L118-L126

Added lines #L118 - L126 were not covered by tests
}
};

for rerun in reruns {
let (kind, ty) = kind_ty(rerun);
let (kind, ty) = non_success_kind_and_type(UnitKind::Test, rerun.result);
let mut test_rerun = TestRerun::new(kind);
test_rerun
.set_timestamp(rerun.start_time)
Expand Down Expand Up @@ -213,27 +217,73 @@ impl<'cfg> MetadataJunit<'cfg> {
Ok(())
}

Check warning on line 218 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L217-L218

Added lines #L217 - L218 were not covered by tests

fn testsuite_for(&mut self, test_instance: TestInstanceId<'cfg>) -> &mut TestSuite {
fn testsuite_for_setup_script(&mut self, script_id: ScriptId) -> &mut TestSuite {
let key = SuiteKey::SetupScript(script_id.clone());
self.test_suites
.entry(key.clone())
.or_insert_with(|| TestSuite::new(key.to_string()))
}

Check warning on line 225 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L220-L225

Added lines #L220 - L225 were not covered by tests

fn testsuite_for_test(&mut self, test_instance: TestInstanceId<'cfg>) -> &mut TestSuite {
let key = SuiteKey::TestBinary(test_instance.binary_id);
self.test_suites
.entry(key)
.entry(key.clone())
.or_insert_with(|| TestSuite::new(key.to_string()))
}

Check warning on line 232 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L227-L232

Added lines #L227 - L232 were not covered by tests
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
enum SuiteKey<'cfg> {
// Each script gets a separate suite, because in the future we'll likely want to set u
SetupScript(ScriptId),
TestBinary(&'cfg RustBinaryId),
}

impl fmt::Display for SuiteKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SuiteKey::SetupScript(script_id) => write!(f, "@setup-script:{}", script_id),
SuiteKey::TestBinary(binary_id) => write!(f, "{}", binary_id),

Check warning on line 246 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L243-L246

Added lines #L243 - L246 were not covered by tests
}
}

Check warning on line 248 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L248

Added line #L248 was not covered by tests
}

fn non_success_kind_and_type(kind: UnitKind, result: ExecutionResult) -> (NonSuccessKind, String) {
match result {

Check warning on line 252 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L251-L252

Added lines #L251 - L252 were not covered by tests
ExecutionResult::Fail {
abort_status: Some(_),
leaked: true,
} => (
NonSuccessKind::Failure,
format!("{kind} abort with leaked handles"),
),

Check warning on line 259 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L256-L259

Added lines #L256 - L259 were not covered by tests
ExecutionResult::Fail {
abort_status: Some(_),
leaked: false,
} => (NonSuccessKind::Failure, format!("{kind} abort")),

Check warning on line 263 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L263

Added line #L263 was not covered by tests
ExecutionResult::Fail {
abort_status: None,
leaked: true,
} => (
NonSuccessKind::Failure,
format!("{kind} failure with leaked handles"),
),

Check warning on line 270 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L267-L270

Added lines #L267 - L270 were not covered by tests
ExecutionResult::Fail {
abort_status: None,
leaked: false,
} => (NonSuccessKind::Failure, format!("{kind} failure")),
ExecutionResult::Timeout => (NonSuccessKind::Failure, format!("{kind} timeout")),
ExecutionResult::ExecFail => (NonSuccessKind::Error, "execution failure".to_owned()),
ExecutionResult::Leak => (
NonSuccessKind::Error,
format!("{kind} passed but leaked handles"),
),

Check warning on line 280 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L274-L280

Added lines #L274 - L280 were not covered by tests
ExecutionResult::Pass => {
unreachable!("this is a failure status")

Check warning on line 282 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L282

Added line #L282 was not covered by tests
}
}
}

Check warning on line 285 in nextest-runner/src/reporter/aggregator/junit.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/aggregator/junit.rs#L285

Added line #L285 was not covered by tests

enum TestcaseOrRerun<'a> {
Testcase(&'a mut TestCase),
Rerun(&'a mut TestRerun),
Expand Down
6 changes: 6 additions & 0 deletions nextest-runner/src/reporter/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ pub enum TestEventKind<'a> {
/// The arguments to the command.
args: &'a [String],

/// Whether the JUnit report should store success output for this script.
junit_store_success_output: bool,

/// Whether the JUnit report should store failure output for this script.
junit_store_failure_output: bool,

/// True if some output from the setup script was passed through.
no_capture: bool,

Expand Down
2 changes: 2 additions & 0 deletions nextest-runner/src/runner/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ where
command: config.program(),
args: config.args(),
no_capture: config.no_capture(),
junit_store_success_output: config.junit.store_success_output,
junit_store_failure_output: config.junit.store_failure_output,
run_status: status,
});

Expand Down

0 comments on commit 4024461

Please sign in to comment.