@@ -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
125129impl 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
9121026pub 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