fallow-cli 3.16.0

CLI for fallow, codebase intelligence for TypeScript and JavaScript
Documentation
#![allow(
    clippy::expect_used,
    reason = "integration tests use expect to keep fixture setup concise"
)]

#[path = "common/mod.rs"]
mod common;

use std::path::{Path, PathBuf};
use std::process::{Command, Output};

use common::fallow_bin;

fn workspace_fixture(path: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../..")
        .join(path)
        .canonicalize()
        .expect("fixture path")
}

fn run(root: &Path, args: &[String]) -> Output {
    Command::new(fallow_bin())
        .current_dir(root)
        .env("NO_COLOR", "1")
        .env("RUST_LOG", "")
        .args(args)
        .output()
        .expect("run fallow")
}

fn analysis_args(command: Option<&str>, root: &Path, format: &str, extra: &[&str]) -> Vec<String> {
    let mut args = Vec::new();
    if let Some(command) = command {
        args.push(command.to_string());
    }
    args.extend([
        "--root".to_string(),
        root.display().to_string(),
        "--quiet".to_string(),
        "--format".to_string(),
        format.to_string(),
    ]);
    args.extend(extra.iter().map(|value| (*value).to_string()));
    args
}

fn assert_saved_report_parity(root: &Path, command: Option<&str>) {
    assert_saved_report_parity_with_args(root, command, &[]);
}

fn assert_saved_report_parity_with_args(root: &Path, command: Option<&str>, extra: &[&str]) {
    let json = run(root, &analysis_args(command, root, "json", extra));
    assert!(
        matches!(json.status.code(), Some(0 | 1)),
        "analysis failed: {}",
        String::from_utf8_lossy(&json.stderr)
    );
    serde_json::from_slice::<serde_json::Value>(&json.stdout).expect("analysis JSON");

    let saved_dir = tempfile::tempdir().expect("saved report tempdir");
    let saved_path = saved_dir.path().join("results.json");
    std::fs::write(&saved_path, &json.stdout).expect("write saved report");

    for format in ["codeclimate", "sarif"] {
        let direct = run(root, &analysis_args(command, root, format, extra));
        assert!(
            matches!(direct.status.code(), Some(0 | 1)),
            "direct {format} failed: {}",
            String::from_utf8_lossy(&direct.stderr)
        );

        let saved = run(
            root,
            &[
                "report".to_string(),
                "--from".to_string(),
                saved_path.display().to_string(),
                "--root".to_string(),
                root.display().to_string(),
                "--quiet".to_string(),
                "--format".to_string(),
                format.to_string(),
            ],
        );
        assert!(
            saved.status.success(),
            "saved {format} failed: {}",
            String::from_utf8_lossy(&saved.stderr)
        );
        assert_eq!(
            saved.stdout, direct.stdout,
            "saved {format} must be byte-identical to direct rendering"
        );
    }
}

#[test]
fn saved_reports_preserve_native_health_duplication_and_combined_output() {
    assert_saved_report_parity(
        &workspace_fixture("tests/fixtures/complexity-project"),
        Some("health"),
    );
    assert_saved_report_parity(
        &workspace_fixture("tests/fixtures/duplicate-code"),
        Some("dupes"),
    );
    assert_saved_report_parity(
        &workspace_fixture("tests/fixtures/complexity-project"),
        None,
    );
    assert_saved_report_parity_with_args(
        &workspace_fixture("tests/fixtures/complexity-project"),
        Some("health"),
        &["--group-by", "directory"],
    );
    assert_saved_report_parity_with_args(
        &workspace_fixture("tests/fixtures/duplicate-code"),
        Some("dupes"),
        &["--group-by", "directory"],
    );
}

fn git(root: &Path, args: &[&str]) {
    let status = Command::new("git")
        .current_dir(root)
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .env("GIT_AUTHOR_NAME", "Fallow Test")
        .env("GIT_AUTHOR_EMAIL", "fallow@example.invalid")
        .env("GIT_COMMITTER_NAME", "Fallow Test")
        .env("GIT_COMMITTER_EMAIL", "fallow@example.invalid")
        .args(args)
        .status()
        .expect("run git");
    assert!(status.success(), "git {args:?} failed");
}

#[test]
fn saved_audit_reports_preserve_all_native_sections() {
    let fixture = workspace_fixture("tests/fixtures/complexity-project");
    let project = tempfile::tempdir().expect("audit project");
    for entry in [
        "package.json",
        "src/index.ts",
        "src/simple.ts",
        "src/complex.ts",
    ] {
        let source = fixture.join(entry);
        let target = project.path().join(entry);
        if let Some(parent) = target.parent() {
            std::fs::create_dir_all(parent).expect("create fixture directory");
        }
        std::fs::copy(source, target).expect("copy fixture file");
    }
    git(project.path(), &["init", "-q"]);
    git(project.path(), &["add", "."]);
    git(project.path(), &["commit", "-qm", "baseline"]);
    std::fs::copy(
        project.path().join("src/complex.ts"),
        project.path().join("src/complex-copy.ts"),
    )
    .expect("create changed duplicate");

    assert_saved_report_parity(project.path(), Some("audit"));
}

#[test]
fn saved_security_report_preserves_native_sarif_and_rejects_codeclimate() {
    let root = workspace_fixture("tests/fixtures/security-dangerous-html");
    let json = run(&root, &analysis_args(Some("security"), &root, "json", &[]));
    assert!(
        matches!(json.status.code(), Some(0 | 1)),
        "security JSON failed: {}",
        String::from_utf8_lossy(&json.stderr)
    );
    let saved_dir = tempfile::tempdir().expect("saved security tempdir");
    let saved_path = saved_dir.path().join("security.json");
    std::fs::write(&saved_path, &json.stdout).expect("write saved security report");

    let direct_sarif = run(&root, &analysis_args(Some("security"), &root, "sarif", &[]));
    let saved_sarif = run(
        &root,
        &[
            "report".to_string(),
            "--from".to_string(),
            saved_path.display().to_string(),
            "--root".to_string(),
            root.display().to_string(),
            "--quiet".to_string(),
            "--format".to_string(),
            "sarif".to_string(),
        ],
    );
    assert_eq!(saved_sarif.status.code(), direct_sarif.status.code());
    assert_eq!(saved_sarif.stdout, direct_sarif.stdout);

    let direct_codeclimate = run(
        &root,
        &analysis_args(Some("security"), &root, "codeclimate", &[]),
    );
    let saved_codeclimate = run(
        &root,
        &[
            "report".to_string(),
            "--from".to_string(),
            saved_path.display().to_string(),
            "--root".to_string(),
            root.display().to_string(),
            "--quiet".to_string(),
            "--format".to_string(),
            "codeclimate".to_string(),
        ],
    );
    assert_eq!(direct_codeclimate.status.code(), Some(2));
    assert_eq!(saved_codeclimate.status.code(), Some(2));
    assert_eq!(saved_codeclimate.stdout, direct_codeclimate.stdout);
}