Skip to main content

gdmutant: banner with Frank the Mutant, the project mascot

Mutation testing for GDScript and Godot: find the bugs your green tests would miss.

CI PyPI Godot 4.3+ GUT 9.x gdUnit4 6.x Python 3.12+ License: MIT

A community tool, not affiliated with or endorsed by the Godot Foundation.

What it is

Coverage tells you a line ran. Mutations tell you if a bug there would be caught: a killed mutant means yes, a survivor means no. A standalone CLI, no AI required.

Same idea, other languages: mutmut for Python, Stryker for JS/TS, PIT for Java.

gdmutant mutates your GDScript (flips >>=, andor, bumps a number, deletes a statement), reruns your tests once per change, and reports the survivors.

The --html report, open on turn_order.gd. A survivor in the source, and marked on line 27:

turn_order.gd:27, return alive and not stunned, with and marked as a survivor

and its detail card: what it means, why it's risky, and how to close it.

The detail card: survived, boolean, turn_order.gd:27, func can_act. Gap: tests pass whether this needs both sides or just one. Risk: a change that loosens or tightens the guard would pass every test. Start: add a test where exactly one side is true and the other false, and assert the outcome

Prerequisites

  • Godot 4.3+ (see Compatibility for exact versions), on your PATH so a plain godot --version works, or pass --godot <full-path> on every gdmutant command instead.
  • GUT or gdUnit4, already installed and enabled in your project, if you use either. A different test runner works too, via --runner command: any headless command that exits non-zero on failure.
  • Python 3.12+ (check with python --version).

Quickstart

This mutates corpus/, a small real Godot project bundled in this repo just for this: a real script and a real GUT/gdUnit4 suite to try gdmutant against before pointing it at your own.

git clone https://github.com/kphutt/gdmutant

cd gdmutant                                # corpus/ lives right here, at the repo root

pip install .                              # installs gdmutant and its own dependencies

python scripts/install_gdunit4.py          # gdUnit4 is a Godot addon that isn't vendored in git; this fetches it

gdmutant run corpus/turn_order.gd --project corpus --runner gdunit4 --html
# mutates one file, reruns corpus/'s real GdUnit4 tests against each mutant

Output:

...

  corpus\turn_order.gd:27   func can_act

     27 |     return alive and not stunned
        |                  ^  changed  and  to  or: every test still passed

  gap    Your tests pass whether this needs both sides (`and`) or just one
         (`or`). No test covers the case that tells them apart: the
         operands disagreeing (one true, one false).

  risk   Your tests can't tell 'needs both' from 'needs either.' A change
         that loosens or tightens this guard would pass every test.

  start  Add a test where exactly one side is true and the other false,
         and assert the outcome.

  more   https://github.com/kphutt/gdmutant/blob/main/docs/survivors/README.md#boolean
──────────────────────────────────────────────────────────────────────────

Results

Mutation score: 61.1%
  killed:   11
  timeout:  0  (counted as killed)
  survived: 7
  ignored:  0  (suppressed, excluded from score)
  invalid:  0
  error:    0

Wrote HTML report to gdmutant-report-turn_order-<timestamp>.html. Open it in a browser.

Point it at your own project

pip install 'gdmutant==0.1.*'   # gdmutant is 0.x: pin the minor so a new one is a move you make on purpose

Pick your test runner with --runner (required). GUT or gdUnit4 — Godot has no built-in test runner, so both ship as addons: a plugin folder (addons/gut/ or addons/gdUnit4/) that lives inside a Godot project. Whichever you pick needs to already be installed there, in the project you're pointing gdmutant at. Godot itself needs to be on PATH, or point at it with --godot <path>. gdUnit4's usual test layout matches gdmutant's default --tests res://test, so a gdUnit4 command needs no --tests flag. GUT needs it spelled out: its stock layout puts suites in test/unit/ instead, and GUT's own -gdir doesn't search subdirectories.

# GUT
gdmutant run ../my-project/src/module.gd --project ../my-project --runner gut --tests res://test/unit --json
# gdUnit4
gdmutant run ../my-project/src/module.gd --project ../my-project --runner gdunit4 --json

--json follows the mutation-testing-elements schema

See the survivor reference for what ignored, invalid and error mean.

Killing Survivors

Kill each survivor with a real test, or mark a genuine equivalent with # gdmutant: ignore and a reason (details). Re-run until nothing survives. A mutation score isn't a target to hit, it's a direction to watch. There's no universal "good" number.

GitHub Actions

gdmutant also ships as a GitHub Action, so a pull request can report its own survivors. Save this as .github/workflows/mutation.yml:

name: Mutation testing
on:
  pull_request:
  push:
    branches: [main]

jobs:
  gdmutant:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0        # `since` below needs the base commit in the clone
      - uses: kphutt/gdmutant@05728864a1c9330d632e2aab2348ff4442f3d61d # v0.1.0
        with:
          godot-version: "4.7.0"   # the only required input
          paths: src                # what to mutate (default: the whole project)
          runner: gdunit4            # or gut, or command
          # A PR diffs against its base commit. A merge to main diffs against the commit
          # it replaced. Either way, only the code that actually changed gets mutated.
          since: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}

It sets up Godot, installs gdmutant, runs it, and writes every survivor with its gap / risk / start explanation into the job summary, where reviewers already look. Survivors are output, not failure: the step fails only on a real error, such as a suite that was already red. Your project brings its own GUT or gdUnit4 addon, the same one your existing test job uses.

Most iteration happens locally with gdmutant run above -- a GitHub Action is too slow a feedback loop for that, since you already run your tests before committing. Its value here is as a CI check: scoped to just what changed, it catches anything a local run missed, without paying to mutate the whole project on every PR and push. Godot boots once per mutant, so that full-project cost is real, which is what since avoids. Drop it (and fetch-depth) for an occasional full-project scan instead.

Every input and output, and how to pin a version, is in the guide.

Compatibility

Verified at every release Expected to work
Godot 4.7.0 4.3+
Runner GUT 9.7.1, gdUnit4 6.1.3 GUT 9.x, gdUnit4 6.x, any headless command

Documentation

License

MIT, © 2026 kphutt.

Release files for gdmutant 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gdmutant 0.1.1
File Size Uploaded
gdmutant-0.1.1.tar.gz 515.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gdmutant 0.1.1
File Interpreter ABI Platform
gdmutant-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 651.0 kB

Release files / gdmutant-0.1.1.tar.gz

Download URL gdmutant-0.1.1.tar.gz
Size 515.6 kB
Tags Source
SHA-256 checksum
How to use checksums
625ff326960fd24d6c690deb11e18cb455ae21ef9357329edf64582607f18600
BLAKE2b-256 checksum
How to use checksums
0ece4edec9e0a3a9e95d3442c556435e64f6464aa2d864f79df1d244569bccfb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 7, 2026.

Transparency log

Release files / gdmutant-0.1.1-py3-none-any.whl

Download URL gdmutant-0.1.1-py3-none-any.whl
Size 135.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
62f5e89a4134a427843b8184a3390b448f9bb59ea7f2d52cccd4f17f4843a798
BLAKE2b-256 checksum
How to use checksums
f7229b9713e82e82c32b64edbd9b13e766545bd612b950ae3ab971db7ee6013f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 7, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.3

2 release files

0.1.2

2 release files

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page