Current section
Files
Jump to
Current section
Files
mutare_decimal
README.md
README.md
# mutare_decimal
[](https://hex.pm/packages/mutare_decimal)
[](https://hexdocs.pm/mutare_decimal)
[](https://github.com/foxbenjaminfox/mutare_decimal/actions/workflows/ci.yml)
[](https://github.com/foxbenjaminfox/mutare_decimal/blob/master/LICENSE)
Mutation-testing mutators for [Decimal](https://hexdocs.pm/decimal), built as a
plugin for [Mutare](https://hexdocs.pm/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:
```elixir
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:
```elixir
# .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:
```elixir
[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:
```elixir
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](LICENSE).