Mutalisk mutates your source, then runs your tests against each mutated version. A mutant that your tests catch is killed; one they don't is survived — a gap in your test suite.
Mutators are split into two tiers. Default-on mutators run with a bare
MIX_ENV=test mix mut. Opt-in mutators run only when you ask for them; they are either
noisier (more equivalent/false-positive mutants) or niche.
Targets and mutator names are independent gates.
--mutatorspicks which mutators to consider;--enablereplaces the enabled target families. If a mutator's target is not enabled by default, pass both, for example--enable module_attribute --mutators attribute_literal. If the target is already enabled by default (dispatch,guard,env_walker,pattern_shape), naming the mutator is enough. Enabling a target without naming a mutator runs the default selectable mutator set unlocked by that target; a few experimental mutators still require--mutatorsexplicitly.
Default-on
These focus on common faults while limiting equivalent or invalid mutants.
| Mutator | What it does |
|---|---|
Arithmetic | swaps + - * / (e.g. a + b → a - b) |
ComparisonBoundary | off-by-one on comparisons (< → <=, > → >=, …) |
ComparisonNegation | negates comparisons (== → !=, < → >=, …) |
Boolean | swaps boolean operators (and ↔ or, && ↔ ||) |
UnaryNot | drops ! / not on boolean expressions |
GuardComparisonBoundary | comparison-boundary, inside when guards |
GuardComparisonNegation | comparison-negation, inside when guards |
GuardTypeTest | swaps guard type tests (is_integer → is_float, …) |
AtomLiteral | rotates known atoms (:ok ↔ :error, :lt/:gt/:eq) |
IntegerLiteral | integer-literal change in pattern position (1 → 0) |
ConcatOperator | ++ → -- on list concatenation |
Pin | unpins a pin in a pattern (^x → x) |
FunctionReplace | closed-allowlist stdlib swaps (Enum.min↔max, filter↔reject, List.first↔last, String.starts_with?↔ends_with?, …) |
Opt-in
Reach for these when you want a deeper check and can tolerate more
equivalent/noise mutants. Enable a whole non-default target with
--enable <target>, or run just one mutator with --mutators <name> plus
--enable <target> when that target is not enabled by default. Names are
comma-separated and use the snake_case module name, e.g.
negate_conditional.
| Mutator | Target | What it does |
|---|---|---|
AttributeLiteral | module_attribute | mutate module-attribute literal values |
IntegerLiteral (body) / BooleanLiteral | body_literal | integer/boolean literals in function bodies |
AtomLiteral / BooleanLiteral / StringLiteral / NilLiteral (pattern) | pattern_literal | opt-in pattern-position literal mutations; IntegerLiteral in patterns is already default-on |
StringLiteral | env_walker | "" / "x" string-literal swaps |
FloatLiteral | env_walker | float-literal change |
NilLiteral | env_walker | nil-literal swaps |
CollectionEmpty | env_walker | empty a list / 2-tuple / map / n-tuple (never a struct) |
VariableReplace | variable | swap a variable reference for another in scope |
VariableToLiteral | variable | replace a typed variable reference with a boundary literal; requires explicit --mutators variable_to_literal |
BitwiseOperator | dispatch | bitwise op swaps (band/bor/bsl/…) |
Membership | dispatch | in → not in |
NegateConditional | conditional | if/unless condition: negate (!cond) / force true / force false |
StatementDelete | statement_delete | delete a non-last statement from a function body |
ClauseDelete | clause_delete | delete a clause from case / cond / with / receive / try |
GuardBoolean | guard_boolean | and ↔ or, drop not, inside when guards |
PipelineDropStage | pipeline_drop | drop a middle stage from a |> chain |
MapUpdateDrop | map_update_drop | %{m | k: v} → m (drop the update) |
ReceiveTimeout | receive_timeout | mutate receive … after t (t → 0 / :infinity / drop) |
Examples:
# run the body-literal target only
MIX_ENV=test mix mut --enable body_literal
# run only NegateConditional + ClauseDelete
MIX_ENV=test mix mut --enable conditional,clause_delete \
--mutators negate_conditional,clause_deleteWhy some mutators are opt-in
Literal, deletion, and structural mutations can produce more equivalent mutants: changes that no test can distinguish because the program behaves the same. Enable them when their extra depth is useful, then assess survivors against the intended behaviour of your code.