@@ -46,6 +46,9 @@ const TEST_ARGS: &[&str] = &[
4646
4747/// Per-platform value: either a boolean (true = enabled, false = disabled)
4848/// or an object describing an expected failure.
49+ ///
50+ /// Uses `#[serde(untagged)]` so that config.jsonc can use both
51+ /// `"windows": false` (boolean) and `"windows": { "exitCode": 1 }` (object).
4952#[ derive( Debug , Clone , Deserialize ) ]
5053#[ serde( untagged) ]
5154enum PlatformExpectation {
@@ -826,85 +829,63 @@ fn run_test(
826829 let expected_failure = test_config. and_then ( resolve_expected_failure) ;
827830
828831 if let Some ( ef) = expected_failure {
829- // Test has an expected-failure spec for this platform
830- if success {
831- // Test passed but was expected to fail
832- results. lock ( ) . unwrap ( ) . insert (
833- data. test_path . clone ( ) ,
834- CollectedResult {
835- passed : Some ( false ) ,
836- error : Some ( ErrorInfo {
837- code : actual_exit_code,
838- stderr : None ,
839- timeout : None ,
840- message : Some ( "expected test to fail but it passed" . to_string ( ) ) ,
841- } ) ,
842- uses_node_test,
843- ignore_reason : None ,
844- } ,
845- ) ;
846- return TestResult :: Failed {
847- duration : None ,
848- output : format ! (
849- "Test was expected to fail but passed\n {}" ,
850- debugging_command_text
851- )
852- . into_bytes ( ) ,
853- } ;
854- }
855-
856- let exit_code_matches =
857- ef. exit_code . map_or ( true , |ec| actual_exit_code == Some ( ec) ) ;
858- let output_matches = ef. output . as_ref ( ) . map_or ( true , |pattern| {
859- util:: wildcard_match_detailed ( pattern, & output_for_error) . is_success ( )
860- } ) ;
832+ return handle_expected_failure (
833+ & ef,
834+ success,
835+ actual_exit_code,
836+ uses_node_test,
837+ & output_for_error,
838+ & debugging_command_text,
839+ & data. test_path ,
840+ results,
841+ ) ;
842+ }
861843
862- if exit_code_matches && output_matches {
863- // Failed as expected — treat as a pass
864- results. lock ( ) . unwrap ( ) . insert (
865- data. test_path . clone ( ) ,
866- CollectedResult {
867- passed : Some ( true ) ,
868- error : None ,
869- uses_node_test,
870- ignore_reason : None ,
871- } ,
872- ) ;
873- if * file_test_runner:: NO_CAPTURE {
874- test_util:: eprintln!( "{}" , debugging_command_text) ;
875- }
876- return TestResult :: Passed { duration : None } ;
877- }
844+ results
845+ . lock ( )
846+ . unwrap ( )
847+ . insert ( data. test_path . clone ( ) , collected) ;
878848
879- // Failed, but not in the expected way
880- let mut mismatch_details = String :: new ( ) ;
881- if !exit_code_matches {
882- mismatch_details. push_str ( & format ! (
883- "Exit code mismatch: expected {:?}, got {:?}\n " ,
884- ef. exit_code, actual_exit_code
885- ) ) ;
849+ if success {
850+ if * file_test_runner:: NO_CAPTURE {
851+ test_util:: eprintln!( "{}" , debugging_command_text) ;
886852 }
887- if !output_matches {
888- let match_result = ef. output . as_ref ( ) . map ( |pattern| {
889- util:: wildcard_match_detailed ( pattern, & output_for_error)
890- } ) ;
891- if let Some ( util:: WildcardMatchResult :: Fail ( detail) ) = match_result {
892- mismatch_details
893- . push_str ( & format ! ( "Output mismatch detail:\n {}\n " , detail) ) ;
894- } else {
895- mismatch_details. push_str ( "Output did not match expected pattern\n " ) ;
896- }
853+ TestResult :: Passed { duration : None }
854+ } else {
855+ TestResult :: Failed {
856+ duration : None ,
857+ output : format ! ( "{}\n {}" , output_for_error, debugging_command_text)
858+ . into_bytes ( ) ,
897859 }
860+ }
861+ }
898862
863+ /// Handle a test that has an expected-failure configuration.
864+ ///
865+ /// Returns `Passed` when the test fails in the expected way (matching exit code
866+ /// and/or output pattern), and `Failed` otherwise.
867+ #[ allow( clippy:: too_many_arguments) ]
868+ fn handle_expected_failure (
869+ ef : & ExpectedFailure ,
870+ success : bool ,
871+ actual_exit_code : Option < i32 > ,
872+ uses_node_test : bool ,
873+ output_for_error : & str ,
874+ debugging_command_text : & str ,
875+ test_path : & str ,
876+ results : & Arc < Mutex < HashMap < String , CollectedResult > > > ,
877+ ) -> TestResult {
878+ if success {
879+ // Test passed but was expected to fail
899880 results. lock ( ) . unwrap ( ) . insert (
900- data . test_path . clone ( ) ,
881+ test_path. to_string ( ) ,
901882 CollectedResult {
902883 passed : Some ( false ) ,
903884 error : Some ( ErrorInfo {
904885 code : actual_exit_code,
905- stderr : Some ( truncate_output ( & output_for_error , 2000 ) ) ,
886+ stderr : None ,
906887 timeout : None ,
907- message : Some ( mismatch_details . clone ( ) ) ,
888+ message : Some ( "expected test to fail but it passed" . to_string ( ) ) ,
908889 } ) ,
909890 uses_node_test,
910891 ignore_reason : None ,
@@ -913,30 +894,80 @@ fn run_test(
913894 return TestResult :: Failed {
914895 duration : None ,
915896 output : format ! (
916- "Test failed but not in the expected way: \n {} \n Actual output: \n {} \n {}" ,
917- mismatch_details , output_for_error , debugging_command_text
897+ "Test was expected to fail but passed \n {}" ,
898+ debugging_command_text
918899 )
919900 . into_bytes ( ) ,
920901 } ;
921902 }
922903
923- results
924- . lock ( )
925- . unwrap ( )
926- . insert ( data. test_path . clone ( ) , collected) ;
904+ // When exitCode/output are omitted, any value is accepted.
905+ let exit_code_matches =
906+ ef. exit_code . map_or ( true , |ec| actual_exit_code == Some ( ec) ) ;
907+ let output_matches = ef. output . as_ref ( ) . map_or ( true , |pattern| {
908+ util:: wildcard_match_detailed ( pattern, output_for_error) . is_success ( )
909+ } ) ;
927910
928- if success {
911+ if exit_code_matches && output_matches {
912+ // Failed as expected — treat as a pass
913+ results. lock ( ) . unwrap ( ) . insert (
914+ test_path. to_string ( ) ,
915+ CollectedResult {
916+ passed : Some ( true ) ,
917+ error : None ,
918+ uses_node_test,
919+ ignore_reason : None ,
920+ } ,
921+ ) ;
929922 if * file_test_runner:: NO_CAPTURE {
930923 test_util:: eprintln!( "{}" , debugging_command_text) ;
931924 }
932- TestResult :: Passed { duration : None }
933- } else {
934- TestResult :: Failed {
935- duration : None ,
936- output : format ! ( "{}\n {}" , output_for_error, debugging_command_text)
937- . into_bytes ( ) ,
925+ return TestResult :: Passed { duration : None } ;
926+ }
927+
928+ // Failed, but not in the expected way
929+ let mut mismatch_details = String :: new ( ) ;
930+ if !exit_code_matches {
931+ mismatch_details. push_str ( & format ! (
932+ "Exit code mismatch: expected {:?}, got {:?}\n " ,
933+ ef. exit_code, actual_exit_code
934+ ) ) ;
935+ }
936+ if !output_matches {
937+ let match_result = ef
938+ . output
939+ . as_ref ( )
940+ . map ( |pattern| util:: wildcard_match_detailed ( pattern, output_for_error) ) ;
941+ if let Some ( util:: WildcardMatchResult :: Fail ( detail) ) = match_result {
942+ mismatch_details
943+ . push_str ( & format ! ( "Output mismatch detail:\n {}\n " , detail) ) ;
944+ } else {
945+ mismatch_details. push_str ( "Output did not match expected pattern\n " ) ;
938946 }
939947 }
948+
949+ results. lock ( ) . unwrap ( ) . insert (
950+ test_path. to_string ( ) ,
951+ CollectedResult {
952+ passed : Some ( false ) ,
953+ error : Some ( ErrorInfo {
954+ code : actual_exit_code,
955+ stderr : Some ( truncate_output ( output_for_error, 2000 ) ) ,
956+ timeout : None ,
957+ message : Some ( mismatch_details. clone ( ) ) ,
958+ } ) ,
959+ uses_node_test,
960+ ignore_reason : None ,
961+ } ,
962+ ) ;
963+ TestResult :: Failed {
964+ duration : None ,
965+ output : format ! (
966+ "Test failed but not in the expected way:\n {}\n Actual output:\n {}\n {}" ,
967+ mismatch_details, output_for_error, debugging_command_text
968+ )
969+ . into_bytes ( ) ,
970+ }
940971}
941972
942973fn generate_report ( results : & HashMap < String , CollectedResult > ) {
0 commit comments