const BOUNDARY: &str = include_str!("../../../docs/v2.md");
const CHANGELOG: &str = include_str!("../../../CHANGELOG.md");
fn changelog_breaking_phrases() -> Vec<String> {
let section = CHANGELOG
.split("### Breaking")
.nth(1)
.expect("CHANGELOG still has a Breaking section under 2.0.0")
.split("### Added")
.next()
.expect("the Breaking section still ends at Added");
section
.lines()
.filter_map(|l| l.trim().strip_prefix("- "))
.filter_map(|l| l.strip_prefix("**"))
.filter_map(|l| l.split_once("**").map(|(phrase, _)| phrase.to_string()))
.collect()
}
fn boundary_rows() -> Vec<(String, String)> {
let table = BOUNDARY
.split("## 破坏性变更")
.nth(1)
.expect("the boundary file still has a breaking-change table")
.split("\n## ")
.next()
.expect("the table still ends at the next heading");
let mut out = Vec::new();
for line in table.lines() {
let line = line.trim();
if !line.starts_with("| ") {
continue;
}
let cells: Vec<&str> = line.trim_matches('|').split(" | ").map(str::trim).collect();
let first = cells.first().copied().unwrap_or("");
if first == "#" || first.starts_with("---") {
continue;
}
assert_eq!(
cells.len(),
4,
"breaking-change row `{first}` has {} cells, not 4 — escape \
any `|` inside a cell as `\\|`. A row this reader cannot \
split is a row nothing checks",
cells.len()
);
out.push((
first.to_string(),
cells[3].trim_matches('`').trim().to_string(),
));
}
out
}
#[test]
fn every_breaking_change_is_in_both_lists() {
let rows = boundary_rows();
let phrases = changelog_breaking_phrases();
assert!(
rows.len() >= 6,
"only {} rows read out of the boundary table — the shape \
changed and this would pass by knowing nothing",
rows.len()
);
assert!(
phrases.len() >= 6,
"only {} bold phrases read out of the CHANGELOG's Breaking \
section — same",
phrases.len()
);
let mut dangling = Vec::new();
for (n, phrase) in &rows {
if !phrases.contains(phrase) {
dangling.push(format!(
"row {n} claims `{phrase}`, which the CHANGELOG does not open an entry with"
));
}
}
let mut orphaned = Vec::new();
for p in &phrases {
if !rows.iter().any(|(_, phrase)| phrase == p) {
orphaned.push(format!(
"the CHANGELOG lists `{p}` and no row of the boundary table claims it"
));
}
}
let mut problems = dangling;
problems.extend(orphaned);
assert!(
problems.is_empty(),
"the two breaking-change lists disagree in {} places:\n {}",
problems.len(),
problems.join("\n ")
);
}
#[test]
fn summary() {
let rows = boundary_rows();
let behaviour = CLAIMS
.lines()
.filter(|l| l.contains("| behaviour |"))
.count();
println!(
"release-record: {} breaking changes, both lists agree · {behaviour} \
behaviour changes in the release notes · publish list {} crates, \
topological",
rows.len(),
publish_list().len()
);
}
#[test]
fn this_gate_runs_where_it_must() {
let preflight = include_str!("../../../scripts/dev/preflight.sh");
assert!(
preflight.contains("include_str!(\\\"[^\\\"]*$d\\\")"),
"preflight no longer maps a changed doc back to the crates \
whose tests read it — edit only the CHANGELOG and nothing \
checks it against the boundary file"
);
for (name, text) in [
("ci.yml", include_str!("../../../.github/workflows/ci.yml")),
("ship.sh", include_str!("../../../scripts/release/ship.sh")),
] {
assert!(
text.contains("cargo test --workspace"),
"{name} no longer runs the whole workspace, so nothing there \
runs this gate"
);
}
}
const CLAIMS: &str = include_str!("../../../docs/guide-executability.md");
fn changelog_phrases() -> Vec<String> {
let section = CHANGELOG
.split("## [2.0.0]")
.nth(1)
.expect("CHANGELOG still has a 2.0.0 section")
.split("\n## [")
.next()
.expect("the 2.0.0 section still ends at the next release");
section
.lines()
.filter_map(|l| l.trim().strip_prefix("- "))
.filter_map(|l| l.strip_prefix("**"))
.filter_map(|l| l.split_once("**").map(|(phrase, _)| phrase.to_string()))
.collect()
}
#[test]
fn every_behaviour_change_reaches_the_release_notes() {
let phrases = changelog_phrases();
assert!(
phrases.len() >= 20,
"only {} bold phrases read out of the 2.0.0 section — the shape \
changed and this would pass by knowing nothing",
phrases.len()
);
let mut behaviour = 0usize;
let mut problems = Vec::new();
for line in CLAIMS.lines() {
let line = line.trim();
if !line.starts_with("| ") {
continue;
}
let cells: Vec<&str> = line
.trim_matches('|')
.split(" | ")
.map(|c| c.trim().trim_matches('`').trim())
.collect();
let id = cells.first().copied().unwrap_or("");
if id == "id" || id.starts_with("---") {
continue;
}
assert_eq!(
cells.len(),
11,
"claim row `{id}` has {} cells, not 11 — escape any `|` \
inside a cell as `\\|`",
cells.len()
);
let kind = cells[9];
let citation = cells[10];
match kind {
"docs" => {
if citation != "—" {
problems.push(format!("{id} is marked docs-only and cites `{citation}`"));
}
}
"behaviour" => {
behaviour += 1;
if !phrases.iter().any(|p| p == citation) {
problems.push(format!(
"{id} changed behaviour and cites `{citation}`, which \
opens no entry under 2.0.0"
));
}
}
other => problems.push(format!(
"{id} has kind `{other}` — the vocabulary is docs / behaviour"
)),
}
}
assert!(
behaviour >= 5,
"only {behaviour} rows marked as behaviour changes — the column \
emptied and this would pass by knowing nothing"
);
assert!(
problems.is_empty(),
"{} claims are not reflected in the release notes:\n {}",
problems.len(),
problems.join("\n ")
);
}
const SHIP: &str = include_str!("../../../scripts/release/ship.sh");
const WORKSPACE: &str = include_str!("../../../Cargo.toml");
fn publish_list() -> Vec<String> {
SHIP.split("CRATES=(")
.nth(1)
.expect("ship.sh still declares a publish DAG")
.split(')')
.next()
.expect("the DAG literal still closes")
.split_whitespace()
.map(str::to_string)
.collect()
}
fn members() -> Vec<(String, bool, Vec<String>)> {
assert!(
WORKSPACE.contains("members = [\"crates/*\"]"),
"the workspace stopped globbing crates/ — this reader assumed \
that shape and would now report members it invented"
);
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("the crates directory");
let mut out = Vec::new();
for entry in std::fs::read_dir(root).expect("reading crates/").flatten() {
let manifest_path = entry.path().join("Cargo.toml");
let Ok(manifest) = std::fs::read_to_string(&manifest_path) else {
continue;
};
let name = entry
.file_name()
.to_str()
.expect("a crate directory name is utf-8")
.to_string();
let opted_out = manifest.lines().any(|l| {
let l = l.trim();
l.starts_with("publish") && l.contains("false")
});
let deps = manifest
.split("[dev-dependencies]")
.next()
.unwrap_or(&manifest)
.lines()
.filter_map(|l| {
let l = l.trim();
let dep = l.split_whitespace().next()?;
(dep.starts_with("smix-") && l.contains("path = \"../")).then(|| dep.to_string())
})
.collect();
out.push((name, opted_out, deps));
}
out.sort();
out
}
#[test]
fn the_publish_list_covers_everything_that_ships() {
let listed = publish_list();
let members = members();
assert!(
members.len() >= 25,
"only {} workspace members read — the manifest's shape changed \
and this would pass by knowing nothing",
members.len()
);
let mut problems = Vec::new();
for (name, opted_out, _) in &members {
match (listed.contains(name), opted_out) {
(false, false) => problems.push(format!(
"{name} is a workspace member that does not opt out of \
publishing and is not in the DAG"
)),
(true, true) => problems.push(format!(
"{name} declares `publish = false` and is in the DAG anyway"
)),
_ => {}
}
}
for name in &listed {
if !members.iter().any(|(m, _, _)| m == name) {
problems.push(format!("the DAG names {name}, which is not a member"));
}
}
let mut published: Vec<&str> = Vec::new();
for name in &listed {
if let Some((_, _, deps)) = members.iter().find(|(m, _, _)| m == name) {
for d in deps {
if listed.contains(d) && !published.contains(&d.as_str()) {
problems.push(format!(
"{name} is published before {d}, which it depends on"
));
}
}
}
published.push(name);
}
assert!(
problems.is_empty(),
"the publish DAG and the workspace disagree in {} places:\n {}",
problems.len(),
problems.join("\n ")
);
}
#[test]
fn the_semver_gate_survives_a_crate_it_cannot_check() {
for needle in [
"SEMVER_EXCLUDE",
"failed to build rustdoc for crate",
"not found in registry",
"of $SEMVER_TOTAL crates checked",
] {
assert!(
SHIP.contains(needle),
"ship.sh no longer contains {needle:?} — the semver step \
stopped tolerating crates the tool refuses, or stopped \
saying how many it really checked"
);
}
}