bashrs 6.66.0

Rust-to-Shell transpiler for deterministic bootstrap scripts
//! Verificar integration tests
//!
//! Tests bashrs against synthetic bash programs generated by verificar.
//! This ensures bashrs handles all common bash patterns correctly.
//!
//! Run with: `cargo test --test verificar_integration`

#![allow(clippy::unwrap_used, clippy::expect_used)]

use std::process::Command;
use verificar::generator::BashEnumerator;

/// Test that bashrs can lint verificar-generated bash programs without panicking
///
/// By default, tests 100 programs for fast coverage runs.
/// Set VERIFICAR_PROGRAMS=1000 for comprehensive testing.
#[test]
fn test_verificar_programs_lint_without_panics() {
    let bashrs = env!("CARGO_BIN_EXE_bashrs");
    let enumerator = BashEnumerator::new(3);
    let all_programs = enumerator.enumerate_programs();

    // Limit programs for fast test runs (similar to PROPTEST_CASES)
    // Default: 100 programs (~10s), Full: set VERIFICAR_PROGRAMS=1000+
    let max_programs: usize = std::env::var("VERIFICAR_PROGRAMS")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(100);

    let programs: Vec<_> = all_programs.into_iter().take(max_programs).collect();

    println!(
        "Testing {} verificar-generated bash programs (limit: {})",
        programs.len(),
        max_programs
    );

    let mut passed = 0;
    let mut panicked = 0;
    let mut panic_details = Vec::new();

    for (i, program) in programs.iter().enumerate() {
        // Create temp file
        let temp_file = format!("/tmp/bashrs_verificar_test_{}.sh", i);
        std::fs::write(&temp_file, &program.code).unwrap();

        // Run bashrs lint
        let output = Command::new(bashrs)
            .args(["lint", &temp_file])
            .output()
            .expect("Failed to run bashrs");

        let stderr = String::from_utf8_lossy(&output.stderr);

        // Check for panics (bashrs should never panic on valid bash)
        if stderr.contains("panic") || stderr.contains("RUST_BACKTRACE") {
            panicked += 1;
            panic_details.push((i, program.code.lines().next().unwrap_or("").to_string()));
        } else {
            passed += 1;
        }

        // Cleanup
        let _ = std::fs::remove_file(&temp_file);
    }

    // Report panics
    if !panic_details.is_empty() {
        eprintln!("\nPrograms that caused panics:");
        for (i, first_line) in &panic_details {
            eprintln!("  Test {}: {}", i, first_line);
        }
    }

    assert!(
        panicked == 0,
        "bashrs panicked on {} out of {} programs",
        panicked,
        programs.len()
    );

    println!(
        "Verificar integration: {}/{} programs tested, {} passed (set VERIFICAR_PROGRAMS for more)",
        programs.len(),
        max_programs,
        passed
    );
}

/// Test that bashrs correctly identifies issues in verificar programs
#[test]
fn test_verificar_programs_issue_detection() {
    let bashrs = env!("CARGO_BIN_EXE_bashrs");

    // Programs that should have warnings (unquoted variables)
    let programs_with_warnings = [
        ("echo $x", "SC2086"),                               // Unquoted variable
        ("for i in 1 2 3; do\n    echo $i\ndone", "SC2086"), // Unquoted in loop
    ];

    for (program, expected_rule) in programs_with_warnings {
        let temp_file = "/tmp/bashrs_verificar_warning_test.sh";
        std::fs::write(temp_file, program).unwrap();

        let output = Command::new(bashrs)
            .args(["lint", temp_file])
            .output()
            .expect("Failed to run bashrs");

        let combined = format!(
            "{}{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );

        assert!(
            combined.contains(expected_rule),
            "Expected {} for program: {}\nGot: {}",
            expected_rule,
            program.lines().next().unwrap_or(""),
            combined
        );

        let _ = std::fs::remove_file(temp_file);
    }
}

/// Test that non-deterministic commands are properly detected
#[test]
fn test_non_deterministic_detection() {
    let bashrs = env!("CARGO_BIN_EXE_bashrs");

    // These should trigger DET002 (non-deterministic)
    let non_deterministic_programs = [("result=$(date)", "DET002"), ("now=$(date +%s)", "DET002")];

    for (program, expected_rule) in non_deterministic_programs {
        let temp_file = "/tmp/bashrs_nondet_test.sh";
        std::fs::write(temp_file, program).unwrap();

        let output = Command::new(bashrs)
            .args(["lint", temp_file])
            .output()
            .expect("Failed to run bashrs");

        let combined = format!(
            "{}{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );

        assert!(
            combined.contains(expected_rule),
            "Expected {} for non-deterministic program: {}\nGot: {}",
            expected_rule,
            program,
            combined
        );

        let _ = std::fs::remove_file(temp_file);
    }
}

/// Test that POSIX parameter modifiers don't trigger SC2299
#[test]
fn test_posix_modifiers_no_sc2299() {
    let bashrs = env!("CARGO_BIN_EXE_bashrs");

    // These should NOT trigger SC2299
    let posix_modifier_programs = [
        r#"echo "${var:-default}""#,
        r#"echo "${var:+alternate}""#,
        r#"echo "${var:=assigned}""#,
        r#"echo "${var:?error}""#,
        r#"x="${PROPTEST_THREADS:-$(nproc)}""#, // Real-world pattern from Makefile
    ];

    for program in posix_modifier_programs {
        let temp_file = "/tmp/bashrs_posix_modifier_test.sh";
        std::fs::write(temp_file, program).unwrap();

        let output = Command::new(bashrs)
            .args(["lint", temp_file])
            .output()
            .expect("Failed to run bashrs");

        let combined = format!(
            "{}{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );

        assert!(
            !combined.contains("SC2299"),
            "SC2299 should NOT trigger for POSIX modifier: {}\nGot: {}",
            program,
            combined
        );

        let _ = std::fs::remove_file(temp_file);
    }
}

/// Test coverage statistics for verificar programs
#[test]
fn test_verificar_coverage_stats() {
    let enumerator = BashEnumerator::new(3);
    let programs = enumerator.enumerate_programs();

    // Collect feature coverage
    let mut feature_counts: std::collections::HashMap<String, usize> =
        std::collections::HashMap::new();
    for program in &programs {
        for feature in &program.features {
            let key: String = feature.clone();
            *feature_counts.entry(key).or_insert(0) += 1;
        }
    }

    println!("\nVerificar coverage statistics:");
    println!("  Total programs: {}", programs.len());
    println!("  Unique features: {}", feature_counts.len());

    // Show top features
    let mut features: Vec<_> = feature_counts.iter().collect();
    features.sort_by(|a, b| b.1.cmp(a.1));

    println!("  Top features:");
    for (feature, count) in features.iter().take(15) {
        println!("    {}: {}", feature, count);
    }

    // Verify minimum feature coverage
    assert!(
        feature_counts.len() >= 20,
        "Expected at least 20 unique features, got {}",
        feature_counts.len()
    );
}