use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
const PUBLISHED: [&str; 7] = [
"pedant-types",
"pedant-core",
"pedant-syntax",
"pedant-snippet",
"pedant-lang",
"pedant-mcp",
"pedant",
];
#[cfg(feature = "resolution-test-support")]
const STEP_COMMAND: &str = "docs/scripts/with_build_lease.sh docs/scripts/verify_step.sh";
#[cfg(feature = "resolution-test-support")]
const AFFECTED_COMMAND: &str = "docs/scripts/with_build_lease.sh docs/scripts/verify_affected.sh";
#[cfg(feature = "resolution-test-support")]
const CLASSIFIER: &str = "docs/scripts/cargo_infrastructure.sh";
#[cfg(feature = "resolution-test-support")]
const CLASSIFIER_RUNNERS: [&str; 3] = [
"docs/scripts/verify_step.sh",
"docs/scripts/verify_affected.sh",
"docs/scripts/run_resolution_proof.sh",
];
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("pedant-core sits one level below the repository root")
.to_path_buf()
}
fn read(relative: &str) -> String {
let path = repo_root().join(relative);
std::fs::read_to_string(&path).unwrap_or_else(|error| panic!("{}: {error}", path.display()))
}
fn parse_toml(relative: &str) -> toml::Table {
toml::from_str(&read(relative)).unwrap_or_else(|error| panic!("{relative}: {error}"))
}
fn declared_version(manifest: &toml::Table, name: &str) -> String {
manifest
.get("package")
.and_then(|package| package.get("version"))
.and_then(toml::Value::as_str)
.unwrap_or_else(|| panic!("{name} declares no package version"))
.to_owned()
}
fn first_party_requirements(manifest: &toml::Table, name: &str) -> Vec<(String, String)> {
["dependencies", "dev-dependencies", "build-dependencies"]
.iter()
.filter_map(|table| manifest.get(*table))
.filter_map(toml::Value::as_table)
.flat_map(|table| table.iter())
.filter(|(dependency, _)| PUBLISHED.contains(&dependency.as_str()))
.map(|(dependency, spec)| {
let version = spec
.get("version")
.and_then(toml::Value::as_str)
.unwrap_or_else(|| panic!("{name} requires {dependency} without a version"));
(dependency.clone(), version.to_owned())
})
.collect()
}
#[test]
fn published_versions_and_requirements_form_releaseable_graph() {
let declared: BTreeMap<&str, String> = PUBLISHED
.iter()
.map(|package| {
let manifest = parse_toml(&format!("{package}/Cargo.toml"));
(*package, declared_version(&manifest, package))
})
.collect();
for package in PUBLISHED {
let manifest = parse_toml(&format!("{package}/Cargo.toml"));
for (dependency, requirement) in first_party_requirements(&manifest, package) {
let published = declared
.get(dependency.as_str())
.expect("a first-party dependency is a published package");
assert_eq!(
&requirement, published,
"{package} requires {dependency} at the version {dependency} publishes"
);
}
}
let release = parse_toml("release-plz.toml");
let entries: Vec<&str> = release
.get("package")
.and_then(toml::Value::as_array)
.expect("release-plz.toml declares a package array")
.iter()
.map(|entry| {
entry
.get("name")
.and_then(toml::Value::as_str)
.expect("every release-plz entry names a package")
})
.collect();
let unique: BTreeSet<&str> = entries.iter().copied().collect();
assert_eq!(
unique.len(),
entries.len(),
"release-plz.toml names each package once: {entries:?}"
);
assert_eq!(
unique,
PUBLISHED.into_iter().collect::<BTreeSet<_>>(),
"release-plz.toml covers exactly the published packages"
);
for (position, name) in entries.iter().enumerate() {
let manifest = parse_toml(&format!("{name}/Cargo.toml"));
for (dependency, _) in first_party_requirements(&manifest, name) {
let dependency_position = entries
.iter()
.position(|entry| *entry == dependency)
.expect("a first-party dependency is released too");
assert!(
dependency_position < position,
"{name} is released before its dependency {dependency}"
);
}
}
}
#[test]
fn unpublished_dev_dependencies_never_become_registry_requirements() {
let consumers: Vec<&str> = PUBLISHED
.into_iter()
.filter_map(|package| {
let manifest = parse_toml(&format!("{package}/Cargo.toml"));
let requirement = manifest
.get("dev-dependencies")
.and_then(|dependencies| dependencies.get("pedant-process-guard"));
requirement.map(|requirement| {
assert_eq!(
requirement.get("path").and_then(toml::Value::as_str),
Some("../test-support/process-guard"),
"{package} uses the shared local process guard"
);
assert!(
requirement.get("version").is_none(),
"{package} must not turn the unpublished process guard into a registry requirement"
);
package
})
})
.collect();
assert_eq!(
consumers,
["pedant-mcp", "pedant"],
"the two process-spawning packages share the guard"
);
}
#[test]
fn process_guard_windows_features_cover_job_creation_types() {
let manifest = parse_toml("test-support/process-guard/Cargo.toml");
let features = manifest
.get("target")
.and_then(|targets| targets.get("cfg(windows)"))
.and_then(|windows| windows.get("dependencies"))
.and_then(|dependencies| dependencies.get("windows-sys"))
.and_then(|dependency| dependency.get("features"))
.and_then(toml::Value::as_array)
.expect("the process guard declares Windows API features");
assert!(
features
.iter()
.any(|feature| feature.as_str() == Some("Win32_Security")),
"CreateJobObjectW is generated only when Win32_Security is enabled"
);
}
#[test]
fn dependency_policy_allows_only_path_wildcards() {
let policy = parse_toml("deny.toml");
let bans = policy
.get("bans")
.and_then(toml::Value::as_table)
.expect("deny.toml declares [bans]");
assert_eq!(
bans.get("wildcards").and_then(toml::Value::as_str),
Some("deny"),
"registry wildcard dependencies remain denied"
);
assert_eq!(
bans.get("allow-wildcard-paths")
.and_then(toml::Value::as_bool),
Some(true),
"unpublished path-only dependencies must remain packageable"
);
}
#[cfg(feature = "resolution-test-support")]
#[test]
fn verification_commands_are_build_lease_wrapped_and_classifier_backed() {
let manifest = parse_toml(".manifest.toml");
let verification = manifest
.get("verification")
.and_then(toml::Value::as_table)
.expect(".manifest.toml declares [verification]");
for (key, expected) in [("step", STEP_COMMAND), ("affected", AFFECTED_COMMAND)] {
let declared = verification
.get(key)
.and_then(toml::Value::as_str)
.unwrap_or_else(|| panic!("[verification].{key} is declared"));
assert_eq!(
declared, expected,
"[verification].{key} must run under the outer build lease"
);
}
let classifier = read(CLASSIFIER);
for owned in [
"CARGO_INFRASTRUCTURE_PATTERNS=",
"CARGO_INFRASTRUCTURE_STATUS=75",
] {
assert!(
classifier.contains(owned),
"{CLASSIFIER} owns {owned}, and it is missing"
);
}
for runner in CLASSIFIER_RUNNERS {
let source = read(runner);
assert!(
source.contains("cargo_infrastructure.sh"),
"{runner} must source the shared classifier"
);
assert!(
!source.contains("CARGO_INFRASTRUCTURE_PATTERNS="),
"{runner} must not restate the classifier's pattern set"
);
assert!(
!source.contains("with_build_lease.sh"),
"{runner} runs under the manifest command's lease; an inner one deadlocks"
);
}
}
#[cfg(feature = "resolution-test-support")]
#[test]
fn ci_installs_every_resolution_runner_tool_before_execution() {
let workflow = read(".github/workflows/ci.yml");
let install = workflow
.find("sudo apt-get install --yes ripgrep")
.expect("CI installs ripgrep for the resolution proof runner");
let proof = workflow
.find("docs/scripts/run_resolution_proof.sh resolution-tier1-dependency-closure")
.expect("CI runs the Tier 1 dependency-closure proof");
assert!(
install < proof,
"CI must install the proof runner's tools before invoking it"
);
}