From ee7b3b0f69357ea3b8e9675fa14c6864364cbb08 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 28 May 2026 21:27:48 +0800 Subject: [PATCH] fix: mutants-protocol invocation --- .github/workflows/mutants.yml | 42 ++++++++++++++++++++++----------- Justfile | 44 +++++++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml index 08d0ee2..1c8e70c 100644 --- a/.github/workflows/mutants.yml +++ b/.github/workflows/mutants.yml @@ -165,21 +165,37 @@ jobs: cargo fuzz build "${target}" done - # cargo-mutants is invoked from the LEZ workspace (sibling directory). - # FUZZ_REPO is exported so the wrapper script can locate the corpus and - # fuzz directory without relying on relative paths. + # cargo-mutants >=24 dropped --test-command; intercept "cargo test" with a + # fake cargo wrapper that runs the corpus oracle instead. cargo-mutants is + # called as a direct binary (not through `cargo`) so the CARGO env var we + # set is respected rather than being overridden by cargo's process launch. - name: Run mutation tests against LEZ (nssa + common) - env: - FUZZ_REPO: ${{ github.workspace }} - working-directory: logos-execution-zone run: | - cargo mutants \ - --package nssa \ - --package common \ - --in-place \ - --test-command "${{ github.workspace }}/scripts/mutants-corpus-test.sh" \ - --output "${{ github.workspace }}/mutants-protocol.out" \ - --timeout-multiplier 5.0 + REAL_CARGO="$(command -v cargo)" + FAKE_CARGO="$(mktemp /tmp/fake-cargo-XXXXXX)" + # Intercept the test *execution* phase only; forward the build phase + # (cargo test --no-run) to the real cargo so mutants are compiled. + # cargo-mutants uses: + # Build phase: cargo test --no-run --verbose --package=... + # Test phase: cargo test --verbose --package=... + printf '#!/bin/bash\n_has_no_run=false\nfor _a in "$@"; do [ "$_a" = "--no-run" ] && _has_no_run=true && break; done\nif [ "${1:-}" = "test" ] && [ "$_has_no_run" = "false" ]; then\n FUZZ_REPO="%s" exec "%s"\nelse\n exec "%s" "$@"\nfi\n' \ + "${{ github.workspace }}" \ + "${{ github.workspace }}/scripts/mutants-corpus-test.sh" \ + "$REAL_CARGO" > "$FAKE_CARGO" + chmod +x "$FAKE_CARGO" + # cargo install places cargo-mutants next to cargo in the same bin dir. + MUTANTS_BIN="$(command -v cargo-mutants 2>/dev/null || echo "$(dirname "$REAL_CARGO")/cargo-mutants")" + cd "${{ github.workspace }}/logos-execution-zone" + # cargo-mutants is a Cargo plugin; when invoked directly (not via + # `cargo mutants`) we must supply "mutants" as argv[1] ourselves. + CARGO="$FAKE_CARGO" \ + "$MUTANTS_BIN" mutants \ + --package nssa \ + --package common \ + --in-place \ + --output "${{ github.workspace }}/mutants-protocol.out" \ + --timeout-multiplier 5.0 + rm -f "$FAKE_CARGO" - name: Upload mutants report if: always() diff --git a/Justfile b/Justfile index a2ad2aa..1e36971 100644 --- a/Justfile +++ b/Justfile @@ -722,14 +722,50 @@ mutants-protocol PACKAGES="nssa common": # --in-place is required because LEZ depends on path crates outside its own # directory (e.g. the Rust standard toolchain); without it cargo-mutants copies # the workspace to a temp dir where those relative paths would not resolve. + # + # cargo-mutants >=24 dropped --test-command and only supports --test-tool cargo|nextest. + # Work around: create a fake `cargo` wrapper that intercepts `cargo test` and + # runs the corpus oracle instead; every other sub-command is delegated to the + # real cargo. We call the cargo-mutants binary directly so that cargo's own + # process launch doesn't override the CARGO env var back to the real binary. + REAL_CARGO="$(command -v cargo)" + FAKE_CARGO=$(mktemp /tmp/fake-cargo-XXXXXX) + FAKE_CARGO_LOG=$(mktemp /tmp/fake-cargo-log-XXXXXX.txt) + trap 'rm -f "$FAKE_CARGO" "$FAKE_CARGO_LOG"' EXIT + # The fake cargo intercepts the test *execution* phase only. + # cargo-mutants drives two kinds of "cargo test" invocations: + # Build phase: cargo test --no-run --verbose --package=... (compile only) + # Test phase: cargo test --verbose --package=... (run tests) + # The oracle must only replace the test execution phase; the build phase + # must be forwarded to the real cargo so mutants are actually compiled. + printf '#!/bin/bash\necho "FAKE_CARGO: $*" >> "%s"\n_has_no_run=false\nfor _a in "$@"; do [ "$_a" = "--no-run" ] && _has_no_run=true && break; done\nif [ "${1:-}" = "test" ] && [ "$_has_no_run" = "false" ]; then\n FUZZ_REPO="%s" exec "%s"\nelse\n exec "%s" "$@"\nfi\n' \ + "$FAKE_CARGO_LOG" \ + "$REPO_DIR" \ + "${REPO_DIR}/scripts/mutants-corpus-test.sh" \ + "$REAL_CARGO" > "$FAKE_CARGO" + chmod +x "$FAKE_CARGO" + + # Locate the cargo-mutants binary (installed by `cargo install cargo-mutants`). + MUTANTS_BIN="$(command -v cargo-mutants 2>/dev/null || true)" + if [ -z "$MUTANTS_BIN" ]; then + MUTANTS_BIN="$(dirname "$REAL_CARGO")/cargo-mutants" + fi + if [ ! -x "$MUTANTS_BIN" ]; then + echo "ERROR: cargo-mutants not found. Install with: cargo install cargo-mutants --locked" + exit 1 + fi + + # cargo-mutants is a Cargo plugin. When invoked via `cargo mutants`, Cargo + # automatically prepends "mutants" as argv[1]. When we invoke the binary + # directly (to keep our CARGO env override alive), we must supply it ourselves. cd "$LEZ_DIR" - FUZZ_REPO="$REPO_DIR" \ - cargo mutants \ + CARGO="$FAKE_CARGO" \ + "$MUTANTS_BIN" mutants \ "${PKG_FLAGS[@]}" \ --in-place \ - --test-command "${REPO_DIR}/scripts/mutants-corpus-test.sh" \ --output "${REPO_DIR}/mutants-protocol.out" \ - --timeout-multiplier 5.0 + --timeout-multiplier 5.0 \ + || { echo "--- fake-cargo invocations ---"; cat "$FAKE_CARGO_LOG"; exit 1; } echo "" echo "=== Mutation report summary ==="