Skip to content
mutastic

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.

OperatorChangesSet
mutastic/math+ and -, * and /defaults
mutastic/arithmetic-replacement+ to *stronger
mutastic/other-arithmetican arithmetic operator to any of the others, on every number typestronger
mutastic/bitwise&, | and ^ to the other two; << and >>, >> and >>>stronger
mutastic/keep-operanda calculation to one of its values: a + b to a, or bstronger
mutastic/constant-valuea number to one next to it, or 0, 1 or -1stronger
mutastic/flip-incrementi++ and i--, i += 2 and i -= 2stronger
mutastic/remove-negation-x to xstronger
library/unchecked-arithmeticMath.addExact and the like to the plain operation, which overflowsstronger

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));
}
Terminal window
✗ 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, m46de907a

The 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.

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));
}
Terminal window
✗ 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-docs

The 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.

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));
}
Terminal window
✗ 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-docs

The 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.

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));
}
Terminal window
✗ 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-docs

The 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.

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));
}
Terminal window
✗ 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-docs

The 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.

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));
}
Terminal window
✗ 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-docs

The 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.

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));
}
Terminal window
✗ 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-docs

The 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.

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));
}
Terminal window
✗ 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-docs

The 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.

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));
}
Terminal window
✗ 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-docs

The 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.