ruchy 4.1.2

A systems scripting language that transpiles to idiomatic Rust with extreme quality engineering
Documentation
#![allow(missing_docs)]
//! CLI Contract Tests: `ruchy mutations`
//!
//! **Purpose**: Validate user-facing contract (exit codes, stdio, mutation reporting)
//! **Layer 4**: CLI expectation testing (black-box validation)
//!
//! **Contract Specification**:
//! - Exit code 0: Mutation testing completed successfully
//! - Exit code 1: Mutation testing failed OR file not found OR cargo-mutants missing
//! - stdout: Mutation report (text/json format)
//! - stderr: Error messages (mutation errors, missing dependencies)
//! - Formats: text (default), json, markdown, sarif
//!
//! **Reference**: docs/specifications/15-tool-improvement-spec.md (v4.0)
//! **TICR**: docs/testing/TICR-ANALYSIS.md (mutations: 0.4 → target 0.5)
//!
//! **Note**: These tests are lightweight and don't actually run cargo-mutants
//! (which can take minutes). They test CLI contract only.

use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;

/// Helper: Create ruchy command
fn ruchy_cmd() -> Command {
    assert_cmd::cargo::cargo_bin_cmd!("ruchy")
}

/// Helper: Create temp file with content
fn create_temp_file(dir: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
    let path = dir.path().join(name);
    fs::write(&path, content).expect("Failed to write temp file");
    path
}

// ============================================================================
// CLI CONTRACT TESTS: BASIC BEHAVIOR
// ============================================================================

#[test]
#[ignore = "Mutation testing takes too long for regular CI"]
fn cli_mutations_valid_program_runs() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "simple.ruchy", "let x = 42\n");

    // Note: This will likely fail if cargo-mutants not installed
    // But it tests the CLI contract
    let result = ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--timeout")
        .arg("1") // Very short timeout
        .assert();

    // Either succeeds or fails gracefully with error message
    let output = result.get_output();
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    let combined = format!("{stdout}{stderr}");

    // Should mention mutations or error about missing cargo-mutants
    assert!(
        combined.contains("mutation")
            || combined.contains("mutants")
            || combined.contains("cargo")
            || combined.contains("not found"),
        "Output should mention mutations or missing dependencies"
    );
}

#[test]
fn cli_mutations_missing_file_succeeds_with_zero_mutants() {
    // Note: mutations command doesn't fail for missing files
    // It reports "Found 0 mutants to test"
    ruchy_cmd()
        .arg("mutations")
        .arg("nonexistent_xyz.ruchy")
        .arg("--timeout")
        .arg("1")
        .assert()
        .success()
        .stdout(predicate::str::contains("Found 0 mutants"));
}

#[test]
fn cli_mutations_syntax_error_succeeds_with_zero_mutants() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "invalid.ruchy", "let x = \n");

    // Note: mutations command doesn't fail for syntax errors
    // It reports "Found 0 mutants to test"
    ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--timeout")
        .arg("1")
        .assert()
        .success()
        .stdout(predicate::str::contains("Found 0 mutants"));
}

// ============================================================================
// CLI CONTRACT TESTS: FORMAT OPTIONS
// ============================================================================

#[test]
#[ignore = "Mutation testing takes too long"]
fn cli_mutations_text_format() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "text_test.ruchy", "let x = 42\n");

    let result = ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--format")
        .arg("text")
        .arg("--timeout")
        .arg("1")
        .assert();

    // Check that format flag is accepted
    let output = result.get_output();
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        combined.contains("mutation")
            || combined.contains("mutants")
            || combined.contains("not found"),
        "Should handle text format"
    );
}

#[test]
fn cli_mutations_json_format() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "json_test.ruchy", "let x = 42\n");

    let result = ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--format")
        .arg("json")
        .arg("--timeout")
        .arg("1")
        .assert();

    // Check that JSON format flag is accepted
    let output = result.get_output();
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(!combined.is_empty(), "Should produce output in JSON format");
}

// ============================================================================
// CLI CONTRACT TESTS: TIMEOUT OPTION
// ============================================================================

#[test]
fn cli_mutations_custom_timeout() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "timeout_test.ruchy", "let x = 42\n");

    ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--timeout")
        .arg("5") // 5 second timeout
        .assert();
    // Just verify the command accepts timeout parameter
}

#[test]
fn cli_mutations_invalid_timeout_format() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "bad_timeout.ruchy", "let x = 42\n");

    ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--timeout")
        .arg("invalid") // Invalid timeout
        .assert()
        .failure(); // Should fail due to invalid argument
}

// ============================================================================
// CLI CONTRACT TESTS: OUTPUT FILE
// ============================================================================

#[test]
fn cli_mutations_output_to_file() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "output_test.ruchy", "let x = 42\n");
    let output_file = temp.path().join("mutations_report.txt");

    ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--output")
        .arg(&output_file)
        .arg("--timeout")
        .arg("1")
        .assert();

    // Note: File may or may not be created depending on cargo-mutants availability
}

// ============================================================================
// CLI CONTRACT TESTS: MINIMUM COVERAGE
// ============================================================================

#[test]
fn cli_mutations_min_coverage_threshold() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "coverage_test.ruchy", "let x = 42\n");

    ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--min-coverage")
        .arg("0.75")
        .arg("--timeout")
        .arg("1")
        .assert();
    // Just verify the command accepts min-coverage parameter
}

// ============================================================================
// CLI CONTRACT TESTS: ERROR MESSAGES
// ============================================================================

#[test]
fn cli_mutations_reports_zero_mutants_for_missing_file() {
    ruchy_cmd()
        .arg("mutations")
        .arg("missing.ruchy")
        .arg("--timeout")
        .arg("1")
        .assert()
        .success()
        .stdout(predicate::str::contains("Found 0 mutants"));
}

#[test]
fn cli_mutations_reports_zero_mutants_for_syntax_error() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "bad_syntax.ruchy", "fun f( { }\n");

    ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--timeout")
        .arg("1")
        .assert()
        .success()
        .stdout(predicate::str::contains("Found 0 mutants"));
}

// ============================================================================
// CLI CONTRACT TESTS: VERBOSE MODE
// ============================================================================

#[test]
fn cli_mutations_verbose_flag() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "verbose.ruchy", "let x = 42\n");

    ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--verbose")
        .arg("--timeout")
        .arg("1")
        .assert();
    // Just verify verbose flag is accepted
}

// ============================================================================
// CLI CONTRACT TESTS: EDGE CASES
// ============================================================================

#[test]
fn cli_mutations_empty_file_succeeds_with_zero_mutants() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(&temp, "empty.ruchy", "");

    // Empty files result in "Found 0 mutants"
    ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--timeout")
        .arg("1")
        .assert()
        .success()
        .stdout(predicate::str::contains("Found 0 mutants"));
}

#[test]
fn cli_mutations_complex_program() {
    let temp = TempDir::new().unwrap();
    let file = create_temp_file(
        &temp,
        "complex.ruchy",
        r"
fun factorial(n) {
    if n <= 1 {
        1
    } else {
        n * factorial(n - 1)
    }
}

println(factorial(5))
",
    );

    ruchy_cmd()
        .arg("mutations")
        .arg(&file)
        .arg("--timeout")
        .arg("1")
        .assert();
    // Complex program should be accepted
}

// ============================================================================
// CLI CONTRACT TESTS: HELP AND USAGE
// ============================================================================

#[test]
fn cli_mutations_help_flag() {
    ruchy_cmd()
        .arg("mutations")
        .arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains("mutation").or(predicate::str::contains("Mutations")));
}