Skip to content

Commit f6ef819

Browse files
committed
fix: scope expectation proposals to recorded source hosts [skip ci]
1 parent cf009ff commit f6ef819

2 files changed

Lines changed: 318 additions & 79 deletions

File tree

‎crates/verify-expect/src/lib.rs‎

Lines changed: 171 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,13 @@ pub struct Xfail {
200200

201201
impl 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`.
766776
pub 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.
803823
pub 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

Comments
 (0)