Mutants #177
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: RAprogramm <andrey.rozanov.vl@gmail.com> | |
| # SPDX-License-Identifier: MIT | |
| # Mutation testing is the strongest available evidence that line/branch | |
| # coverage is not coverage theatre. cargo-mutants injects mutations into | |
| # the source and reports those that survive the test suite — a survived | |
| # mutant is a behaviour the tests do not exercise. | |
| # | |
| # The full run happens nightly (cron) and on demand (workflow_dispatch); | |
| # a single full mutants run can take 30-60 minutes, so it stays out of | |
| # the PR critical path. Pull requests get a scoped `--in-diff` pass | |
| # instead: only mutants touching the PR's changed code are generated, | |
| # which keeps the signal per-change without the full-run cost. The | |
| # in-diff pass is advisory (continue-on-error) until its runtime on this | |
| # repo's PR sizes is established. | |
| name: Mutants | |
| on: | |
| schedule: | |
| - cron: "0 4 * * *" | |
| workflow_dispatch: | |
| pull_request: | |
| branches: | |
| - main | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| CARGO_INCREMENTAL: 0 | |
| jobs: | |
| mutants: | |
| name: cargo-mutants | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 90 | |
| if: github.event_name != 'pull_request' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Rust toolchain | |
| uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master | |
| with: | |
| toolchain: stable | |
| - name: Load cached target directory | |
| uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| key: mutants | |
| save-if: true | |
| - name: Install cargo-mutants | |
| uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5 | |
| with: | |
| tool: cargo-mutants | |
| - name: Run cargo-mutants | |
| continue-on-error: true | |
| run: cargo mutants --no-shuffle --timeout-multiplier 2 --output mutants.out | |
| - name: Append mutants summary to workflow output | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| if [ -f mutants.out/outcomes.json ]; then | |
| total=$(jq '[.outcomes[]] | length' mutants.out/outcomes.json) | |
| caught=$(jq '[.outcomes[] | select(.summary == "CaughtMutant")] | length' mutants.out/outcomes.json) | |
| missed=$(jq '[.outcomes[] | select(.summary == "MissedMutant")] | length' mutants.out/outcomes.json) | |
| timeout=$(jq '[.outcomes[] | select(.summary == "Timeout")] | length' mutants.out/outcomes.json) | |
| unviable=$(jq '[.outcomes[] | select(.summary == "Unviable")] | length' mutants.out/outcomes.json) | |
| { | |
| echo "## Mutation testing" | |
| echo | |
| printf '| Metric | Count |\n' | |
| printf '|---|---:|\n' | |
| printf '| Total mutants | %s |\n' "$total" | |
| printf '| Caught | %s |\n' "$caught" | |
| printf '| Missed (survived) | %s |\n' "$missed" | |
| printf '| Timeout | %s |\n' "$timeout" | |
| printf '| Unviable | %s |\n' "$unviable" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "(mutants.out/outcomes.json not produced)" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Upload mutants report | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: mutants-report | |
| path: mutants.out/ | |
| retention-days: 30 | |
| mutants-diff: | |
| name: cargo-mutants (PR diff) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| if: github.event_name == 'pull_request' | |
| continue-on-error: true | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Setup Rust toolchain | |
| uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master | |
| with: | |
| toolchain: stable | |
| - name: Load cached target directory | |
| uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| key: mutants-diff | |
| save-if: false | |
| - name: Install cargo-mutants | |
| uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5 | |
| with: | |
| tool: cargo-mutants | |
| - name: Compute the PR diff | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: git diff "$BASE_SHA"...HEAD > pr.diff | |
| - name: Run cargo-mutants on the diff | |
| run: cargo mutants --no-shuffle --timeout-multiplier 2 --in-diff pr.diff --output mutants.out | |
| - name: Append mutants summary to workflow output | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| if [ -f mutants.out/outcomes.json ]; then | |
| total=$(jq '[.outcomes[]] | length' mutants.out/outcomes.json) | |
| caught=$(jq '[.outcomes[] | select(.summary == "CaughtMutant")] | length' mutants.out/outcomes.json) | |
| missed=$(jq '[.outcomes[] | select(.summary == "MissedMutant")] | length' mutants.out/outcomes.json) | |
| { | |
| echo "## Mutation testing (PR diff)" | |
| echo | |
| printf '| Metric | Count |\n' | |
| printf '|---|---:|\n' | |
| printf '| Total mutants in diff | %s |\n' "$total" | |
| printf '| Caught | %s |\n' "$caught" | |
| printf '| Missed (survived) | %s |\n' "$missed" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "No mutants report produced" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Upload mutants report | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: mutants-diff-report | |
| path: mutants.out | |
| retention-days: 14 |