Java edition
Arithmetic
Operators that change a calculation: its operator, one of its values, a number in the code, or a bit. Together they ask whether your tests’ values are ones where getting the arithmetic wrong would show.
| Operator | Changes | Set |
|---|---|---|
mutastic/math | + and -, * and / | defaults |
mutastic/arithmetic-replacement | + to * | stronger |
mutastic/other-arithmetic | an arithmetic operator to any of the others, on every number type | stronger |
mutastic/bitwise | &, | and ^ to the other two; << and >>, >> and >>> | stronger |
mutastic/keep-operand | a calculation to one of its values: a + b to a, or b | stronger |
mutastic/constant-value | a number to one next to it, or 0, 1 or -1 | stronger |
mutastic/flip-increment | i++ and i--, i += 2 and i -= 2 | stronger |
mutastic/remove-negation | -x to x | stronger |
library/unchecked-arithmetic | Math.addExact and the like to the plain operation, which overflows | stronger |
Each example is a class in samples/operator-docs with a test that misses its mutant, and each card
is what mutastic run samples/operator-docs --operators=all prints for it.
mutastic/math, in the defaults. + becomes -, * becomes /, and back, on int and long.
The bug it stands for: the wrong operator in a calculation: - for +, / for *. It costs in
money, quantities and time: prices, tax, totals, stock levels, durations. A wrong calculation is
often silent: nothing crashes, the number is just wrong, and it’s found later on an invoice or in a
report, by a customer.
public static int total(int subtotal, int shipping) { return subtotal + shipping; }The test only tries free shipping, and adding 0 gives the same answer as taking it away:
@Test void freeShipping() { assertEquals(30, Checkout.total(30, 0)); } ✗ MISSED tests pass even when + becomes - in total() · macafa41b ┌─ src/main/java/com/example/arithmetic/Checkout.java:5 │ 4 │ public static int total(int subtotal, int shipping) { 5 │ return subtotal + shipping; │ ^ + changed to - 6 │ } │ = mutant - return subtotal + shipping; + return subtotal - shipping; = also ✗ no test needs shipping in subtotal + shipping in total() · m46de907a + return subtotal; = note: inputs seen: subtotal = 30; shipping = 0 covered by CheckoutTest#freeShipping (its 1 covering test still passes) = help: use inputs where subtotal + shipping and subtotal - shipping differ, and assert the result
// in CheckoutTest @Test void totalWithSubtotal30AndShipping30() { assertEquals(60, Checkout.total(30, 30)); // today 60 · with macafa41b 0 } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify macafa41b,m46de907a samples/operator-docs passes reproduce mutastic run --only=macafa41b,m46de907a samples/operator-docs or ignore mutastic ignore <id> --reason="<why>" samples/operator-docs for each of macafa41b, m46de907aThe test that catches it: one whose values tell the operators apart. Tests often use values that
don’t: a quantity of 1 gives the same answer for * and /, and adding 0 the same for + and
-. Those are exactly the values a quick test tends to pick.
@Test void shippingIsAdded() { assertEquals(35, Checkout.total(30, 5)); }Edition notes: the Rust edition has this operator too, mutastic/math, and swaps += and -=,
*= and /=, and the checked_, wrapping_, saturating_ and overflowing_ methods’ pairs too.
Arithmetic replacement
Section titled “Arithmetic replacement”mutastic/arithmetic-replacement, in the stronger set. + becomes *, on int and long.
The bug it stands for: one operation mixed up with another entirely: adding where it should
multiply, a total that should be a product. Its real value is as a check on the tests’ values: 2 + 2 == 2 * 2 and 0 + 0 == 0 * 0 are the numbers quick tests pick. It’s in the stronger set because
the values that let it through are rarer than math’s, so it finds less, but what it finds is a test
that checks almost nothing.
public static int seats(int adults, int children) { return adults + children; } @Test void aFamilyOfFour() { assertEquals(4, Tickets.seats(2, 2)); } ✗ MISSED tests pass even when + becomes * in seats() ┌─ src/main/java/com/example/arithmetic/Tickets.java:5 │ 4 │ public static int seats(int adults, int children) { 5 │ return adults + children; │ ^ + changed to * 6 │ } │ = mutant - return adults + children; + return adults * children; = note: inputs seen: adults = 2; children = 2 covered by TicketsTest#aFamilyOfFour (its 1 covering test still passes) = help: use inputs where adults + children and adults * children differ, and assert the result
// in TicketsTest @Test void seatsWithAdults2AndChildren0() { assertEquals(2, Tickets.seats(2, 0)); // today 2 · with this mutant 0 } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify m6048b95b samples/operator-docs passes reproduce mutastic run --only=m6048b95b samples/operator-docs or ignore mutastic ignore m6048b95b --reason="<why>" samples/operator-docsThe test that catches it: one with ordinary values, where the sum and the product are far apart.
@Test void twoAdultsAndThreeChildren() { assertEquals(5, Tickets.seats(2, 3)); }Edition notes: run it with --operators=stronger. The Rust edition has this operator too,
mutastic/arithmetic-replacement, and turns += into *= too, but not += 1.
Other arithmetic
Section titled “Other arithmetic”mutastic/other-arithmetic, in the stronger set. An arithmetic operator becomes each of the others
among +, -, *, / and %, on int, long, float and double, beyond the changes math and
arithmetic replacement make.
The bug it stands for: any operator mixed up with another: % for /, a - where a * belongs,
and the same on float and double, where the defaults don’t look. It costs in rates, ratios,
averages and paging, where the wrong operator gives a plausible number. It’s in the stronger set
for its cost: each operator has several changes.
/// Each person's share, in whole euros. public static int perPerson(int total, int people) { return total / people; }A bill of 10 shared by 4 is 2 each, and 2 is also what’s left over:
@Test void aBillIsShared() { assertEquals(2, Bill.perPerson(10, 4)); } ✗ MISSED tests pass even when / becomes % in perPerson() ┌─ src/main/java/com/example/arithmetic/Bill.java:6 │ 5 │ public static int perPerson(int total, int people) { 6 │ return total / people; │ ^ / changed to % 7 │ } │ = mutant - return total / people; + return total % people; = note: inputs seen: total = 10; people = 4 covered by BillTest#aBillIsShared (its 1 covering test still passes) = help: use inputs where total / people and total % people differ, and assert the result
// in BillTest @Test void perPersonWithTotal10AndPeople10() { assertEquals(1, Bill.perPerson(10, 10)); // today 1 · with this mutant 0 } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify mfced7dd2 samples/operator-docs passes reproduce mutastic run --only=mfced7dd2 samples/operator-docs or ignore mutastic ignore mfced7dd2 --reason="<why>" samples/operator-docsThe test that catches it: one with values that tell every operator apart, and neither 0, 1
nor two values the same.
@Test void aBillOf11SharedByFour() { assertEquals(2, Bill.perPerson(11, 4)); }Edition notes: run it with --operators=stronger. The Rust edition has this operator too,
mutastic/other-arithmetic, with the compound forms too.
Bitwise
Section titled “Bitwise”mutastic/bitwise, in the stronger set. &, | and ^ each become the other two, << and >>
swap, and so do >> and >>>, on int, long and boolean.
The bug it stands for: the wrong bit operation: setting a flag where it should be cleared,
toggling where it should be set. It costs in permissions, flags and packed fields, where tests use
bits that don’t overlap, and on those | and ^ agree.
/// Permissions as bits: 1 to read, 2 to write, 4 to delete.public final class Permissions { public static int grant(int held, int granted) { return held | granted; }} @Test void writeIsAddedToRead() { assertEquals(3, Permissions.grant(1, 2)); } ✗ MISSED tests pass even when | becomes ^ in grant() ┌─ src/main/java/com/example/arithmetic/Permissions.java:6 │ 5 │ public static int grant(int held, int granted) { 6 │ return held | granted; │ ^ | changed to ^ 7 │ } │ = mutant - return held | granted; + return held ^ granted; = note: inputs seen: held = 1; granted = 2 covered by PermissionsTest#writeIsAddedToRead (its 1 covering test still passes) = help: use inputs where held | granted and held ^ granted differ, and assert the result
// in PermissionsTest @Test void grantWithHeld1AndGranted1() { assertEquals(1, Permissions.grant(1, 1)); // today 1 · with this mutant 0 } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify me2065481 samples/operator-docs passes reproduce mutastic run --only=me2065481 samples/operator-docs or ignore mutastic ignore me2065481 --reason="<why>" samples/operator-docsThe test that catches it: one where the bits overlap: granting what’s already held keeps it.
@Test void grantingWhatsHeldKeepsIt() { assertEquals(3, Permissions.grant(3, 1)); }Edition notes: run it with --operators=stronger. ~x isn’t changed: the compiler writes it as
x ^ -1, which no one wrote as a ^. Setting or clearing bits, flags |= bit or flags &= ~bit,
never becomes flipping them with ^=. The Rust edition has this operator too, mutastic/bitwise.
Keep operand
Section titled “Keep operand”mutastic/keep-operand, in the stronger set. A calculation becomes one of its two values, and a
mutant is made for each.
The bug it stands for: a part of a calculation left out: the fee not added, the discount not taken off, the offset forgotten. It costs in totals of several parts, where tests set the one being looked at and leave the rest at values where leaving a part out gives the same answer.
public int slotAfter(int steps) { return steps % slots; }The test never goes round the wheel, so the % never does anything:
@Test void threeStepsOn() { assertEquals(3, new Wheel(10).slotAfter(3)); } ✗ MISSED no test needs slots in steps % slots in slotAfter() ┌─ src/main/java/com/example/arithmetic/Wheel.java:12 │ 11 │ public int slotAfter(int steps) { 12 │ return steps % slots; │ ^^^^^^^^^^^^^^^^^^^^^ one value kept 13 │ } │ = mutant - return steps % slots; + return steps; = note: inputs seen: steps = 3, with slots = 10 (Wheel's field, from new Wheel(10)) none has steps == slots covered by WheelTest#threeStepsOn (its 1 covering test still passes) = help: add a test where slots changes the result of steps % slots, and assert it
// in WheelTest @Test void slotAfterAtSlots() { var wheel = new Wheel(10); // as WheelTest#threeStepsOn builds it assertEquals(0, wheel.slotAfter(10)); // steps == slots · today 0 · with this mutant 10 } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify m7088ebb8 samples/operator-docs passes reproduce mutastic run --only=m7088ebb8 samples/operator-docs or ignore mutastic ignore m7088ebb8 --reason="<why>" samples/operator-docsThe test that catches it: one where each part has a value that changes the result: here, enough steps to go round.
@Test void twelveStepsGoRound() { assertEquals(2, new Wheel(10).slotAfter(12)); }Edition notes: run it with --operators=stronger. Both values are still worked out, so a call
in the other still runs. A constant beside a variable isn’t what’s kept: count + 1 becomes
count, not 1. The Rust edition has this operator too, mutastic/keep-operand.
Constant value
Section titled “Constant value”mutastic/constant-value, in the stronger set. A number in the code becomes one next to it, and
0, 1 and -1, each once.
The bug it stands for: a number set wrong: a threshold of 100 that should be 1000, a retry
limit one too high, a rate with a digit out. Nothing else shows these: the comparison is right, and
only its number is wrong. It costs in limits and rates held as static final constants, which tests
often reuse instead of stating: a test that says assertTrue(isLarge(LIMIT)) passes whatever LIMIT
is. It’s in stronger because every number has several changes, which makes it the costliest
arithmetic operator to run.
/// A parcel over 500 grams goes as heavy.public final class Postage { public static boolean isHeavy(int grams) { return grams > 500; }}The test tries the edge, so >= and 499 are caught, but nothing just above it:
@Test void heavyFromJustOver500() { assertFalse(Postage.isHeavy(100)); assertFalse(Postage.isHeavy(500)); assertTrue(Postage.isHeavy(1000)); } ✗ MISSED no test tells 500 from 501 in isHeavy() ┌─ src/main/java/com/example/arithmetic/Postage.java:6 │ 5 │ public static boolean isHeavy(int grams) { 6 │ return grams > 500; │ ^^^^^^^^^^^^^^^^^^^ 500 changed to 501 7 │ } │ = mutant - return grams > 500; + return grams > 501; = note: inputs seen: grams = 100, 500, 1000 covered by PostageTest#heavyFromJustOver500 (its 1 covering test still passes) = help: add a test near where 500 takes effect, whose result would differ with 501, and assert it
done when mutastic verify m783ff84d samples/operator-docs passes reproduce mutastic run --only=m783ff84d samples/operator-docs or ignore mutastic ignore m783ff84d --reason="<why>" samples/operator-docsThe test that catches it: one that states the number, with values next to it on both sides.
@Test void a501GramParcelIsHeavy() { assertTrue(Postage.isHeavy(501)); }Edition notes: run it with --operators=stronger. A static final number counts too: javac
writes its value where it’s used, so a threshold set wrong shows here and in no other operator, and
the card names the value, as the line may only have the name. 0 and 1 are changed only where
what uses them shows they’re numbers, as in the bytecode they could be false and true, and a
number that isn’t shown to be plain stays in the range of a byte, short or char. The size and
indexes of an array’s {…}, the -1 in ~x, and what javac stores in temporaries of its own have
no mutants. Some are skipped as arid: an array’s size, a capacity, a wait, a split’s limit, and a
variable’s first value of 0 or 1, where a counter or a total starts. The Rust edition has this
operator too, mutastic/constant-value, and gives -1 only where a number is visibly signed.
Flip increment
Section titled “Flip increment”mutastic/flip-increment, in the stronger set. A local variable’s step goes the other way: i++
becomes i--, and i += 2 becomes i -= 2.
The bug it stands for: a counter going the wrong way: -- for ++, a retry count that climbs
when it should fall, a position that steps back. It costs in loops and hand-written parsers, where a
position moves through text or an array, and in counters checked against a limit. A loop counter
that goes the wrong way usually fails fast or hangs; a count compared later doesn’t, and a limit
that’s never reached never shows it.
/// The first `max` of `words`, in order. public static List<String> upTo(List<String> words, int max) { var kept = new ArrayList<String>(); int count = 0; for (String word : words) { if (count == max) { break; } kept.add(word); count++; } return kept; }The test’s list is shorter than the limit, so the count is never compared with anything it reaches:
@Test void shortListsAreKeptWhole() { assertEquals(List.of("tea", "milk"), Words.upTo(List.of("tea", "milk"), 5)); } ✗ MISSED no test needs count to go up in upTo() ┌─ src/main/java/com/example/arithmetic/Words.java:16 │ 15 │ kept.add(word); 16 │ count++; │ ^^^^^^^^ now goes down 17 │ } │ = mutant - count++; + count--; = note: inputs seen: max = 5 covered by WordsTest#shortListsAreKeptWhole (its 1 covering test still passes) = help: add a test whose result depends on count going up here, and assert it
done when mutastic verify ma294e600 samples/operator-docs passes reproduce mutastic run --only=ma294e600 samples/operator-docs or ignore mutastic ignore ma294e600 --reason="<why>" samples/operator-docsThe test that catches it: one that depends on the value after the step: here, a list longer than the limit.
@Test void longListsAreCut() { assertEquals(List.of("tea", "milk"), Words.upTo(List.of("tea", "milk", "eggs"), 2)); }Edition notes: Java only: Rust has no ++, and += and -= are math’s there. Run it
with --operators=stronger. A step nothing reads afterwards is skipped as equivalent. The index
javac writes for an enhanced for isn’t one of these, as no source names it, and code without its
table of local variables has none. A loop counter that goes the wrong way either fails at once, on
an index out of range, or never ends, which takes a whole time limit to catch.
Remove negation
Section titled “Remove negation”mutastic/remove-negation, in the stronger set. A value’s minus sign is removed.
The bug it stands for: a missing sign: a refund that adds, a delta applied the wrong way round. It costs in money and coordinates, where a value and its opposite are both plausible, and a test that only uses zero can’t tell them apart.
/// Whether a balance is below what the account may go down to. public boolean overdrawn(int balance) { return balance < -overdraft; }The test only tries an account with no overdraft, and -0 is 0:
@Test void noOverdraft() { var account = new Account(0); assertTrue(account.overdrawn(-5)); assertFalse(account.overdrawn(0)); assertFalse(account.overdrawn(5)); } ✗ MISSED no test checks the sign of the negated value in overdrawn() ┌─ src/main/java/com/example/arithmetic/Account.java:12 │ 11 │ public boolean overdrawn(int balance) { 12 │ return balance < -overdraft; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ minus sign removed 13 │ } │ = mutant - return balance < -overdraft; + return balance < overdraft; = note: inputs seen: balance = -5, 0, 5 the edge is overdraft (Account's field, set by its constructor) covered by AccountTest#noOverdraft (its 1 covering test still passes) = help: add a test with a value that isn't zero, and assert the sign of the result
// in AccountTest @Test void overdrawnAtOverdraft() { int overdraft = -5; assertTrue(new Account(overdraft).overdrawn(overdraft)); // today true · with this mutant false } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify mf71d3780 samples/operator-docs passes reproduce mutastic run --only=mf71d3780 samples/operator-docs or ignore mutastic ignore mf71d3780 --reason="<why>" samples/operator-docsThe test that catches it: one with a value that isn’t zero, where the sign shows.
@Test void anOverdraftOf100() { assertFalse(new Account(100).overdrawn(-50)); }Edition notes: run it with --operators=stronger. A negative literal or a negated constant is
worked out by javac, so there’s no negation left in the code to remove. The Rust edition has this
operator too, mutastic/remove-negation.
Unchecked arithmetic
Section titled “Unchecked arithmetic”library/unchecked-arithmetic, in the stronger set. Math.addExact and the like become the plain
operation, which overflows instead of throwing.
The bug it stands for: arithmetic that overflows silently: a total that wraps round to a negative
number, because nothing checked that it fits. It costs in money and sizes, where someone chose
addExact because a wrong number is worse than an exception. If no test overflows, nothing shows
that the check is still there.
/// Fails rather than wrap round when the points don't fit. public static int add(int points, int more) { return Math.addExact(points, more); } @Test void pointsAddUp() { assertEquals(3, Points.add(1, 2)); } ✗ MISSED no test makes addExact overflow in add() ┌─ src/main/java/com/example/arithmetic/Points.java:6 │ 5 │ public static int add(int points, int more) { 6 │ return Math.addExact(points, more); │ ^^^^^^^^ no longer throws on overflow 7 │ } │ = mutant - return Math.addExact(points, more); + return points + more; = note: inputs seen: points = 1; more = 2 covered by PointsTest#pointsAddUp (its 1 covering test still passes) = help: add a test whose result doesn't fit its type, and assert it throws ArithmeticException
done when mutastic verify m9972224c samples/operator-docs passes reproduce mutastic run --only=m9972224c samples/operator-docs or ignore mutastic ignore m9972224c --reason="<why>" samples/operator-docsThe test that catches it: one whose result doesn’t fit the type, asserting the exception.
@Test void tooManyPointsAreRefused() { assertThrows(ArithmeticException.class, () -> Points.add(Integer.MAX_VALUE, 1)); }Edition notes: run it with --operators=stronger. It covers addExact, subtractExact,
multiplyExact, incrementExact, decrementExact, negateExact and toIntExact, on Math and
StrictMath. The Rust edition has this operator too, library/unchecked-arithmetic:
a.checked_add(b) becomes Some(a.wrapping_add(b)), and saturating_sub becomes wrapping_sub.