Skip to content

Commit d46ed4e

Browse files
chore: begin to port nextest-rs#1707 (comment) to main
1 parent 681cffa commit d46ed4e

File tree

10 files changed

+430
-110
lines changed

10 files changed

+430
-110
lines changed

cargo-nextest/src/dispatch.rs

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use nextest_runner::{
3737
redact::Redactor,
3838
reporter::{
3939
FinalStatusLevel, ReporterBuilder, StatusLevel, TestOutputDisplay, TestOutputErrorSlice,
40+
displayer::TestOutputDisplayStreams,
4041
events::{FinalRunStats, RunStatsFailureKind},
4142
highlight_end, structured,
4243
},
@@ -57,6 +58,7 @@ use std::{
5758
env::VarError,
5859
fmt,
5960
io::{Cursor, Write},
61+
str::FromStr,
6062
sync::{Arc, OnceLock},
6163
time::Duration,
6264
};
@@ -1027,13 +1029,19 @@ fn non_zero_duration(input: &str) -> Result<Duration, String> {
10271029
#[derive(Debug, Default, Args)]
10281030
#[command(next_help_heading = "Reporter options")]
10291031
struct ReporterOpts {
1030-
/// Output stdout and stderr on failure
1032+
/// Output stdout and/or stderr on failure
1033+
///
1034+
/// Takes the form of: '{value}' or 'stdout={value}' or 'stdout={value},stderr={value}'
1035+
/// where {value} is one of: 'immediate', 'immediate-final', 'final', 'never'
10311036
#[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_FAILURE_OUTPUT")]
1032-
failure_output: Option<TestOutputDisplayOpt>,
1037+
failure_output: Option<TestOutputDisplayStreamsOpt>,
10331038

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

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

1163+
#[derive(Debug, Clone, Copy)]
1164+
struct TestOutputDisplayStreamsOpt {
1165+
stdout: Option<TestOutputDisplayOpt>,
1166+
stderr: Option<TestOutputDisplayOpt>,
1167+
}
1168+
1169+
impl FromStr for TestOutputDisplayStreamsOpt {
1170+
type Err = String;
1171+
1172+
fn from_str(s: &str) -> Result<Self, Self::Err> {
1173+
// expected input has three forms
1174+
// - "{value}": where value is one of [immediate, immediate-final, final, never]
1175+
// - "{stream}={value}": where {stream} is one of [stdout, stderr]
1176+
// - "{stream}={value},{stream=value}": where the two {stream} keys cannot be the same
1177+
let (stdout, stderr) = if let Some((left, right)) = s.split_once(',') {
1178+
// the "{stream}={value},{stream=value}" case
1179+
let left = left
1180+
.split_once('=')
1181+
.map(|l| (l.0, TestOutputDisplayOpt::from_str(l.1, false)));
1182+
let right = right
1183+
.split_once('=')
1184+
.map(|r| (r.0, TestOutputDisplayOpt::from_str(r.1, false)));
1185+
match (left, right) {
1186+
(Some(("stderr", Ok(stderr))), Some(("stdout", Ok(stdout)))) => (Some(stdout), Some(stderr)),
1187+
(Some(("stdout", Ok(stdout))), Some(("stderr", Ok(stderr)))) => (Some(stdout), Some(stderr)),
1188+
(Some((stream @ "stdout" | stream @ "stderr", Err(_))), _) => return Err(format!("\n unrecognized setting for {stream}: [possible values: immediate, immediate-final, final, never]")),
1189+
(_, Some((stream @ "stdout" | stream @ "stderr", Err(_)))) => return Err(format!("\n unrecognized setting for {stream}: [possible values: immediate, immediate-final, final, never]")),
1190+
(Some(("stdout", _)), Some(("stdout", _))) => return Err("\n stdout specified twice".to_string()),
1191+
(Some(("stderr", _)), Some(("stderr", _))) => return Err("\n stderr specified twice".to_string()),
1192+
(Some((stream, _)), Some(("stdout" | "stderr", _))) => return Err(format!("\n unrecognized output stream '{stream}': [possible values: stdout, stderr]")),
1193+
(Some(("stdout" | "stderr", _)), Some((stream, _))) => return Err(format!("\n unrecognized output stream '{stream}': [possible values: stdout, stderr]")),
1194+
(_, _) => return Err("\n [possible values: immediate, immediate-final, final, never], or specify one or both output streams: stdout={}, stderr={}, stdout={},stderr={}".to_string()),
1195+
}
1196+
} else if let Some((stream, right)) = s.split_once('=') {
1197+
// the "{stream}={value}" case
1198+
let value = TestOutputDisplayOpt::from_str(right, false);
1199+
match (stream, value) {
1200+
("stderr", Ok(stderr)) => (None, Some(stderr)),
1201+
("stdout", Ok(stdout)) => (Some(stdout), None),
1202+
("stdout" | "stderr", Err(_)) => return Err(format!("\n unrecognized setting for {stream}: [possible values: immediate, immediate-final, final, never]")),
1203+
(_, _) => return Err("\n unrecognized output stream, possible values: [stdout={}, stderr={}, stdout={},stderr={}]".to_string())
1204+
}
1205+
} else if let Ok(value) = TestOutputDisplayOpt::from_str(s, false) {
1206+
// the "{value}" case
1207+
(Some(value), Some(value))
1208+
} else {
1209+
// did not recognize one of the three cases
1210+
return Err("\n [possible values: immediate, immediate-final, final, never], or specify one or both output streams: stdout={}, stderr={}, stdout={},stderr={}".to_string());
1211+
};
1212+
Ok(Self { stdout, stderr })
1213+
}
1214+
}
1215+
1216+
impl From<TestOutputDisplayStreamsOpt> for TestOutputDisplayStreams {
1217+
fn from(value: TestOutputDisplayStreamsOpt) -> Self {
1218+
Self {
1219+
stdout: value.stdout.map(TestOutputDisplay::from),
1220+
stderr: value.stderr.map(TestOutputDisplay::from),
1221+
}
1222+
}
1223+
}
1224+
11551225
#[derive(Clone, Copy, Debug, ValueEnum)]
11561226
enum TestOutputDisplayOpt {
11571227
Immediate,

nextest-runner/src/config/core/imp.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ use crate::{
2828
helpers::plural,
2929
list::TestList,
3030
platform::BuildPlatforms,
31-
reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplay},
31+
reporter::{
32+
FinalStatusLevel, StatusLevel, TestOutputDisplay, displayer::TestOutputDisplayStreams,
33+
},
3234
};
3335
use camino::{Utf8Path, Utf8PathBuf};
3436
use config::{
@@ -1049,14 +1051,14 @@ impl<'cfg> EvaluatableProfile<'cfg> {
10491051
}
10501052

10511053
/// Returns the failure output config for this profile.
1052-
pub fn failure_output(&self) -> TestOutputDisplay {
1054+
pub fn failure_output(&self) -> TestOutputDisplayStreams {
10531055
self.custom_profile
10541056
.and_then(|profile| profile.failure_output)
10551057
.unwrap_or(self.default_profile.failure_output)
10561058
}
10571059

10581060
/// Returns the failure output config for this profile.
1059-
pub fn success_output(&self) -> TestOutputDisplay {
1061+
pub fn success_output(&self) -> TestOutputDisplayStreams {
10601062
self.custom_profile
10611063
.and_then(|profile| profile.success_output)
10621064
.unwrap_or(self.default_profile.success_output)
@@ -1225,8 +1227,8 @@ pub(in crate::config) struct DefaultProfileImpl {
12251227
retries: RetryPolicy,
12261228
status_level: StatusLevel,
12271229
final_status_level: FinalStatusLevel,
1228-
failure_output: TestOutputDisplay,
1229-
success_output: TestOutputDisplay,
1230+
failure_output: TestOutputDisplayStreams,
1231+
success_output: TestOutputDisplayStreams,
12301232
max_fail: MaxFail,
12311233
slow_timeout: SlowTimeout,
12321234
global_timeout: GlobalTimeout,
@@ -1314,9 +1316,9 @@ pub(in crate::config) struct CustomProfileImpl {
13141316
#[serde(default)]
13151317
final_status_level: Option<FinalStatusLevel>,
13161318
#[serde(default)]
1317-
failure_output: Option<TestOutputDisplay>,
1319+
failure_output: Option<TestOutputDisplayStreams>,
13181320
#[serde(default)]
1319-
success_output: Option<TestOutputDisplay>,
1321+
success_output: Option<TestOutputDisplayStreams>,
13201322
#[serde(
13211323
default,
13221324
rename = "fail-fast",

nextest-runner/src/config/overrides/imp.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::{
1717
ConfigCompileError, ConfigCompileErrorKind, ConfigCompileSection, ConfigParseErrorKind,
1818
},
1919
platform::BuildPlatforms,
20-
reporter::TestOutputDisplay,
20+
reporter::{TestOutputDisplay, displayer::TestOutputDisplayStreams},
2121
};
2222
use guppy::graph::cargo::BuildPlatform;
2323
use nextest_filtering::{
@@ -109,8 +109,8 @@ pub struct TestSettings<'p, Source = ()> {
109109
slow_timeout: (SlowTimeout, Source),
110110
leak_timeout: (LeakTimeout, Source),
111111
test_group: (TestGroup, Source),
112-
success_output: (TestOutputDisplay, Source),
113-
failure_output: (TestOutputDisplay, Source),
112+
success_output: (TestOutputDisplayStreams, Source),
113+
failure_output: (TestOutputDisplayStreams, Source),
114114
junit_store_success_output: (bool, Source),
115115
junit_store_failure_output: (bool, Source),
116116
}
@@ -217,12 +217,12 @@ impl<'p> TestSettings<'p> {
217217
}
218218

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

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

@@ -704,8 +704,8 @@ pub(in crate::config) struct ProfileOverrideData {
704704
slow_timeout: Option<SlowTimeout>,
705705
leak_timeout: Option<LeakTimeout>,
706706
pub(in crate::config) test_group: Option<TestGroup>,
707-
success_output: Option<TestOutputDisplay>,
708-
failure_output: Option<TestOutputDisplay>,
707+
success_output: Option<TestOutputDisplayStreams>,
708+
failure_output: Option<TestOutputDisplayStreams>,
709709
junit: DeserializedJunitOutput,
710710
}
711711

@@ -947,9 +947,9 @@ pub(in crate::config) struct DeserializedOverride {
947947
#[serde(default)]
948948
test_group: Option<TestGroup>,
949949
#[serde(default)]
950-
success_output: Option<TestOutputDisplay>,
950+
success_output: Option<TestOutputDisplayStreams>,
951951
#[serde(default)]
952-
failure_output: Option<TestOutputDisplay>,
952+
failure_output: Option<TestOutputDisplayStreams>,
953953
#[serde(default)]
954954
junit: DeserializedJunitOutput,
955955
}
@@ -1122,8 +1122,14 @@ mod tests {
11221122
}
11231123
);
11241124
assert_eq!(overrides.test_group(), &test_group("my-group"));
1125-
assert_eq!(overrides.success_output(), TestOutputDisplay::Never);
1126-
assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
1125+
assert_eq!(
1126+
overrides.success_output(),
1127+
TestOutputDisplayStreams::create_never()
1128+
);
1129+
assert_eq!(
1130+
overrides.failure_output(),
1131+
TestOutputDisplayStreams::create_final()
1132+
);
11271133
// For clarity.
11281134
#[expect(clippy::bool_assert_comparison)]
11291135
{
@@ -1173,9 +1179,12 @@ mod tests {
11731179
assert_eq!(overrides.test_group(), &test_group("my-group"));
11741180
assert_eq!(
11751181
overrides.success_output(),
1176-
TestOutputDisplay::ImmediateFinal
1182+
TestOutputDisplayStreams::create_immediate_final()
1183+
);
1184+
assert_eq!(
1185+
overrides.failure_output(),
1186+
TestOutputDisplayStreams::create_final()
11771187
);
1178-
assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
11791188
// For clarity.
11801189
#[expect(clippy::bool_assert_comparison)]
11811190
{

0 commit comments

Comments
 (0)