use super::super::model::Dependency;
use super::*;
fn tooling_fixture() -> Result<tempfile::TempDir, Box<dyn Error>> {
let directory = fixture()?;
let manifest = directory.path().join("Cargo.toml");
fs::write(
&manifest,
fs::read_to_string(&manifest)?.replace(
"members = [",
"members = [\"tools/generator\", \"tools/derive\", ",
),
)?;
for (name, target) in [("generator", ""), ("derive", "[lib]\nproc-macro = true\n")] {
let root = directory.path().join("tools").join(name);
fs::create_dir_all(root.join("src"))?;
fs::write(
root.join("Cargo.toml"),
format!(
"[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n{target}[package.metadata.pipecircuit]\nrole = \"tooling\"\n",
),
)?;
fs::write(
root.join("src/lib.rs"),
"compile_error!(\"inspection must not compile tools\");\n",
)?;
}
Ok(directory)
}
#[test]
fn cargo_host_tools_are_allowed_without_becoming_runtime_core() -> Result<(), Box<dyn Error>> {
let directory = tooling_fixture()?;
let manifest = directory.path().join("core/Cargo.toml");
fs::write(
&manifest,
format!(
"{}\nderive = {{ path = \"../tools/derive\" }}\n[build-dependencies]\ngenerator = {{ path = \"../tools/generator\" }}\n",
fs::read_to_string(&manifest)?,
),
)?;
let before = fs::read(directory.path().join("Cargo.lock")).ok();
let architecture = analyze(directory.path())?;
assert!(
architecture.findings.is_empty(),
"{:?}",
architecture.findings
);
assert_eq!(architecture.work.metadata_invocations, 1);
assert_eq!(architecture.work.sources_read, 3);
assert_eq!(architecture.work.dependency_edges_checked, 5);
assert_eq!(before, fs::read(directory.path().join("Cargo.lock")).ok());
let macros = architecture
.packages
.values()
.find(|package| package.name == "derive")
.ok_or("missing derive")?;
let generator = architecture
.packages
.values()
.find(|package| package.name == "generator")
.ok_or("missing generator")?;
assert!(macros.proc_macro);
assert!(!generator.proc_macro);
let text = render(&architecture, Action::Visualize, Format::Text)?;
assert!(text.contains("Development and build tooling"));
assert!(!text.contains("Invalid ownership"));
Ok(())
}
#[test]
fn tooling_allowance_checks_edge_kind_and_actual_cargo_target() -> Result<(), Box<dyn Error>> {
let directory = tooling_fixture()?;
for owner in ["stock", "bakery-pipes", "bakery-routing"] {
for (target_name, kind, target_role, permitted) in [
("generator", "normal", Role::Tooling, false),
("generator", "build", Role::Tooling, true),
("derive", "normal", Role::Tooling, true),
("generator", "build", Role::Wiring, false),
("derive", "normal", Role::Wiring, false),
] {
let mut architecture = discover::load(&directory.path().join("Cargo.toml"))?;
let target = architecture
.packages
.values_mut()
.find(|package| package.name == target_name)
.ok_or("missing tool")?;
target.declaration = Some(Declaration {
role: target_role,
app: None,
scope: (target_role == Role::Wiring).then_some(Scope::Shared),
forbid_dependencies: vec![],
});
let path = target.manifest.parent().ok_or("missing parent")?.to_owned();
let source = architecture
.packages
.values_mut()
.find(|package| package.name == owner)
.ok_or("missing source")?;
source.dependencies.push(Dependency {
name: target_name.into(),
alias: None,
path: Some(path),
kind: kind.into(),
target: Some("cfg(unix)".into()),
optional: true,
default_features: false,
features: vec![],
});
check::validate(&mut architecture)?;
let codes: Vec<_> = architecture
.findings
.iter()
.map(|finding| finding.code)
.collect();
assert_eq!(
codes,
if permitted {
vec![]
} else {
vec!["arch.dependency.outward"]
},
"{owner} -> {target_name}, {kind}, {target_role}"
);
}
}
Ok(())
}
#[test]
fn host_tools_do_not_bypass_explicit_dependency_isolation() -> Result<(), Box<dyn Error>> {
let directory = tooling_fixture()?;
let manifest = directory.path().join("core/Cargo.toml");
fs::write(
&manifest,
format!(
"{}\nderive = {{ path = \"../tools/derive\" }}\n[package.metadata.pipecircuit]\nrole = \"core\"\nforbid_dependencies = [\"derive\"]\n",
fs::read_to_string(&manifest)?,
),
)?;
let architecture = analyze(directory.path())?;
let codes: Vec<_> = architecture
.findings
.iter()
.map(|finding| finding.code)
.collect();
assert_eq!(codes, vec!["arch.isolation.reachable"]);
Ok(())
}
#[test]
fn tooling_cannot_override_a_canonical_semantic_role() -> Result<(), Box<dyn Error>> {
let directory = fixture()?;
let manifest = directory.path().join("core/Cargo.toml");
fs::write(
&manifest,
format!(
"{}\n[package.metadata.pipecircuit]\nrole = \"tooling\"\n",
fs::read_to_string(&manifest)?
),
)?;
fs::write(
directory.path().join("core/src/lib.rs"),
"pub struct NotPure;\n",
)?;
let architecture = analyze(directory.path())?;
let codes: Vec<_> = architecture
.findings
.iter()
.map(|finding| finding.code)
.collect();
assert_eq!(codes, vec!["arch.purity.no-std", "arch.role.conflict"]);
Ok(())
}
#[test]
fn lint_diagnostics_identify_missing_and_weakened_rules_at_their_source()
-> Result<(), Box<dyn Error>> {
let directory = fixture()?;
let manifest = directory.path().join("Cargo.toml");
fs::write(
&manifest,
fs::read_to_string(&manifest)?
.replace("std_instead_of_core = \"deny\"", "")
.replace("unwrap_used = \"deny\"", "unwrap_used = \"allow\""),
)?;
let architecture = analyze(directory.path())?;
assert_eq!(architecture.findings.len(), 6);
for finding in &architecture.findings {
assert_eq!(finding.code, "arch.purity.lint");
assert!(finding.explanation.contains(&format!(
"[workspace.lints.clippy] at {}",
manifest.display()
)));
assert!(!finding.explanation.contains("check-cfg"));
assert!(
finding.explanation.contains("found missing")
|| finding.explanation.contains("found allow")
);
}
let mut architecture = discover::load(&manifest)?;
let package = architecture
.packages
.values_mut()
.find(|package| package.name == "stock")
.ok_or("missing core")?;
let mut document = package.document.as_ref().clone();
let local_lints = document.get_mut("lints").ok_or("missing local lints")?;
*local_lints = package.workspace_lints.as_ref().clone();
package.document = std::rc::Rc::new(document);
check::validate(&mut architecture)?;
for finding in architecture
.findings
.iter()
.filter(|finding| finding.subject == "stock")
{
assert!(finding.explanation.contains(&format!(
"[lints.clippy] at {}",
directory.path().join("core/Cargo.toml").display()
)));
}
Ok(())
}
#[test]
fn third_party_dependencies_cannot_launder_first_party_edges() -> Result<(), Box<dyn Error>> {
let directory = tooling_fixture()?;
for (source_name, target_name, kind, permitted) in [
("stock", "generator", "normal", true),
("bakery-pipes", "generator", "normal", false),
("bakery-routing", "generator", "normal", false),
("bakery-routing", "derive", "normal", true),
("bakery-pipes", "generator", "build", true),
("generator", "derive", "normal", true),
("generator", "stock", "normal", false),
("generator", "bakery-wiring", "normal", false),
("generator", "bakery-wiring", "build", false),
] {
let mut architecture = discover::load(&directory.path().join("Cargo.toml"))?;
for package in architecture
.packages
.values_mut()
.filter(|package| matches!(package.name.as_str(), "generator" | "derive"))
{
package.declaration = Some(Declaration {
role: Role::ThirdParty,
app: None,
scope: None,
forbid_dependencies: vec![],
});
}
let target = architecture
.packages
.values()
.find(|package| package.name == target_name)
.ok_or("missing target")?;
let path = target.manifest.parent().ok_or("missing parent")?.to_owned();
let source = architecture
.packages
.values_mut()
.find(|package| package.name == source_name)
.ok_or("missing source")?;
source.dependencies.push(Dependency {
name: target_name.into(),
alias: None,
path: Some(path),
kind: kind.into(),
target: None,
optional: false,
default_features: false,
features: vec![],
});
check::validate(&mut architecture)?;
let codes: Vec<_> = architecture
.findings
.iter()
.map(|finding| finding.code)
.collect();
assert_eq!(
codes,
if permitted {
vec![]
} else {
vec!["arch.dependency.outward"]
},
"{source_name} -> {target_name}, {kind}"
);
assert!(
render(&architecture, Action::Visualize, Format::Text)?
.contains("Third-party dependencies (not audited as core)")
);
}
Ok(())
}
#[test]
fn third_party_cannot_reenter_first_party_through_a_proc_macro() -> Result<(), Box<dyn Error>> {
let directory = tooling_fixture()?;
let manifest = directory.path().join("tools/generator/Cargo.toml");
fs::write(
&manifest,
format!(
"{}\n[dependencies]\nderive = {{ path = \"../derive\" }}\n",
fs::read_to_string(&manifest)?.replace("role = \"tooling\"", "role = \"third-party\"")
),
)?;
let architecture = analyze(directory.path())?;
let codes: Vec<_> = architecture
.findings
.iter()
.map(|finding| finding.code)
.collect();
assert_eq!(codes, vec!["arch.dependency.outward"]);
let finding = architecture.findings.first().ok_or("missing finding")?;
assert!(
finding
.guidance
.starts_with("Third-party packages cannot depend back into first-party crates")
);
Ok(())
}
#[test]
fn missing_cfg_validation_names_the_required_configuration() -> Result<(), Box<dyn Error>> {
let directory = fixture()?;
let manifest = directory.path().join("Cargo.toml");
fs::write(
&manifest,
fs::read_to_string(&manifest)?.replace(
"unexpected_cfgs = { level = \"warn\", check-cfg = ['cfg(kani)'] }",
"unexpected_cfgs = \"warn\"",
),
)?;
let architecture = analyze(directory.path())?;
assert_eq!(architecture.findings.len(), 3);
for finding in &architecture.findings {
assert_eq!(finding.code, "arch.purity.lint");
assert!(
finding
.explanation
.contains("found warn in [workspace.lints.rust]")
);
assert!(
finding
.explanation
.ends_with("Required check-cfg = [\"cfg(kani)\"].")
);
}
Ok(())
}
#[test]
fn third_party_declaration_cannot_disable_canonical_purity() -> Result<(), Box<dyn Error>> {
let directory = fixture()?;
let manifest = directory.path().join("core/Cargo.toml");
fs::write(
&manifest,
format!(
"{}\n[package.metadata.pipecircuit]\nrole = \"third-party\"\n",
fs::read_to_string(&manifest)?
),
)?;
fs::write(
directory.path().join("core/src/lib.rs"),
"pub struct NotPure;\n",
)?;
let architecture = analyze(directory.path())?;
let codes: Vec<_> = architecture
.findings
.iter()
.map(|finding| finding.code)
.collect();
assert_eq!(codes, vec!["arch.purity.no-std", "arch.role.conflict"]);
Ok(())
}