From 4c990b7ccd9fbdeacae6be11ee66e5414f876d0d Mon Sep 17 00:00:00 2001 From: Jonathan Baldie Date: Thu, 6 Aug 2026 05:52:16 +0100 Subject: [PATCH 1/5] WIP: strengthen CLI entry tests for issue #40 Add integration tests through messrust::run for option validation, usage text, exit-code paths, and list splitting. Include a memory-gated function-scoped mutarust batch helper for 16 GB hosts. Not done: full 75/80 acceptance gate on src/lib.rs and src/main.rs. --- scripts/mutarust-fn-batch.sh | 73 ++++++++ tests/cli.rs | 342 +++++++++++++++++++++++++++++++++++ 2 files changed, 415 insertions(+) create mode 100755 scripts/mutarust-fn-batch.sh diff --git a/scripts/mutarust-fn-batch.sh b/scripts/mutarust-fn-batch.sh new file mode 100755 index 0000000..2054820 --- /dev/null +++ b/scripts/mutarust-fn-batch.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Memory-safe single-function mutarust batch for 16 GB hosts. +set -euo pipefail + +MIN_FREE_MB="${MIN_FREE_MB:-1800}" +MATCH="${1:?usage: mutarust-fn-batch.sh [extra mutarust args...]}" +shift || true + +free_mb() { + local pages + pages=$(vm_stat | awk '/Pages free/ {gsub("\\.","",$3); print $3}') + echo $((pages * 16384 / 1024 / 1024)) +} + +cleanup() { + pkill -9 -x mutarust 2>/dev/null || true + pkill -9 -x rustc 2>/dev/null || true + # Do not pkill all cargo — may kill unrelated jobs; only children die with mutarust. + rm -rf "${TMPDIR:-/tmp}"/mutarust-* 2>/dev/null || true +} + +free=$(free_mb) +if (( free < MIN_FREE_MB )); then + echo "ABORT: free_mb=${free} < MIN_FREE_MB=${MIN_FREE_MB}" >&2 + exit 2 +fi + +export TMPDIR="${TMPDIR:-$HOME/tmp/mutarust-run}" +mkdir -p "$TMPDIR" +export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}" + +LOG="${LOG:-/tmp/mutarust-fn-${MATCH}.log}" +rm -f "$LOG" + +echo "START match=${MATCH} free_mb=${free} TMPDIR=${TMPDIR}" + +# Watchdog: kill mutarust if free RAM collapses mid-run. +( + while pgrep -x mutarust >/dev/null 2>&1; do + f=$(free_mb) + if (( f < 400 )); then + echo "WATCHDOG free_mb=${f} — killing mutarust" >&2 + pkill -9 -x mutarust 2>/dev/null || true + pkill -9 -x rustc 2>/dev/null || true + exit 3 + fi + sleep 5 + done +) & +WD_PID=$! + +set +e +script -q "$LOG" env TMPDIR="$TMPDIR" CARGO_BUILD_JOBS="$CARGO_BUILD_JOBS" mutarust \ + --config mutarust.yml \ + --workers 1 \ + --exec-timeout 90 \ + --min-msi 0 \ + --min-covered-msi 0 \ + --test-flags "--test cli --lib" \ + --match "$MATCH" \ + "$@" \ + src/lib.rs src/main.rs +rc=$? +set -e + +kill "$WD_PID" 2>/dev/null || true +wait "$WD_PID" 2>/dev/null || true + +cleanup + +echo "==== summary match=${MATCH} rc=${rc} free_after=$(free_mb) ====" +tr '\r' '\n' < "$LOG" | rg 'Killed:|Escaped:|Errored:|Not covered:|Skipped:|Total:|Mutation score|Covered-code|ABORT|WATCHDOG' | tail -20 +exit "$rc" diff --git a/tests/cli.rs b/tests/cli.rs index 1f40cb6..bd79f1e 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -76,6 +76,131 @@ fn unknown_option_exits_one() { let (code, _out, err) = run_cli(&["src", "text", "codesize", "--not-a-real-flag"]); assert_eq!(code, EXIT_ERROR); assert!(err.contains("error:"), "stderr={err:?}"); + assert!( + err.contains("unknown option: --not-a-real-flag"), + "stderr={err:?}" + ); +} + +#[test] +fn bare_help_word_prints_usage_and_exits_zero() { + let (code, out, err) = run_cli(&["help"]); + assert_eq!(code, EXIT_SUCCESS); + assert!(out.contains("Usage:"), "stdout={out:?}"); + assert!(err.is_empty(), "stderr={err:?}"); +} + +#[test] +fn unknown_single_dash_option_names_it_and_exits_one() { + let (code, _out, err) = run_cli(&["src", "text", "codesize", "-x"]); + assert_eq!(code, EXIT_ERROR); + assert!(err.contains("unknown option: -x"), "stderr={err:?}"); +} + +#[test] +fn lone_dash_is_treated_as_a_positional_not_an_option() { + // A single "-" does not start the unknown-option branch; it becomes a + // fourth positional, so the ruleset parses fine and "-" is an unmatched + // path that yields a discovery error, not an "unknown option" error. + let (code, _out, err) = run_cli(&["-", "text", "codesize"]); + assert_eq!(code, EXIT_ERROR); + assert!(!err.contains("unknown option"), "stderr={err:?}"); +} + +#[test] +fn missing_value_for_option_names_the_flag_and_exits_one() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let (code, _out, err) = run_cli(&[path.to_str().unwrap(), "text", "codesize", "--reportfile"]); + assert_eq!(code, EXIT_ERROR); + assert!( + err.contains("missing value for --reportfile"), + "stderr={err:?}" + ); +} + +#[test] +fn minimumpriority_rejects_non_integer_value() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let (code, _out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--minimumpriority", + "nope", + ]); + assert_eq!(code, EXIT_ERROR); + assert!( + err.contains("--minimumpriority requires an integer"), + "stderr={err:?}" + ); +} + +#[test] +fn minimumpriority_rejects_out_of_range_value() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + for value in ["0", "6"] { + let (code, _out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--minimumpriority", + value, + ]); + assert_eq!(code, EXIT_ERROR, "value={value}"); + assert!( + err.contains("--minimumpriority must be between 1 and 5"), + "value={value} stderr={err:?}" + ); + } +} + +#[test] +fn maximumpriority_rejects_non_integer_and_out_of_range_value() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let (code, _out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--maximumpriority", + "bogus", + ]); + assert_eq!(code, EXIT_ERROR); + assert!( + err.contains("--maximumpriority requires an integer"), + "stderr={err:?}" + ); + + let (code, _out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--maximumpriority", + "9", + ]); + assert_eq!(code, EXIT_ERROR); + assert!( + err.contains("--maximumpriority must be between 1 and 5"), + "stderr={err:?}" + ); +} + +#[test] +fn suffixes_without_leading_dot_are_normalized() { + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "b.txt", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + dir.path().to_str().unwrap(), + "text", + "codesize", + "--suffixes", + "txt", + ]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("b.txt"), "stdout={out:?}"); } #[test] @@ -727,3 +852,220 @@ fn verbose_prints_ruleset_load_diagnostics() { ); } } + +#[test] +fn two_positionals_print_usage_and_exits_one() { + let (code, out, err) = run_cli(&["src", "text"]); + assert_eq!(code, EXIT_ERROR); + assert!(out.is_empty(), "stdout={out:?}"); + assert!(err.contains("Usage:"), "stderr={err:?}"); +} + +#[test] +fn usage_lists_every_documented_option_name() { + let (code, out, err) = run_cli(&["--help"]); + assert_eq!(code, EXIT_SUCCESS); + assert!(err.is_empty(), "stderr={err:?}"); + for needle in [ + "Usage: messrust ", + "--minimumpriority", + "--maximumpriority", + "--reportfile", + "--suffixes", + "--exclude", + "--only, --enable", + "--disable", + "--ignore-tests", + "--strict", + "--color", + "--verbose, -v", + "--ignore-errors-on-exit", + "--ignore-violations-on-exit", + "--version", + "--help, -h", + "text", + "json", + "sarif", + ] { + assert!(out.contains(needle), "missing {needle:?} in {out:?}"); + } +} + +#[test] +fn missing_value_for_each_value_option_names_the_flag() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + for flag in [ + "--suffixes", + "--exclude", + "--only", + "--enable", + "--disable", + "--minimumpriority", + "--maximumpriority", + ] { + let (code, _out, err) = run_cli(&[path.to_str().unwrap(), "text", "codesize", flag]); + assert_eq!(code, EXIT_ERROR, "flag={flag}"); + let expected = format!("missing value for {flag}"); + assert!(err.contains(&expected), "flag={flag} stderr={err:?}"); + } +} + +#[test] +fn comma_separated_paths_are_each_analyzed() { + let dir = TempDir::new().unwrap(); + let a = write_file(dir.path(), "a.rs", &fixture_with_params(11)); + let b = write_file(dir.path(), "b.rs", &fixture_with_params(11)); + let paths = format!("{},{}", a.display(), b.display()); + let (code, out, err) = run_cli(&[&paths, "text", "codesize"]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("a.rs"), "stdout={out:?}"); + assert!(out.contains("b.rs"), "stdout={out:?}"); +} + +#[test] +fn empty_path_segments_in_comma_list_are_dropped() { + let dir = TempDir::new().unwrap(); + let a = write_file(dir.path(), "only.rs", &fixture_with_params(11)); + let paths = format!(",{},", a.display()); + let (code, out, err) = run_cli(&[&paths, "text", "codesize"]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("only.rs"), "stdout={out:?}"); +} + +#[test] +fn only_wins_when_both_only_and_enable_are_set() { + let dir = TempDir::new().unwrap(); + // ExcessiveMethodLength needs a long body; ExcessiveParameterList is the + // easy codesize finding. Keep only ExcessiveMethodLength via --only while + // --enable names the parameter-list rule — --only must win, so no finding. + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--only", + "ExcessiveMethodLength", + "--enable", + "ExcessiveParameterList", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?} stdout={out:?}"); + assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); +} + +#[test] +fn ignore_errors_alone_with_only_processing_errors_exits_zero() { + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "bad.rs", "fn broken( {\n"); + let (code, out, err) = run_cli(&[ + dir.path().to_str().unwrap(), + "text", + "codesize", + "--ignore-errors-on-exit", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?} stdout={out:?}"); + assert!(out.contains("bad.rs"), "stdout={out:?}"); +} + +#[test] +fn without_verbose_unimplemented_rule_is_silent() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let stub = dir.path().join("stub.xml"); + fs::write( + &stub, + r#" + + + +"#, + ) + .unwrap(); + let (code, _out, err) = run_cli(&[path.to_str().unwrap(), "text", stub.to_str().unwrap()]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(!err.contains("warning:"), "stderr={err:?}"); +} + +#[test] +fn unknown_format_lists_available_formats() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let (code, _out, err) = run_cli(&[path.to_str().unwrap(), "not-a-format", "codesize"]); + assert_eq!(code, EXIT_ERROR); + assert!(err.contains("unknown report format"), "stderr={err:?}"); + assert!(err.contains("Available:"), "stderr={err:?}"); + assert!(err.contains("text"), "stderr={err:?}"); + assert!(err.contains("json"), "stderr={err:?}"); +} + +#[test] +fn comma_separated_rulesets_all_load() { + let dir = TempDir::new().unwrap(); + // Use two component names so split_list on the ruleset positional runs. + let hot = write_file(dir.path(), "hot.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[hot.to_str().unwrap(), "text", "codesize,naming"]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("ExcessiveParameterList"), "stdout={out:?}"); +} + +#[test] +fn default_suffixes_analyze_rs_only() { + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "hot.rs", &fixture_with_params(11)); + write_file(dir.path(), "hot.txt", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[dir.path().to_str().unwrap(), "text", "codesize"]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("hot.rs"), "stdout={out:?}"); + assert!(!out.contains("hot.txt"), "stdout={out:?}"); +} + +#[test] +fn exclude_accepts_comma_separated_substrings() { + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "keep/a.rs", &fixture_with_params(11)); + write_file(dir.path(), "drop_one/b.rs", &fixture_with_params(11)); + write_file(dir.path(), "drop_two/c.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + dir.path().to_str().unwrap(), + "text", + "codesize", + "--exclude", + "drop_one,drop_two", + ]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("a.rs"), "stdout={out:?}"); + assert!(!out.contains("b.rs"), "stdout={out:?}"); + assert!(!out.contains("c.rs"), "stdout={out:?}"); +} + +#[test] +fn reportfile_parent_missing_is_an_error() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let missing = dir.path().join("nope").join("report.txt"); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--reportfile", + missing.to_str().unwrap(), + ]); + assert_eq!(code, EXIT_ERROR, "stderr={err:?}"); + assert!(out.is_empty(), "stdout={out:?}"); + assert!(err.contains("error:"), "stderr={err:?}"); +} + +#[test] +fn version_does_not_run_analysis() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + // --version is only recognized as argv[0]; later args are ignored because + // handle_info_flags returns before parse/analysis. + let (code, out, err) = run_cli(&["--version", path.to_str().unwrap()]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(out.starts_with("messrust "), "stdout={out:?}"); + assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); + assert!(err.is_empty(), "stderr={err:?}"); +} From c3086f1aad0dc3411c3d3b8905beb92cb75bc4e2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 05:26:10 +0000 Subject: [PATCH 2/5] Kill parse_args/parse_priority escapes; fix Linux mutarust batch Add messrust::run seam tests for priority 1/5 bounds and value-option index skip mutants. Adapt mutarust-fn-batch.sh free_mb and script(1) invocation for Linux 16 GB hosts. Co-authored-by: Jonathan Baldie --- scripts/mutarust-fn-batch.sh | 35 ++++++++++++----- tests/cli.rs | 75 ++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 9 deletions(-) diff --git a/scripts/mutarust-fn-batch.sh b/scripts/mutarust-fn-batch.sh index 2054820..1ce2b10 100755 --- a/scripts/mutarust-fn-batch.sh +++ b/scripts/mutarust-fn-batch.sh @@ -7,6 +7,10 @@ MATCH="${1:?usage: mutarust-fn-batch.sh [extra mutarust args...] shift || true free_mb() { + if command -v free >/dev/null 2>&1; then + free -m | awk '/^Mem:/ {print $7}' + return + fi local pages pages=$(vm_stat | awk '/Pages free/ {gsub("\\.","",$3); print $3}') echo $((pages * 16384 / 1024 / 1024)) @@ -50,16 +54,29 @@ echo "START match=${MATCH} free_mb=${free} TMPDIR=${TMPDIR}" WD_PID=$! set +e -script -q "$LOG" env TMPDIR="$TMPDIR" CARGO_BUILD_JOBS="$CARGO_BUILD_JOBS" mutarust \ - --config mutarust.yml \ - --workers 1 \ - --exec-timeout 90 \ - --min-msi 0 \ - --min-covered-msi 0 \ - --test-flags "--test cli --lib" \ - --match "$MATCH" \ - "$@" \ +EXTRA_ARGS=() +if (($# > 0)); then + EXTRA_ARGS=("$@") +fi +MUTA_CMD=( + env "TMPDIR=$TMPDIR" "CARGO_BUILD_JOBS=$CARGO_BUILD_JOBS" mutarust + --config mutarust.yml + --workers 1 + --exec-timeout 90 + --min-msi 0 + --min-covered-msi 0 + --test-flags "--test cli --lib" + --match "$MATCH" + "${EXTRA_ARGS[@]}" src/lib.rs src/main.rs +) +if script --version >/dev/null 2>&1; then + # GNU script (Linux): -c runs a shell command string. + script -q -c "$(printf '%q ' "${MUTA_CMD[@]}")" "$LOG" +else + # BSD script (macOS): log path then command argv. + script -q "$LOG" "${MUTA_CMD[@]}" +fi rc=$? set -e diff --git a/tests/cli.rs b/tests/cli.rs index bd79f1e..1fffa66 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -732,6 +732,22 @@ fn minimumpriority_drops_lower_priority_rules() { assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); } +#[test] +fn minimumpriority_one_is_accepted() { + // Kills parse_priority range mutant `1..=5` → `2..=5`. + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--minimumpriority", + "1", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); +} + #[test] fn maximumpriority_drops_higher_priority_rules() { // maximumpriority 4 keeps priority >= 4; ExcessiveParameterList (3) drops. @@ -748,6 +764,65 @@ fn maximumpriority_drops_higher_priority_rules() { assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); } +#[test] +fn maximumpriority_five_is_accepted() { + // Kills parse_priority range mutant that rejects 5; filters EPL (priority 3). + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--maximumpriority", + "5", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); +} + +#[test] +fn value_option_before_positionals_consumes_exactly_one_argument() { + // Kills parse_args `i += 1` body/±1 mutants on the value skip. + // Correct: suffixes=[.txt] so hit.txt is analyzed and miss.rs is not. + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "hit.txt", &fixture_with_params(11)); + write_file(dir.path(), "miss.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + "--suffixes", + "txt", + dir.path().to_str().unwrap(), + "text", + "codesize", + ]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("hit.txt"), "stdout={out:?}"); + assert!(!out.contains("miss.rs"), "stdout={out:?}"); +} + +#[test] +fn value_option_does_not_skip_the_following_flag() { + // Kills parse_args `i += 2` mutant: must still honour ignore-violations. + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let report = dir.path().join("out.txt"); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--reportfile", + report.to_str().unwrap(), + "--ignore-violations-on-exit", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(out.is_empty(), "stdout={out:?}"); + assert!(report.is_file(), "report missing"); + let body = fs::read_to_string(&report).unwrap(); + assert!( + body.contains("ExcessiveParameterList"), + "report={body:?}" + ); +} + #[test] fn xml_ruleset_path_loads_refs_excludes_and_overrides() { let dir = TempDir::new().unwrap(); From a8e8fcc4035869c28c30f9675bcbcf005a933e1d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 07:10:16 +0000 Subject: [PATCH 3/5] Tighten mutarust batch script memory guards Raise free-RAM abort and watchdog thresholds, shorten exec timeout, and scope batches to src/lib.rs so 16 GB hosts stay stable. Co-authored-by: Jonathan Baldie --- scripts/mutarust-fn-batch.sh | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/mutarust-fn-batch.sh b/scripts/mutarust-fn-batch.sh index 1ce2b10..9951e85 100755 --- a/scripts/mutarust-fn-batch.sh +++ b/scripts/mutarust-fn-batch.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash # Memory-safe single-function mutarust batch for 16 GB hosts. +# Prefer one function per run. For large functions, also pass --enable . set -euo pipefail -MIN_FREE_MB="${MIN_FREE_MB:-1800}" +MIN_FREE_MB="${MIN_FREE_MB:-3000}" +WATCHDOG_FREE_MB="${WATCHDOG_FREE_MB:-1000}" MATCH="${1:?usage: mutarust-fn-batch.sh [extra mutarust args...]}" shift || true @@ -21,6 +23,7 @@ cleanup() { pkill -9 -x rustc 2>/dev/null || true # Do not pkill all cargo — may kill unrelated jobs; only children die with mutarust. rm -rf "${TMPDIR:-/tmp}"/mutarust-* 2>/dev/null || true + sync || true } free=$(free_mb) @@ -36,19 +39,19 @@ export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}" LOG="${LOG:-/tmp/mutarust-fn-${MATCH}.log}" rm -f "$LOG" -echo "START match=${MATCH} free_mb=${free} TMPDIR=${TMPDIR}" +echo "START match=${MATCH} free_mb=${free} TMPDIR=${TMPDIR} jobs=${CARGO_BUILD_JOBS}" # Watchdog: kill mutarust if free RAM collapses mid-run. ( while pgrep -x mutarust >/dev/null 2>&1; do f=$(free_mb) - if (( f < 400 )); then - echo "WATCHDOG free_mb=${f} — killing mutarust" >&2 + if (( f < WATCHDOG_FREE_MB )); then + echo "WATCHDOG free_mb=${f} < ${WATCHDOG_FREE_MB} — killing mutarust" >&2 pkill -9 -x mutarust 2>/dev/null || true pkill -9 -x rustc 2>/dev/null || true exit 3 fi - sleep 5 + sleep 3 done ) & WD_PID=$! @@ -62,19 +65,17 @@ MUTA_CMD=( env "TMPDIR=$TMPDIR" "CARGO_BUILD_JOBS=$CARGO_BUILD_JOBS" mutarust --config mutarust.yml --workers 1 - --exec-timeout 90 + --exec-timeout 60 --min-msi 0 --min-covered-msi 0 --test-flags "--test cli --lib" --match "$MATCH" "${EXTRA_ARGS[@]}" - src/lib.rs src/main.rs + src/lib.rs ) if script --version >/dev/null 2>&1; then - # GNU script (Linux): -c runs a shell command string. script -q -c "$(printf '%q ' "${MUTA_CMD[@]}")" "$LOG" else - # BSD script (macOS): log path then command argv. script -q "$LOG" "${MUTA_CMD[@]}" fi rc=$? From 43ef58203ae5abd4191532c682cc323891e6c2f2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 07:51:53 +0000 Subject: [PATCH 4/5] Fix batch-script thresholds and strengthen ruleset test Keep mutarust.yml 75/80 gates in the batch helper; pass overrides only as extra args for iterative work. Arm the RAM watchdog after mutarust starts. Assert both codesize and naming fire for comma-separated rulesets. Co-authored-by: Jonathan Baldie --- scripts/mutarust-fn-batch.sh | 14 ++++++++++---- tests/cli.rs | 9 +++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/mutarust-fn-batch.sh b/scripts/mutarust-fn-batch.sh index 9951e85..5d92fe1 100755 --- a/scripts/mutarust-fn-batch.sh +++ b/scripts/mutarust-fn-batch.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash # Memory-safe single-function mutarust batch for 16 GB hosts. -# Prefer one function per run. For large functions, also pass --enable . +# Prefer one function per run. For large functions, also pass --enable 'group/*'. +# +# Uses mutarust.yml thresholds (75 / 80). For iterative kill work before the +# score is ready, pass overrides as extra args, e.g.: +# ./scripts/mutarust-fn-batch.sh parse_args --min-msi 0 --min-covered-msi 0 set -euo pipefail MIN_FREE_MB="${MIN_FREE_MB:-3000}" @@ -41,8 +45,12 @@ rm -f "$LOG" echo "START match=${MATCH} free_mb=${free} TMPDIR=${TMPDIR} jobs=${CARGO_BUILD_JOBS}" -# Watchdog: kill mutarust if free RAM collapses mid-run. +# Watchdog: wait for mutarust to appear, then kill it if free RAM collapses. ( + for _ in $(seq 1 120); do + pgrep -x mutarust >/dev/null 2>&1 && break + sleep 0.5 + done while pgrep -x mutarust >/dev/null 2>&1; do f=$(free_mb) if (( f < WATCHDOG_FREE_MB )); then @@ -66,8 +74,6 @@ MUTA_CMD=( --config mutarust.yml --workers 1 --exec-timeout 60 - --min-msi 0 - --min-covered-msi 0 --test-flags "--test cli --lib" --match "$MATCH" "${EXTRA_ARGS[@]}" diff --git a/tests/cli.rs b/tests/cli.rs index 1fffa66..11d4157 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1078,11 +1078,16 @@ fn unknown_format_lists_available_formats() { #[test] fn comma_separated_rulesets_all_load() { let dir = TempDir::new().unwrap(); - // Use two component names so split_list on the ruleset positional runs. - let hot = write_file(dir.path(), "hot.rs", &fixture_with_params(11)); + // codesize finds ExcessiveParameterList; naming (opinionated) finds ShortVariable. + let hot = write_file( + dir.path(), + "hot.rs", + "fn entry_point(param_0: i32, param_1: i32, param_2: i32, param_3: i32, param_4: i32, param_5: i32, param_6: i32, param_7: i32, param_8: i32, param_9: i32, param_10: i32) { let x = 1; let _ = x; }\n", + ); let (code, out, err) = run_cli(&[hot.to_str().unwrap(), "text", "codesize,naming"]); assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); assert!(out.contains("ExcessiveParameterList"), "stdout={out:?}"); + assert!(out.contains("ShortVariable"), "naming ruleset must load: stdout={out:?}"); } #[test] From a8554f15ac4fd24d487659ba22f331995bd886b1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 08:01:25 +0000 Subject: [PATCH 5/5] Remove task-local mutarust batch script The helper hardcodes CLI test flags and src/lib.rs, so it is not shared tooling. Keep mutation batch commands on the follow-up tickets instead. Co-authored-by: Jonathan Baldie --- scripts/mutarust-fn-batch.sh | 97 ------------------------------------ 1 file changed, 97 deletions(-) delete mode 100755 scripts/mutarust-fn-batch.sh diff --git a/scripts/mutarust-fn-batch.sh b/scripts/mutarust-fn-batch.sh deleted file mode 100755 index 5d92fe1..0000000 --- a/scripts/mutarust-fn-batch.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# Memory-safe single-function mutarust batch for 16 GB hosts. -# Prefer one function per run. For large functions, also pass --enable 'group/*'. -# -# Uses mutarust.yml thresholds (75 / 80). For iterative kill work before the -# score is ready, pass overrides as extra args, e.g.: -# ./scripts/mutarust-fn-batch.sh parse_args --min-msi 0 --min-covered-msi 0 -set -euo pipefail - -MIN_FREE_MB="${MIN_FREE_MB:-3000}" -WATCHDOG_FREE_MB="${WATCHDOG_FREE_MB:-1000}" -MATCH="${1:?usage: mutarust-fn-batch.sh [extra mutarust args...]}" -shift || true - -free_mb() { - if command -v free >/dev/null 2>&1; then - free -m | awk '/^Mem:/ {print $7}' - return - fi - local pages - pages=$(vm_stat | awk '/Pages free/ {gsub("\\.","",$3); print $3}') - echo $((pages * 16384 / 1024 / 1024)) -} - -cleanup() { - pkill -9 -x mutarust 2>/dev/null || true - pkill -9 -x rustc 2>/dev/null || true - # Do not pkill all cargo — may kill unrelated jobs; only children die with mutarust. - rm -rf "${TMPDIR:-/tmp}"/mutarust-* 2>/dev/null || true - sync || true -} - -free=$(free_mb) -if (( free < MIN_FREE_MB )); then - echo "ABORT: free_mb=${free} < MIN_FREE_MB=${MIN_FREE_MB}" >&2 - exit 2 -fi - -export TMPDIR="${TMPDIR:-$HOME/tmp/mutarust-run}" -mkdir -p "$TMPDIR" -export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}" - -LOG="${LOG:-/tmp/mutarust-fn-${MATCH}.log}" -rm -f "$LOG" - -echo "START match=${MATCH} free_mb=${free} TMPDIR=${TMPDIR} jobs=${CARGO_BUILD_JOBS}" - -# Watchdog: wait for mutarust to appear, then kill it if free RAM collapses. -( - for _ in $(seq 1 120); do - pgrep -x mutarust >/dev/null 2>&1 && break - sleep 0.5 - done - while pgrep -x mutarust >/dev/null 2>&1; do - f=$(free_mb) - if (( f < WATCHDOG_FREE_MB )); then - echo "WATCHDOG free_mb=${f} < ${WATCHDOG_FREE_MB} — killing mutarust" >&2 - pkill -9 -x mutarust 2>/dev/null || true - pkill -9 -x rustc 2>/dev/null || true - exit 3 - fi - sleep 3 - done -) & -WD_PID=$! - -set +e -EXTRA_ARGS=() -if (($# > 0)); then - EXTRA_ARGS=("$@") -fi -MUTA_CMD=( - env "TMPDIR=$TMPDIR" "CARGO_BUILD_JOBS=$CARGO_BUILD_JOBS" mutarust - --config mutarust.yml - --workers 1 - --exec-timeout 60 - --test-flags "--test cli --lib" - --match "$MATCH" - "${EXTRA_ARGS[@]}" - src/lib.rs -) -if script --version >/dev/null 2>&1; then - script -q -c "$(printf '%q ' "${MUTA_CMD[@]}")" "$LOG" -else - script -q "$LOG" "${MUTA_CMD[@]}" -fi -rc=$? -set -e - -kill "$WD_PID" 2>/dev/null || true -wait "$WD_PID" 2>/dev/null || true - -cleanup - -echo "==== summary match=${MATCH} rc=${rc} free_after=$(free_mb) ====" -tr '\r' '\n' < "$LOG" | rg 'Killed:|Escaped:|Errored:|Not covered:|Skipped:|Total:|Mutation score|Covered-code|ABORT|WATCHDOG' | tail -20 -exit "$rc"