use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TargetOs {
Linux,
MacOs,
Windows,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Features {
Default,
All,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Targets {
LibsAndBins,
AllTargets,
}
impl Targets {
#[must_use]
pub fn compiles_tests(self) -> bool {
self == Self::AllTargets
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Conclusion {
Success,
Failure,
Inconclusive,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckRun {
pub job: String,
pub sha: String,
pub conclusion: Conclusion,
pub toolchain: String,
pub platform: TargetOs,
pub features: Features,
pub targets: Targets,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClaimSite {
pub sha: String,
pub path: String,
pub platform: Option<TargetOs>,
pub features: Option<Features>,
pub is_test_code: bool,
pub toolchain: Option<String>,
}
impl ClaimSite {
#[must_use]
pub fn unknown(sha: impl Into<String>, path: impl Into<String>) -> Self {
Self {
sha: sha.into(),
path: path.into(),
..Self::default()
}
}
#[must_use]
pub fn covered_by(&self, run: &CheckRun) -> bool {
if self.platform.is_some_and(|p| p != run.platform) {
return false;
}
if self.features.is_some_and(|f| f != run.features) {
return false;
}
if self.is_test_code && !run.targets.compiles_tests() {
return false;
}
if self
.toolchain
.as_deref()
.is_some_and(|t| t != run.toolchain)
{
return false;
}
true
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Suppression {
Refuted {
job: String,
reason: String,
},
Unrefuted {
reason: String,
},
}
impl Suppression {
#[must_use]
pub fn is_refuted(&self) -> bool {
matches!(self, Self::Refuted { .. })
}
#[must_use]
pub fn reason(&self) -> &str {
match self {
Self::Refuted { reason, .. } | Self::Unrefuted { reason } => reason,
}
}
}
#[must_use]
pub fn suppression(site: &ClaimSite, checks: &[CheckRun]) -> Suppression {
let at_sha: Vec<&CheckRun> = checks.iter().filter(|c| c.sha == site.sha).collect();
if at_sha.is_empty() {
return Suppression::Unrefuted {
reason: format!(
"no check run recorded at {} — a green run on any other commit says \
nothing about the tree the claim was made against",
short(&site.sha)
),
};
}
let mut green_covering: Vec<&CheckRun> = at_sha
.iter()
.copied()
.filter(|c| c.conclusion == Conclusion::Success && site.covered_by(c))
.collect();
green_covering.sort_by(|a, b| a.job.cmp(&b.job));
if let Some(run) = green_covering.first() {
return Suppression::Refuted {
job: run.job.clone(),
reason: format!(
"`{}` was green at {} and compiled {} ({}), so the claim that it \
does not build is already refuted",
run.job,
short(&site.sha),
site.path,
configuration(run),
),
};
}
let covering: Vec<&CheckRun> = at_sha
.iter()
.copied()
.filter(|c| site.covered_by(c))
.collect();
if covering.is_empty() {
return Suppression::Unrefuted {
reason: format!(
"no check run at {} compiled {} ({}) — {}",
short(&site.sha),
site.path,
requirement(site),
"turning features on cannot find a defect in code cfg'd out, and \
no job here compiles another platform's code, so this claim is \
unrefuted and owes a real look",
),
};
}
Suppression::Unrefuted {
reason: format!(
"the check run(s) covering {} at {} did not conclude green ({}), so \
nothing refutes the claim",
site.path,
short(&site.sha),
covering
.iter()
.map(|c| format!("{}: {:?}", c.job, c.conclusion))
.collect::<Vec<_>>()
.join(", "),
),
}
}
#[must_use]
pub fn jobs_covering(site: &ClaimSite, checks: &[CheckRun]) -> BTreeSet<String> {
checks
.iter()
.filter(|c| site.covered_by(c))
.map(|c| c.job.clone())
.collect()
}
fn configuration(run: &CheckRun) -> String {
let features = match run.features {
Features::Default => "default features",
Features::All => "--all-features",
Features::None => "--no-default-features",
};
let targets = match run.targets {
Targets::LibsAndBins => "libs and bins",
Targets::AllTargets => "--all-targets",
};
format!(
"{:?}, {}, {}, toolchain {}",
run.platform, features, targets, run.toolchain
)
}
fn requirement(site: &ClaimSite) -> String {
let mut parts = Vec::new();
if let Some(p) = site.platform {
parts.push(format!("needs {p:?}"));
}
if let Some(f) = site.features {
parts.push(format!("needs {f:?} features"));
}
if site.is_test_code {
parts.push("is test code, so needs --all-targets".to_owned());
}
if let Some(t) = &site.toolchain {
parts.push(format!("the claim names toolchain {t}"));
}
if parts.is_empty() {
"unconditional code".to_owned()
} else {
parts.join("; ")
}
}
fn short(sha: &str) -> &str {
sha.get(..8).unwrap_or(sha)
}
#[cfg(test)]
mod tests {
use super::{
CheckRun, ClaimSite, Conclusion, Features, Suppression, TargetOs, Targets, jobs_covering,
suppression,
};
fn ci_at(sha: &str) -> Vec<CheckRun> {
vec![
CheckRun {
job: "msrv".to_owned(),
sha: sha.to_owned(),
conclusion: Conclusion::Success,
toolchain: "1.94".to_owned(),
platform: TargetOs::Linux,
features: Features::All,
targets: Targets::LibsAndBins,
},
CheckRun {
job: "checks".to_owned(),
sha: sha.to_owned(),
conclusion: Conclusion::Success,
toolchain: "stable".to_owned(),
platform: TargetOs::Linux,
features: Features::All,
targets: Targets::AllTargets,
},
CheckRun {
job: "default-features".to_owned(),
sha: sha.to_owned(),
conclusion: Conclusion::Success,
toolchain: "stable".to_owned(),
platform: TargetOs::Linux,
features: Features::Default,
targets: Targets::AllTargets,
},
]
}
#[test]
fn every_known_false_compile_claim_is_refuted() {
let rows = [
("2b761ce7", "crates/rto-llama/src/slot.rs", None),
("5e25f921", "crates/rto-graph/src/media.rs", None),
("add397f2", "crates/roteiro/src/main.rs", None),
(
"c1481836",
"crates/rto-exec/src/boxlite.rs",
Some(Features::All),
),
];
for (sha, path, features) in rows {
let site = ClaimSite {
features,
..ClaimSite::unknown(sha, path)
};
let verdict = suppression(&site, &ci_at(sha));
assert!(
verdict.is_refuted(),
"{path} at {sha} should be refuted: {}",
verdict.reason()
);
assert!(
verdict.reason().contains(sha) && verdict.reason().contains(path),
"the reason names the commit and the file: {}",
verdict.reason()
);
}
}
#[test]
fn a_claim_naming_the_msrv_toolchain_is_refuted_only_by_the_msrv_job() {
let sha = "c1481836";
let site = ClaimSite {
features: Some(Features::All),
toolchain: Some("1.94".to_owned()),
..ClaimSite::unknown(sha, "crates/rto-exec/src/boxlite.rs")
};
let Suppression::Refuted { ref job, .. } = suppression(&site, &ci_at(sha)) else {
panic!("the msrv job compiled it");
};
assert_eq!(job, "msrv");
let stable_only: Vec<CheckRun> =
ci_at(sha).into_iter().filter(|c| c.job != "msrv").collect();
let verdict = suppression(&site, &stable_only);
assert!(!verdict.is_refuted(), "{}", verdict.reason());
assert!(
verdict.reason().contains("1.94"),
"says which toolchain went uncovered: {}",
verdict.reason()
);
}
#[test]
fn a_macos_only_site_is_never_refuted_by_this_repositorys_ci() {
let sha = "0123456789abcdef0123456789abcdef01234567";
let site = ClaimSite {
platform: Some(TargetOs::MacOs),
..ClaimSite::unknown(sha, "crates/rto-llama/src/backend.rs")
};
let verdict = suppression(&site, &ci_at(sha));
assert!(
!verdict.is_refuted(),
"a green ubuntu CI must not refute macOS-only code: {}",
verdict.reason()
);
assert!(
verdict.reason().contains("MacOs"),
"names the uncovered platform: {}",
verdict.reason()
);
assert!(
jobs_covering(&site, &ci_at(sha)).is_empty(),
"no job in this repository compiles macOS code"
);
}
#[test]
fn a_no_default_features_site_is_unrefuted() {
let sha = "abcdefabcdefabcdefabcdefabcdefabcdefabcd";
let site = ClaimSite {
features: Some(Features::None),
..ClaimSite::unknown(sha, "crates/rto-graph/src/lib.rs")
};
assert!(!suppression(&site, &ci_at(sha)).is_refuted());
}
#[test]
fn a_default_features_site_is_covered_only_by_the_default_features_job() {
let sha = "1111111111111111111111111111111111111111";
let site = ClaimSite {
features: Some(Features::Default),
..ClaimSite::unknown(sha, "crates/rto-llama/src/lib.rs")
};
assert_eq!(
jobs_covering(&site, &ci_at(sha)),
["default-features".to_owned()].into_iter().collect()
);
}
#[test]
fn an_msrv_claim_about_test_code_is_refuted_by_no_job_here() {
let sha = "2222222222222222222222222222222222222222";
let site = ClaimSite {
is_test_code: true,
toolchain: Some("1.94".to_owned()),
..ClaimSite::unknown(sha, "crates/rto-graph/tests/review_corpus.rs")
};
let verdict = suppression(&site, &ci_at(sha));
assert!(
!verdict.is_refuted(),
"no job compiles test code on the MSRV toolchain: {}",
verdict.reason()
);
assert!(
jobs_covering(&site, &ci_at(sha)).is_empty(),
"msrv omits --all-targets; the --all-targets jobs are stable"
);
let stable_claim = ClaimSite {
toolchain: None,
..site
};
assert!(suppression(&stable_claim, &ci_at(sha)).is_refuted());
}
#[test]
fn a_green_run_on_another_commit_refutes_nothing() {
let site = ClaimSite::unknown(
"3333333333333333333333333333333333333333",
"crates/rto-graph/src/lib.rs",
);
let elsewhere = ci_at("4444444444444444444444444444444444444444");
let verdict = suppression(&site, &elsewhere);
assert!(!verdict.is_refuted());
assert!(
verdict.reason().contains("no check run recorded"),
"{}",
verdict.reason()
);
}
#[test]
fn a_non_green_conclusion_is_not_a_refutation() {
let sha = "5555555555555555555555555555555555555555";
for conclusion in [Conclusion::Failure, Conclusion::Inconclusive] {
let runs: Vec<CheckRun> = ci_at(sha)
.into_iter()
.map(|c| CheckRun { conclusion, ..c })
.collect();
let site = ClaimSite::unknown(sha, "crates/roteiro/src/main.rs");
let verdict = suppression(&site, &runs);
assert!(!verdict.is_refuted(), "{conclusion:?}");
assert!(
verdict.reason().contains(&format!("{conclusion:?}")),
"names the conclusion: {}",
verdict.reason()
);
}
}
#[test]
fn no_evidence_suppresses_nothing() {
let site = ClaimSite::unknown(
"6666666666666666666666666666666666666666",
"crates/rto-graph/src/lib.rs",
);
assert!(!suppression(&site, &[]).is_refuted());
}
#[test]
fn the_reported_job_is_order_independent() {
let sha = "7777777777777777777777777777777777777777";
let site = ClaimSite::unknown(sha, "crates/roteiro/src/main.rs");
let mut reversed = ci_at(sha);
reversed.reverse();
let forward = suppression(&site, &ci_at(sha));
let backward = suppression(&site, &reversed);
assert_eq!(forward, backward);
let Suppression::Refuted { ref job, .. } = forward else {
panic!("unconditional code is refuted by a green all-features job");
};
assert_eq!(job, "checks", "job-name order, not collection order");
}
}