Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
---
name: Mutation Tests
on:
schedule:
- cron: '0 3 * * 1' # Weekly: Monday 03:00 UTC
workflow_dispatch:
workflow_call:
permissions: {}
env:
# Lets tests catch text I/O without an explicit encoding (see [tool.pytest.ini_options] in cmd/scadm/pyproject.toml and cmd/scadfmt/pyproject.toml).
PYTHONWARNDEFAULTENCODING: '1'
# Silences the same warning from mutmut and other tooling. pytest's own filter still turns it into an error inside tests.
PYTHONWARNINGS: 'ignore::EncodingWarning'
concurrency:
group: mutation-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# Mutates only the functions a PR changed, one comment section per package. A change to a package's run setup gets a full run of it.
# Release PRs only bump versions and changelogs; their code was mutation-tested on its own PRs.
pr:
if: github.event_name == 'pull_request' && !startsWith(github.head_ref, 'release-please--')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
- name: Install dependencies
run: |
set -euo pipefail
pip install -e cmd/scadm -e cmd/scadfmt pytest -r requirements.txt
# Per package: pick the scope, run mutmut capped at 5 minutes so a growing suite can't stall PRs
# (results collected before the cap are kept), then render its section of the PR comment.
- name: Run mutation tests
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
sections=()
for package in scadm scadfmt; do
globs="$RUNNER_TEMP/$package-globs.txt"
python cmd/test/mutation_report.py scope --package "$package" --base "$BASE_SHA" > "$globs"
setup_changed=$(git diff --name-only "$BASE_SHA...HEAD" -- \
"cmd/$package/pyproject.toml" cmd/test/mutation_report.py requirements.txt .github/workflows/mutation-tests.yml)
args=()
names=()
if [[ -z "$setup_changed" ]]; then
args+=(--globs-file "$globs")
# grep exits 1 on an empty scope; any other failure stops the job.
scoped=$(grep . "$globs" || [[ $? -eq 1 ]])
if [[ -n "$scoped" ]]; then
mapfile -t names <<< "$scoped"
fi
fi
if [[ -n "$setup_changed" || ${#names[@]} -gt 0 ]]; then
rc=0
(cd "cmd/$package" && timeout --kill-after=30s 300 mutmut run "${names[@]}") || rc=$?
if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then
echo "::warning title=Mutation run timed out ($package)::Results are partial. If this keeps happening, move per-PR mutation runs to a nightly job."
args+=(--timed-out)
elif [[ "$rc" -ne 0 ]]; then
exit "$rc"
fi
fi
python cmd/test/mutation_report.py report --package "$package" "${args[@]}" > "$RUNNER_TEMP/$package.md"
sections+=("$RUNNER_TEMP/$package.md")
done
python cmd/test/mutation_report.py combine "${sections[@]}" > "$RUNNER_TEMP/report.md"
cat "$RUNNER_TEMP/report.md" >> "$GITHUB_STEP_SUMMARY"
# Fork PRs get a read-only token, so they rely on the job summary above.
- name: Upsert PR comment
if: github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
ids=$(gh api "repos/$REPO/issues/$PR/comments" --paginate \
--jq '.[] | select(.body | startswith("<!-- mutation-report -->")) | .id')
existing=${ids%%$'\n'*}
if [[ -z "$existing" ]]; then
gh api -X POST "repos/$REPO/issues/$PR/comments" -F body=@"$RUNNER_TEMP/report.md" --silent
else
gh api -X PATCH "repos/$REPO/issues/comments/$existing" -F body=@"$RUNNER_TEMP/report.md" --silent
fi
# Whole package, to keep overall test debt visible: report to Discord, refresh the package's mutation badge.
full:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- package: scadm
badge: mutation.json
- package: scadfmt
badge: scadfmt-mutation.json
env:
PACKAGE: ${{ matrix.package }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true # publish-badge pushes with these
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
- name: Install dependencies
run: |
set -euo pipefail
pip install -e "cmd/$PACKAGE" pytest -r requirements.txt
- name: Run mutation tests
working-directory: cmd/${{ matrix.package }}
run: |
set -euo pipefail
mutmut run
mutmut export-cicd-stats
mutmut badge --output "$RUNNER_TEMP/mutation.json"
- name: Build report
run: |
set -euo pipefail
python cmd/test/mutation_report.py report --package "$PACKAGE" >> "$GITHUB_STEP_SUMMARY"
- name: Post to Discord
env:
DISCORD_CI_WEBHOOK_URL: ${{ secrets.DISCORD_CI_WEBHOOK_URL }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [[ -z "$DISCORD_CI_WEBHOOK_URL" ]]; then
echo "::warning::DISCORD_CI_WEBHOOK_URL is not set, skipping the Discord report"
exit 0
fi
python cmd/test/mutation_report.py discord --package "$PACKAGE" --run-url "$RUN_URL" > "$RUNNER_TEMP/discord.json"
curl -fsS -H "Content-Type: application/json" -d @"$RUNNER_TEMP/discord.json" "$DISCORD_CI_WEBHOOK_URL"
- name: Publish mutation badge
if: github.ref == 'refs/heads/main'
uses: ./.github/actions/publish-badge
with:
file: ${{ runner.temp }}/mutation.json
name: ${{ matrix.badge }}
4121c9fb7This file is pinned to the selected release. Download the original to open it in your design tool.