#![expect(clippy::expect_used, reason = "test scaffolding")]
#![expect(clippy::panic, reason = "test scaffolding")]
use std::fs;
use std::path::Path;
const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");
fn read(rel: &str) -> String {
let path = Path::new(MANIFEST_DIR).join(rel);
fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
}
fn table_lines<'a>(toml: &'a str, section: &str) -> Vec<&'a str> {
let header = format!("[{section}]");
let mut in_section = false;
let mut lines = Vec::new();
for raw in toml.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') {
in_section = line == header;
continue;
}
if in_section {
lines.push(line);
}
}
lines
}
fn key_value<'a>(line: &'a str, key: &str) -> Option<&'a str> {
let rest = line.strip_prefix(key)?.trim_start();
let rest = rest.strip_prefix('=')?;
let value = rest.split('#').next().unwrap_or("").trim();
Some(value.trim_matches('"'))
}
fn package_version(manifest: &str) -> String {
for line in table_lines(manifest, "package") {
if let Some(value) = key_value(line, "version") {
return value.to_string();
}
}
panic!("no `version =` key found in Cargo.toml [package]");
}
fn top_level_value(toml: &str, key: &str) -> Option<String> {
for raw in toml.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') {
break;
}
if let Some(value) = key_value(line, key) {
return Some(value.to_string());
}
}
None
}
#[test]
fn release_version_has_changelog_section() {
let version = package_version(&read("Cargo.toml"));
if version.contains('-') {
return;
}
let changelog = read("CHANGELOG.md");
let heading = format!("## [{version}]");
assert!(
changelog.contains(&heading),
"CHANGELOG.md is missing a `{heading}` section for the current crate version"
);
}
#[test]
fn release_toml_disables_cargo_release_publish() {
let publish = top_level_value(&read("release.toml"), "publish");
assert_eq!(
publish.as_deref(),
Some("false"),
"release.toml must set `publish = false`; crates.io publishing is owned \
by the publish-crate job in release.yml, and cargo-release publishing \
too would double-publish"
);
}
#[test]
fn manifest_has_crates_io_discovery_metadata() {
let manifest = read("Cargo.toml");
let package = table_lines(&manifest, "package");
for key in ["keywords", "categories"] {
let present = package
.iter()
.filter_map(|line| key_value(line, key))
.any(|value| value.starts_with('['));
assert!(
present,
"Cargo.toml [package] is missing a `{key}` array for crates.io discovery"
);
}
}
#[test]
fn release_toml_tag_name_matches_ci_trigger() {
let tag_name =
top_level_value(&read("release.toml"), "tag-name").expect("release.toml must set tag-name");
assert!(
tag_name.starts_with('v'),
"release.toml tag-name `{tag_name}` must start with `v` to match the `v*` \
tag trigger in .github/workflows/release.yml"
);
let workflow = read(".github/workflows/release.yml");
assert!(
workflow.contains("v*"),
".github/workflows/release.yml must trigger on `v*` tags to match \
release.toml tag-name `{tag_name}`"
);
}