@@ -200,8 +200,13 @@ pub struct Xfail {
200200
201201impl Xfail {
202202 pub fn applies_on ( & self , platform : & str ) -> bool {
203+ self . applies_on_host ( platform, host ( ) . as_deref ( ) )
204+ }
205+
206+ fn applies_on_host ( & self , platform : & str , source_host : Option < & str > ) -> bool {
203207 ( self . platforms . is_empty ( ) || self . platforms . iter ( ) . any ( |p| p == platform) )
204- && ( self . hosts . is_empty ( ) || host ( ) . is_some_and ( |h| self . hosts . contains ( & h) ) )
208+ && ( self . hosts . is_empty ( )
209+ || source_host. is_some_and ( |h| self . hosts . iter ( ) . any ( |candidate| candidate == h) ) )
205210 }
206211
207212 pub fn matches ( & self , case : & str , platform : & str ) -> bool {
@@ -623,12 +628,16 @@ fn covered_by<'a>(
623628 case : & str ,
624629 class : & str ,
625630 platform : & str ,
631+ source_host : Option < & str > ,
626632 seeds : & BTreeSet < u64 > ,
627633 evidence : & Evidence ,
628634) -> BTreeSet < u64 > {
629635 let mut covered = BTreeSet :: new ( ) ;
630636 for x in xfails. filter ( |x| {
631- x. class == class && x. matches ( case, platform) && excuses ( x, case, class, evidence) . is_ok ( )
637+ x. class == class
638+ && x. applies_on_host ( platform, source_host)
639+ && glob_match ( & x. case , case)
640+ && excuses ( x, case, class, evidence) . is_ok ( )
632641 } ) {
633642 match & x. seeds {
634643 None => return seeds. clone ( ) ,
@@ -694,6 +703,7 @@ pub fn judge_with(
694703 case,
695704 class,
696705 platform,
706+ host ( ) . as_deref ( ) ,
697707 seeds,
698708 evidence,
699709 ) ;
@@ -759,9 +769,9 @@ pub fn judge_with(
759769 verdict
760770}
761771
762- /// Rewrites `xfails` so that this platform's run passes exactly, without
763- /// touching what other platforms or cases that did not run expect. Links
764- /// of replaced entries are kept; new proposals get the reason
772+ /// Proposes changes for this platform and host without narrowing entries
773+ /// for unobserved hosts, platforms or cases. Links of replaced entries
774+ /// are kept; new proposals get the reason
765775/// `TODO: <problem>` from `problems`.
766776pub fn bless (
767777 xfails : & [ Xfail ] ,
@@ -794,15 +804,26 @@ pub fn bless_observing(
794804 unobserved : unobserved. clone ( ) ,
795805 ..Default :: default ( )
796806 } ;
797- bless_with ( xfails, platform, ran, observed, & evidence, problems)
807+ bless_with (
808+ xfails,
809+ platform,
810+ host ( ) . as_deref ( ) ,
811+ ran,
812+ observed,
813+ & evidence,
814+ problems,
815+ )
798816}
799817
800818/// [`bless`] with measurements: a failure outside every entry's bound gets a
801819/// new entry (TODO reason) whose bound is the measured range, to be
802- /// reviewed against the mechanism.
820+ /// reviewed against the mechanism. Unknown-host observations cannot remove
821+ /// entries. All-host entries are retained because finite host observations
822+ /// cannot establish their behavior on every possible host.
803823pub fn bless_with (
804824 xfails : & [ Xfail ] ,
805825 platform : & str ,
826+ source_host : Option < & str > ,
806827 ran : & Ran ,
807828 observed : & Observed ,
808829 evidence : & Evidence ,
@@ -819,9 +840,15 @@ pub fn bless_with(
819840 let unmeasured = matched
820841 . iter ( )
821842 . any ( |( case, _) | unobserved. contains ( & ( ( * case) . clone ( ) , x. class . clone ( ) ) ) ) ;
822- if !x. applies_on ( platform) || matched. is_empty ( ) || x. flaky || unmeasured {
843+ if source_host. is_none ( )
844+ || x. hosts . is_empty ( )
845+ || !x. applies_on_host ( platform, source_host)
846+ || matched. is_empty ( )
847+ || x. flaky
848+ || unmeasured
849+ {
823850 out. push ( x. clone ( ) ) ;
824- if x. applies_on ( platform) {
851+ if x. applies_on_host ( platform, source_host ) {
825852 kept_here. push ( x. clone ( ) ) ;
826853 }
827854 continue ;
@@ -847,6 +874,19 @@ pub fn bless_with(
847874 if !others. is_empty ( ) {
848875 out. push ( x. with_platforms ( others) ) ;
849876 }
877+ let other_hosts: Vec < String > = x
878+ . hosts
879+ . iter ( )
880+ . filter ( |h| Some ( h. as_str ( ) ) != source_host)
881+ . cloned ( )
882+ . collect ( ) ;
883+ if !other_hosts. is_empty ( ) {
884+ out. push ( Xfail {
885+ platforms : vec ! [ platform. to_string( ) ] ,
886+ hosts : other_hosts,
887+ ..x. clone ( )
888+ } ) ;
889+ }
850890 if let Some ( listed) = & x. seeds {
851891 for ( case, ran_seeds) in & matched {
852892 let not_ran: BTreeSet < u64 > = listed
@@ -859,6 +899,7 @@ pub fn bless_with(
859899 case : ( * case) . clone ( ) ,
860900 seeds : Some ( not_ran) ,
861901 platforms : vec ! [ platform. to_string( ) ] ,
902+ hosts : source_host. into_iter ( ) . map ( str:: to_string) . collect ( ) ,
862903 ..x. clone ( )
863904 } ;
864905 out. push ( entry. clone ( ) ) ;
@@ -868,28 +909,39 @@ pub fn bless_with(
868909 }
869910 }
870911 for ( ( case, class) , seeds) in observed {
871- let covered = covered_by ( kept_here. iter ( ) , case, class, platform, seeds, evidence) ;
912+ let covered = covered_by (
913+ kept_here. iter ( ) ,
914+ case,
915+ class,
916+ platform,
917+ source_host,
918+ seeds,
919+ evidence,
920+ ) ;
872921 let fully = kept_here. iter ( ) . any ( |x| {
873922 x. class == * class
874- && x. matches ( case, platform)
923+ && x. applies_on_host ( platform, source_host)
924+ && glob_match ( & x. case , case)
875925 && x. seeds . is_none ( )
876926 && excuses ( x, case, class, evidence) . is_ok ( )
877927 } ) ;
878928 if fully || ( !seeds. is_empty ( ) && covered == * seeds) {
879929 continue ;
880930 }
881931 let missing: BTreeSet < u64 > = seeds. difference ( & covered) . copied ( ) . collect ( ) ;
882- let previous = xfails
883- . iter ( )
884- . find ( |x| x. class == * class && glob_match ( & x. case , case) ) ;
932+ let previous = xfails. iter ( ) . find ( |x| {
933+ x. class == * class
934+ && x. applies_on_host ( platform, source_host)
935+ && glob_match ( & x. case , case)
936+ } ) ;
885937 let entry = Xfail {
886938 case : case. clone ( ) ,
887939 class : class. clone ( ) ,
888940 platforms : vec ! [ platform. to_string( ) ] ,
889941 seeds : ( !seeds. is_empty ( ) ) . then_some ( missing) ,
890942 flaky : false ,
891943 starved : false ,
892- hosts : Vec :: new ( ) ,
944+ hosts : source_host . into_iter ( ) . map ( str :: to_string ) . collect ( ) ,
893945 bound : if seeds. is_empty ( ) {
894946 evidence
895947 . measures
@@ -1055,6 +1107,7 @@ fn write_records_to(dir: &Path, records: &[(Record, serde_json::Value)]) {
10551107 . expect ( "open report file" ) ;
10561108 for ( record, extra) in records {
10571109 let mut value = serde_json:: to_value ( record) . expect ( "serialize report record" ) ;
1110+ value[ "host" ] = serde_json:: to_value ( host ( ) ) . expect ( "serialize report host" ) ;
10581111 if let ( Some ( map) , Some ( extra) ) = ( value. as_object_mut ( ) , extra. as_object ( ) ) {
10591112 for ( k, v) in extra {
10601113 map. entry ( k. clone ( ) ) . or_insert_with ( || v. clone ( ) ) ;
@@ -1082,7 +1135,7 @@ mod tests {
10821135 seeds : seeds. map ( set) ,
10831136 flaky : false ,
10841137 starved : false ,
1085- hosts : Vec :: new ( ) ,
1138+ hosts : host ( ) . into_iter ( ) . collect ( ) ,
10861139 bound : Bound :: new ( ) ,
10871140 reason : "r" . into ( ) ,
10881141 link : String :: new ( ) ,
@@ -1122,7 +1175,13 @@ mod tests {
11221175 repro : String :: new ( ) ,
11231176 seconds : 0.0 ,
11241177 } ;
1125- write_records_to ( dir, & [ ( record, serde_json:: json!( { "writer" : writer} ) ) ] ) ;
1178+ write_records_to (
1179+ dir,
1180+ & [ (
1181+ record,
1182+ serde_json:: json!( { "writer" : writer, "host" : "incorrect" } ) ,
1183+ ) ] ,
1184+ ) ;
11261185 }
11271186 } ) ;
11281187 }
@@ -1132,6 +1191,8 @@ mod tests {
11321191 let cases: BTreeSet < String > = text
11331192 . lines ( )
11341193 . map ( |line| {
1194+ let value: serde_json:: Value = serde_json:: from_str ( line) . unwrap ( ) ;
1195+ assert_eq ! ( value[ "host" ] , serde_json:: json!( host( ) ) ) ;
11351196 let record: Record = serde_json:: from_str ( line) . unwrap ( ) ;
11361197 assert_eq ! ( record. outcome, Outcome :: Unsupported ) ;
11371198 record. case
@@ -1333,6 +1394,90 @@ mod tests {
13331394 assert_eq ! ( blessed, vec![ xfails[ 0 ] . clone( ) , proposed] ) ;
13341395 }
13351396
1397+ #[ test]
1398+ fn bless_preserves_unobserved_host_and_platform_slices ( ) {
1399+ let xfail = Xfail {
1400+ hosts : vec ! [ "github-hosted" . into( ) , "local" . into( ) ] ,
1401+ ..entry ( "a" , "stale" , & [ "macos" , "windows" ] , None )
1402+ } ;
1403+ let ran = Ran :: from ( [ ( "a" . into ( ) , None ) ] ) ;
1404+ let blessed = bless_with (
1405+ & [ xfail. clone ( ) ] ,
1406+ "macos" ,
1407+ Some ( "local" ) ,
1408+ & ran,
1409+ & Observed :: new ( ) ,
1410+ & Evidence :: default ( ) ,
1411+ & BTreeMap :: new ( ) ,
1412+ ) ;
1413+ assert_eq ! ( blessed. len( ) , 2 ) ;
1414+ assert ! (
1415+ blessed
1416+ . iter( )
1417+ . any( |x| x. platforms == [ "windows" ] && x. hosts == xfail. hosts)
1418+ ) ;
1419+ assert ! (
1420+ blessed
1421+ . iter( )
1422+ . any( |x| x. platforms == [ "macos" ] && x. hosts == [ "github-hosted" ] )
1423+ ) ;
1424+ assert ! (
1425+ !blessed
1426+ . iter( )
1427+ . any( |x| x. applies_on_host( "macos" , Some ( "local" ) ) )
1428+ ) ;
1429+
1430+ let untested = bless_with (
1431+ & [ xfail. clone ( ) ] ,
1432+ "macos" ,
1433+ Some ( "blacksmith" ) ,
1434+ & ran,
1435+ & Observed :: new ( ) ,
1436+ & Evidence :: default ( ) ,
1437+ & BTreeMap :: new ( ) ,
1438+ ) ;
1439+ assert_eq ! ( untested, vec![ xfail] ) ;
1440+ }
1441+
1442+ #[ test]
1443+ fn bless_unknown_hosts_cannot_remove_or_narrow_entries ( ) {
1444+ let xfails = vec ! [
1445+ entry( "a" , "stale" , & [ "macos" ] , Some ( & [ 1 , 2 ] ) ) ,
1446+ Xfail {
1447+ hosts: Vec :: new( ) ,
1448+ ..entry( "b" , "stale" , & [ "macos" ] , None )
1449+ } ,
1450+ ] ;
1451+ let ran = Ran :: from ( [ ( "a" . into ( ) , Some ( set ( & [ 1 , 2 ] ) ) ) , ( "b" . into ( ) , None ) ] ) ;
1452+ for observed in [
1453+ Observed :: new ( ) ,
1454+ Observed :: from ( [ ( ( "a" . into ( ) , "stale" . into ( ) ) , set ( & [ 1 ] ) ) ] ) ,
1455+ ] {
1456+ let blessed = bless_with (
1457+ & xfails,
1458+ "macos" ,
1459+ None ,
1460+ & ran,
1461+ & observed,
1462+ & Evidence :: default ( ) ,
1463+ & BTreeMap :: new ( ) ,
1464+ ) ;
1465+ for entry in & xfails {
1466+ assert ! ( blessed. contains( entry) ) ;
1467+ }
1468+ }
1469+ let blessed = bless_with (
1470+ & xfails[ 1 ..] ,
1471+ "macos" ,
1472+ Some ( "blacksmith" ) ,
1473+ & ran,
1474+ & Observed :: new ( ) ,
1475+ & Evidence :: default ( ) ,
1476+ & BTreeMap :: new ( ) ,
1477+ ) ;
1478+ assert_eq ! ( blessed, xfails[ 1 ..] ) ;
1479+ }
1480+
13361481 fn measured ( case : & str , class : & str , metrics : & [ ( & str , f64 , f64 ) ] ) -> Evidence {
13371482 let mut evidence = Evidence :: default ( ) ;
13381483 evidence. measures . insert (
@@ -1450,7 +1595,15 @@ mod tests {
14501595 let ran = Ran :: from ( [ ( "a" . to_string ( ) , None ) ] ) ;
14511596 let observed = Observed :: from ( [ ( ( "a" . into ( ) , "sync" . into ( ) ) , set ( & [ ] ) ) ] ) ;
14521597 let evidence = measured ( "a" , "sync" , & [ ( "ms" , -201.0 , -199.0 ) ] ) ;
1453- let blessed = bless_with ( & [ ] , "macos" , & ran, & observed, & evidence, & BTreeMap :: new ( ) ) ;
1598+ let blessed = bless_with (
1599+ & [ ] ,
1600+ "macos" ,
1601+ host ( ) . as_deref ( ) ,
1602+ & ran,
1603+ & observed,
1604+ & evidence,
1605+ & BTreeMap :: new ( ) ,
1606+ ) ;
14541607 assert_eq ! ( blessed. len( ) , 1 ) ;
14551608 assert_eq ! (
14561609 blessed[ 0 ] . bound[ "ms" ] ,
0 commit comments