Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 74 additions & 4 deletions cargo-nextest/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use nextest_runner::{
redact::Redactor,
reporter::{
FinalStatusLevel, ReporterBuilder, StatusLevel, TestOutputDisplay, TestOutputErrorSlice,
displayer::TestOutputDisplayStreams,
events::{FinalRunStats, RunStatsFailureKind},
highlight_end, structured,
},
Expand All @@ -57,6 +58,7 @@ use std::{
env::VarError,
fmt,
io::{Cursor, Write},
str::FromStr,
sync::{Arc, OnceLock},
time::Duration,
};
Expand Down Expand Up @@ -1027,13 +1029,19 @@ fn non_zero_duration(input: &str) -> Result<Duration, String> {
#[derive(Debug, Default, Args)]
#[command(next_help_heading = "Reporter options")]
struct ReporterOpts {
/// Output stdout and stderr on failure
/// Output stdout and/or stderr on failure
///
/// Takes the form of: '{value}' or 'stdout={value}' or 'stdout={value},stderr={value}'
/// where {value} is one of: 'immediate', 'immediate-final', 'final', 'never'
#[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_FAILURE_OUTPUT")]
failure_output: Option<TestOutputDisplayOpt>,
failure_output: Option<TestOutputDisplayStreamsOpt>,

/// Output stdout and stderr on success
/// Output stdout and/or stderr on success
///
/// Takes the form of: '{value}' or 'stdout={value}' or 'stdout={value},stderr={value}'
/// where {value} is one of: 'immediate', 'immediate-final', 'final', 'never'
#[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_SUCCESS_OUTPUT")]
success_output: Option<TestOutputDisplayOpt>,
success_output: Option<TestOutputDisplayStreamsOpt>,

// status_level does not conflict with --no-capture because pass vs skip still makes sense.
/// Test statuses to output
Expand Down Expand Up @@ -1152,6 +1160,68 @@ impl ReporterOpts {
}
}

#[derive(Debug, Clone, Copy)]
struct TestOutputDisplayStreamsOpt {
stdout: Option<TestOutputDisplayOpt>,
stderr: Option<TestOutputDisplayOpt>,
}

impl FromStr for TestOutputDisplayStreamsOpt {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
// expected input has three forms
// - "{value}": where value is one of [immediate, immediate-final, final, never]
// - "{stream}={value}": where {stream} is one of [stdout, stderr]
// - "{stream}={value},{stream=value}": where the two {stream} keys cannot be the same
let (stdout, stderr) = if let Some((left, right)) = s.split_once(',') {
Comment on lines +1173 to +1177
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's got to be a much better way to write this. One way would be to write a small left-to-right parser using winnow for this, reading characters until you reach a comma or equals sign. Another would be to first decompose the string into components, and then match on them.

This also needs tests.

// the "{stream}={value},{stream=value}" case
let left = left
.split_once('=')
.map(|l| (l.0, TestOutputDisplayOpt::from_str(l.1, false)));
let right = right
.split_once('=')
.map(|r| (r.0, TestOutputDisplayOpt::from_str(r.1, false)));
match (left, right) {
(Some(("stderr", Ok(stderr))), Some(("stdout", Ok(stdout)))) => (Some(stdout), Some(stderr)),
(Some(("stdout", Ok(stdout))), Some(("stderr", Ok(stderr)))) => (Some(stdout), Some(stderr)),
(Some((stream @ "stdout" | stream @ "stderr", Err(_))), _) => return Err(format!("\n unrecognized setting for {stream}: [possible values: immediate, immediate-final, final, never]")),
(_, Some((stream @ "stdout" | stream @ "stderr", Err(_)))) => return Err(format!("\n unrecognized setting for {stream}: [possible values: immediate, immediate-final, final, never]")),
Comment on lines +1188 to +1189
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return a structured error (TestOutputDisplayParseError or similar) and the formatting should be handled in the Display impl for the error.

(Some(("stdout", _)), Some(("stdout", _))) => return Err("\n stdout specified twice".to_string()),
(Some(("stderr", _)), Some(("stderr", _))) => return Err("\n stderr specified twice".to_string()),
(Some((stream, _)), Some(("stdout" | "stderr", _))) => return Err(format!("\n unrecognized output stream '{stream}': [possible values: stdout, stderr]")),
(Some(("stdout" | "stderr", _)), Some((stream, _))) => return Err(format!("\n unrecognized output stream '{stream}': [possible values: stdout, stderr]")),
(_, _) => return Err("\n [possible values: immediate, immediate-final, final, never], or specify one or both output streams: stdout={}, stderr={}, stdout={},stderr={}".to_string()),
}
} else if let Some((stream, right)) = s.split_once('=') {
// the "{stream}={value}" case
let value = TestOutputDisplayOpt::from_str(right, false);
match (stream, value) {
("stderr", Ok(stderr)) => (None, Some(stderr)),
("stdout", Ok(stdout)) => (Some(stdout), None),
("stdout" | "stderr", Err(_)) => return Err(format!("\n unrecognized setting for {stream}: [possible values: immediate, immediate-final, final, never]")),
(_, _) => return Err("\n unrecognized output stream, possible values: [stdout={}, stderr={}, stdout={},stderr={}]".to_string())
}
} else if let Ok(value) = TestOutputDisplayOpt::from_str(s, false) {
// the "{value}" case
(Some(value), Some(value))
} else {
// did not recognize one of the three cases
return Err("\n [possible values: immediate, immediate-final, final, never], or specify one or both output streams: stdout={}, stderr={}, stdout={},stderr={}".to_string());
};
Ok(Self { stdout, stderr })
}
}

impl From<TestOutputDisplayStreamsOpt> for TestOutputDisplayStreams {
fn from(value: TestOutputDisplayStreamsOpt) -> Self {
Self {
stdout: value.stdout.map(TestOutputDisplay::from),
stderr: value.stderr.map(TestOutputDisplay::from),
}
}
}

#[derive(Clone, Copy, Debug, ValueEnum)]
enum TestOutputDisplayOpt {
Immediate,
Expand Down
14 changes: 7 additions & 7 deletions nextest-runner/src/config/core/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::{
helpers::plural,
list::TestList,
platform::BuildPlatforms,
reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplay},
reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplayStreams},
};
use camino::{Utf8Path, Utf8PathBuf};
use config::{
Expand Down Expand Up @@ -1049,14 +1049,14 @@ impl<'cfg> EvaluatableProfile<'cfg> {
}

/// Returns the failure output config for this profile.
pub fn failure_output(&self) -> TestOutputDisplay {
pub fn failure_output(&self) -> TestOutputDisplayStreams {
self.custom_profile
.and_then(|profile| profile.failure_output)
.unwrap_or(self.default_profile.failure_output)
}

/// Returns the failure output config for this profile.
pub fn success_output(&self) -> TestOutputDisplay {
pub fn success_output(&self) -> TestOutputDisplayStreams {
self.custom_profile
.and_then(|profile| profile.success_output)
.unwrap_or(self.default_profile.success_output)
Expand Down Expand Up @@ -1225,8 +1225,8 @@ pub(in crate::config) struct DefaultProfileImpl {
retries: RetryPolicy,
status_level: StatusLevel,
final_status_level: FinalStatusLevel,
failure_output: TestOutputDisplay,
success_output: TestOutputDisplay,
failure_output: TestOutputDisplayStreams,
success_output: TestOutputDisplayStreams,
max_fail: MaxFail,
slow_timeout: SlowTimeout,
global_timeout: GlobalTimeout,
Expand Down Expand Up @@ -1314,9 +1314,9 @@ pub(in crate::config) struct CustomProfileImpl {
#[serde(default)]
final_status_level: Option<FinalStatusLevel>,
#[serde(default)]
failure_output: Option<TestOutputDisplay>,
failure_output: Option<TestOutputDisplayStreams>,
#[serde(default)]
success_output: Option<TestOutputDisplay>,
success_output: Option<TestOutputDisplayStreams>,
#[serde(
default,
rename = "fail-fast",
Expand Down
35 changes: 22 additions & 13 deletions nextest-runner/src/config/overrides/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::{
ConfigCompileError, ConfigCompileErrorKind, ConfigCompileSection, ConfigParseErrorKind,
},
platform::BuildPlatforms,
reporter::TestOutputDisplay,
reporter::TestOutputDisplayStreams,
};
use guppy::graph::cargo::BuildPlatform;
use nextest_filtering::{
Expand Down Expand Up @@ -109,8 +109,8 @@ pub struct TestSettings<'p, Source = ()> {
slow_timeout: (SlowTimeout, Source),
leak_timeout: (LeakTimeout, Source),
test_group: (TestGroup, Source),
success_output: (TestOutputDisplay, Source),
failure_output: (TestOutputDisplay, Source),
success_output: (TestOutputDisplayStreams, Source),
failure_output: (TestOutputDisplayStreams, Source),
junit_store_success_output: (bool, Source),
junit_store_failure_output: (bool, Source),
}
Expand Down Expand Up @@ -217,12 +217,12 @@ impl<'p> TestSettings<'p> {
}

/// Returns the success output setting for this test.
pub fn success_output(&self) -> TestOutputDisplay {
pub fn success_output(&self) -> TestOutputDisplayStreams {
self.success_output.0
}

/// Returns the failure output setting for this test.
pub fn failure_output(&self) -> TestOutputDisplay {
pub fn failure_output(&self) -> TestOutputDisplayStreams {
self.failure_output.0
}

Expand Down Expand Up @@ -704,8 +704,8 @@ pub(in crate::config) struct ProfileOverrideData {
slow_timeout: Option<SlowTimeout>,
leak_timeout: Option<LeakTimeout>,
pub(in crate::config) test_group: Option<TestGroup>,
success_output: Option<TestOutputDisplay>,
failure_output: Option<TestOutputDisplay>,
success_output: Option<TestOutputDisplayStreams>,
failure_output: Option<TestOutputDisplayStreams>,
junit: DeserializedJunitOutput,
}

Expand Down Expand Up @@ -947,9 +947,9 @@ pub(in crate::config) struct DeserializedOverride {
#[serde(default)]
test_group: Option<TestGroup>,
#[serde(default)]
success_output: Option<TestOutputDisplay>,
success_output: Option<TestOutputDisplayStreams>,
#[serde(default)]
failure_output: Option<TestOutputDisplay>,
failure_output: Option<TestOutputDisplayStreams>,
#[serde(default)]
junit: DeserializedJunitOutput,
}
Expand Down Expand Up @@ -1122,8 +1122,14 @@ mod tests {
}
);
assert_eq!(overrides.test_group(), &test_group("my-group"));
assert_eq!(overrides.success_output(), TestOutputDisplay::Never);
assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
assert_eq!(
overrides.success_output(),
TestOutputDisplayStreams::create_never()
);
assert_eq!(
overrides.failure_output(),
TestOutputDisplayStreams::create_final()
);
// For clarity.
#[expect(clippy::bool_assert_comparison)]
{
Expand Down Expand Up @@ -1173,9 +1179,12 @@ mod tests {
assert_eq!(overrides.test_group(), &test_group("my-group"));
assert_eq!(
overrides.success_output(),
TestOutputDisplay::ImmediateFinal
TestOutputDisplayStreams::create_immediate_final()
);
assert_eq!(
overrides.failure_output(),
TestOutputDisplayStreams::create_final()
);
assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
// For clarity.
#[expect(clippy::bool_assert_comparison)]
{
Expand Down
Loading
Loading