mutgate
Status: alpha (0.1.x). The verdict names, the
Mutationfields and the exit-code contract are stable; the declaration file may gain fields (control=is planned).
Named mutations as contracts on a test suite. Each mutation is a deliberate, named
change to the code under test, together with the list of tests it must turn red — or the
statement that it must turn nothing red. mutgate applies each one in a sandbox copy, runs
the suite, and judges the contract.
# tests/mutations.py
from mutgate import Mutation
MUTATIONS = [
Mutation("keep-larger", file="pkg/engine.py",
old="if (birth[a], -a) >= (birth[b], -b):",
new="if size[a] >= size[b]:",
fires=("TestElderRule",)),
Mutation("skip-absolute-floor", file="pkg/engine.py",
old="if u.birth < abs_floor:", new="if False:",
fires=("TestAbsoluteFloor",)),
Mutation("tie-to-higher-index", file="pkg/engine.py",
old="(birth[a], -a) >= (birth[b], -b)",
new="(birth[a], a) >= (birth[b], b)",
invisible=True), # an invariance: the measurement must not depend on it
]
$ mutgate run tests/mutations.py
mutation verdict fired expected
keep-larger OK 3 TestElderRule
skip-absolute-floor DECORATION 0 TestAbsoluteFloor
did not fire: TestAbsoluteFloor
tie-to-higher-index OK 0 nothing
2 OK, 1 DECORATION
Why
A green test suite guards only the defects that shaped it. The usual way to find out
whether a test is load-bearing is to break the code on purpose and see whether the test
notices — and in practice that is done by hand, in a scratch copy, with sed, once, and then
the result is written into a design note as prose. mutgate makes that a checked artefact:
- the mutation is named and exact.
oldmust occur exactlycounttimes in the file, or the verdict isNOT_APPLIED. A mutation that silently does not land looks identical to a working guard; this is the trap that motivated the tool. - the contract says which tests must fire. A named test that does not fire is a
DECORATION— a guard that would not go red. - a test outside the contract firing is a finding, not noise.
OVERREACHmeans the mutation reaches further than its author believed. When a convention (a tie-breaking rule, a sign, an ordering) is changed and tests that should only relabel things fail on values, the convention exists in two places that have drifted apart. - an invariance is a mutation that must fire nothing.
invisible=Truepins that the suite passes unchanged under the alternative convention; if it does not, the verdict isVISIBLE. ⚠ AnOKon an invariance is a negative result, and it is only as strong as the evidence that the mutated line ran: a dead function, a site the namedTESTSnever reach, or a suite that imports an installed copy instead of the sandbox all readOKhonestly. Pair every invariance with a firing mutation at the same site in the same declaration — the known-positive control — so the file itself proves the site is exercised by these tests. An unpairedOKon an invariance is unevidenced. - the working tree is never touched. Each mutation is applied and restored in a
temporary copy (
git ls-files, so uncommitted work is included and the venv is not), and the copy is put first onPYTHONPATHso an editable install elsewhere cannot shadow it. - a red baseline aborts. Mutations mean nothing on a suite that already fails.
- a run whose failures cannot be read is an error, never a verdict. If the project's
pytest configuration suppresses the short summary (
--no-summary), pytest's exit code says "failed" while nothing parses; that is reported asERROR, not silently asOK. And a run cut short is never a verdict either:mutgateowns--maxfail=0after every user argument (a project's-xwould truncate the fired set to one test), and aconftestthat forcesmaxfailanyway trips pytest's own "stopping after N failures" line, which reads asERROR.
The sandbox holds the project's files but not its .git; a suite that shells out to git
(a setuptools_scm-style version check, say) goes red at baseline and says so.
What it is not
It is not automatic mutation testing. Tools like mutmut and cosmic-ray generate operator
mutations at random and report a kill rate; that answers "how thorough is this suite?".
mutgate answers a narrower question that random mutation cannot: does this specific guard
fire under the specific defect it was written for, and only that? The two are complementary.
Declaration file
A Python file (conventionally tests/mutations.py) defining:
| name | required | meaning |
|---|---|---|
MUTATIONS |
yes | a sequence of Mutation |
TESTS |
no | pytest targets; default: the declaration file's own directory |
PATHS |
no | PYTHONPATH entries relative to the root; default ("src", ".") |
ROOT |
no | project root, relative to the declaration file; default: the nearest ancestor with pyproject.toml or .git |
PYTHON |
no | interpreter to run pytest with: a bare name is looked up on PATH, a relative path is relative to the root; default: the one running mutgate |
Mutation(name, file, old, new, fires=(), may_fire=(), invisible=False, count=1, note="").
Entries in fires and may_fire are substrings of pytest node ids, so "TestElderRule",
"test_engine.py::TestElderRule" and "test_tie_goes_to_lower_index" all work.
CLI
mutgate run tests/mutations.py [--tests T ...] [--only NAME ...] [--python PY]
[--pytest-arg ARG] [--markdown] [--keep] [-x] [-v]
mutgate list tests/mutations.py
--markdown prints a table meant to be pasted into a design note's build record. Exit code
0 when every contract holds, 1 when a verdict is not OK, 2 when the baseline is red or the
file cannot be loaded.
In CI
- run: pip install mutgate
- run: mutgate run tests/mutations.py
Each mutation runs the named tests once, so the cost is the suite's cost times the number of
mutations; point TESTS at the module the mutations concern.
Install
pip install mutgate
No runtime dependencies. Python 3.10+. MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mutgate-0.1.1.tar.gz.
File metadata
- Download URL: mutgate-0.1.1.tar.gz
- Upload date:
- Size: 24.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2b97dbf490d233aab4e16af4d3b842777e4302642a5553bd68ff54d454dad9e
|
|
| MD5 |
729a2d0dfb8e55d0588ead61438d7464
|
|
| BLAKE2b-256 |
cb528f523cfde5c66d0235ea51f57572829b65f965f9f1c31af1b797dabaebc0
|
Provenance
The following attestation bundles were made for mutgate-0.1.1.tar.gz:
Publisher:
publish-pypi.yml on JimGalasyn/mutgate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mutgate-0.1.1.tar.gz -
Subject digest:
c2b97dbf490d233aab4e16af4d3b842777e4302642a5553bd68ff54d454dad9e - Sigstore transparency entry: 2758246651
- Sigstore integration time:
-
Permalink:
JimGalasyn/mutgate@b3b0bf6ab7ff2ff5da0dd317238039647946149d -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/JimGalasyn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@b3b0bf6ab7ff2ff5da0dd317238039647946149d -
Trigger Event:
release
-
Statement type:
File details
Details for the file mutgate-0.1.1-py3-none-any.whl.
File metadata
- Download URL: mutgate-0.1.1-py3-none-any.whl
- Upload date:
- Size: 15.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8be329870730973cc2f3951f85cdea894f4223abfc4741cacedbb5144004e201
|
|
| MD5 |
bc6a5e791306755927cec087f2493216
|
|
| BLAKE2b-256 |
cd3db5ad776cb9a187febb3ba5ea4d3b599102ebd9f38e1e230cbc03a64c3d87
|
Provenance
The following attestation bundles were made for mutgate-0.1.1-py3-none-any.whl:
Publisher:
publish-pypi.yml on JimGalasyn/mutgate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mutgate-0.1.1-py3-none-any.whl -
Subject digest:
8be329870730973cc2f3951f85cdea894f4223abfc4741cacedbb5144004e201 - Sigstore transparency entry: 2758246693
- Sigstore integration time:
-
Permalink:
JimGalasyn/mutgate@b3b0bf6ab7ff2ff5da0dd317238039647946149d -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/JimGalasyn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@b3b0bf6ab7ff2ff5da0dd317238039647946149d -
Trigger Event:
release
-
Statement type: