Output
mutastic prints its result on stdout and its progress on stderr. How the result looks is --format’s choice.
Formats
Section titled “Formats”--format chooses how much each missed mutant says, and for whom:
| Format | For | Each missed mutant |
|---|---|---|
full | People at a terminal, the default there | A card: the gap, the code with the change marked, the evidence, the test to add, and how to prove it |
brief | People who know the cards | The gap, the changed line and the evidence |
concise | Logs and CI, the default off a terminal | One line |
grouped | Reading many misses at once | One line, under its file and method |
agent | Coding agents, the default with MUTASTIC_AGENT=1 | Plain text: the verdict first, then the change, the fix and how to prove it |
jsonl | Tools and scripts | One JSON event per line, as each mutant finishes |
The same run of the pricing sample, in each:
Missed ────────────────────────────────────────────────────────────
❯ src/main/java/com/example/pricing/Pricing.java 1 missed in 1 method
✗ MISSED no test checks discount() at total == 100 ┌─ src/main/java/com/example/pricing/Pricing.java:17 │ 16 │ public int discount(int total) { 17 │ if (total >= 100) { │ ^^ changed to > 18 │ return total / 10; │ = mutant - if (total >= 100) { + if (total > 100) { = note: every test calls discount() away from the boundary inputs seen: 50, 250 (none is 100) covered by PricingTest#bigOrdersGetTenPercentOff, PricingTest#smallOrdersPayFullPrice (all 2 covering tests still pass) = help: test discount() with total = 100 and assert the result
// in PricingTest @Test void discountWithTotal100() { assertEquals(10, new Pricing().discount(100)); // today 10 · with this mutant 0 } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify m855c8e3c samples/pricing passes reproduce mutastic run --only=m855c8e3c samples/pricing or ignore mutastic ignore m855c8e3c --reason="<why>" samples/pricing
Classes 0 clean | 1 with misses (1) Mutants ✓ 8 caught · ✗ 1 missed · 2 skipped (2 equivalent) (11) Worst at Pricing.discount() 1 ✗ Test runs 11 (27 without coverage selection) Start at 17:19:23 Duration 2.1s (setup 1.5s, coverage 206ms, mutants 382ms) Tests 3 on JDK 17 · 4 workers Reports build/mutastic/mutation.json · build/mutastic/mutation.html Score 88.9% threshold 80
PASS 1 missed, above the threshold. Next: mutastic verify <id> samples/pricing · mutastic run --only=<id> samples/pricing · mutastic explain <id> samples/pricing Missed ────────────────────────────────────────────────────────────
❯ src/main/java/com/example/pricing/Pricing.java 1 missed in 1 method
✗ MISSED no test checks discount() at total == 100 ┌─ src/main/java/com/example/pricing/Pricing.java:17 │ 17 │ if (total >= 100) { │ ^^ changed to > │ = note: every test calls discount() away from the boundary inputs seen: 50, 250 (none is 100) = id: m855c8e3c · mutastic run --only=m855c8e3c samples/pricing --format=full for the whole card
Classes 0 clean | 1 with misses (1) Mutants ✓ 8 caught · ✗ 1 missed · 2 skipped (2 equivalent) (11) Worst at Pricing.discount() 1 ✗ Test runs 11 (27 without coverage selection) Start at 16:30:11 Duration 8.2s (setup 7.6s, coverage 388ms, mutants 220ms) Tests 3 on JDK 17 · 1 worker Reports build/mutastic/mutation.json · build/mutastic/mutation.html Score 88.9% threshold 80
PASS 1 missed, above the threshold. Next: mutastic verify <id> samples/pricing · mutastic run --only=<id> samples/pricing · mutastic explain <id> samples/pricing Missed ──────────────────────────────────────────────────────────── ✗ src/main/java/com/example/pricing/Pricing.java:17 no test checks discount() at total == 100 m855c8e3c
Classes 0 clean | 1 with misses (1) Mutants ✓ 8 caught · ✗ 1 missed · 2 skipped (2 equivalent) (11) Worst at Pricing.discount() 1 ✗ Test runs 11 (27 without coverage selection) Start at 16:30:20 Duration 2.4s (setup 2.0s, coverage 253ms, mutants 149ms) Tests 3 on JDK 17 · 1 worker Reports build/mutastic/mutation.json · build/mutastic/mutation.html Score 88.9% threshold 80
PASS 1 missed, above the threshold. Next: mutastic verify <id> samples/pricing · mutastic run --only=<id> samples/pricing · mutastic explain <id> samples/pricing Missed ────────────────────────────────────────────────────────────
src/main/java/com/example/pricing/Pricing.java discount() 1 missed 17:15 ✗ no test checks discount() at total == 100 m855c8e3c
Classes 0 clean | 1 with misses (1) Mutants ✓ 8 caught · ✗ 1 missed · 2 skipped (2 equivalent) (11) Worst at Pricing.discount() 1 ✗ Test runs 11 (27 without coverage selection) Start at 16:30:23 Duration 2.4s (setup 2.1s, coverage 244ms, mutants 110ms) Tests 3 on JDK 17 · 1 worker Reports build/mutastic/mutation.json · build/mutastic/mutation.html Score 88.9% threshold 80
PASS 1 missed, above the threshold. Next: mutastic verify <id> samples/pricing · mutastic run --only=<id> samples/pricing · mutastic explain <id> samples/pricingmutastic: PASS · score 88.9% (threshold 80) · 1 missed, 0 not covered, 8 caught of 9 mutants (2 more skipped)rules: add or change tests only, never production code; a fix counts when `mutastic verify <id> samples/pricing passes`decline: if a mutant's case can't happen in practice, or the change is one no caller can see, don't contrive a test: say why, and propose ignoring it with that reason (and --as equivalent, unreachable, telemetry, deprecated or accepted-risk) for a person to accept (`mutastic ignore <id> --propose --reason="<why>" samples/pricing`)
1. missed m855c8e3c: no test checks `discount()` at `total == 100` at src/main/java/com/example/pricing/Pricing.java:17 - if (total >= 100) { + if (total > 100) { covered by: PricingTest#bigOrdersGetTenPercentOff, PricingTest#smallOrdersPayFullPrice; all still pass with the change evidence: every test calls `discount()` away from the boundary evidence: inputs seen: 50, 250 (none is 100) fix: test `discount()` with `total = 100` and assert the result test to add in PricingTest: @Test void discountWithTotal100() { assertEquals(10, new Pricing().discount(100)); // today 10 · with this mutant 0 } // check today's value is the one you intend: the test pins what the code does now done when: mutastic verify m855c8e3c samples/pricing passes reproduce: mutastic run --only=m855c8e3c samples/pricing or propose: mutastic ignore m855c8e3c --propose --reason="<why>" samples/pricing{"schema":"mutastic.events/v1","reason":"run-started","project":"pricing","rules":"add or change tests only, never production code","decline":"if a mutant's case can't happen in practice, or the change is one no caller can see, don't contrive a test: say why, and propose ignoring it with that reason (and --as equivalent, unreachable, telemetry, deprecated or accepted-risk) for a person to accept"}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"m855c8e3c","fingerprint":"855c8e3ca8a39847","status":"Survived","operator":"mutastic/boundary","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":17,"class":"com.example.pricing.Pricing","method":"discount(I)I"},"mutation":{"original":">=","replacement":">","description":"`>=` becomes `>`","bytecode":"IF_ICMPLT -> IF_ICMPLE"},"coveredBy":["com.example.pricing.PricingTest#bigOrdersGetTenPercentOff","com.example.pricing.PricingTest#smallOrdersPayFullPrice"],"inputs":{"total":[50,250]},"suggestedTest":{"testClass":"PricingTest","name":"discountWithTotal100","code":"@Test\nvoid discountWithTotal100() {\n assertEquals(10, new Pricing().discount(100)); // today 10 · with this mutant 0\n}","call":"new Pricing().discount(100)","today":"10","withMutant":"0"},"testsRun":2,"durationMs":12,"doneWhen":"mutastic verify m855c8e3c samples/pricing passes","reproduce":"mutastic run --only=m855c8e3c samples/pricing","ignore":"mutastic ignore m855c8e3c --reason=\"<why>\" samples/pricing","propose":"mutastic ignore m855c8e3c --propose --reason=\"<why>\" samples/pricing","rendered":"missed m855c8e3c: no test checks `discount()` at `total == 100`\n at src/main/java/com/example/pricing/Pricing.java:17\n - if (total >= 100) {\n + if (total > 100) {\n covered by: PricingTest#bigOrdersGetTenPercentOff, PricingTest#smallOrdersPayFullPrice; all still pass with the change\n evidence: every test calls `discount()` away from the boundary\n evidence: inputs seen: 50, 250 (none is 100)\n fix: test `discount()` with `total = 100` and assert the result\n test to add in PricingTest:\n @Test\n void discountWithTotal100() {\n assertEquals(10, new Pricing().discount(100)); // today 10 · with this mutant 0\n }\n // check today's value is the one you intend: the test pins what the code does now\n done when: mutastic verify m855c8e3c samples/pricing passes\n reproduce: mutastic run --only=m855c8e3c samples/pricing\n or propose: mutastic ignore m855c8e3c --propose --reason=\"<why>\" samples/pricing\n"}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"m53e1dc80","fingerprint":"53e1dc806373872b","status":"Killed","operator":"mutastic/remove-conditional","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":17,"class":"com.example.pricing.Pricing","method":"discount(I)I"},"mutation":{"original":">=","description":"`total >= 100` is always false","bytecode":"IF_ICMPLT -> always jump"},"coveredBy":["com.example.pricing.PricingTest#bigOrdersGetTenPercentOff","com.example.pricing.PricingTest#smallOrdersPayFullPrice"],"killedBy":"com.example.pricing.PricingTest#bigOrdersGetTenPercentOff","killedWith":"org.opentest4j.AssertionFailedError","failureOrigin":"assertion","inputs":{"total":[50,250]},"testsRun":1,"durationMs":25}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"mfb551430","fingerprint":"fb5514301b9d532a","status":"Killed","operator":"mutastic/remove-conditional","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":17,"class":"com.example.pricing.Pricing","method":"discount(I)I"},"mutation":{"original":">=","description":"`total >= 100` is always true","bytecode":"IF_ICMPLT -> never jump"},"coveredBy":["com.example.pricing.PricingTest#bigOrdersGetTenPercentOff","com.example.pricing.PricingTest#smallOrdersPayFullPrice"],"killedBy":"com.example.pricing.PricingTest#smallOrdersPayFullPrice","killedWith":"org.opentest4j.AssertionFailedError","failureOrigin":"assertion","inputs":{"total":[50,250]},"testsRun":2,"durationMs":11}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"mabd5e44c","fingerprint":"abd5e44c174dc289","status":"Killed","operator":"mutastic/math","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":18,"class":"com.example.pricing.Pricing","method":"discount(I)I"},"mutation":{"original":"/","replacement":"*","description":"`/` becomes `*`","bytecode":"IDIV -> IMUL"},"coveredBy":["com.example.pricing.PricingTest#bigOrdersGetTenPercentOff"],"killedBy":"com.example.pricing.PricingTest#bigOrdersGetTenPercentOff","killedWith":"org.opentest4j.AssertionFailedError","failureOrigin":"assertion","inputs":{"total":[250]},"testsRun":1,"durationMs":7}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"m1cbe1e8b","fingerprint":"1cbe1e8bede1d5c6","status":"Killed","operator":"mutastic/return-default","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":18,"class":"com.example.pricing.Pricing","method":"discount(I)I"},"mutation":{"replacement":"return 0;","description":"returns `0` instead","bytecode":"IRETURN -> return default"},"coveredBy":["com.example.pricing.PricingTest#bigOrdersGetTenPercentOff"],"killedBy":"com.example.pricing.PricingTest#bigOrdersGetTenPercentOff","killedWith":"org.opentest4j.AssertionFailedError","failureOrigin":"assertion","inputs":{"total":[250]},"testsRun":1,"durationMs":5}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"ma98e3f3e","fingerprint":"a98e3f3e4310886a","status":"Ignored","operator":"mutastic/return-default","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":20,"class":"com.example.pricing.Pricing","method":"discount(I)I"},"mutation":{"replacement":"return 0;","description":"returns `0` instead","bytecode":"IRETURN -> return default"},"statusReason":"equivalent: already returns the default","skipKind":"equivalent","coveredBy":[],"testsRun":0,"durationMs":0}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"m2de3396d","fingerprint":"2de3396dd2bf9472","status":"Killed","operator":"mutastic/boundary","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":25,"class":"com.example.pricing.Pricing","method":"shipping(I)I"},"mutation":{"original":"<","replacement":"<=","description":"`<` becomes `<=`","bytecode":"IF_ICMPGE -> IF_ICMPGT"},"coveredBy":["com.example.pricing.PricingTest#shippingIsFreeFromTheThreshold"],"killedBy":"com.example.pricing.PricingTest#shippingIsFreeFromTheThreshold","killedWith":"org.opentest4j.AssertionFailedError","failureOrigin":"assertion","inputs":{"total":[39,40]},"testsRun":1,"durationMs":8}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"m73f3386e","fingerprint":"73f3386e9057735e","status":"Killed","operator":"mutastic/remove-conditional","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":25,"class":"com.example.pricing.Pricing","method":"shipping(I)I"},"mutation":{"original":"<","description":"`total < freeShippingFrom` is always false","bytecode":"IF_ICMPGE -> always jump"},"coveredBy":["com.example.pricing.PricingTest#shippingIsFreeFromTheThreshold"],"killedBy":"com.example.pricing.PricingTest#shippingIsFreeFromTheThreshold","killedWith":"org.opentest4j.AssertionFailedError","failureOrigin":"assertion","inputs":{"total":[39,40]},"testsRun":1,"durationMs":6}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"m091729b1","fingerprint":"091729b183cefc89","status":"Killed","operator":"mutastic/remove-conditional","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":25,"class":"com.example.pricing.Pricing","method":"shipping(I)I"},"mutation":{"original":"<","description":"`total < freeShippingFrom` is always true","bytecode":"IF_ICMPGE -> never jump"},"coveredBy":["com.example.pricing.PricingTest#shippingIsFreeFromTheThreshold"],"killedBy":"com.example.pricing.PricingTest#shippingIsFreeFromTheThreshold","killedWith":"org.opentest4j.AssertionFailedError","failureOrigin":"assertion","inputs":{"total":[39,40]},"testsRun":1,"durationMs":6}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"m5342bee9","fingerprint":"5342bee987b8df21","status":"Killed","operator":"mutastic/return-default","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":26,"class":"com.example.pricing.Pricing","method":"shipping(I)I"},"mutation":{"replacement":"return 0;","description":"returns `0` instead","bytecode":"IRETURN -> return default"},"coveredBy":["com.example.pricing.PricingTest#shippingIsFreeFromTheThreshold"],"killedBy":"com.example.pricing.PricingTest#shippingIsFreeFromTheThreshold","killedWith":"org.opentest4j.AssertionFailedError","failureOrigin":"assertion","inputs":{"total":[39,40]},"testsRun":1,"durationMs":10}{"schema":"mutastic.events/v1","reason":"mutant-finished","id":"m9e438bef","fingerprint":"9e438bef951f2de2","status":"Ignored","operator":"mutastic/return-default","location":{"file":"src/main/java/com/example/pricing/Pricing.java","line":28,"class":"com.example.pricing.Pricing","method":"shipping(I)I"},"mutation":{"replacement":"return 0;","description":"returns `0` instead","bytecode":"IRETURN -> return default"},"statusReason":"equivalent: already returns the default","skipKind":"equivalent","coveredBy":[],"testsRun":0,"durationMs":0}{"schema":"mutastic.events/v1","reason":"run-finished","jdk":17,"mutants":11,"Killed":8,"Survived":1,"Timeout":0,"NoCoverage":0,"Ignored":2,"score":88.9,"threshold":80,"passed":true,"coveredScore":88.9,"tests":3,"flaky":[],"flakyEvidence":[],"likelyFlaky":[],"unrelatedCatches":[],"reports":["build/mutastic/mutation.json","build/mutastic/mutation.html"],"staleIgnores":[],"durationMs":400}- Cards are grouped by file, and in each file the method with the most misses comes first. A file shows at most seven cards, and counts the rest (
--each-mutantshows them all): more than a few at once go unread. On a terminal, file locations are links (--hyperlinks) that terminals supporting OSC 8 open; others show the text. - The agent format’s ten entries are those most worth a test first: a condition or a removal no test checks before arithmetic, and among those, code more tests run without checking.
- The agent format puts the verdict first, so a truncated read still has it, and the rule that a fix changes tests, never production code. It also says when not to write one: a mutant whose case can’t happen in practice, or whose change no caller can see, is declined with its reason and ignored, not given a contrived test.
MUTASTIC_FORMATsets the format when--formatisn’t given. While it runs, each source file gets a line as it finishes, on stderr, with its misses under it.--vocabulary=classic(orMUTASTIC_VOCABULARY=classic) says killed and survived instead of caught and missed. JSON and the report files always use the standard statuses.- Colour follows the usual rules: only on a terminal, never with
NO_COLORorTERM=dumb, always withFORCE_COLOR. Every colour has a symbol beside it (✓ caught, ✗ missed, ○ not covered, ⏱ timed out), so nothing is lost without it. Labels such asMISSEDandPASSare coloured blocks,[MISSED]without colour, and have rounded ends withMUTASTIC_PILLS=roundedfor terminals with a Nerd Font.MUTASTIC_PILLS=offdraws no blocks: the label’s words in its colour. jsonlwritesmutastic.events/v1events. Statuses follow the mutation-testing report schema:Killed,Survived,Timeout,NoCoverage.- A mutant’s id comes from its class, method, position and change, not its line number, so it stays the same while code around it moves.
Shared cards
Section titled “Shared cards”The mutants of one line share a card, and an entry in the agent format: one test there usually
catches them all, the edge of a condition its boundary and its removal alike. The card is written
about one of them, with its id after the headline, and lists each of the others under also, with
its id and the line as it changes. A suggested test says which mutant it was found for. The card’s
done when checks them all at once, so the test proves the whole fix; each is still counted, and
ignored, on its own.
In the suggestions sample, inCredit() misses both its boundary and the condition always being
true. A test at balance = 0 catches both:
✗ MISSED no test checks inCredit() at balance == 0 · md4db94d8 ┌─ src/main/java/com/example/rates/Rates.java:18 │ 17 │ public static boolean inCredit(int balance) { 18 │ return balance > 0; │ ^ changed to >= 19 │ } │ = mutant - return balance > 0; + return balance >= 0; = also ✗ no test needs balance > 0 to be false in inCredit() · m05382fc9 + return true; = note: every test calls inCredit() away from the boundary inputs seen: 50 (none is 0) covered by RatesTest#aPositiveBalanceIsInCredit (its 1 covering test still passes) = help: test inCredit() with balance = 0 and assert the result
// in RatesTest @Test void inCreditWithBalance0() { assertThat(Rates.inCredit(0)).isFalse(); // today false · with md4db94d8 true } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify md4db94d8,m05382fc9 samples/suggestions passes reproduce mutastic run --only=md4db94d8,m05382fc9 samples/suggestions or ignore mutastic ignore <id> --reason="<why>" samples/suggestions for each of md4db94d8, m05382fc9--each-mutant gives every missed mutant a card of its own.
Streams
Section titled “Streams”The result goes to stdout and progress to stderr, so mutastic run > result.txt keeps only the result.
The commands it offers
Section titled “The commands it offers”The commands mutastic prints for a mutant name the project it ran on (mutastic verify m855c8e3c samples/pricing), so they work as copied. A mutant’s id belongs to its project: run in a directory with no Java code, or one that isn’t a Gradle or Maven build, mutastic says so (for Gradle, it names the modules that have Java code). A Maven module is named by its directory, as -pl takes it: mutastic run . :services/api.
One mutant, explained
Section titled “One mutant, explained”mutastic explain ID [dir] [:module] runs that one mutant and shows its card, whatever became of
it, then what it is. The summary’s Next: line offers it, for the one-line formats. On the pricing
sample, after a run:
✗ MISSED no test checks discount() at total == 100 ┌─ src/main/java/com/example/pricing/Pricing.java:17 │ 16 │ public int discount(int total) { 17 │ if (total >= 100) { │ ^^ changed to > 18 │ return total / 10; │ = mutant - if (total >= 100) { + if (total > 100) { = note: every test calls discount() away from the boundary inputs seen: 50, 250 (none is 100) covered by PricingTest#bigOrdersGetTenPercentOff, PricingTest#smallOrdersPayFullPrice (all 2 covering tests still pass) = help: test discount() with total = 100 and assert the result
// in PricingTest @Test void discountWithTotal100() { assertEquals(10, new Pricing().discount(100)); // today 10 · with this mutant 0 } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify m855c8e3c samples/pricing passes reproduce mutastic run --only=m855c8e3c samples/pricing or ignore mutastic ignore m855c8e3c --reason="<why>" samples/pricing
About this mutant ─────────────────────────────────────────────── = bytecode IF_ICMPLT → IF_ICMPLE IF_ICMPLT: jump if the first int is less than the second IF_ICMPLE: jump if the first int is less than or equal to the second javac compiles `total >= 100` to IF_ICMPLT, a jump past the body when `total < 100`: the opposite of the source. With IF_ICMPLE it jumps when `total <= 100`, so the body runs only when `total > 100` = operator mutastic/boundary: `<` becomes `<=` and the other way round: the off-by-one at the edge of a range, which tests that only check values far from the edge never notice. = id m855c8e3c: `m` and the first 8 hex digits of its fingerprint, 855c8e3ca8a39847, a SHA-256 of the class, method, site and change no line number goes in, so the id stays the same when code above it moves = site instruction site 1 of discount(I)I, counting from 1 (arithmetic, branches and returns are counted in bytecode order; calls apart) = last run missed: every covering test still passedUnder About this mutant:
- bytecode: the change, opcode by opcode (
IF_ICMPLT → IF_ICMPLE), and how the jump relates to the source. javac compilesif (total >= 100)to a jump taken whentotal < 100, the opposite of what the source says, which is why a boundary mutant can look backwards in the bytecode. - operator: what its operator does, as on Operators.
- id: how the id is made, and why it stays the same when code above it moves.
- site: which of the method’s sites it is.
- last run: how the last full run went, or
none yet.