Skip to content
mutastic

Rust edition

Conditions

Operators that change a comparison, a condition, or how two conditions combine. Together they ask whether your tests look at both sides of every decision, and at its edge.

OperatorChangesSet
mutastic/boundary< and <=, > and >=; lo..hi and lo..=hidefaults
mutastic/remove-conditionala condition to always true, and to always falsedefaults
mutastic/logical-connector&& and ||defaults
mutastic/comparisona comparison to the one that differs on one side: < to !=stronger
mutastic/other-comparisona comparison to the others; a condition to its oppositestronger

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

mutastic/boundary, in the defaults. < becomes <=, >= becomes >, and the other way round. In a for loop, a slice (&s[lo..hi]) or a contains, lo..hi becomes lo..=hi, and back. It finds the off-by-one at the edge of a range, which tests that only try values far from the edge never notice.

The bug it stands for: the off-by-one. >= and > read almost the same, and reviews rarely stop on them, so the wrong one gets written and gets through. It costs wherever a rule has a limit: free delivery from 50, a discount from 10 items, a lock after 3 failures, the last day a refund is allowed. Someone who spends exactly 50 because the site said “free delivery from 50” is the case the rule exists for.

/// Free delivery from 50 up.
pub fn free_delivery(total: u32) -> bool {
total >= 50
}

The test tries an order on each side, and neither at the edge:

#[test]
fn small_orders_pay_for_delivery() {
assert!(!free_delivery(20));
assert!(free_delivery(80));
}
Terminal window
✗ MISSED no test checks free_delivery() at total == 50
┌─ src/conditions.rs:7
│
6 │ pub fn free_delivery(total: u32) -> bool {
7 │ total >= 50
│ ^^ >= changed to >
8 │ }
│
= mutant - total >= 50
+ total > 50
= note: every test calls free_delivery() away from the boundary
inputs seen: 20, 80 (none is 50)
covered by conditions::tests::small_orders_pay_for_delivery (its 1 covering test still passes)
= help: test free_delivery() with total = 50 and assert the result
// in mod conditions::tests
#[test]
fn free_delivery_at_the_total_edge() {
assert!(free_delivery(50)); // 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 m27535e4c fixtures/operators passes
reproduce mutastic run --only=m27535e4c fixtures/operators

The test that catches it: one at the edge. Totals of 20 and 80 pass either way; only 50 tells >= from >. A boundary miss is also a question for whoever owns the rule: should 50 get free delivery? The test records the answer.

#[test]
fn delivery_is_free_from_exactly_50() {
assert!(free_delivery(50));
}

Where both sides give the same value at the edge, the boundary changes nothing, and is skipped as equivalent (equivalent: both sides give the same value at the edge), as in the Java edition: if x > max { max = x } stores at the edge what max already holds, if a < b { a } else { b } gives the same value either way, and so does if fee < 10 { 10 } else { fee }, and if x < 2 { x = 2 }. The comparison has to be the if’s whole condition and its branch just that one assignment, or one side in each branch; each side a whole number of a type the source declares, or a literal. A float runs: -0.0 equals 0.0 but isn’t the same value.

Inside code that runs only when a comparison’s two sides differ, they can’t be equal, so the change of its edge is skipped as equivalent: major > than_major inside if major != than_major, the else of an if a == b, after an if a == b that returns, or to the right of a != b &&. So are the other changes that differ only when the sides are equal, a == b made always false there, for example. Each side has to be a whole number that can’t change on the way: a parameter, a let or the x of a Some(x), without mut and bound once, a field of one, or a literal. A float, a type that’s inferred or generic, or a name a macro is given, and the mutants run.

pub fn is_newer(major: u32, minor: u32, than_major: u32, than_minor: u32) -> bool {
if major != than_major {
return major > than_major;
}
minor > than_minor
}
Terminal window
{
"schema": "mutastic.events/v1",
"edition": "rust",
"reason": "mutant-finished",
"id": "mbe0ed92f",
"fingerprint": "be0ed92fc9d72e1b",
"status": "Ignored",
"operator": "mutastic/boundary",
"docs": "https://docs.mutastic.io/rust/operators/conditions/#boundary",
"location": {
"file": "src/conditions.rs",
"line": 44,
"column": 22,
"class": "conditions",
"method": "is_newer"
},
"mutation": {
"original": ">",
"replacement": ">=",
"description": "`>` becomes `>=`",
"applied": {
"level": "source",
"text": "major > than_major -> (major) >= (than_major)"
}
},
"statusReason": "equivalent: differs only when major is than_major. That can't happen because the if major != than_major around it rules it out.",
"skipKind": "equivalent",
"coveredBy": [],
"testsRun": 0,
"durationMs": 0
}

Edition notes: the Java edition has this operator too, mutastic/boundary, for <, <=, > and >=; it has no ranges to move.

mutastic/remove-conditional, in the defaults. A condition stops deciding: the code it guards always runs, or never does. Each condition gets two mutants, one each way. It reaches if, while, a match arm’s guard, and let Some(x) = e in an if let, while let or let-else, where the pattern is made never to match. It goes unnoticed when no test needs the condition to be true one time and false another.

The bug it stands for: a check that doesn’t matter as much as its author thought, or that’s the wrong check entirely. It costs in guards: a permission check, a validation, an “only if it’s changed” before a write. If the code behaves the same with the guard always passing, no test has ever been refused by it; if it behaves the same with the guard never passing, no test has ever needed it to let something through.

pub fn greeting(member: bool) -> &'static str {
if member {
"welcome back"
} else {
"welcome"
}
}
#[test]
fn members_are_welcomed_back() {
assert_eq!(greeting(true), "welcome back");
}

if false makes the test fail, so that mutant is caught; if true gets past it:

Terminal window
✗ MISSED no test needs member to be false in greeting()
┌─ src/conditions.rs:16
│
15 │ pub fn greeting(member: bool) -> &'static str {
16 │ if member {
│ ^^^^^^ now always true
17 │ "welcome back"
│
= mutant - if member {
+ if true {
= note: covered by conditions::tests::members_are_welcomed_back (its 1 covering test still passes)
= help: add a test where member is false, and assert what greeting() does then
done when mutastic verify m62a2d487 fixtures/operators passes
reproduce mutastic run --only=m62a2d487 fixtures/operators

The test that catches it: one for each answer the condition can give. The card names the one no test has tried: here, someone who isn’t a member.

#[test]
fn others_are_welcomed() {
assert_eq!(greeting(false), "welcome");
}

A zero check on a NonZero* value is never true, so the mutant that makes it never true is skipped as equivalent: x.get() == 0 where x is a NonZeroU32, a NonZero<u64> or another of std’s, declared as a parameter, a let with its type written, or a field. Its other changes that differ only at 0 are skipped too, parts.get() == 0 becoming parts.get() <= 0 and != 0 becoming always true, for example. A signed NonZeroI32 can be below 0, so n.get() < 0 made never true still runs. Only a type the source names is taken as proof: where it’s inferred, generic, imported from elsewhere, or a name a macro is given, the mutants run.

pub fn share(total: u32, parts: NonZeroU32) -> u32 {
if parts.get() == 0 {
unreachable!("a NonZeroU32 is never 0");
}
total / parts.get()
}
Terminal window
{
"schema": "mutastic.events/v1",
"edition": "rust",
"reason": "mutant-finished",
"id": "m497f89e4",
"fingerprint": "497f89e437964e71",
"status": "Ignored",
"operator": "mutastic/remove-conditional",
"docs": "https://docs.mutastic.io/rust/operators/conditions/#remove-conditional",
"location": {
"file": "src/conditions.rs",
"line": 34,
"column": 8,
"class": "conditions",
"method": "share"
},
"mutation": {
"replacement": "false",
"description": "`parts.get() == 0` is always false",
"applied": {
"level": "source",
"text": "parts.get() == 0 -> false"
}
},
"statusReason": "equivalent: differs only when parts.get() is 0. That can't happen because parts is a NonZeroU32, which can't hold 0.",
"skipKind": "equivalent",
"coveredBy": [],
"testsRun": 0,
"durationMs": 0
}

A value that can’t be below 0 is the same: an unsigned one compared with 0 skips the changes that differ only below 0, n == 0 becoming n <= 0 and n > 0 becoming n != 0, while n != 0, the check made never true and the rest run. Only what the source proves counts: a name or a field whose type is written u8 to u128 or usize, a cast as one of them, a call of a function declared beside it to return one, the index a for loop takes from .enumerate(), a let from a literal with an unsigned suffix (let n = 5u32;), a whole-number count of a Duration the source names (d.as_secs(), d.subsec_nanos()), or arithmetic on those. An inferred or generic type, or a type of the project’s own with one of those names, and the mutants run.

cfg!(debug_assertions) is always true under mutastic, which builds with debug assertions on, as the test profile does, whatever the project’s own profiles say. So the mutant that forces it true is skipped as equivalent, alone, under ! or as one side of &&, and the one that forces it false runs: that’s the build without them, which the tests can tell apart. Only cfg!(debug_assertions) itself is taken: cfg!(all(debug_assertions, unix)), or a name it was stored in, and the mutants run.

Edition notes: the Java edition has this operator too, mutastic/remove-conditional, for every condition the bytecode jumps on, && and ||’s sides among them.

mutastic/logical-connector, in the defaults. && becomes ||, and || becomes &&. The two give different answers exactly when one side holds and the other doesn’t, so it goes unnoticed when no test has one side true and the other false.

The bug it stands for: && where || was meant, or the other way round: a rule that needs both conditions written as either, a discount for members or big orders written as and. It costs in eligibility and permission rules, and validation that should reject on any of several problems.

pub fn can_ship(paid: bool, in_stock: bool) -> bool {
paid && in_stock
}
#[test]
fn ships_when_paid_and_in_stock() {
assert!(can_ship(true, true));
assert!(!can_ship(false, false));
}
Terminal window
✗ MISSED no test checks can_ship() with one of paid and in_stock true and the other false
┌─ src/conditions.rs:24
│
23 │ pub fn can_ship(paid: bool, in_stock: bool) -> bool {
24 │ paid && in_stock
│ ^^ && changed to ||
25 │ }
│
= mutant - paid && in_stock
+ paid || in_stock
= note: covered by conditions::tests::ships_when_paid_and_in_stock (its 1 covering test still passes)
= help: add a test where one of paid and in_stock is true and the other false, and assert what can_ship() does then
done when mutastic verify m069e3be4 fixtures/operators passes
reproduce mutastic run --only=m069e3be4 fixtures/operators

The test that catches it: one where the two sides disagree, each way round: paid but out of stock, and in stock but unpaid.

#[test]
fn neither_alone_is_enough() {
assert!(!can_ship(true, false));
assert!(!can_ship(false, true));
}

Edition notes: the Java edition has this operator too, mutastic/logical-connector. It reads && from the bytecode’s jumps, so it leaves alone a condition of three or more tests, which this edition, reading the source, doesn’t.

mutastic/comparison, in the stronger set. A comparison becomes the one that gives the same answer at its edge and on one side of it, and the other answer on the other side:

WrittenBecomesDiffers where
a < ba != ba > b
a <= ba == ba < b
a > ba != ba < b
a >= ba == ba > b
a == ba <= b, and a >= ba < b, and a > b
a != ba < b, and a > ba > b, and a < b

The bug it stands for: a comparison that’s right at the edge and wrong past it: == where >= was meant, so a rule holds for exactly 100 and stops at 101. It costs in the same rules as a boundary, on the side tests skip: tests often try a value below a limit and one exactly at it, and never one well past it.

== and != get these only beside a number, a character, a length or a cast, where an order is plain from the code. With boundary and remove conditional, they’re the smallest set of changes that’s enough for a comparison: tests that catch the edge moved, the side changed, and the condition always true or false catch every other way it could be written wrong.

/// Time to restock once stock falls below the low mark.
pub fn needs_restock(stock: u32, low: u32) -> bool {
stock < low
}

The test tries stock below the mark and at it, so the boundary’s mutant is caught, but never above it:

#[test]
fn restock_when_low() {
assert!(needs_restock(2, 5));
assert!(!needs_restock(5, 5));
}
Terminal window
✗ MISSED no test checks needs_restock() with stock > low
┌─ src/conditions.rs:12
│
11 │ pub fn needs_restock(stock: u32, low: u32) -> bool {
12 │ stock < low
│ ^ < changed to !=
13 │ }
│
= mutant - stock < low
+ stock != low
= note: inputs seen: low = 5; stock = 2, 5
covered by conditions::tests::restock_when_low (its 1 covering test still passes)
= help: add a test where stock > low, and assert what needs_restock() does then
// in mod conditions::tests
#[test]
fn needs_restock_with_stock_5_and_low_2() {
assert!(!needs_restock(5, 2)); // today false · with this mutant true
}
check today's value is the one you intend: the test pins what the code does now
done when mutastic verify m0afcf739 fixtures/operators passes
reproduce mutastic run --only=m0afcf739 fixtures/operators

The test that catches it: one on the side where the two disagree, which the card names: here, stock above the low mark.

#[test]
fn plenty_in_stock_needs_no_restock() {
assert!(!needs_restock(8, 5));
}

It’s in stronger because on real code much of what it misses is noise: a loop counter that counts up to its end, where i < n and i != n stop at the same place.

Edition notes: the Java edition has this operator too, mutastic/comparison, with the same changes, and makes them only between numbers, as only numbers have an order in its bytecode.

mutastic/other-comparison, in the stronger set. A comparison becomes each of the others the two operators above don’t make (< becomes >, >= or ==), if c becomes if !c, and matches!(..) becomes !matches!(..). It goes unnoticed when no test checks one side of a decision at all. A test that catches boundary, comparison and remove conditional catches these too, so they’d only add to the score in the defaults.

The bug it stands for: a condition the wrong way round: a missing !, == for !=, a check that guards the wrong case. It costs where code decides which way to go: whether an order is paid before it ships, whether a user may see a page. Getting it backwards usually means doing the wrong thing in exactly the case that matters, and a permission check whose opposite no test notices is one nobody has proved refuses anyone.

/// Saturday is 6 and Sunday 7.
pub fn is_weekend(day: u8) -> bool {
matches!(day, 6 | 7)
}

This test runs the function and prints what it says, but checks nothing:

#[test]
fn weekend_days_are_known() {
let saturday = is_weekend(6);
println!("saturday: {saturday}");
}
Terminal window
✗ MISSED tests pass even when matches!(day, 6 | 7) becomes !matches!(day, 6 | 7) in is_weekend()
┌─ src/conditions.rs:29
│
28 │ pub fn is_weekend(day: u8) -> bool {
29 │ matches!(day, 6 | 7)
│ ^^^^^^^^^^^^^^^^^^^^ changed to !matches!(day, 6 | 7)
30 │ }
│
= mutant - matches!(day, 6 | 7)
+ !matches!(day, 6 | 7)
= note: inputs seen: day = 6
covered by conditions::tests::weekend_days_are_known (its 1 covering test still passes)
= help: use inputs where matches!(day, 6 | 7) and !matches!(day, 6 | 7) differ, and assert what is_weekend() does
// in mod conditions::tests
#[test]
fn is_weekend_with_day_6() {
assert!(is_weekend(6)); // 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 m4f55c224 fixtures/operators passes
reproduce mutastic run --only=m4f55c224 fixtures/operators

The test that catches it: one that checks the answer on each side: a weekend day, and a weekday.

#[test]
fn saturday_is_the_weekend_and_wednesday_isnt() {
assert!(is_weekend(6));
assert!(!is_weekend(3));
}

Edition notes: the Java edition has this operator too, mutastic/other-comparison, and turns a Predicate’s and and or, negate() and Predicate.not round with predicate/* operators of its own; here, && and ! inside a closure are logical connector’s and this one’s.