Skip to content

Commit 30c6f1d

Browse files
committed
feat(test): add GNU matching checks to sort error tests and extend CmdResult with execution metadata
- Added `.matches_gnu()` assertions to sort tests for invalid key specifications to ensure error messages align with GNU sort behavior, improving cross-platform compatibility. - Extended `CmdResult` struct in test utilities with fields for args, env_vars, current_dir, and stdin_bytes to capture more command execution details, aiding in debugging and test reliability.
1 parent 98e5c17 commit 30c6f1d

File tree

2 files changed

+186
-9
lines changed

2 files changed

+186
-9
lines changed

tests/by-util/test_sort.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -638,54 +638,62 @@ fn test_keys_invalid_field() {
638638
new_ucmd!()
639639
.args(&["-k", "1."])
640640
.fails()
641-
.stderr_only("sort: invalid number after '.': invalid count at start of ''\n");
641+
.stderr_only("sort: invalid number after '.': invalid count at start of ''\n")
642+
.matches_gnu();
642643
}
643644

644645
#[test]
645646
fn test_keys_invalid_field_option() {
646647
new_ucmd!()
647648
.args(&["-k", "1.1x"])
648649
.fails()
649-
.stderr_only("sort: stray character in field spec: invalid field specification '1.1x'\n");
650+
.stderr_only("sort: stray character in field spec: invalid field specification '1.1x'\n")
651+
.matches_gnu();
650652
}
651653

652654
#[test]
653655
fn test_keys_invalid_field_zero() {
654656
new_ucmd!()
655657
.args(&["-k", "0.1"])
656658
.fails()
657-
.stderr_only("sort: field number is zero: invalid field specification '0.1'\n");
659+
.stderr_only("sort: field number is zero: invalid field specification '0.1'\n")
660+
.matches_gnu();
658661
}
659662

660663
#[test]
661664
fn test_keys_invalid_char_zero() {
662665
new_ucmd!()
663666
.args(&["-k", "1.0"])
664667
.fails()
665-
.stderr_only("sort: character offset is zero: invalid field specification '1.0'\n");
668+
.stderr_only("sort: character offset is zero: invalid field specification '1.0'\n")
669+
.matches_gnu();
666670
}
667671

668672
#[test]
669673
fn test_keys_invalid_number_formats() {
670674
new_ucmd!()
671675
.args(&["-k", "0"])
672676
.fails_with_code(2)
673-
.stderr_only("sort: field number is zero: invalid field specification '0'\n");
677+
.stderr_only("sort: field number is zero: invalid field specification '0'\n")
678+
.matches_gnu();
674679

675680
new_ucmd!()
676681
.args(&["-k", "2.,3"])
677682
.fails_with_code(2)
678-
.stderr_only("sort: invalid number after '.': invalid count at start of ',3'\n");
683+
.stderr_only("sort: invalid number after '.': invalid count at start of ',3'\n")
684+
.matches_gnu();
679685

680686
new_ucmd!()
681687
.args(&["-k", "2,"])
682688
.fails_with_code(2)
683-
.stderr_only("sort: invalid number after ',': invalid count at start of ''\n");
689+
.stderr_only("sort: invalid number after ',': invalid count at start of ''\n")
690+
.matches_gnu();
684691

685692
new_ucmd!()
686693
.args(&["-k", "1.1,-k0"])
687694
.fails_with_code(2)
688-
.stderr_only("sort: invalid number after ',': invalid count at start of '-k0'\n");
695+
.stderr_only("sort: invalid number after ',': invalid count at start of '-k0'\n")
696+
.matches_gnu();
689697
}
690698

691699
#[test]

tests/uutests/src/lib/util.rs

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ 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
124+
args: Vec<OsString>,
125+
/// environment variables passed to the command
126+
env_vars: Vec<(OsString, OsString)>,
127+
/// current directory used to run the command
128+
current_dir: Option<PathBuf>,
129+
/// stdin bytes provided to the command (if any)
130+
stdin_bytes: Option<Vec<u8>>,
123131
}
124132

125133
impl CmdResult {
@@ -130,6 +138,10 @@ impl CmdResult {
130138
exit_status: Option<ExitStatus>,
131139
stdout: U,
132140
stderr: V,
141+
args: Vec<OsString>,
142+
env_vars: Vec<(OsString, OsString)>,
143+
current_dir: Option<PathBuf>,
144+
stdin_bytes: Option<Vec<u8>>,
133145
) -> Self
134146
where
135147
S: Into<PathBuf>,
@@ -144,6 +156,10 @@ impl CmdResult {
144156
exit_status,
145157
stdout: stdout.into(),
146158
stderr: stderr.into(),
159+
args,
160+
env_vars,
161+
current_dir,
162+
stdin_bytes,
147163
}
148164
}
149165

@@ -160,6 +176,10 @@ impl CmdResult {
160176
self.exit_status,
161177
function(&self.stdout),
162178
self.stderr.as_slice(),
179+
self.args.clone(),
180+
self.env_vars.clone(),
181+
self.current_dir.clone(),
182+
self.stdin_bytes.clone(),
163183
)
164184
}
165185

@@ -176,6 +196,10 @@ impl CmdResult {
176196
self.exit_status,
177197
function(self.stdout_str()),
178198
self.stderr.as_slice(),
199+
self.args.clone(),
200+
self.env_vars.clone(),
201+
self.current_dir.clone(),
202+
self.stdin_bytes.clone(),
179203
)
180204
}
181205

@@ -192,6 +216,10 @@ impl CmdResult {
192216
self.exit_status,
193217
self.stdout.as_slice(),
194218
function(&self.stderr),
219+
self.args.clone(),
220+
self.env_vars.clone(),
221+
self.current_dir.clone(),
222+
self.stdin_bytes.clone(),
195223
)
196224
}
197225

@@ -208,6 +236,10 @@ impl CmdResult {
208236
self.exit_status,
209237
self.stdout.as_slice(),
210238
function(self.stderr_str()),
239+
self.args.clone(),
240+
self.env_vars.clone(),
241+
self.current_dir.clone(),
242+
self.stdin_bytes.clone(),
211243
)
212244
}
213245

@@ -700,6 +732,119 @@ impl CmdResult {
700732
self.no_stdout().stderr_is_bytes(msg)
701733
}
702734

735+
/// Compare output and exit status with the GNU coreutils implementation.
736+
/// If GNU coreutils isn't available, this is a no-op and prints a skip reason.
737+
#[cfg(unix)]
738+
#[track_caller]
739+
pub fn matches_gnu(&self) -> &Self {
740+
match self.gnu_result() {
741+
Ok(expected) => {
742+
self.stdout_is(expected.stdout_str());
743+
self.stderr_is(expected.stderr_str());
744+
self.code_is(expected.code());
745+
}
746+
Err(error) => {
747+
println!("test skipped: {error}");
748+
}
749+
}
750+
self
751+
}
752+
753+
/// On non-Unix platforms GNU coreutils may not be available.
754+
#[cfg(not(unix))]
755+
#[track_caller]
756+
pub fn matches_gnu(&self) -> &Self {
757+
println!("test skipped: GNU comparison is not supported on this platform");
758+
self
759+
}
760+
761+
#[cfg(unix)]
762+
fn gnu_result(&self) -> std::result::Result<CmdResult, String> {
763+
let util_name = self.util_name.as_ref().ok_or_else(|| {
764+
format!("{UUTILS_WARNING}: matches_gnu requires a utility name")
765+
})?;
766+
println!("{}", check_coreutil_version(util_name, VERSION_MIN)?);
767+
let gnu_name = host_name_for(util_name);
768+
769+
let mut args = self.args.clone();
770+
if let Some(first) = args.first() {
771+
if first == &OsString::from(util_name) {
772+
args.remove(0);
773+
}
774+
}
775+
776+
let mut command = Command::new(gnu_name.as_ref());
777+
command.env_clear();
778+
command.args(&args);
779+
780+
if let Some(current_dir) = &self.current_dir {
781+
command.current_dir(current_dir);
782+
} else if let Some(tmpd) = &self.tmpd {
783+
command.current_dir(tmpd.path());
784+
}
785+
786+
command
787+
.env("PATH", PATH)
788+
.envs(DEFAULT_ENV)
789+
.envs(self.env_vars.iter().cloned());
790+
791+
if let Some(ld_preload) = env::var_os("LD_PRELOAD") {
792+
command.env("LD_PRELOAD", ld_preload);
793+
}
794+
795+
if let Some(profile) = env::var_os("LLVM_PROFILE_FILE") {
796+
command.env("LLVM_PROFILE_FILE", profile);
797+
}
798+
799+
let output = if let Some(stdin_bytes) = &self.stdin_bytes {
800+
let mut child = command
801+
.stdin(Stdio::piped())
802+
.stdout(Stdio::piped())
803+
.stderr(Stdio::piped())
804+
.spawn()
805+
.map_err(|e| format!("{UUTILS_WARNING}: {e}"))?;
806+
if let Some(mut stdin) = child.stdin.take() {
807+
stdin
808+
.write_all(stdin_bytes)
809+
.map_err(|e| format!("{UUTILS_WARNING}: {e}"))?;
810+
}
811+
child
812+
.wait_with_output()
813+
.map_err(|e| format!("{UUTILS_WARNING}: {e}"))?
814+
} else {
815+
command
816+
.stdin(Stdio::null())
817+
.output()
818+
.map_err(|e| format!("{UUTILS_WARNING}: {e}"))?
819+
};
820+
821+
let (stdout, stderr) = if cfg!(target_os = "linux") {
822+
(
823+
String::from_utf8_lossy(&output.stdout).to_string(),
824+
String::from_utf8_lossy(&output.stderr).to_string(),
825+
)
826+
} else {
827+
let from = gnu_name.to_string() + ":";
828+
let to = &from[1..];
829+
(
830+
String::from_utf8_lossy(&output.stdout).replace(&from, to),
831+
String::from_utf8_lossy(&output.stderr).replace(&from, to),
832+
)
833+
};
834+
835+
Ok(CmdResult::new(
836+
gnu_name.to_string(),
837+
Some(util_name.clone()),
838+
self.tmpd.clone(),
839+
Some(output.status),
840+
stdout.as_bytes(),
841+
stderr.as_bytes(),
842+
args,
843+
self.env_vars.clone(),
844+
self.current_dir.clone(),
845+
self.stdin_bytes.clone(),
846+
))
847+
}
703848
#[track_caller]
704849
pub fn fails_silently(&self) -> &Self {
705850
assert!(!self.succeeded());
@@ -2198,6 +2343,10 @@ impl<'a> UChildAssertion<'a> {
21982343
exit_status,
21992344
stdout,
22002345
stderr,
2346+
self.uchild.args.clone(),
2347+
self.uchild.env_vars.clone(),
2348+
self.uchild.current_dir.clone(),
2349+
self.uchild.stdin_bytes.clone(),
22012350
)
22022351
}
22032352

@@ -2272,6 +2421,10 @@ pub struct UChild {
22722421
raw: Child,
22732422
bin_path: PathBuf,
22742423
util_name: Option<String>,
2424+
args: Vec<OsString>,
2425+
env_vars: Vec<(OsString, OsString)>,
2426+
current_dir: Option<PathBuf>,
2427+
stdin_bytes: Option<Vec<u8>>,
22752428
captured_stdout: Option<CapturedOutput>,
22762429
captured_stderr: Option<CapturedOutput>,
22772430
stdin_pty: Option<File>,
@@ -2294,6 +2447,10 @@ impl UChild {
22942447
raw: child,
22952448
bin_path: ucommand.bin_path.clone().unwrap(),
22962449
util_name: ucommand.util_name.clone(),
2450+
args: ucommand.args.iter().cloned().collect(),
2451+
env_vars: ucommand.env_vars.clone(),
2452+
current_dir: ucommand.current_dir.clone(),
2453+
stdin_bytes: ucommand.bytes_into_stdin.clone(),
22972454
captured_stdout,
22982455
captured_stderr,
22992456
stdin_pty,
@@ -2475,10 +2632,14 @@ impl UChild {
24752632
///
24762633
/// Returns the error from the call to `wait_with_output` if any
24772634
pub fn wait(self) -> io::Result<CmdResult> {
2478-
let (bin_path, util_name, tmpd) = (
2635+
let (bin_path, util_name, tmpd, args, env_vars, current_dir, stdin_bytes) = (
24792636
self.bin_path.clone(),
24802637
self.util_name.clone(),
24812638
self.tmpd.clone(),
2639+
self.args.clone(),
2640+
self.env_vars.clone(),
2641+
self.current_dir.clone(),
2642+
self.stdin_bytes.clone(),
24822643
);
24832644

24842645
let output = self.wait_with_output()?;
@@ -2490,6 +2651,10 @@ impl UChild {
24902651
exit_status: Some(output.status),
24912652
stdout: output.stdout,
24922653
stderr: output.stderr,
2654+
args,
2655+
env_vars,
2656+
current_dir,
2657+
stdin_bytes,
24932658
})
24942659
}
24952660

@@ -3075,6 +3240,10 @@ pub fn gnu_cmd_result(
30753240
result.exit_status,
30763241
stdout.as_bytes(),
30773242
stderr.as_bytes(),
3243+
result.args.clone(),
3244+
result.env_vars.clone(),
3245+
result.current_dir.clone(),
3246+
result.stdin_bytes.clone(),
30783247
))
30793248
}
30803249

0 commit comments

Comments
 (0)