Rust edition
Iterators
Operators that change an iterator chain: a stage left out, or one consumer swapped for its opposite. They ask whether your tests would notice the chain doing less, or answering the other question.
| Operator | Changes | Set |
|---|---|---|
stream/swap-match | any and all | defaults |
stream/remove-stage | .filter(p), .skip(n), .take(n), .skip_while(p), .take_while(p), .step_by(n) or .rev(), left out | defaults |
stream/remove-other-stage | .inspect(f) or .map(f), left out | extended |
The ids are the Java edition’s, where these are stream operators. Each card below comes from
mutastic run fixtures/operators with these operators, on src/iterators.rs.
Swap match
Section titled “Swap match”stream/swap-match, in the defaults. it.any(p) becomes it.all(p), and the other way round.
With one item, or items that all agree, the two give the same answer.
The bug it stands for: “any” where “all” was meant, or the other way round. They read alike, and with the inputs tests usually pick, all of them matching or none, they give the same answer. It costs in checks over collections: whether any item is late, whether all approvers have signed.
/// Whether any invoice is more than 30 days late.pub fn any_overdue(days_late: &[u32]) -> bool { days_late.iter().any(|days| *days > 30)}#[test]fn one_late_invoice() { assert!(any_overdue(&[45]));} ✗ MISSED tests pass even when any becomes all in any_overdue() ┌─ src/iterators.rs:5 │ 4 │ pub fn any_overdue(days_late: &[u32]) -> bool { 5 │ days_late.iter().any(|days| *days > 30) │ ^^^ any changed to all 6 │ } │ = mutant - days_late.iter().any(|days| *days > 30) + days_late.iter().all(|days| *days > 30) = note: covered by iterators::tests::one_late_invoice (its 1 covering test still passes) = help: use inputs where any and all give different results, and assert what any_overdue() does
done when mutastic verify m2f11fb5d fixtures/operators passes reproduce mutastic run --only=m2f11fb5d fixtures/operatorsThe test that catches it: one with a mix: some elements that match and some that don’t.
#[test]fn one_late_invoice_among_others() { assert!(any_overdue(&[10, 45]));}Edition notes: the Java edition has this operator too, stream/swap-match, for a stream’s
anyMatch, allMatch and noneMatch.
Remove stage
Section titled “Remove stage”stream/remove-stage, in the defaults. A stage is left out of a chain that plainly starts as an
iterator, up to its consumer or the for loop it feeds: .filter(p), .skip(n), .take(n),
.skip_while(p), .take_while(p), .step_by(n) and .rev(). Not .take(n) on an iterator that
never ends, which would only hang, nor .rev() before a consumer that ignores order, such as
sum.
The bug it stands for: a stage that’s wrong, or in the wrong place, and no test would know: a
filter with the wrong condition, a take that’s off by one, a skip someone forgot. Chains are
where business rules hide in one line: only the paid orders, the first ten results.
/// The total of the orders that are paid.pub fn paid_total(orders: &[(u32, bool)]) -> u32 { orders .iter() .filter(|(_, paid)| *paid) .map(|(total, _)| total) .sum()}#[test]fn a_paid_order() { assert_eq!(paid_total(&[(10, true)]), 10);}No test has an unpaid order to leave out:
✗ MISSED tests pass even without .filter(|(_, paid)| *paid) in paid_total() ┌─ src/iterators.rs:12 │ 11 │ .iter() 12 │ .filter(|(_, paid)| *paid) │ ^^^^^^ left out 13 │ .map(|(total, _)| total) │ = mutant - orders - .iter() - .filter(|(_, paid)| *paid) - .map(|(total, _)| total) - .sum() + orders + .iter() + .map(|(total, _)| total) + .sum() = note: covered by iterators::tests::a_paid_order (its 1 covering test still passes) = help: use inputs that .filter(|(_, paid)| *paid) makes a difference to, and assert what paid_total() does
done when mutastic verify ma6f92819 fixtures/operators passes reproduce mutastic run --only=ma6f92819 fixtures/operatorsThe test that catches it: one whose input the stage has to change: here, an unpaid order the filter must drop.
#[test]fn unpaid_orders_dont_count() { assert_eq!(paid_total(&[(10, true), (5, false)]), 10);}Edition notes: the Java edition has this operator too, stream/remove-stage, for a stream’s
filter, distinct, sorted, limit, skip, dropWhile and takeWhile.
Remove other stage
Section titled “Remove other stage”stream/remove-other-stage, in the extended set. .inspect(f) or .map(f) is left out of an
iterator chain, as remove stage leaves out its stages. Leaving out a map usually changes the
item’s type, and the build withdraws those mutants: in paid_total above, the map that picks the
total is withdrawn.
The bug it stands for: a conversion that’s wrong, or forgotten, and an inspect that does real
work someone relies on.
/// The total, with each value written to `log` as it's added.pub fn logged_total(values: &[u32], log: &mut Vec<u32>) -> u32 { values.iter().inspect(|value| log.push(**value)).sum()}#[test]fn totals_add_up() { let mut log = Vec::new(); assert_eq!(logged_total(&[1, 2], &mut log), 3);} ✗ MISSED tests pass even without .inspect(|value| log.push(**value)) in logged_total() ┌─ src/iterators.rs:19 │ 18 │ pub fn logged_total(values: &[u32], log: &mut Vec<u32>) -> u32 { 19 │ values.iter().inspect(|value| log.push(**value)).sum() │ ^^^^^^^ left out 20 │ } │ = mutant - values.iter().inspect(|value| log.push(**value)).sum() + values.iter().sum() = note: covered by iterators::tests::totals_add_up (its 1 covering test still passes) = help: use inputs that .inspect(|value| log.push(**value)) makes a difference to, and assert what logged_total() does
done when mutastic verify m23f62e2d fixtures/operators passes reproduce mutastic run --only=m23f62e2d fixtures/operatorsThe test that catches it: one that checks what the stage does.
#[test]fn each_value_is_logged() { let mut log = Vec::new(); logged_total(&[1, 2], &mut log); assert_eq!(log, vec![1, 2]);}Edition notes: the Java edition has this operator too, stream/remove-other-stage, for a
stream’s peek and a primitive stream’s map.