Plugins
mutastic’s operators cover the language and the JDK’s streams. A plugin adds more, for the libraries and patterns your code uses. There are two kinds:
| Kind | File | Adds | Needs |
|---|---|---|---|
| A rules file | .yaml | Operators on calls: remove one, rename it, or call another method instead | Nothing but the file |
| A jar | .jar | Operators on any instruction, and filters | Java, built against io.mutastic:mutastic-api |
List each under plugins: in mutastic.yaml, relative to the project’s directory:
plugins: - mutastic/mutiny.yaml - build/libs/acme-mutastic.jarA plugin’s operators then run like mutastic’s own: each in the defaults set unless it says it’s
stronger, chosen by its id or namespace with --operators
(--operators=defaults,-mutiny), and reported, explained and ignored the same way. A plugin’s code
runs in mutastic’s own process, so only list plugins you trust.
Rules files
Section titled “Rules files”A rules file describes call operators as data: which calls to match, by the type that declares the method and the method’s name, and how to rewrite them. mutastic’s own stream operators are a rules file of exactly this shape. This one adds two operators for Mutiny, and mutastic’s own tests run it on a Quarkus app:
# Mutiny (Uni and Multi) operators, as a plugin: only data. List this file under `plugins:` in# mutastic.yaml.## Mutiny groups its operators (`multi.select().where(p)`), so removing `where` would leave a# MultiSelect where a Multi belongs. Instead the call becomes one on the same group that lets# everything through, or passes the failure on unchanged.- id: mutiny/remove-selection explanation: A selection on a Multi lets every item through, as if `where`, `distinct` or `first` weren't there. It goes unnoticed when no test has an item the selection must leave out. owners: io.smallrye.mutiny.groups.MultiSelect methods: where, distinct, first rewrite: call first(J) with Long.MAX_VALUE source: first(Long.MAX_VALUE) changed: selection removed headline: no test needs `.select().{name}(…)` in {method} headline.distinct: no test has duplicate items for `.select().distinct()` in {method} to remove headline.first: no test has more items than `.select().first(…)` in {method} lets through help: add a test with an item the selection must leave out, and assert it's gone
- id: mutiny/remove-recovery explanation: A recovery from failure is removed, so the failure goes on downstream. It goes unnoticed when no test makes the upstream fail. owners: io.smallrye.mutiny.groups.UniOnFailure, io.smallrye.mutiny.groups.MultiOnFailure methods: recoverWithItem, recoverWithNull, recoverWithUni, recoverWithMulti, recoverWithCompletion rewrite: call transform(Ljava/util/function/Function;) with java.util.function.Function.identity() source: transform(Function.identity()) changed: recovery removed, so the failure propagates headline: no test makes {method} fail, so `.onFailure().{name}(…)` is never needed help: add a test where the upstream fails, and assert what {method} gives insteadEach rule is a list item with these fields:
| Field | ||
|---|---|---|
id | required | namespace/name, lower case: how reports, --operators and ignores name it |
explanation | required | What it changes, and when a test misses that: shown by mutastic explain |
owners | required | The types whose calls it matches, in full: java.lang.String |
methods | required | The method names it matches |
rewrite | required | What the call becomes: see below |
headline | required | A missed mutant’s headline, the gap in the tests |
help | required | The test to write |
changed | What the line now does, in a code frame: stage removed | |
source | How a call rewrite reads in the source, for its label: first(Long.MAX_VALUE) | |
tier | defaults (unless it says) or stronger |
headline and help can have a version per method, headline.sorted, used for that method
instead. In all three wordings, {name} is the method called, {to} what a rename calls instead,
and {method} the method the call is in.
Rewrites
Section titled “Rewrites”| Rewrite | Does | For example |
|---|---|---|
remove | Drops the call and its arguments, and keeps the object it was called on as the result. Only for a method that returns its own type: a stream stage, a builder step, String.strip() | stream.filter(p).count() becomes stream.count() |
rename a->b, c->d | Calls another method of the same type with the same arguments; each a->b is one mutant | anyMatch->allMatch, anyMatch->noneMatch |
call name(ARGS) with A, B | Drops the arguments and calls another method on the same object, which must return the same type. ARGS are the method’s parameter types as the JVM writes them (J for long, Ljava/util/function/Function;) | call first(J) with Long.MAX_VALUE |
A call rewrite’s arguments are 0, 0L, true, false, null, Integer.MAX_VALUE,
Long.MAX_VALUE, or a static method with no arguments, written in full:
java.util.function.Function.identity().
A rewrite that doesn’t fit a call it meets, because the types don’t line up, isn’t run: the mutant
is skipped as unviable:, with the reason, and stays out of the score. A label, the replacement a
mutant shows, is part of its id, so once a rule is in use, change its rewrite and source only
knowing that its mutants get new ids.
Java plugins
Section titled “Java plugins”For an operator a rules file can’t describe, write it in Java: implement
io.mutastic.api.Mutator,
with the JDK’s java.lang.classfile API, which mutastic is built on. This one makes every
whole-number constant one more:
package acme;
import io.mutastic.api.Mutant;import io.mutastic.api.Mutator;import java.lang.classfile.CodeBuilder;import java.lang.classfile.Instruction;import java.lang.classfile.instruction.ConstantInstruction;import java.util.List;
public class ConstantPlusOne implements Mutator { public String id() { return "acme/constant-plus-one"; }
public String explanation() { return "A whole-number constant goes up by one."; }
public boolean onCalls() { return false; // every instruction is a site, not only calls }
public List<Change> changes(Site site) { return site.instruction() instanceof ConstantInstruction constant && constant.constantValue() instanceof Integer ? List.of(Change.of("plus one")) : List.of(); }
public void write(CodeBuilder builder, Instruction instruction, String label) { var value = (Integer) ((ConstantInstruction) instruction).constantValue(); builder.loadConstant(value + 1); }
public Words words(Mutant mutant) { return new Words( "constant plus one", "no test tells the constant in " + mutant.methodName() + "() from one more", "test at the constant itself, and assert the result"); }}changesis asked about each instruction, with the method and its code around it (site.code(),site.index()), and gives a label for each change it makes there, if any, and why one shouldn’t run (equivalent: …), if it shouldn’t.writeputs the changed instruction in its place. Afterwards the stack must hold what the original left; a change that doesn’t verify is skipped asunviable:, never run.wordsis what a missed mutant’s card says: the change, the headline and the test to write.onCallssays which sites the operator counts: calls, as mutastic’s call operators count them, or every instruction in the method.tierdefaults todefaults; returnTier.STRONGERto leave an operator out unless asked for.
Build it against io.mutastic:mutastic-api (compileOnly, or Maven’s provided: mutastic brings
it), on JDK 25, and name the class in the jar’s
META-INF/services/io.mutastic.api.Mutator:
acme.ConstantPlusOneLabels are part of mutant ids, as a rules file’s are: keep them once the operator is in use.
Filters
Section titled “Filters”A jar can also hold filters, which decide that a method’s mutants shouldn’t run:
io.mutastic.api.Filter,
named in META-INF/services/io.mutastic.api.Filter. Unlike operators, a plugin’s filters run only
when filters: in mutastic.yaml names them: see Configuration and
skipping.