Skip to content
mutastic

Rust edition

Arguments

Operators that change what a function is given, or what a value goes through on its way: a call left out, arguments swapped or dropped, a parameter replaced. They ask whether your tests would notice a value arriving wrong.

OperatorChangesSet
mutastic/omit-calla call that returns what it’s given, left out: s.trim() to sstronger
mutastic/swap-argumentsa call’s last two arguments of one type, swappedstronger
mutastic/null-parameteran Option parameter, None from the start of the functionstronger
mutastic/drop-varargthe last value of an array or vec! passed to a call, left outextended
mutastic/default-parametera parameter, its type’s default from the start of the functionextended

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

mutastic/omit-call, in the stronger set. A call that gives back the kind of value it’s given is left out: s.trim() becomes s, x.abs() becomes x, and the same for to_lowercase, round and std’s other such methods; a builder’s (self) -> Self step; and a crate function that returns its one argument’s type, escape(text) becoming text. Some of these change the type, as to_lowercase turns a &str into a String; the build withdraws those as unviable.

The bug it stands for: the step that was never written: the trim that isn’t on a pasted value, the escape that isn’t around user input. The code reads as finished without it, and on clean input, which is what tests tend to have, it makes no difference.

pub fn username(input: &str) -> String {
input.trim().to_lowercase()
}
#[test]
fn usernames_are_lowercase() {
assert_eq!(username("Ann"), "ann");
}

No test gives a name with spaces round it:

Terminal window
✗ MISSED tests pass even without .trim() in username()
┌─ src/arguments.rs:4
│
3 │ pub fn username(input: &str) -> String {
4 │ input.trim().to_lowercase()
│ ^^^^ left out
5 │ }
│
= mutant - input.trim().to_lowercase()
+ input.to_lowercase()
= note: covered by arguments::tests::usernames_are_lowercase (its 1 covering test still passes)
= help: use inputs that .trim() makes a difference to, and assert what username() does
done when mutastic verify m3b69ecd2 fixtures/operators passes
reproduce mutastic run --only=m3b69ecd2 fixtures/operators

The test that catches it: one with a value the call changes: here, spaces to trim.

#[test]
fn spaces_are_not_part_of_a_username() {
assert_eq!(username(" Ann "), "ann");
}

Edition notes: the Java edition has this operator too, mutastic/omit-call, for String’s methods, Math.abs and your own.

mutastic/swap-arguments, in the stronger set. A call to a crate function or method whose last two parameters have one type gets those two arguments swapped: transfer(amount, from, to) becomes transfer(amount, to, from).

The bug it stands for: two arguments the wrong way round. When they have the same type the compiler has nothing to say, and a test whose values give the same answer either way has nothing to say either. It costs in anything with a direction: a range’s start and end, a transfer’s source and destination.

pub fn between(value: i32, low: i32, high: i32) -> bool {
low <= value && value <= high
}
pub fn is_percent(value: i32) -> bool {
between(value, 0, 100)
}
#[test]
fn too_much_is_not_a_percent() {
assert!(!is_percent(150));
}

With the range turned round, nothing is a percent, and 150 still isn’t:

Terminal window
✗ MISSED tests pass even when 0, 100 becomes 100, 0 in is_percent()
┌─ src/arguments.rs:12
│
11 │ pub fn is_percent(value: i32) -> bool {
12 │ between(value, 0, 100)
│ ^^^^^^ 0, 100 changed to 100, 0
13 │ }
│
= mutant - between(value, 0, 100)
+ between(value, 100, 0)
= note: inputs seen: value = 150
covered by arguments::tests::too_much_is_not_a_percent (its 1 covering test still passes)
= help: use inputs where 0, 100 and 100, 0 give different results, and assert what is_percent() does
// in mod arguments::tests
#[test]
fn is_percent_when_value_is_zero() {
assert!(is_percent(0)); // 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 m1a3c85a8 fixtures/operators passes
reproduce mutastic run --only=m1a3c85a8 fixtures/operators

The test that catches it: one that passes values whose order shows: here, a percentage that is one.

#[test]
fn fifty_is_a_percent() {
assert!(is_percent(50));
}

Edition notes: the Java edition has this operator too, mutastic/swap-arguments, for any call’s last two arguments of one type.

mutastic/null-parameter, in the stronger set. A parameter of type Option<T> is set to None as the function starts, as if every caller passed None.

The bug it stands for: a value that’s handed in and doesn’t matter as it should: a parameter ignored on one path. It costs in code with many inputs where tests pin only some.

pub fn price_with(base: u32, coupon: Option<u32>) -> u32 {
base - coupon.unwrap_or(0)
}
#[test]
fn full_price_without_a_coupon() {
assert_eq!(price_with(10, None), 10);
}
Terminal window
✗ MISSED tests pass even when coupon is None from the start of price_with()
┌─ src/arguments.rs:23
│
22 │
23 │ pub fn price_with(base: u32, coupon: Option<u32>) -> u32 {
│ ^^^^^^ now None
24 │ base - coupon.unwrap_or(0)
│
= mutant - pub fn price_with(base: u32, coupon: Option<u32>) -> u32 {
- base - coupon.unwrap_or(0)
- }
+ pub fn price_with(base: u32, coupon: Option<u32>) -> u32 { let coupon: Option<u32> = None;
+ base - coupon.unwrap_or(0)
+ }
= note: inputs seen: base = 10
covered by arguments::tests::full_price_without_a_coupon (its 1 covering test still passes)
= help: add a test where the coupon given changes what price_with() does, and assert it
done when mutastic verify m51d65a09 fixtures/operators passes
reproduce mutastic run --only=m51d65a09 fixtures/operators

The test that catches it: one whose result depends on the parameter: here, a coupon.

#[test]
fn a_coupon_comes_off() {
assert_eq!(price_with(10, Some(3)), 7);
}

Edition notes: the Java edition has this operator too, mutastic/null-parameter, for any parameter holding an object, which becomes null.

mutastic/drop-vararg, in the extended set. The last value of an array or a vec! built for a call is left out: f(&[a, b, c]) becomes f(&[a, b]), and f(vec![a, b, c]) becomes f(vec![a, b]). Where the length is part of the type, the build withdraws the mutant, which is why it’s in extended.

The bug it stands for: a value left out of a list: the role not in the set, the last column not in the query. It costs in lists built in one expression, where one element’s absence shows only for inputs that need it.

pub fn largest(values: &[u32]) -> u32 {
values.iter().copied().max().unwrap_or(0)
}
pub fn best_of_three(a: u32, b: u32, c: u32) -> u32 {
largest(&[a, b, c])
}
#[test]
fn the_best_comes_first() {
assert_eq!(best_of_three(9, 2, 1), 9);
}
Terminal window
✗ MISSED tests pass even without c in best_of_three()
┌─ src/arguments.rs:20
│
19 │ pub fn best_of_three(a: u32, b: u32, c: u32) -> u32 {
20 │ largest(&[a, b, c])
│ ^^^ left out
21 │ }
│
= mutant - largest(&[a, b, c])
+ largest(&[a, b])
= note: inputs seen: a = 9; b = 2; c = 1
covered by arguments::tests::the_best_comes_first (its 1 covering test still passes)
= help: use inputs that c makes a difference to, and assert what best_of_three() does
// in mod arguments::tests
#[test]
fn best_of_three_with_a_9_and_b_9_and_c_10() {
assert_eq!(best_of_three(9, 9, 10), 10); // today 10 · with this mutant 9
}
check today's value is the one you intend: the test pins what the code does now
done when mutastic verify m6bcfe468 fixtures/operators passes
reproduce mutastic run --only=m6bcfe468 fixtures/operators

The test that catches it: one that depends on the last value, not only on the others.

#[test]
fn the_best_comes_last() {
assert_eq!(best_of_three(1, 2, 9), 9);
}

Edition notes: the Java edition has this operator too, mutastic/drop-vararg, in stronger, for an array built for a varargs call.

mutastic/default-parameter, in the extended set, and only in Rust. A parameter is set to its type’s Default::default() as the function starts: for std’s numbers, bool, char, String and collections, and for a type the crate plainly derives or implements Default for. An Option is null parameter’s.

The bug it stands for: a value that’s handed in and makes no difference: a flag ignored, an option the function never reads on the path tests take.

pub fn greet(name: String, excited: bool) -> String {
if excited {
format!("Hi {name}!")
} else {
format!("Hi {name}")
}
}
#[test]
fn a_calm_greeting() {
assert_eq!(greet("Ann".into(), false), "Hi Ann");
}

name as an empty string is caught. excited as false isn’t, as the only test passes false:

Terminal window
✗ MISSED tests pass even when excited is Default::default() from the start of greet()
┌─ src/arguments.rs:27
│
26 │
27 │ pub fn greet(name: String, excited: bool) -> String {
│ ^^^^^^^ now Default::default()
28 │ if excited {
│
= mutant - pub fn greet(name: String, excited: bool) -> String {
- if excited {
- format!("Hi {name}!")
- } else {
- format!("Hi {name}")
- }
- …
+ pub fn greet(name: String, excited: bool) -> String { let excited: bool = Default::default();
+ if excited {
+ format!("Hi {name}!")
+ } else {
+ format!("Hi {name}")
+ }
+ …
= note: covered by arguments::tests::a_calm_greeting (its 1 covering test still passes)
= help: add a test where the excited given changes what greet() does, and assert it
done when mutastic verify m68aafb6e fixtures/operators passes
reproduce mutastic run --only=m68aafb6e fixtures/operators

The test that catches it: one where the value given changes the result.

#[test]
fn an_excited_greeting() {
assert_eq!(greet("Ann".into(), true), "Hi Ann!");
}

Edition notes: Rust only. The Java edition’s nearest is null parameter.