pipecircuit-cli 0.0.14

Command-line architectural tools for Pipecircuit programs
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(())
}