Skip to content
mutastic

Rust edition

Control flow

Operators that change which arm of a match runs, or how a loop goes on. They ask whether your tests reach every arm with a value that matters, and every way out of a loop.

OperatorChangesSet
mutastic/remove-switch-arma match arm stops matching, so its values go to a later armstronger
mutastic/swap-breakbreak and continueextended

Each card below comes from mutastic run fixtures/operators with these operators, on src/control.rs.

mutastic/remove-switch-arm, in the stronger set (extended in the Java edition). An arm stops matching, so its values go on to the next arm that matches. mutastic does it with a guard the switch decides, and only for an arm that comes before an unguarded catch-all, so there’s always an arm for the values to go to, and not for an arm that binds a variable. A test reaches the mutant only when it passes a value that arm matches.

The bug it stands for: a case that’s missing or never worked, so its values get another arm’s behaviour: the wrong rate, the wrong message. Tests tend to try one or two arms, and the others can be wrong for a long time, as the catch-all gives an answer that looks fine.

pub fn delivery_days(zone: u8) -> u32 {
match zone {
1 => 1,
2 => 3,
_ => 7,
}
}

One test checks zones 1 and 9 exactly. Another reaches zone 2, but only checks that it’s within a week, as the catch-all’s 7 is too:

#[test]
fn every_zone_within_a_week() {
for zone in 1..=3 {
assert!(delivery_days(zone) <= 7);
}
}
Terminal window
✗ MISSED tests pass even when delivery_days() has no arm for 2
┌─ src/control.rs:6
│
5 │ 1 => 1,
6 │ 2 => 3,
│ ^ arm removed
7 │ _ => 7,
│
= mutant - 2 => 3,
= note: inputs seen: zone = 1, 2, 3
covered by control::tests::every_zone_within_a_week (its 1 covering test still passes)
= help: add a test where delivery_days() takes the 2 arm, and assert what it does there
// in mod control::tests
#[test]
fn delivery_days_with_zone_2() {
assert_eq!(delivery_days(2), 3); // today 3 · with this mutant 7
}
check today's value is the one you intend: the test pins what the code does now
done when mutastic verify mc9468457 fixtures/operators passes
reproduce mutastic run --only=mc9468457 fixtures/operators

The test that catches it: one with a value that reaches the arm, asserting what it does.

#[test]
fn zone_two_takes_three_days() {
assert_eq!(delivery_days(2), 3);
}

Edition notes: the Java edition has this operator too, mutastic/remove-switch-arm, in extended, for a switch, whose arm’s values go to the default.

mutastic/swap-break, in the extended set, and only in Rust. break becomes continue, and continue becomes break, where it has no label and no value. It goes unnoticed when the loop ends the same way on every input the tests try.

The bug it stands for: a loop that stops where it should go on, or goes on where it should stop: the first match wanted and the last one found. Input with one match can’t tell them apart.

pub fn first_negative(values: &[i32]) -> Option<usize> {
let mut found = None;
for (index, value) in values.iter().enumerate() {
if *value < 0 {
found = Some(index);
break;
}
}
found
}
#[test]
fn finds_a_negative() {
assert_eq!(first_negative(&[4, -1, 2]), Some(1));
}

With continue, the loop finds the last negative, not the first, and with one negative there’s no difference:

Terminal window
✗ MISSED tests pass even when break becomes continue in first_negative()
┌─ src/control.rs:16
│
15 │ found = Some(index);
16 │ break;
│ ^^^^^ break changed to continue
17 │ }
│
= mutant - break;
+ continue;
= note: covered by control::tests::finds_a_negative (its 1 covering test still passes)
= help: use inputs where break and continue give different results, and assert what first_negative() does
done when mutastic verify mde47e51e fixtures/operators passes
reproduce mutastic run --only=mde47e51e fixtures/operators

The test that catches it: one with a second match after the first.

#[test]
fn the_first_negative_of_two() {
assert_eq!(first_negative(&[4, -1, -2]), Some(1));
}

Edition notes: Rust only.