test: verify headless dependency closure

This commit is contained in:
2026-06-23 23:05:01 +04:00
parent e6778d43af
commit 4c1edef21b
2 changed files with 90 additions and 1 deletions
+89
View File
@@ -425,6 +425,7 @@ fn run_policy(root: &Path) -> Result<(), String> {
let mut failures = Vec::new();
scan_policy_dir(root, &mut failures)?;
validate_cargo_metadata(root, &mut failures)?;
validate_cargo_metadata_dependency_closures(root, &mut failures)?;
validate_lockfile(root, &mut failures);
validate_workspace_license(root, &mut failures)?;
validate_dependency_boundaries(root, &mut failures)?;
@@ -456,6 +457,69 @@ fn validate_cargo_metadata(root: &Path, failures: &mut Vec<String>) -> Result<()
Ok(())
}
fn validate_cargo_metadata_dependency_closures(
root: &Path,
failures: &mut Vec<String>,
) -> Result<(), String> {
let mut manifests = Vec::new();
collect_cargo_manifests(root, &mut manifests)?;
let mut deps_by_package = BTreeMap::new();
for manifest in manifests {
let text = fs::read_to_string(&manifest)
.map_err(|err| format!("{}: {err}", manifest.display()))?;
let Some(package) = parse_package_name(&text) else {
continue;
};
deps_by_package.insert(package, parse_manifest_dependencies(&text));
}
validate_package_closure_excludes("fparkan-headless", &deps_by_package, failures);
Ok(())
}
fn validate_package_closure_excludes(
package: &str,
deps_by_package: &BTreeMap<String, BTreeSet<String>>,
failures: &mut Vec<String>,
) {
if !deps_by_package.contains_key(package) {
failures.push(format!(
"workspace manifest graph missing package {package}"
));
return;
}
let closure = dependency_closure_names(package, deps_by_package);
if let Some(forbidden) = first_forbidden_platform_bridge_dependency(&closure) {
failures.push(format!(
"workspace manifest closure: package {package} depends on forbidden platform/render dependency {forbidden}"
));
}
}
fn dependency_closure_names(
root: &str,
deps_by_package: &BTreeMap<String, BTreeSet<String>>,
) -> BTreeSet<String> {
let mut seen = BTreeSet::new();
let mut names = BTreeSet::new();
let mut stack = deps_by_package
.get(root)
.cloned()
.unwrap_or_default()
.into_iter()
.collect::<Vec<_>>();
while let Some(name) = stack.pop() {
if !seen.insert(name.clone()) {
continue;
}
names.insert(name.clone());
if let Some(deps) = deps_by_package.get(&name) {
stack.extend(deps.iter().cloned());
}
}
names
}
fn validate_lockfile(root: &Path, failures: &mut Vec<String>) {
let lockfile = root.join("Cargo.lock");
if !lockfile.is_file() {
@@ -1861,6 +1925,31 @@ fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
assert!(deps.contains("fparkan-render-vulkan"));
}
#[test]
fn workspace_manifest_closure_detects_transitive_platform_bridge() {
let deps_by_package = [
(
"fparkan-headless".to_string(),
["fparkan-runtime".to_string()].into_iter().collect(),
),
(
"fparkan-runtime".to_string(),
["fparkan-render-vulkan".to_string()].into_iter().collect(),
),
("fparkan-render-vulkan".to_string(), BTreeSet::new()),
]
.into_iter()
.collect::<BTreeMap<_, _>>();
let closure = dependency_closure_names("fparkan-headless", &deps_by_package);
assert!(closure.contains("fparkan-runtime"));
assert_eq!(
first_forbidden_platform_bridge_dependency(&closure),
Some("fparkan-render-vulkan")
);
}
#[test]
fn detects_forbidden_domain_dependencies() {
assert!(!is_forbidden_domain_dependency("fparkan-render-vulkan"));