Skip to content

Mutation

Mutation #10

Workflow file for this run

name: Mutation
# Coverage says a line ran; mutation testing asks whether anything would have NOTICED had it been
# wrong. It is a review tool, not a gate: most survivors are equivalent mutants (message text no test
# asserts on, formatting, `__repr__`), so the job stays green on survivors and publishes the list to
# triage. Weekly, not nightly - the score only moves when assertpy2/ or tests/ move, and a recurring
# red that means nothing is a red people learn to ignore.
#
# It does go red when the RUN itself is broken. mutmut runs the unmutated suite once to learn which
# tests cover which function, and exits 1 if that fails, so a test the trampoline breaks stops the
# whole run instead of quietly leaving mutants unjudged. That red is never routine: it means the
# config rotted, which is the failure the previous cosmic-ray setup hid for five weeks behind a
# stale module path.
#
# Linux only: mutmut forks and calls os.wait, and does not run on Windows.
on:
schedule:
- cron: "0 4 * * 1" # weekly, Monday 04:00 UTC
workflow_dispatch: {}
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
mutation:
runs-on: ubuntu-latest
# 10 060 mutants over the whole package. Measured on four pinned cores with `--max-children 4`,
# which is this runner's shape: 41m15s. Slower runner cores put the real figure somewhere
# around an hour, so this leaves comfortable headroom without letting a hang burn two hours.
timeout-minutes: 120
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
- name: Install dependencies
# Every optional integration, matching the full-deps cell of ci.yml. A skipped test cannot
# kill a mutant, so mutating dataframe.py or json_mixin.py without pandas/polars/jsonschema
# installed reports its whole surface as untested.
run: uv sync --locked --extra json --extra data --extra inline --group integrations --group mutation
- name: Run the mutants
# Exits non-zero when it cannot collect stats, which is what a baseline failure looks like.
# Survivors alone do not fail it.
#
# `--no-sync` on every `uv run` below: without it uv re-resolves the environment to the
# project's default groups and uninstalls the extras the step above just installed, so the
# mutants would be judged by a suite that skips pandas, polars and jsonschema.
run: uv run --no-sync mutmut run
- name: Report
if: always()
run: |
uv run --no-sync python scripts/mutation_report.py >> "$GITHUB_STEP_SUMMARY"
uv run --no-sync mutmut results > not-killed.txt
- name: Upload the mutants that lived
# `mutmut results` lists every mutant that was not killed, which is survivors plus timeouts
# plus the ones no test covered. Each line carries its own status, so the file is named for
# what it holds rather than for survivors alone - reading it as a survivor list overcounts.
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: mutants-not-killed
path: not-killed.txt