test: verify headless dependency closure
This commit is contained in:
@@ -7,7 +7,7 @@ L0-P2-001 covered cargo test -p fparkan-corpus --offline licensed_part2_manifest
|
|||||||
L0-P2-002 covered cargo test -p fparkan-corpus --offline licensed_part2_has_no_casefold_relative_path_collisions
|
L0-P2-002 covered cargo test -p fparkan-corpus --offline licensed_part2_has_no_casefold_relative_path_collisions
|
||||||
S0-ARCH-001 covered cargo xtask policy runs cargo metadata --offline --no-deps successfully
|
S0-ARCH-001 covered cargo xtask policy runs cargo metadata --offline --no-deps successfully
|
||||||
S0-ARCH-002 covered cargo xtask policy rejects forbidden GUI/adapter dependencies from domain crates
|
S0-ARCH-002 covered cargo xtask policy rejects forbidden GUI/adapter dependencies from domain crates
|
||||||
S0-ARCH-003 covered cargo xtask policy rejects platform/render adapter dependencies from fparkan-headless
|
S0-ARCH-003 covered cargo xtask policy rejects platform/render adapter dependencies from the transitive fparkan-headless workspace manifest closure
|
||||||
S0-ARCH-004 covered cargo xtask policy scans workspace-owned Rust/TOML for unsafe constructs and workspace lints forbid unsafe_code
|
S0-ARCH-004 covered cargo xtask policy scans workspace-owned Rust/TOML for unsafe constructs and workspace lints forbid unsafe_code
|
||||||
S0-ARCH-005 covered cargo xtask policy rejects Python source files, Python shebangs, and Python CI workflow steps while allowing docs requirements.txt
|
S0-ARCH-005 covered cargo xtask policy rejects Python source files, Python shebangs, and Python CI workflow steps while allowing docs requirements.txt
|
||||||
S0-ARCH-006 covered cargo xtask policy rejects non-fparkan package directories under crates/
|
S0-ARCH-006 covered cargo xtask policy rejects non-fparkan package directories under crates/
|
||||||
|
|||||||
|
@@ -425,6 +425,7 @@ fn run_policy(root: &Path) -> Result<(), String> {
|
|||||||
let mut failures = Vec::new();
|
let mut failures = Vec::new();
|
||||||
scan_policy_dir(root, &mut failures)?;
|
scan_policy_dir(root, &mut failures)?;
|
||||||
validate_cargo_metadata(root, &mut failures)?;
|
validate_cargo_metadata(root, &mut failures)?;
|
||||||
|
validate_cargo_metadata_dependency_closures(root, &mut failures)?;
|
||||||
validate_lockfile(root, &mut failures);
|
validate_lockfile(root, &mut failures);
|
||||||
validate_workspace_license(root, &mut failures)?;
|
validate_workspace_license(root, &mut failures)?;
|
||||||
validate_dependency_boundaries(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(())
|
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>) {
|
fn validate_lockfile(root: &Path, failures: &mut Vec<String>) {
|
||||||
let lockfile = root.join("Cargo.lock");
|
let lockfile = root.join("Cargo.lock");
|
||||||
if !lockfile.is_file() {
|
if !lockfile.is_file() {
|
||||||
@@ -1861,6 +1925,31 @@ fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
|
|||||||
assert!(deps.contains("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]
|
#[test]
|
||||||
fn detects_forbidden_domain_dependencies() {
|
fn detects_forbidden_domain_dependencies() {
|
||||||
assert!(!is_forbidden_domain_dependency("fparkan-render-vulkan"));
|
assert!(!is_forbidden_domain_dependency("fparkan-render-vulkan"));
|
||||||
|
|||||||
Reference in New Issue
Block a user