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.
| Operator | Changes | Set |
|---|---|---|
mutastic/boundary | < and <=, > and >=; lo..hi and lo..=hi | defaults |
mutastic/remove-conditional | a condition to always true, and to always false | defaults |
mutastic/logical-connector | && and || | defaults |
mutastic/comparison | a comparison to the one that differs on one side: < to != | stronger |
mutastic/other-comparison | a comparison to the others; a condition to its opposite | stronger |
Each card below comes from mutastic run fixtures/operators with these operators, on
src/conditions.rs.
Boundary
Section titled “Boundary”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));} ✗ 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/operatorsThe 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}{ "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.
Remove conditional
Section titled “Remove conditional”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:
✗ 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/operatorsThe 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()}{ "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.
Logical connector
Section titled “Logical connector”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));} ✗ 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/operatorsThe 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.
Comparison
Section titled “Comparison”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:
| Written | Becomes | Differs where |
|---|---|---|
a < b | a != b | a > b |
a <= b | a == b | a < b |
a > b | a != b | a < b |
a >= b | a == b | a > b |
a == b | a <= b, and a >= b | a < b, and a > b |
a != b | a < b, and a > b | a > 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));} ✗ 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/operatorsThe 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.
Other comparison
Section titled “Other comparison”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}");} ✗ 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/operatorsThe 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.