wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The check that would have caught the moves.

use super::*;

fn write(root: &Path, rel: &str, text: &str) {
    let path = root.join(rel);
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(path, text).unwrap();
}

/// TEST9009: a cargo path dependency that names nothing is reported.
///
/// This is the shape the restructure produced: a manifest that still parses,
/// still reads, and points one `..` short of where its dependency went.
#[test]
fn test9009_a_cargo_path_that_moved_is_found() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    write(root, "capdag/capdag-rs/Cargo.toml", "[package]\nname = \"capdag\"\n");
    write(
        root,
        "app/daemon/Cargo.toml",
        "[dependencies.capdag]\npath = \"../capdag/capdag-rs\"\n",
    );

    let broken = check(root);
    assert_eq!(broken.len(), 1, "{broken:?}");
    assert_eq!(broken[0].manifest, "app/daemon/Cargo.toml");
    assert_eq!(broken[0].declared, "../capdag/capdag-rs");
    assert_eq!(broken[0].resolves_to, "app/capdag/capdag-rs");

    // Corrected, it is silent.
    write(
        root,
        "app/daemon/Cargo.toml",
        "[dependencies.capdag]\npath = \"../../capdag/capdag-rs\"\n",
    );
    assert!(check(root).is_empty());
}

/// TEST9010: a lockfile's link is checked, not just the manifest's.
///
/// The manifest can be right and the build still broken: npm acts on the
/// LOCKFILE, which holds its own copy of the path and does not change when the
/// manifest is re-rendered. That is exactly how a correct `package.json` sat
/// beside a `package-lock.json` pointing into a directory that had moved.
#[test]
fn test9010_a_lockfiles_link_is_checked_too() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    write(root, "capdag/capdag-js/package.json", "{\"name\":\"capdag\"}");
    write(
        root,
        "app/client/package.json",
        "{\"dependencies\":{\"capdag\":\"^1.0.0\"}}",
    );
    write(
        root,
        "app/client/package-lock.json",
        r#"{"packages":{"../capdag/capdag-js":{"version":"1.0.0"},
                        "node_modules/capdag":{"resolved":"../capdag/capdag-js","link":true}}}"#,
    );

    let broken = check(root);
    assert_eq!(broken.len(), 2, "the entry and the link both name it: {broken:?}");
    assert!(broken.iter().all(|b| b.manifest.ends_with("package-lock.json")));
    assert!(broken.iter().any(|b| b.kind == "lockfile link"));
}

/// TEST9011: an npm `file:` dependency is checked.
#[test]
fn test9011_a_file_dependency_that_moved_is_found() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    write(root, "app/ui/package.json", "{\"name\":\"ui\"}");
    write(
        root,
        "app/client/package.json",
        "{\"dependencies\":{\"@x/ui\":\"file:../../ui\"}}",
    );

    let broken = check(root);
    assert_eq!(broken.len(), 1, "{broken:?}");
    assert_eq!(broken[0].kind, "npm file: dependency");
    assert_eq!(broken[0].declared, "file:../../ui".strip_prefix("file:").unwrap());
}

/// TEST9012: a Swift package path is checked.
#[test]
fn test9012_a_swift_package_path_that_moved_is_found() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    write(root, "sdks/objc/Package.swift", "// swift-tools-version:5.9\n");
    write(
        root,
        "app/mac/Package.swift",
        "let package = Package(\n    dependencies: [ .package(path: \"../../objc\") ]\n)\n",
    );

    let broken = check(root);
    assert_eq!(broken.len(), 1, "{broken:?}");
    assert_eq!(broken[0].kind, "swift package path");
    assert_eq!(broken[0].resolves_to, "objc");
}

/// TEST9013: what a build actually reads is skipped.
///
/// A workspace holds thousands of manifests it did not write — every installed
/// npm package has one, and many of them declare paths that mean nothing
/// outside their own registry. Reporting those would bury the handful that are
/// this worktree's own.
#[test]
fn test9013_other_peoples_trees_are_not_checked() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    for holder in ["node_modules/left-pad", "target/debug/x", ".build/y", "vendor/z"] {
        write(
            root,
            &format!("app/{holder}/package.json"),
            "{\"dependencies\":{\"a\":\"file:../nowhere\"}}",
        );
    }
    assert!(check(root).is_empty(), "{:?}", check(root));
}

/// TEST9014: an absolute path is left alone.
///
/// It is a claim about a machine, not about this tree, and one that is wrong
/// is wrong for a reason this check cannot speak to.
#[test]
fn test9014_absolute_paths_are_not_this_checks_business() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    write(
        root,
        "app/Cargo.toml",
        "[dependencies.x]\npath = \"/opt/somewhere/else\"\n",
    );
    assert!(check(root).is_empty());
}

/// TEST9015: a source directory an ignore rule swallows is reported.
///
/// This is the failure it exists for: `logs/` without a leading slash matches
/// `src/logs/` too, so the module that implements logging was invisible to
/// git — a clean `git status` over a tree missing a thousand lines, and a
/// machine that pulled it could not compile.
#[test]
fn test9015_source_hidden_by_an_ignore_rule_is_reported() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    assert!(
        crate::git_backend::git_command()
            .arg("-C")
            .arg(root)
            .args(["init", "-q"])
            .status()
            .map(|s| s.success())
            .unwrap_or(false),
        "git is needed for this test"
    );
    // The pattern as it was written: no leading slash.
    write(root, ".gitignore", "logs/\ntarget/\n");
    write(root, "src/logs/mod.rs", "pub fn f() {}\n");
    write(root, "src/main.rs", "fn main() {}\n");
    // Output, ignored on purpose, and not a finding.
    write(root, "target/debug/build.rs", "fn main() {}\n");

    let hidden = hidden_source(root);
    assert_eq!(hidden.len(), 1, "{hidden:?}");
    assert_eq!(hidden[0].path, "src/logs/mod.rs");
    assert!(
        hidden[0].rule.contains("logs/"),
        "the report has to name the rule to change: {}",
        hidden[0].rule
    );

    // Anchored, the module is git's again and nothing is reported.
    write(root, ".gitignore", "/logs/\ntarget/\n");
    assert!(hidden_source(root).is_empty());
}

/// TEST9024: a committed absolute path into a home directory is reported.
///
/// The Xcode legacy target that broke the macOS build carried the full path to
/// a script in one developer's checkout. It was wrong for everybody else from
/// the day it was written, and wrong for its author from the day the client
/// moved — and nothing said so until Xcode refused to spawn it.
#[test]
fn test9024_a_committed_home_path_is_reported() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    assert!(
        crate::git_backend::git_command()
            .arg("-C")
            .arg(root)
            .args(["init", "-q"])
            .status()
            .map(|s| s.success())
            .unwrap_or(false),
        "git is needed for this test"
    );
    write(
        root,
        "App.xcodeproj/project.pbxproj",
        "\t\tbuildToolPath = \"/Users/someone/ws/app/scripts/build.sh\";\n         \t\tpath = \"../shared\";\n",
    );
    // Untracked: somebody's own state, which is where absolute paths belong.
    write(root, ".wsb/state.json", "{\"project_root\": \"/home/someone/ws/app\"}\n");
    let staged = crate::git_backend::git_command()
        .arg("-C")
        .arg(root)
        .args(["add", "App.xcodeproj/project.pbxproj"])
        .status();
    assert!(staged.map(|s| s.success()).unwrap_or(false));

    let found = home_rooted(root);
    assert_eq!(found.len(), 1, "{found:?}");
    assert_eq!(found[0].manifest, "App.xcodeproj/project.pbxproj");
    assert_eq!(found[0].path, "/Users/someone/ws/app/scripts/build.sh");
    assert_eq!(found[0].line, 1);
}

/// TEST9025: what is not one person's home is not reported.
#[test]
fn test9025_shared_and_system_paths_are_not_findings() {
    let scan = super::regex_lite_home();
    for benign in [
        "path = /Applications/Xcode.app/Contents/Developer",
        "prefix = /usr/local/bin",
        "shared = /Users/Shared",
        "root = /home",
        "nothing here at all",
    ] {
        assert!(scan(benign).is_empty(), "{benign} is not a home path: {:?}", scan(benign));
    }
    // And the real shapes are.
    assert_eq!(
        scan("buildToolPath = \"/Users/b/ws/p/x.sh\";"),
        vec!["/Users/b/ws/p/x.sh".to_string()]
    );
    assert_eq!(
        scan("dir = /home/b/ws/p"),
        vec!["/home/b/ws/p".to_string()]
    );
}