sprawl-guard 0.1.0

Repository sprawl checker CLI.
use assert_fs::TempDir;
use indoc::indoc;
use serde_json::json;

use super::common::*;

mod when_rust_production_source_has_file_wide_cfg_test {
    use super::*;

    #[test]
    fn it_excludes_the_file_from_production_loc() {
        let root = TempDir::new().unwrap();
        write_config(
            &root,
            indoc! {r#"
                [limits.max_lines_per_file]
                code_non_test = 1

                [languages.rust]
                enabled = true
            "#},
        );
        write_file(
            &root,
            "src/lib.rs",
            indoc! {r#"
                #![cfg(test)]

                fn fixture_one() {}
                fn fixture_two() {}
            "#},
        );

        let output = sprawl_guard(&root).arg("check").output().unwrap();

        assert_exit_code(&output, exit_codes::SUCCESS);
        assert_eq!(stdout(&output), "No sprawl-guard violations found.\n");
        assert_eq!(stderr(&output), "");
    }
}

mod when_rust_cfg_test_preprocessing_fails {
    use super::*;

    #[test]
    fn it_serializes_a_structured_warning_in_json_output() {
        let root = TempDir::new().unwrap();
        write_config(
            &root,
            indoc! {r#"
                [limits.max_lines_per_file]
                code_non_test = 1

                [languages.rust]
                enabled = true
            "#},
        );
        write_file(
            &root,
            "src/lib.rs",
            indoc! {r#"
                #[cfg(test)]
                fn broken( {
            "#},
        );

        let (output, json) = run_check_json(&root);

        assert_exit_code(&output, exit_codes::CHECK_FAILURE);
        let warning = &json["warnings"][0];
        let detail = warning["detail"].as_str().unwrap();
        assert!(!detail.is_empty());
        assert_eq!(
            warning,
            &json!({
                "detail": detail,
                "kind": "rust_cfg_test_parse_failed",
                "path": "src/lib.rs",
            })
        );
        assert_eq!(stderr(&output), "");
    }
}

#[cfg(unix)]
mod when_traversal_error_policy_is_overridden {
    use super::*;

    #[test]
    fn it_reports_partial_json_without_failing_on_traversal_warnings() {
        let root = TempDir::new().unwrap();
        write_directory_symlink_loop_fixture(&root);

        let (output, json) = run_json(
            &root,
            &[
                "check",
                "--format",
                "json",
                "--traversal-error-policy",
                "warn",
            ],
        );

        assert_exit_code(&output, exit_codes::SUCCESS);
        assert_eq!(
            json["coverage"],
            json!({
                "kind": "partial",
                "traversal_incidents": [{
                    "detail": "symlink loop found",
                    "kind": "symlink_loop",
                    "path": "real/cycle",
                    "severity": "warning",
                }],
            })
        );
        assert_eq!(stderr(&output), "");
    }
}

#[cfg(unix)]
mod when_default_traversal_error_policy_fails {
    use super::*;

    #[test]
    fn it_serializes_collected_incidents_for_json_output() {
        let root = TempDir::new().unwrap();
        write_directory_symlink_loop_fixture(&root);

        let (output, json) = run_check_json(&root);

        assert_exit_code(&output, exit_codes::OPERATIONAL_ERROR);
        assert_eq!(
            json,
            json!({
                "status": "error",
                "root": root.path().canonicalize().unwrap().display().to_string(),
                "coverage": {
                    "kind": "partial",
                    "traversal_incidents": [{
                        "detail": "symlink loop found",
                        "kind": "symlink_loop",
                        "path": "real/cycle",
                        "severity": "error",
                    }],
                },
                "warnings": [],
                "error": {
                    "kind": "traversal_failed",
                    "message": "traversal completed with 1 incident(s)",
                },
                "violations": [],
            })
        );
        assert_eq!(stderr(&output), "");
    }

    #[test]
    fn it_prints_collected_incidents_for_human_output() {
        let root = TempDir::new().unwrap();
        write_directory_symlink_loop_fixture(&root);

        let output = sprawl_guard(&root).arg("check").output().unwrap();
        let stderr = stderr(&output);

        assert_exit_code(&output, exit_codes::OPERATIONAL_ERROR);
        assert_eq!(stdout(&output), "");
        assert!(stderr.contains("Traversal failed under"));
        assert!(stderr.contains("symlink loop found at real/cycle"));
    }
}

mod when_production_directory_exceeds_the_leaf_file_limit {
    use super::*;

    #[test]
    fn it_serializes_a_directory_violation_in_json_output() {
        let root = TempDir::new().unwrap();
        write_config(
            &root,
            indoc! {r#"
                [limits]
                max_leaf_files_per_directory = { production = 1 }

                [languages.rust]
                enabled = true
            "#},
        );
        write_file(&root, "src/a.rs", "fn a() {}\n");
        write_file(&root, "src/b.rs", "fn b() {}\n");

        let (output, json) = run_check_json(&root);

        assert_exit_code(&output, exit_codes::CHECK_FAILURE);
        assert_eq!(
            json["violations"][0],
            json!({
                "kind": "directory_fanout",
                "path": "src",
                "actual": 2,
                "limit": 1,
                "files": ["src/a.rs", "src/b.rs"],
                "ratchet": null,
            })
        );
        assert_eq!(stderr(&output), "");
    }
}

mod when_sarif_output_is_requested {
    use super::*;

    #[test]
    fn it_serializes_check_violations_as_sarif_results() {
        let root = TempDir::new().unwrap();
        write_rust_file_limit_config(&root, 1);
        write_rust_source_with_function_count(&root, "src/lib.rs", 2);

        let output = sprawl_guard(&root)
            .args(["check", "--format", "sarif"])
            .output()
            .unwrap();
        let sarif = serde_json::from_str::<serde_json::Value>(&stdout(&output)).unwrap();

        assert_exit_code(&output, exit_codes::CHECK_FAILURE);
        assert_eq!(
            json!({
                "version": sarif["version"],
                "semantic_version": sarif["runs"][0]["tool"]["driver"]["semanticVersion"],
                "result": sarif["runs"][0]["results"][0],
            }),
            json!({
                "version": "2.1.0",
                "semantic_version": env!("CARGO_PKG_VERSION"),
                "result": {
                    "level": "error",
                    "locations": [{
                        "physicalLocation": {
                            "artifactLocation": {
                                "uri": "src/lib.rs",
                            },
                            "region": {
                                "startLine": 1,
                            },
                        },
                    }],
                    "message": {
                        "text": "src/lib.rs has 2 code lines, exceeding limit 1.",
                    },
                    "properties": {
                        "actual": 2,
                        "kind": "file_loc",
                        "language": "Rust",
                        "limit": 1,
                        "path": "src/lib.rs",
                        "ratchet": null,
                    },
                    "ruleId": "sprawl-guard/file-loc",
                },
            })
        );
        assert_eq!(stderr(&output), "");
    }
}