rch-common 2.1.21

Shared types and utilities for Remote Compilation Helper
Documentation
#![cfg(unix)]

use rch_common::e2e::{MultiRepoFixtureConfig, reset_multi_repo_fixtures};
use rch_common::{
    DependencyClosurePlanState, DependencyRiskClass, DependencySyncReason, PathTopologyPolicy,
    build_dependency_closure_plan_with_policy,
};
use std::fs;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

static COUNTER: AtomicU64 = AtomicU64::new(0);

struct TopologyFixture {
    root: PathBuf,
    canonical_root: PathBuf,
    alias_root: PathBuf,
}

impl TopologyFixture {
    fn new(prefix: &str) -> Self {
        let id = COUNTER.fetch_add(1, Ordering::SeqCst);
        let root = std::env::temp_dir().join(format!(
            "rch-dependency-planner-{}-{}-{}",
            prefix,
            std::process::id(),
            id
        ));
        let canonical_root = root.join("data/projects");
        let alias_root = root.join("dp");
        fs::create_dir_all(&canonical_root).expect("create canonical root");
        symlink(&canonical_root, &alias_root).expect("create alias symlink");

        Self {
            root,
            canonical_root,
            alias_root,
        }
    }

    fn policy(&self) -> PathTopologyPolicy {
        PathTopologyPolicy::new(self.canonical_root.clone(), self.alias_root.clone())
    }
}

impl Drop for TopologyFixture {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.root);
    }
}

fn write_bin_crate(root: &Path, crate_name: &str, deps: &[(&str, &str)]) {
    fs::create_dir_all(root.join("src")).expect("create crate src");
    fs::write(root.join("Cargo.toml"), crate_manifest(crate_name, deps)).expect("write manifest");
    fs::write(
        root.join("src/main.rs"),
        format!("fn main() {{ println!(\"{}\"); }}\n", crate_name),
    )
    .expect("write main.rs");
}

fn write_lib_crate(root: &Path, crate_name: &str, deps: &[(&str, &str)]) {
    fs::create_dir_all(root.join("src")).expect("create crate src");
    fs::write(root.join("Cargo.toml"), crate_manifest(crate_name, deps)).expect("write manifest");
    fs::write(root.join("src/lib.rs"), "pub fn marker() {}\n").expect("write lib.rs");
}

fn crate_manifest(crate_name: &str, deps: &[(&str, &str)]) -> String {
    let mut dependencies = String::new();
    for (name, path) in deps {
        dependencies.push_str(&format!("{name} = {{ path = \"{path}\" }}\n"));
    }
    format!(
        "[package]\nname = \"{crate_name}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n{dependencies}"
    )
}

#[test]
fn planner_single_repo_generates_ready_plan() {
    let fixture = TopologyFixture::new("single");
    let app_root = fixture.canonical_root.join("single_repo/app");
    write_bin_crate(&app_root, "single_repo_app", &[]);

    let plan = build_dependency_closure_plan_with_policy(&app_root, &fixture.policy());

    assert_eq!(plan.state, DependencyClosurePlanState::Ready);
    assert!(!plan.fail_open);
    assert_eq!(plan.sync_order.len(), 1);
    assert_eq!(
        plan.sync_order[0].metadata.reason,
        DependencySyncReason::EntryPoint
    );
    assert_eq!(plan.sync_order[0].risk, DependencyRiskClass::Low);
    assert_eq!(plan.sync_order[0].order_index, 0);
}

#[test]
fn planner_multi_repo_produces_deterministic_dependency_first_order() {
    let fixture = TopologyFixture::new("multi");
    let config = MultiRepoFixtureConfig::new(
        fixture.canonical_root.clone(),
        fixture.alias_root.clone(),
        "planner_multi_repo",
    );
    let fixtures = reset_multi_repo_fixtures(&config).expect("generate fixture set");
    let scenario = fixtures
        .fixture("ready_relative_transitive")
        .expect("ready transitive fixture metadata");

    let plan =
        build_dependency_closure_plan_with_policy(&scenario.alias_entrypoint, &fixture.policy());

    assert_eq!(plan.state, DependencyClosurePlanState::Ready);
    assert!(!plan.fail_open);

    let expected_order = scenario
        .canonical_repo_paths
        .iter()
        .map(|path| path.canonicalize().expect("canonical fixture path"))
        .collect::<Vec<_>>();
    let observed_order = plan
        .sync_order
        .iter()
        .map(|action| action.package_root.clone())
        .collect::<Vec<_>>();
    assert_eq!(observed_order, expected_order);

    let reasons = plan
        .sync_order
        .iter()
        .map(|action| action.metadata.reason)
        .collect::<Vec<_>>();
    assert_eq!(
        reasons,
        vec![
            DependencySyncReason::TransitivePathDependency,
            DependencySyncReason::TransitivePathDependency,
            DependencySyncReason::EntryPoint,
        ]
    );
}

#[test]
fn planner_materializes_inactive_optional_cycle_as_full_sync_roots() {
    let fixture = TopologyFixture::new("optional-materialization");
    let scenario_root = fixture.canonical_root.join("optional_materialization");
    let app_root = scenario_root.join("app");
    let real_dep_root = scenario_root.join("real_dep");
    let optional_a_root = scenario_root.join("optional_a");
    let optional_b_root = scenario_root.join("optional_b");

    write_lib_crate(&real_dep_root, "real_dep", &[]);
    write_lib_crate(
        &optional_a_root,
        "optional_a",
        &[("optional_b", "../optional_b")],
    );
    write_lib_crate(
        &optional_b_root,
        "optional_b",
        &[("optional_a", "../optional_a")],
    );
    fs::create_dir_all(app_root.join("src")).expect("create app src");
    fs::write(
        app_root.join("Cargo.toml"),
        r#"[package]
name = "optional_materialization_app"
version = "0.1.0"
edition = "2024"

[dependencies]
real_dep = { path = "../real_dep" }
optional_a = { path = "../optional_a", optional = true }
"#,
    )
    .expect("write app manifest");
    fs::write(app_root.join("src/main.rs"), "fn main() {}\n").expect("write app main");

    let plan = build_dependency_closure_plan_with_policy(&app_root, &fixture.policy());

    assert!(
        plan.is_ready(),
        "the optional cycle must not enter the active topological-order calculation"
    );
    let expected_roots = [
        &app_root,
        &real_dep_root,
        &optional_a_root,
        &optional_b_root,
    ]
    .into_iter()
    .map(|root| root.canonicalize().expect("canonical fixture package"))
    .collect::<std::collections::BTreeSet<_>>();
    assert_eq!(
        plan.sync_roots()
            .into_iter()
            .collect::<std::collections::BTreeSet<_>>(),
        expected_roots,
        "all potentially activatable source roots must enter full synchronization"
    );
    for package_name in ["optional_a", "optional_b"] {
        let action = plan
            .sync_order
            .iter()
            .find(|action| action.package_name == package_name)
            .expect("materialization-only sync action");
        assert!(
            action
                .metadata
                .notes
                .iter()
                .any(|note| note == "materialization_only=true")
        );
    }
}

#[test]
fn planner_uses_workspace_root_for_inherited_optional_materialization() {
    let fixture = TopologyFixture::new("workspace-inherited-materialization");
    let scenario_root = fixture
        .canonical_root
        .join("workspace_inherited_materialization");
    let workspace_root = scenario_root.join("workspace");
    let app_root = workspace_root.join("crates/app");
    let optional_dep_root = scenario_root.join("optional_dep");

    write_lib_crate(&optional_dep_root, "workspace_optional_dep", &[]);
    fs::create_dir_all(app_root.join("src")).expect("create app src");
    fs::write(
        app_root.join("Cargo.toml"),
        r#"[package]
name = "workspace_inherited_app"
version = "0.1.0"
edition = "2024"

[dependencies]
workspace_optional_dep = { workspace = true, optional = true }
"#,
    )
    .expect("write app manifest");
    fs::write(app_root.join("src/lib.rs"), "pub fn app() {}\n").expect("write app lib");
    fs::create_dir_all(&workspace_root).expect("create workspace root");
    fs::write(
        workspace_root.join("Cargo.toml"),
        r#"[workspace]
members = ["crates/app"]
resolver = "3"

[workspace.dependencies]
workspace_optional_dep = { path = "../optional_dep" }
"#,
    )
    .expect("write workspace manifest");

    let plan = build_dependency_closure_plan_with_policy(&app_root, &fixture.policy());

    assert!(plan.is_ready());
    let app_root = app_root.canonicalize().expect("canonical app");
    let optional_dep_root = optional_dep_root
        .canonicalize()
        .expect("canonical optional dep");
    assert_eq!(
        plan.sync_roots()
            .into_iter()
            .collect::<std::collections::BTreeSet<_>>(),
        std::collections::BTreeSet::from([app_root, optional_dep_root.clone()])
    );
    let optional_action = plan
        .sync_order
        .iter()
        .find(|action| action.package_root == optional_dep_root)
        .expect("workspace-inherited optional materialization action");
    assert!(
        optional_action
            .metadata
            .notes
            .iter()
            .any(|note| note == "materialization_only=true")
    );
}

#[test]
fn planner_marks_missing_inactive_path_as_materialization_failure() {
    let fixture = TopologyFixture::new("missing-optional-materialization");
    let app_root = fixture
        .canonical_root
        .join("missing_optional_materialization/app");
    fs::create_dir_all(app_root.join("src")).expect("create app src");
    fs::write(
        app_root.join("Cargo.toml"),
        r#"[package]
name = "missing_optional_materialization_app"
version = "0.1.0"
edition = "2024"

[dependencies]
missing_optional = { path = "../missing_optional", optional = true }
"#,
    )
    .expect("write app manifest");
    fs::write(app_root.join("src/lib.rs"), "pub fn app() {}\n").expect("write app lib");

    let plan = build_dependency_closure_plan_with_policy(&app_root, &fixture.policy());

    assert_eq!(plan.state, DependencyClosurePlanState::FailOpen);
    assert!(plan.fail_open);
    assert!(plan.sync_order.is_empty());
    assert!(
        plan.issues
            .iter()
            .any(|issue| issue.code == "materialization-closure-unavailable"),
        "missing inactive paths must be distinguished from ordinary active graph fallback"
    );
    assert!(
        plan.fail_open_reason
            .as_deref()
            .is_some_and(|reason| reason.contains("materialization closure produced"))
    );
}

#[test]
fn planner_broken_graphs_mark_fail_open_with_issues() {
    let fixture = TopologyFixture::new("broken");
    let config = MultiRepoFixtureConfig::new(
        fixture.canonical_root.clone(),
        fixture.alias_root.clone(),
        "planner_broken_graphs",
    );
    let fixtures = reset_multi_repo_fixtures(&config).expect("generate fixture set");

    for scenario_id in ["fail_invalid_manifest", "fail_outside_canonical_dep"] {
        let scenario = fixtures
            .fixture(scenario_id)
            .unwrap_or_else(|| panic!("missing fixture metadata for {scenario_id}"));

        let plan = build_dependency_closure_plan_with_policy(
            &scenario.canonical_entrypoint,
            &fixture.policy(),
        );
        assert_eq!(plan.state, DependencyClosurePlanState::FailOpen);
        assert!(plan.fail_open);
        assert!(
            !plan.issues.is_empty(),
            "expected fail-open issue metadata for {scenario_id}"
        );
        assert!(
            plan.fail_open_reason.is_some(),
            "expected fallback rationale for {scenario_id}"
        );
    }
}