Skip to content

Commit fa467ae

Browse files
committed
test: add matches_gnu() method for comparing output against GNU coreutils
1 parent c085cd1 commit fa467ae

File tree

2 files changed

+140
-3
lines changed

2 files changed

+140
-3
lines changed

tests/by-util/test_echo.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,21 @@ use uutests::util::UCommand;
1111

1212
#[test]
1313
fn test_default() {
14-
new_ucmd!().arg("hi").succeeds().stdout_only("hi\n");
14+
new_ucmd!()
15+
.arg("hi")
16+
.succeeds()
17+
.stdout_only("hi\n")
18+
.matches_gnu();
1519
}
1620

1721
#[test]
1822
fn test_no_trailing_newline() {
19-
new_ucmd!().arg("-n").arg("hi").succeeds().stdout_only("hi");
23+
new_ucmd!()
24+
.arg("-n")
25+
.arg("hi")
26+
.succeeds()
27+
.stdout_only("hi")
28+
.matches_gnu();
2029
}
2130

2231
#[test]

tests/uutests/src/lib/util.rs

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,23 @@ pub struct CmdResult {
120120
stdout: Vec<u8>,
121121
/// captured standard error after running the Command
122122
stderr: Vec<u8>,
123+
/// arguments used to run the command (for GNU comparison)
124+
args: Vec<OsString>,
125+
/// environment variables used to run the command (for GNU comparison)
126+
env_vars: Vec<(OsString, OsString)>,
123127
}
124128

125129
impl CmdResult {
130+
#[allow(clippy::too_many_arguments)]
126131
pub fn new<S, T, U, V>(
127132
bin_path: S,
128133
util_name: Option<T>,
129134
tmpd: Option<Rc<TempDir>>,
130135
exit_status: Option<ExitStatus>,
131136
stdout: U,
132137
stderr: V,
138+
args: Vec<OsString>,
139+
env_vars: Vec<(OsString, OsString)>,
133140
) -> Self
134141
where
135142
S: Into<PathBuf>,
@@ -144,6 +151,8 @@ impl CmdResult {
144151
exit_status,
145152
stdout: stdout.into(),
146153
stderr: stderr.into(),
154+
args,
155+
env_vars,
147156
}
148157
}
149158

@@ -160,6 +169,8 @@ impl CmdResult {
160169
self.exit_status,
161170
function(&self.stdout),
162171
self.stderr.as_slice(),
172+
self.args.clone(),
173+
self.env_vars.clone(),
163174
)
164175
}
165176

@@ -176,6 +187,8 @@ impl CmdResult {
176187
self.exit_status,
177188
function(self.stdout_str()),
178189
self.stderr.as_slice(),
190+
self.args.clone(),
191+
self.env_vars.clone(),
179192
)
180193
}
181194

@@ -192,6 +205,8 @@ impl CmdResult {
192205
self.exit_status,
193206
self.stdout.as_slice(),
194207
function(&self.stderr),
208+
self.args.clone(),
209+
self.env_vars.clone(),
195210
)
196211
}
197212

@@ -208,6 +223,8 @@ impl CmdResult {
208223
self.exit_status,
209224
self.stdout.as_slice(),
210225
function(self.stderr_str()),
226+
self.args.clone(),
227+
self.env_vars.clone(),
211228
)
212229
}
213230

@@ -907,6 +924,103 @@ impl CmdResult {
907924
);
908925
self
909926
}
927+
928+
/// Compare this result against GNU coreutils. Auto-skips if GNU unavailable.
929+
#[track_caller]
930+
#[cfg(unix)]
931+
pub fn matches_gnu(&self) -> &Self {
932+
let Some(util_name) = &self.util_name else {
933+
eprintln!("Skipping GNU comparison: util_name not set");
934+
return self;
935+
};
936+
937+
let gnu_version = match check_coreutil_version(util_name, VERSION_MIN) {
938+
Ok(v) => v,
939+
Err(e) => {
940+
eprintln!("Skipping GNU comparison: {e}");
941+
return self;
942+
}
943+
};
944+
945+
let gnu_name = host_name_for(util_name);
946+
// Skip first arg (util_name) since UCommand prepends it for multicall binary
947+
let args: Vec<&str> = self
948+
.args
949+
.iter()
950+
.skip(1)
951+
.filter_map(|s| s.to_str())
952+
.collect();
953+
954+
let Ok(gnu_output) = std::process::Command::new(gnu_name.as_ref())
955+
.args(&args)
956+
.env("PATH", PATH)
957+
.envs(DEFAULT_ENV)
958+
.envs(
959+
self.env_vars
960+
.iter()
961+
.filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?))),
962+
)
963+
.output()
964+
else {
965+
eprintln!("Skipping GNU comparison: failed to run GNU {util_name}");
966+
return self;
967+
};
968+
969+
let (gnu_stdout, gnu_stderr) = if cfg!(target_os = "linux") {
970+
(gnu_output.stdout, gnu_output.stderr)
971+
} else {
972+
let from = format!("{gnu_name}:");
973+
let to = format!("{util_name}:");
974+
(
975+
String::from_utf8_lossy(&gnu_output.stdout)
976+
.replace(&from, &to)
977+
.into_bytes(),
978+
String::from_utf8_lossy(&gnu_output.stderr)
979+
.replace(&from, &to)
980+
.into_bytes(),
981+
)
982+
};
983+
984+
let stdout_match = self.stdout == gnu_stdout;
985+
let stderr_match = self.stderr == gnu_stderr;
986+
let code_match = self.exit_status.and_then(|s| s.code()) == gnu_output.status.code();
987+
988+
if !stdout_match || !stderr_match || !code_match {
989+
let mut msg = format!("Output differs from GNU {util_name} ({gnu_version}):\n");
990+
if !stdout_match {
991+
msg.push_str(&format!(
992+
"stdout:\n uutils: {:?}\n GNU: {:?}\n",
993+
String::from_utf8_lossy(&self.stdout),
994+
String::from_utf8_lossy(&gnu_stdout)
995+
));
996+
}
997+
if !stderr_match {
998+
msg.push_str(&format!(
999+
"stderr:\n uutils: {:?}\n GNU: {:?}\n",
1000+
String::from_utf8_lossy(&self.stderr),
1001+
String::from_utf8_lossy(&gnu_stderr)
1002+
));
1003+
}
1004+
if !code_match {
1005+
msg.push_str(&format!(
1006+
"exit code:\n uutils: {:?}\n GNU: {:?}\n",
1007+
self.exit_status.and_then(|s| s.code()),
1008+
gnu_output.status.code()
1009+
));
1010+
}
1011+
panic!("{msg}");
1012+
}
1013+
1014+
self
1015+
}
1016+
1017+
/// No-op on non-unix platforms.
1018+
///
1019+
/// GNU coreutils comparison only makes sense on unix systems.
1020+
#[cfg(not(unix))]
1021+
pub fn matches_gnu(&self) -> &Self {
1022+
self
1023+
}
9101024
}
9111025

9121026
pub fn log_info<T: AsRef<str>, U: AsRef<str>>(msg: T, par: U) {
@@ -2198,6 +2312,8 @@ impl<'a> UChildAssertion<'a> {
21982312
exit_status,
21992313
stdout,
22002314
stderr,
2315+
self.uchild.args.clone(),
2316+
self.uchild.env_vars.clone(),
22012317
)
22022318
}
22032319

@@ -2279,6 +2395,8 @@ pub struct UChild {
22792395
stderr_to_stdout: bool,
22802396
join_handle: Option<JoinHandle<io::Result<()>>>,
22812397
timeout: Option<Duration>,
2398+
args: Vec<OsString>,
2399+
env_vars: Vec<(OsString, OsString)>,
22822400
tmpd: Option<Rc<TempDir>>, // drop last
22832401
}
22842402

@@ -2301,6 +2419,8 @@ impl UChild {
23012419
stderr_to_stdout: ucommand.stderr_to_stdout,
23022420
join_handle: None,
23032421
timeout: ucommand.timeout,
2422+
args: ucommand.args.iter().cloned().collect(),
2423+
env_vars: ucommand.env_vars.clone(),
23042424
tmpd: ucommand.tmpd.clone(),
23052425
}
23062426
}
@@ -2475,10 +2595,12 @@ impl UChild {
24752595
///
24762596
/// Returns the error from the call to `wait_with_output` if any
24772597
pub fn wait(self) -> io::Result<CmdResult> {
2478-
let (bin_path, util_name, tmpd) = (
2598+
let (bin_path, util_name, tmpd, args, env_vars) = (
24792599
self.bin_path.clone(),
24802600
self.util_name.clone(),
24812601
self.tmpd.clone(),
2602+
self.args.clone(),
2603+
self.env_vars.clone(),
24822604
);
24832605

24842606
let output = self.wait_with_output()?;
@@ -2490,6 +2612,8 @@ impl UChild {
24902612
exit_status: Some(output.status),
24912613
stdout: output.stdout,
24922614
stderr: output.stderr,
2615+
args,
2616+
env_vars,
24932617
})
24942618
}
24952619

@@ -3082,6 +3206,10 @@ pub fn gnu_cmd_result(
30823206
result.exit_status,
30833207
stdout.as_bytes(),
30843208
stderr.as_bytes(),
3209+
args.iter().map(OsString::from).collect(),
3210+
envs.iter()
3211+
.map(|(k, v)| (OsString::from(k), OsString::from(v)))
3212+
.collect(),
30853213
))
30863214
}
30873215

0 commit comments

Comments
 (0)