mutare_decimal
Mutation-testing mutators for Decimal, built as a plugin for Mutare.
In money and precision code, tests may miss a sub where an add belongs,
a lt? that should be lte?, or an incorrect rounding mode. Mutare's built-in
mutators treat Decimal.add(a, b) as an ordinary remote call. This plugin adds
Decimal-specific mutations — a swapped operation, a reversed comparison, a
dropped rounding refinement — to test whether those changes cause test failures.
A surviving mutant can indicate a missing assertion.
Install
Add it (and Mutare) as dev/test dependencies:
def deps do
[
{:mutare, "~> 0.4.1", only: [:dev, :test], runtime: false},
{:mutare_decimal, "~> 0.2", only: [:dev, :test], runtime: false}
]
end
(Decimal itself is not a dependency of this plugin — the mutators match calls purely syntactically. Your own project already supplies Decimal.)
Enable
List the families in .mutare.exs, alongside Mutare's built-ins:
# .mutare.exs
[mutators: [:builtins] ++ Mutare.Decimal.all()]
This configuration keeps Mutare's default families and adds the Decimal ones. Each Decimal family is reported separately and can be enabled on its own:
[mutators: [:builtins, Mutare.Decimal.Comparison]] # just the comparison swaps
Then run Mutare as usual:
mix mutare
The families
Mutare.Decimal.all/0 returns eleven mutator families:
| Family | Name | Mutation |
|---|---|---|
Mutare.Decimal.Arithmetic |
:decimal_arithmetic |
swaps same-arity arithmetic calls (add/sub/mult/div/div_int/rem) and reverses the operands of the non-commutative ones |
Mutare.Decimal.Comparison |
:decimal_comparison |
swaps comparison predicates (gt?/gte?/lt?/lte?), swaps min/max, and reverses operands of compare/2,3 and the deprecated cmp/2 |
Mutare.Decimal.Classification |
:decimal_classification |
swaps classification predicates (positive? ↔ negative?, nan? ↔ inf?) |
Mutare.Decimal.ZeroBoundary |
:decimal_zero_boundary |
replaces positive?/negative? with the inclusive zero-boundary comparison (gte?/lte? against zero), testing whether zero is handled distinctly |
Mutare.Decimal.Sign |
:decimal_sign |
swaps abs/1 ↔ negate/1 and flips literal signs in Decimal.new/3 |
Mutare.Decimal.DivRem |
:decimal_div_rem |
rebuilds div_rem/2 with the quotient/remainder pair in the wrong tuple order |
Mutare.Decimal.Transform |
:decimal_transform |
removes value-transforming calls (normalize, apply_context, abs, negate, sqrt, round/1,2,3), collapsing to the argument |
Mutare.Decimal.ModeSwap |
:decimal_mode_swap |
swaps round/3 rounding modes and to_string/2,3 output formats for another valid mode or format |
Mutare.Decimal.DefaultDrop |
:decimal_default_drop |
drops refinement/default arguments (round/2,3, compare/3, eq?/3), falling back to Decimal's defaults |
Mutare.Decimal.LimitsDrop |
:decimal_limits_drop |
drops parsing/rendering limit arguments (parse/2, cast/2, new/2, to_string/2,3) |
Mutare.Decimal.Context |
:decimal_context |
removes Decimal.Context.with/set/update effects |
The mutators match calls written qualified (Decimal.add(a, b)), aliased,
imported, or as a pipe stage; a pipe stage gets the same mutants as the direct
call it stands for (a |> Decimal.sub(b) is reversed like Decimal.sub(a, b)).
Replacements use valid Decimal operations.
To avoid duplicate mutants across these families and Mutare's built-ins,
the mutation rules exclude swaps equivalent to another family's argument drop
and drops equivalent to a core literal mutation. Each family's module docs
describe these exclusions.
Ignoring one kind of mutant
Every family declares ignore-variant labels — the target operation, target
mode, dropped argument, or operation kind — so a # mutare:ignore[family:label]
directive can suppress one kind of mutant while retaining the family's other mutations:
Decimal.mult(price, tax) # mutare:ignore[decimal_arithmetic:div]
Decimal.compare(left, right) # mutare:ignore[decimal_comparison:operands]
Decimal.parse(input, opts) # mutare:ignore[decimal_limits_drop:parse_options]
Decimal.round(total, 2, :half_up) # mutare:ignore[decimal_mode_swap:half_even]
Decimal.positive?(amount) # mutare:ignore[decimal_zero_boundary:gte?]
Decimal.normalize(amount) # mutare:ignore[decimal_transform:normalize]
Decimal.negate(amount) # mutare:ignore[decimal_sign:abs]
Decimal.Context.set(context) # mutare:ignore[decimal_context:set]
A bare # mutare:ignore[decimal_arithmetic] suppresses the whole family on
that line. See Mutare.Ignore for the directive grammar.
Supported versions
Decimal >= 2.2.0 and < 4.0.0. Matching is syntactic, so calls that only
exist in one line (e.g. to_string/3) simply never occur in codebases on the
other — every call the mutators emit exists across the whole range.
Development
mix deps.get
mix test # unit diffs + cross-family duplicate checks + variant-label contract
mix check # format + credo + dialyzer
License
MIT — see LICENSE.