use std::fs;
use std::path::PathBuf;
fn repo_file(rel: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(rel)
}
fn read(rel: &str) -> String {
fs::read_to_string(repo_file(rel))
.unwrap_or_else(|err| panic!("{rel} must exist at the repo root; read error: {err}"))
.replace("\r\n", "\n")
}
fn declared_msrv() -> String {
read("Cargo.toml")
.lines()
.find_map(|line| line.strip_prefix("rust-version = \""))
.and_then(|rest| rest.split('"').next())
.expect("Cargo.toml must declare `rust-version = \"X.Y\"` at the left margin")
.to_string()
}
const LIVE_CLAIMS: &[(&str, &str)] = &[
("README.md", "badge/rust-{msrv}%2B-orange"),
("CONTRIBUTING.md", "stable channel, {msrv}+"),
("ROADMAP.md", "MSRV is **{msrv}**"),
("docs/1.getting-started/1.install.md", "MSRV is **{msrv}**"),
("docs/fr/1.getting-started/1.install.md", "le MSRV est **{msrv}**"),
("docs/6.development/3.stability.md", "(currently **{msrv}**)"),
("docs/fr/6.development/3.stability.md", "(actuellement **{msrv}**)"),
("docs/7.roadmap.md", "The project MSRV is **{msrv}**."),
("docs/fr/7.roadmap.md", "Le MSRV du projet est **{msrv}**."),
("skills/SKILL.md", "MSRV {msrv})"),
("skills/SKILL.md", "({msrv}+ — the crate MSRV)"),
];
#[test]
fn every_live_msrv_claim_matches_cargo_toml() {
let msrv = declared_msrv();
for (file, pattern) in LIVE_CLAIMS {
let expected = pattern.replace("{msrv}", &msrv);
assert!(
read(file).contains(&expected),
"{file} must advertise the declared MSRV: expected to find {expected:?} \
(Cargo.toml declares `rust-version = \"{msrv}\"`). An MSRV bump has to \
reach every user-facing claim, not just the manifest (#491)"
);
}
}
fn msrv_job() -> serde_yaml_ng::Value {
let workflow: serde_yaml_ng::Value =
serde_yaml_ng::from_str(&read(".github/workflows/ci.yml")).expect("ci.yml must be valid YAML");
workflow["jobs"]["msrv"].clone()
}
fn steps(job: &serde_yaml_ng::Value) -> Vec<serde_yaml_ng::Value> {
job["steps"].as_sequence().cloned().unwrap_or_default()
}
#[test]
fn ci_runs_the_msrv_check_on_every_supported_platform() {
let job = msrv_job();
let matrix = job["strategy"]["matrix"]["os"]
.as_sequence()
.cloned()
.unwrap_or_default()
.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect::<Vec<_>>();
for os in ["ubuntu-latest", "macos-latest", "windows-latest"] {
assert!(
matrix.iter().any(|m| m == os),
"the msrv job must run on {os}, like the `test` job (matrix is {matrix:?}). \
A single-platform run compiles neither the \
`[target.\"cfg(windows)\".dependencies]` block nor the `#[cfg(windows)]` \
code, so a Windows-only dependency raising its own floor stays green on \
Linux while `cargo install` breaks for those users"
);
}
}
#[test]
fn ci_feeds_the_msrv_job_the_toolchain_declared_in_cargo_toml() {
let job = msrv_job();
let steps = steps(&job);
let reader = steps
.iter()
.find(|s| s["id"].as_str() == Some("msrv"))
.expect("the msrv job needs a step with `id: msrv` that reads the declared floor");
let script = reader["run"].as_str().unwrap_or_default();
assert!(
script.contains("rust-version") && script.contains("Cargo.toml"),
"the `id: msrv` step must read `rust-version` out of Cargo.toml, got: {script:?}"
);
assert!(
reader["shell"].as_str() == Some("bash"),
"the reader step must force `shell: bash`: windows-latest defaults to \
PowerShell, which has no grep or cut"
);
let toolchain = steps
.iter()
.find(|s| {
s["uses"]
.as_str()
.is_some_and(|u| u.starts_with("dtolnay/rust-toolchain@"))
})
.expect("the msrv job must install a toolchain via dtolnay/rust-toolchain");
assert_eq!(
toolchain["uses"].as_str(),
Some("dtolnay/rust-toolchain@master"),
"pin the action at @master and pass the version as an input; a \
`rust-toolchain@<version>` ref is a literal that drifts from the manifest"
);
assert_eq!(
toolchain["with"]["toolchain"].as_str(),
Some("${{ steps.msrv.outputs.version }}"),
"the installed toolchain must be the value read from Cargo.toml, not a \
literal. A literal here is the drift this whole job exists to catch (#491)"
);
}
#[test]
fn ci_checks_the_msrv_locked_and_without_default_features() {
let commands = steps(&msrv_job())
.iter()
.filter_map(|s| s["run"].as_str().map(str::to_owned))
.collect::<Vec<_>>()
.join("\n");
let checks: Vec<&str> = commands
.lines()
.map(str::trim)
.filter(|l| l.starts_with("cargo check"))
.collect();
assert!(
!checks.is_empty(),
"the msrv job must run `cargo check` at the declared floor"
);
assert!(
checks.iter().all(|c| c.contains("--locked")),
"every msrv `cargo check` must pass `--locked`: cargo's `rust-version` gate \
fires at *resolve* time against the committed lockfile, which is what turns \
a dependency declaring a higher floor into a red job. Got: {checks:?}"
);
assert!(
checks.iter().any(|c| c.contains("--no-default-features")),
"the msrv job must also check `--no-default-features`. `daemon` is default-on, \
so the default-features check never compiles the \
`#[cfg(not(all(any(unix, windows), feature = \"daemon\")))]` arms of \
`cmd_daemon` / `cmd_statusline`, and that build is documented as supported. \
Got: {checks:?}"
);
}