@@ -579,6 +579,7 @@ pub struct Execs {
579579 expect_stderr_not_contains : Vec < String > ,
580580 expect_stderr_unordered : Vec < String > ,
581581 expect_neither_contains : Vec < String > ,
582+ expect_stderr_with_without : Vec < ( Vec < String > , Vec < String > ) > ,
582583 expect_json : Option < Vec < Value > > ,
583584 expect_json_contains_unordered : Vec < Value > ,
584585 stream_output : bool ,
@@ -696,6 +697,37 @@ impl Execs {
696697 self
697698 }
698699
700+ /// Verify that a particular line appears in stderr with and without the
701+ /// given substrings. Exactly one line must match.
702+ ///
703+ /// The substrings are matched as `contains`. Example:
704+ ///
705+ /// ```no_run
706+ /// execs.with_stderr_line_without(
707+ /// &[
708+ /// "[RUNNING] `rustc --crate-name build_script_build",
709+ /// "-C opt-level=3",
710+ /// ],
711+ /// &["-C debuginfo", "-C incremental"],
712+ /// )
713+ /// ```
714+ ///
715+ /// This will check that a build line includes `-C opt-level=3` but does
716+ /// not contain `-C debuginfo` or `-C incremental`.
717+ ///
718+ /// Be careful writing the `without` fragments, see note in
719+ /// `with_stderr_does_not_contain`.
720+ pub fn with_stderr_line_without < S : ToString > (
721+ & mut self ,
722+ with : & [ S ] ,
723+ without : & [ S ] ,
724+ ) -> & mut Self {
725+ let with = with. iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
726+ let without = without. iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
727+ self . expect_stderr_with_without . push ( ( with, without) ) ;
728+ self
729+ }
730+
699731 /// Verifies the JSON output matches the given JSON.
700732 /// Typically used when testing cargo commands that emit JSON.
701733 /// Each separate JSON object should be separated by a blank line.
@@ -830,6 +862,7 @@ impl Execs {
830862 && self . expect_stderr_not_contains . is_empty ( )
831863 && self . expect_stderr_unordered . is_empty ( )
832864 && self . expect_neither_contains . is_empty ( )
865+ && self . expect_stderr_with_without . is_empty ( )
833866 && self . expect_json . is_none ( )
834867 && self . expect_json_contains_unordered . is_empty ( )
835868 {
@@ -1004,6 +1037,10 @@ impl Execs {
10041037 }
10051038 }
10061039
1040+ for ( with, without) in self . expect_stderr_with_without . iter ( ) {
1041+ self . match_with_without ( & actual. stderr , with, without) ?;
1042+ }
1043+
10071044 if let Some ( ref objects) = self . expect_json {
10081045 let stdout = str:: from_utf8 ( & actual. stdout )
10091046 . map_err ( |_| "stdout was not utf8 encoded" . to_owned ( ) ) ?;
@@ -1063,6 +1100,32 @@ impl Execs {
10631100 )
10641101 }
10651102
1103+ fn normalize_actual ( & self , description : & str , actual : & [ u8 ] ) -> Result < String , String > {
1104+ let actual = match str:: from_utf8 ( actual) {
1105+ Err ( ..) => return Err ( format ! ( "{} was not utf8 encoded" , description) ) ,
1106+ Ok ( actual) => actual,
1107+ } ;
1108+ // Let's not deal with \r\n vs \n on windows...
1109+ let actual = actual. replace ( "\r " , "" ) ;
1110+ let actual = actual. replace ( "\t " , "<tab>" ) ;
1111+ Ok ( actual)
1112+ }
1113+
1114+ fn replace_expected ( & self , expected : & str ) -> String {
1115+ // Do the template replacements on the expected string.
1116+ let replaced = match self . process_builder {
1117+ None => expected. to_string ( ) ,
1118+ Some ( ref p) => match p. get_cwd ( ) {
1119+ None => expected. to_string ( ) ,
1120+ Some ( cwd) => expected. replace ( "[CWD]" , & cwd. display ( ) . to_string ( ) ) ,
1121+ } ,
1122+ } ;
1123+
1124+ // On Windows, we need to use a wildcard for the drive,
1125+ // because we don't actually know what it will be.
1126+ replaced. replace ( "[ROOT]" , if cfg ! ( windows) { r#"[..]:\"# } else { "/" } )
1127+ }
1128+
10661129 fn match_std (
10671130 & self ,
10681131 expected : Option < & String > ,
@@ -1072,30 +1135,11 @@ impl Execs {
10721135 kind : MatchKind ,
10731136 ) -> MatchResult {
10741137 let out = match expected {
1075- Some ( out) => {
1076- // Do the template replacements on the expected string.
1077- let replaced = match self . process_builder {
1078- None => out. to_string ( ) ,
1079- Some ( ref p) => match p. get_cwd ( ) {
1080- None => out. to_string ( ) ,
1081- Some ( cwd) => out. replace ( "[CWD]" , & cwd. display ( ) . to_string ( ) ) ,
1082- } ,
1083- } ;
1084-
1085- // On Windows, we need to use a wildcard for the drive,
1086- // because we don't actually know what it will be.
1087- replaced. replace ( "[ROOT]" , if cfg ! ( windows) { r#"[..]:\"# } else { "/" } )
1088- }
1138+ Some ( out) => self . replace_expected ( out) ,
10891139 None => return Ok ( ( ) ) ,
10901140 } ;
10911141
1092- let actual = match str:: from_utf8 ( actual) {
1093- Err ( ..) => return Err ( format ! ( "{} was not utf8 encoded" , description) ) ,
1094- Ok ( actual) => actual,
1095- } ;
1096- // Let's not deal with `\r\n` vs `\n` on Windows.
1097- let actual = actual. replace ( "\r " , "" ) ;
1098- let actual = actual. replace ( "\t " , "<tab>" ) ;
1142+ let actual = self . normalize_actual ( description, actual) ?;
10991143
11001144 match kind {
11011145 MatchKind :: Exact => {
@@ -1219,6 +1263,47 @@ impl Execs {
12191263 }
12201264 }
12211265
1266+ fn match_with_without (
1267+ & self ,
1268+ actual : & [ u8 ] ,
1269+ with : & [ String ] ,
1270+ without : & [ String ] ,
1271+ ) -> MatchResult {
1272+ let actual = self . normalize_actual ( "stderr" , actual) ?;
1273+ let contains = |s, line| {
1274+ let mut s = self . replace_expected ( s) ;
1275+ s. insert_str ( 0 , "[..]" ) ;
1276+ s. push_str ( "[..]" ) ;
1277+ lines_match ( & s, line)
1278+ } ;
1279+ let matches: Vec < & str > = actual
1280+ . lines ( )
1281+ . filter ( |line| with. iter ( ) . all ( |with| contains ( with, line) ) )
1282+ . filter ( |line| !without. iter ( ) . any ( |without| contains ( without, line) ) )
1283+ . collect ( ) ;
1284+ match matches. len ( ) {
1285+ 0 => Err ( format ! (
1286+ "Could not find expected line in output.\n \
1287+ With contents: {:?}\n \
1288+ Without contents: {:?}\n \
1289+ Actual stderr:\n \
1290+ {}\n ",
1291+ with, without, actual
1292+ ) ) ,
1293+ 1 => Ok ( ( ) ) ,
1294+ _ => Err ( format ! (
1295+ "Found multiple matching lines, but only expected one.\n \
1296+ With contents: {:?}\n \
1297+ Without contents: {:?}\n \
1298+ Matching lines:\n \
1299+ {}\n ",
1300+ with,
1301+ without,
1302+ matches. join( "\n " )
1303+ ) ) ,
1304+ }
1305+ }
1306+
12221307 fn match_json ( & self , expected : & Value , line : & str ) -> MatchResult {
12231308 let actual = match line. parse ( ) {
12241309 Err ( e) => return Err ( format ! ( "invalid json, {}:\n `{}`" , e, line) ) ,
@@ -1436,6 +1521,7 @@ pub fn execs() -> Execs {
14361521 expect_stderr_not_contains : Vec :: new ( ) ,
14371522 expect_stderr_unordered : Vec :: new ( ) ,
14381523 expect_neither_contains : Vec :: new ( ) ,
1524+ expect_stderr_with_without : Vec :: new ( ) ,
14391525 expect_json : None ,
14401526 expect_json_contains_unordered : Vec :: new ( ) ,
14411527 stream_output : false ,
@@ -1529,7 +1615,7 @@ fn substitute_macros(input: &str) -> String {
15291615 ( "[UNPACKING]" , " Unpacking" ) ,
15301616 ( "[SUMMARY]" , " Summary" ) ,
15311617 ( "[FIXING]" , " Fixing" ) ,
1532- ( "[EXE]" , env:: consts:: EXE_SUFFIX ) ,
1618+ ( "[EXE]" , env:: consts:: EXE_SUFFIX ) ,
15331619 ] ;
15341620 let mut result = input. to_owned ( ) ;
15351621 for & ( pat, subst) in & macros {
0 commit comments