Find the bugs hiding in your test suite
A Python port of Marc Brooker's Morris,
adapted from Rust/cargo to embedded C projects with a host-side CMake + CTest
unit suite (e.g. Unity). It works
with any project laid out that way β see Project layout.
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β Your C β βββ> β Morris Minor β βββ> β Test Gaps β
β + Unity β β (Fixed Flow) β β + Fixes β
β tests β ββββββββββββββββ βββββββββββββββ
βββββββββββββββ
Like the original, Morris Minor follows a fixed, deterministic workflow. The AI (Claude) is consulted exactly twice β once to propose mutations, once to analyse the survivors. All file discovery, building, test execution, and mutation application is plain deterministic code.
The original drives cargo test, where build and test are one step. C splits
them, which actually makes the outcomes cleaner:
| After applying the mutant | Outcome |
|---|---|
| Source fails to compile | π§ BUILD ERROR (not counted) |
| Compiles, a test fails | β KILLED (good β the suite caught it) |
| Compiles, all tests pass | β SURVIVED (a coverage gap) |
| Build/tests exceed the timeout (e.g. a mutated loop bound now spins forever) | β±οΈ TIMEOUT |
| The target line no longer matches what the AI quoted |
Everything else mirrors Morris: Β±10-line fuzzy matching to locate the line to
mutate, automatic backup/restore, a 3Γ-baseline timeout (min 30 s), and an
optional --auto mode that writes and verifies new tests.
- Python 3.10+ (standard library only for the default backend).
- A project with a host-side CMake test directory (see Project layout).
- CMake, a generator (Ninja by default), and CTest on
PATH. - A native C compiler for the host tests (
gcc/clang/MSVC). Note this is not the firmware's cross-compiler β the host suite builds for your machine. - An AI backend (pick one):
cli(default, no API key) β theclaudeCLI onPATH, signed in. Morris Minor callsclaude -p.apiβpip install anthropicand setANTHROPIC_API_KEY.openaiβpip install openaiand setOPENAI_API_KEY.
Morris Minor is a single file β there's nothing to build or install. Copy
morris-minor.py wherever you like and run it with Python 3.10+ (the default
cli backend needs no extra packages; the api backend wants
pip install anthropic). Point it at your firmware project root (the directory
containing the test dir):
python morris-minor.py --project path/to/firmwareThat's it. Morris Minor configures the test build, runs the baseline, asks Claude for 5β8 strategic mutations, tries each one, and reports what survived.
𧬠Morris Minor (Morris embedded C port) - AI-Powered Mutation Testing
β±οΈ Configuring + running baseline tests...
β
Baseline passed in 0.1s (mutation timeout: 30.0s)
π Discovering source files...
Core/dsp/filter.c
Core/util/ringbuf.c
𧬠Asking AI for mutation plan...
Got 4 mutations
π§ͺ Testing mutations...
[1/4] Core/util/ringbuf.c:42 - Change >= to > in the full check... β
KILLED
[2/4] Core/dsp/filter.c:88 - Change + to - in the accumulator... β
KILLED
[3/4] Core/util/ringbuf.c:57 - Off-by-one: head+1 -> head on wrap... β
KILLED
[4/4] Core/dsp/filter.c:31 - Change <= to < on the tap loop... β
KILLED
π Results: 4 killed, 0 survived out of 4 testable mutations
π All mutations were killed! Your tests look solid.
| Flag | Description |
|---|---|
paths... |
Specific .c files/dirs to mutate, relative to project (default: auto-discover from the test build). |
--project DIR |
Firmware project root containing the test dir (default: cwd). |
--test-dir DIR |
CMake test source dir, relative to project (default: test). |
--build-dir DIR |
CMake build dir, relative to project (default: <test-dir>/build). |
--source-root DIR |
Subtree holding the modules under test, relative to project (default: Core). |
--generator NAME |
CMake generator (default: Ninja). |
--backend {auto,cli,api,openai} |
AI backend (default: auto). api = Anthropic, openai = OpenAI. |
--auto |
Write & verify new Unity tests for survivors. |
--quick |
Use the faster Haiku model. |
-n, --mutations N |
Request exactly N mutations (default: 5β8). |
--temperature T |
Sampling temperature for the AI calls, 0.0β1.0 (default: 1.0). Lower = more repeatable but less varied mutation selection across re-runs. Honored by the api and openai backends. |
-v, --verbose |
Print the CMake/CTest commands as they run. |
--backend auto uses api (Anthropic) if ANTHROPIC_API_KEY is set, then
openai if OPENAI_API_KEY is set, otherwise the claude CLI.
# Standard analysis
python morris-minor.py --project path/to/firmware
# Only mutate one module, quick model
python morris-minor.py --project path/to/firmware --quick Core/dsp/filter.c
# Hands-free: auto-write tests that kill the survivors, then verify
python morris-minor.py --project path/to/firmware --auto
# Force the Anthropic API backend
ANTHROPIC_API_KEY=sk-... python morris-minor.py --project path/to/firmware --backend api
# Use the OpenAI API backend
OPENAI_API_KEY=sk-... python morris-minor.py --project path/to/firmware --backend openaiThis repo ships a Claude Code
skill at .claude/skills/morris-minor/.
When you run Claude Code inside this repo it's picked up automatically, so you can
just ask in natural language β e.g. "find the gaps in my Unity test suite" or
"run mutation testing on ../my-firmware" β and Claude drives morris-minor.py
for you, interprets the survivors, and (with your OK) writes verified tests. The
skill references the morris-minor.py at the repo root, so there's nothing extra
to install. To make it available in every local session, copy that folder to
~/.claude/skills/morris-minor/ (bundle morris-minor.py alongside its
SKILL.md so it works from any directory).
- Configure + baseline β
cmake -S <test-dir> -B <build-dir> -G <gen> -DCMAKE_EXPORT_COMPILE_COMMANDS=ON, then build +ctest. Must pass, and times the run to set a3Γmutation timeout (min 30 s). - Discover β reads
compile_commands.jsonand keeps the.cfiles under--source-root(the modules actually compiled into the tests), excluding the test harness, Unity, and stubs. Or use the explicitpathsyou pass. - Mutation plan (AI call #1) β Claude returns 5β8 single-line mutations as JSON (boundaries, arithmetic, logic, off-by-one, return values).
- Test loop β for each mutation: back up the file, apply the one-line
change, rebuild (incremental), run
ctest, classify the outcome, restore. - Summary β killed / survived / testable counts.
- Analysis (AI call #2, only if something survived) β explains why each survivor slips through and shows a Unity test that would catch it.
- Auto mode (optional) β Claude returns new Unity test functions + their
RUN_TESTregistrations as JSON; Morris Minor inserts each function beforemain()and its runner afterUNITY_BEGIN(), rebuilds, and runs the suite. If the build or tests fail, every touched file is reverted.
<project>/ # your firmware project root (--project)
βββ Core/ # modules under test (--source-root)
β βββ dsp/filter.c
β βββ util/ringbuf.c
βββ test/ # host CMake test project (--test-dir)
βββ CMakeLists.txt # enable_testing(); add_test(...)
βββ unity/ # vendored Unity
βββ test_filter.c # main() with UNITY_BEGIN()/RUN_TEST/UNITY_END()
βββ test_ringbuf.c
βββ build/ # generated by Morris Minor (--build-dir)
All four path flags are relative to --project: --source-root (Core),
--test-dir (test), and --build-dir (defaults to test/build, where Morris Minor
writes the CMake build and reads compile_commands.json for discovery).
--auto relies on each test_*.c having the standard Unity main() shape
(UNITY_BEGIN() β¦ RUN_TEST(...) β¦ return UNITY_END();).
-
It only mutates host-buildable logic. Morris Minor mutates exactly the
.cfiles yourtest/build compiles (it reads them fromcompile_commands.json). On a typical STM32CubeMX project that's the portable logic you've factored out β e.g.Core/dsp/filter.c,Core/util/ringbuf.c. HAL, peripheral drivers,main.c,Drivers/,Middlewares/, and the USB stack aren't in the host build, so they're skipped automatically. To widen coverage, extract more hardware-independent modules and add them totest/CMakeLists.txtβ with stubs for their hardware surface (e.g. a fake filesystem layer so a file-parsing module can run against an in-memory buffer). -
Host compiler, not the ARM cross-compiler. The host suite builds for your machine, so you need native
gcc/clang/MSVC β notarm-none-eabi-gcc. -
--source-root Corematches the CubeMX convention (application code underCore/). Override it if your project keeps its logic elsewhere. -
It mirrors your CI. Morris Minor runs the same
cmake -S test -B test/build -G Ninjaβcmake --buildβctestsequence a typical host-test CI job uses, so a clean Morris Minor run reproduces CI locally before you push. -
Deterministic tests only. Mutation testing assumes repeatable pass/fail. Code with randomness or timing should be asserted on invariants or membership (e.g. "the output stays within the expected set") rather than an exact value, so a mutant isn't flagged inconsistently between runs.
-
Host-side, not on-target. Morris Minor exercises the host unit tests on your machine. It does not build, flash, or test on the MCU β on-target/hardware testing is out of scope.
Tools like Mull (LLVM-IR based) and Dextool mutate do exhaustive mutation testing for C/C++:
- Systematically generate every mutation β often hundreds to thousands
- Work at the AST / LLVM-IR level
- Produce comprehensive mutation-score reports
- Best for: CI gates and full audits
Morris Minor takes the AI-guided approach instead:
- Fixed workflow; the AI only selects ~5β8 strategic mutations and explains the survivors (it never drives the build or files)
- Source-level, single-line edits rebuilt with your existing CMake/CTest
- Contextual, actionable explanations β plus optional
--autotest writing - Best for: interactive development, learning, and a fast "where are my test gaps?" pass
The exhaustive tools are more mature and thorough β reach for them when you want a complete audit. Morris Minor is the quick, conversational complement.
The deterministic logic (line matching, mutation apply/restore, JSON/fence extraction, discovery filtering, prompt building, Unity insertion) has its own suite that needs no compiler or AI backend:
python -m unittest test_morris -vMorris Minor is a modified derivative work of marcbrooker/morris β original concept and Rust implementation by Marc Brooker.
Morris is licensed under the Apache License, Version 2.0, and Morris Minor is distributed under the same license. See LICENSE for the full terms and NOTICE for attribution and a summary of the changes made in this port.