mutare_decimal

Mutation-testing mutators for Decimal, built as a plugin for Mutare.

Money and precision code is exactly where a wrong operation hides best: a sub where an add belongs, a lt? that should be lte?, a rounding mode nobody pinned. Mutare's built-in mutators see Decimal.add(a, b) as just another remote call; this plugin knows the Decimal API and mints well-formed-but-wrong Decimal programs — a swapped operation, a reversed comparison, a dropped rounding refinement — so a surviving mutant points at a precise assertion your suite is missing.

Install

Add it (and Mutare) as dev/test dependencies:

def deps do
[
{:mutare, "~> 0.1", only: [:dev, :test], runtime: false},
{:mutare_decimal, "~> 0.1", 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()]

:builtins keeps Mutare's default families and adds the Decimal ones; each records under its own report family, and any 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:

FamilyNameMutation
Mutare.Decimal.Arithmetic:decimal_arithmeticswaps same-arity arithmetic calls (add/sub/mult/div/div_int/rem) and reverses visible operands of the non-commutative ones
Mutare.Decimal.Comparison:decimal_comparisonswaps 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_classificationswaps classification predicates (positive?negative?, nan?inf?)
Mutare.Decimal.ZeroBoundary:decimal_zero_boundaryreplaces positive?/negative? with the inclusive zero-boundary comparison (gte?/lte? against zero) — the off-by-the-boundary question
Mutare.Decimal.Sign:decimal_signswaps abs/1negate/1 and flips literal signs in Decimal.new/3
Mutare.Decimal.DivRem:decimal_div_remrebuilds div_rem/2 with the quotient/remainder pair in the wrong tuple order
Mutare.Decimal.Transform:decimal_transformremoves value-transforming calls (normalize, apply_context, abs, negate, sqrt, round/1,2,3), collapsing to the argument
Mutare.Decimal.ModeSwap:decimal_mode_swapswaps round/3 rounding modes and to_string/2,3 output formats for a valid sibling
Mutare.Decimal.DefaultDrop:decimal_default_dropdrops refinement/default arguments (round/2,3, compare/3, eq?/3), falling back to Decimal's defaults
Mutare.Decimal.LimitsDrop:decimal_limits_dropdrops parsing/rendering limit arguments (parse/2, cast/2, new/2, to_string/2,3)
Mutare.Decimal.Context:decimal_contextremoves Decimal.Context.with/set/update effects

Calls match written qualified (Decimal.add(a, b)), aliased, imported, or as a pipe stage. Every replacement is itself a valid Decimal call, so a survivor means a genuine missing assertion rather than a crash — and the families carry ownership splits (with each other and with Mutare's built-ins) so no two families mint the same mutant: a swap whose result another family's drop — or a core literal mutation — already produces is excluded. The split rules live in each family's moduledoc.

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 without silencing the family:

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 ownership + variant-label contract
mix check # format + credo + dialyzer

License

MIT — see LICENSE.