Skip to content

Commit 992572e

Browse files
authored
feat: add --mode and --operators flags to mutator and testing tool (#116)
* feat: add `--mode` and `--operators` flags to mutation testing tools (#115) * fix: realign operators in the different --mode and add new ones (#117) * fix: clippy errors * fix: assert macro treated as an operator * fix: failing ci
1 parent 28743dd commit 992572e

File tree

13 files changed

+767
-34
lines changed

13 files changed

+767
-34
lines changed

move-mutation-test/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,41 @@ If the user wants to mutate only the `sum` function in the `Sum` module, the use
161161
RUST_LOG=info ./target/release/move-mutation-test run --package-dir move-mutator/tests/move-assets/simple --output report.txt --move-2 --mutate-functions sum --mutate-modules Sum
162162
./target/release/move-mutation-test display-report coverage --path-to-report report.txt --modules Sum
163163
```
164+
------------------------------------------------------------------------------------------------------------
165+
To optimize mutation testing by selecting operators based on their ability to [detect test coverage gaps](../move-mutator/doc/design.md#operator-effectiveness-analysis), use the `--mode` option. Operators that produce more surviving mutants are more effective at revealing gaps in test coverage, as surviving mutants indicate untested code paths.
166+
167+
```bash
168+
# Light mode - operators optimized for detecting test gaps with fewest mutants
169+
RUST_LOG=info ./target/release/move-mutation-test run --package-dir move-mutator/tests/move-assets/simple --output report.txt --mode light
170+
171+
# Medium mode - light + additional operators for broader test gap detection
172+
RUST_LOG=info ./target/release/move-mutation-test run --package-dir move-mutator/tests/move-assets/simple --output report.txt --mode medium
173+
174+
# Medium-only mode - only the operator added in medium (not including light)
175+
RUST_LOG=info ./target/release/move-mutation-test run --package-dir move-mutator/tests/move-assets/simple --output report.txt --mode medium-only
176+
177+
# Heavy mode - default, all operators for maximum test gap detection
178+
RUST_LOG=info ./target/release/move-mutation-test run --package-dir move-mutator/tests/move-assets/simple --output report.txt --mode heavy
179+
180+
# Heavy-only mode - only the operators added in heavy (not including light/medium)
181+
RUST_LOG=info ./target/release/move-mutation-test run --package-dir move-mutator/tests/move-assets/simple --output report.txt --mode heavy-only
182+
```
183+
184+
The modes include:
185+
- **light**: `binary_operator_swap`, `break_continue_replacement`, `delete_statement` (3 operators)
186+
- **medium**: light + `literal_replacement` (4 operators)
187+
- **medium-only**: `literal_replacement` (1 operator - only what's added in medium)
188+
- **heavy**: all 7 operators
189+
- **heavy-only**: `unary_operator_replacement`, `binary_operator_replacement`, `if_else_replacement` (3 operators - only what's added in heavy)
190+
191+
------------------------------------------------------------------------------------------------------------
192+
For fine-grained control over which operators to apply, use the `--operators` option with a comma-separated list:
193+
```bash
194+
RUST_LOG=info ./target/release/move-mutation-test run --package-dir move-mutator/tests/move-assets/simple --output report.txt --operators delete_statement,binary_operator_replacement,if_else_replacement
195+
```
196+
197+
Available operators: `unary_operator_replacement`, `delete_statement`, `break_continue_replacement`, `binary_operator_replacement`, `if_else_replacement`, `literal_replacement`, `binary_operator_swap`.
198+
199+
**Note:** The `--mode` and `--operators` options are mutually exclusive.
164200
165201
[nextest]: https://github.com/nextest-rs/nextest

move-mutation-test/src/cli.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use aptos::common::types::MovePackageOptions;
66
use aptos_framework::extended_checks;
77
use clap::Parser;
88
use move_model::metadata::{CompilerVersion, LanguageVersion};
9-
use move_mutator::cli::{FunctionFilter, ModuleFilter};
9+
use move_mutator::cli::{FunctionFilter, ModuleFilter, OperatorModeArg};
1010
use move_package::CompilerConfig;
1111
use std::path::PathBuf;
1212

@@ -42,6 +42,33 @@ pub struct CLIOptions {
4242
/// Remove averagely given percentage of mutants. See the doc for more details.
4343
#[clap(long, conflicts_with = "use_generated_mutants")]
4444
pub downsampling_ratio_percentage: Option<usize>,
45+
46+
/// Mutation operator mode to balance speed and test gap detection.
47+
///
48+
/// - light: binary_operator_swap, break_continue_replacement, delete_statement
49+
/// - medium: light + literal_replacement
50+
/// - medium-only: literal_replacement (only what's added in medium)
51+
/// - heavy (default): all 7 operators
52+
/// - heavy-only: unary_operator_replacement, binary_operator_replacement, if_else_replacement (only what's added in heavy)
53+
#[clap(
54+
long,
55+
value_enum,
56+
conflicts_with = "operators",
57+
conflicts_with = "use_generated_mutants"
58+
)]
59+
pub mode: Option<OperatorModeArg>,
60+
61+
/// Custom operator selection to run mutations on (comma-separated).
62+
///
63+
/// Available operators: unary_operator_replacement, delete_statement, break_continue_replacement, binary_operator_replacement, if_else_replacement, literal_replacement, binary_operator_swap
64+
#[clap(
65+
long,
66+
value_parser,
67+
value_delimiter = ',',
68+
conflicts_with = "mode",
69+
conflicts_with = "use_generated_mutants"
70+
)]
71+
pub operators: Option<Vec<String>>,
4572
}
4673

4774
/// This function creates a mutator CLI options from the given mutation-test options.
@@ -57,6 +84,8 @@ pub fn create_mutator_options(
5784
apply_coverage,
5885
// To run tests, compilation must succeed
5986
verify_mutants: true,
87+
mode: options.mode,
88+
operators: options.operators.clone(),
6089
..Default::default()
6190
}
6291
}

move-mutator/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,42 @@ directory. They can be used to check the mutator tool as well.
107107
To check possible options, use the `--help` option.
108108

109109
[nextest]: https://github.com/nextest-rs/nextest
110+
111+
### Operator modes
112+
113+
The mutator tool supports different operator modes to control which mutation operators are applied. Modes are designed to balance speed and the ability to detect test gaps. Operators that produce more surviving mutants are more effective at revealing gaps in test coverage, as surviving mutants indicate untested code paths.
114+
115+
Use the `--mode` option to select a predefined operator set:
116+
```bash
117+
# Light mode: operators optimized for detecting test gaps with fewest mutants
118+
./target/release/move-mutator --package-dir move-mutator/tests/move-assets/simple/ --mode light
119+
120+
# Medium mode: light + additional operators for broader test gap detection
121+
./target/release/move-mutator --package-dir move-mutator/tests/move-assets/simple/ --mode medium
122+
123+
# Medium-only mode: only the operator added in medium (not including light)
124+
./target/release/move-mutator --package-dir move-mutator/tests/move-assets/simple/ --mode medium-only
125+
126+
# Heavy mode (default): all available operators for maximum test gap detection
127+
./target/release/move-mutator --package-dir move-mutator/tests/move-assets/simple/ --mode heavy
128+
129+
# Heavy-only mode: only the operators added in heavy (not including light/medium)
130+
./target/release/move-mutator --package-dir move-mutator/tests/move-assets/simple/ --mode heavy-only
131+
```
132+
133+
The operator modes are based on [effectiveness analysis](doc/design.md#operator-effectiveness-analysis) where effectiveness measures the ability to detect test coverage gaps:
134+
- **light**: `binary_operator_swap`, `break_continue_replacement`, `delete_statement` (3 operators)
135+
- **medium**: light + `literal_replacement` (4 operators)
136+
- **medium-only**: `literal_replacement` (1 operator - only what's added in medium)
137+
- **heavy**: all 7 operators
138+
- **heavy-only**: `unary_operator_replacement`, `binary_operator_replacement`, `if_else_replacement` (3 operators - only what's added in heavy)
139+
140+
For fine-grained control, use the `--operators` option to specify exactly which operators to apply:
141+
```bash
142+
# Apply only specific operators
143+
./target/release/move-mutator --package-dir move-mutator/tests/move-assets/simple/ --operators delete_statement,binary_operator_replacement,if_else_replacement
144+
```
145+
146+
Available operators: `unary_operator_replacement`, `delete_statement`, `break_continue_replacement`, `binary_operator_replacement`, `if_else_replacement`, `literal_replacement`, `binary_operator_swap`.
147+
148+
**Note:** The `--mode` and `--operators` options are mutually exclusive.

move-mutator/doc/design.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,70 @@ inefficient. Once mutation places are identified, mutants are generated in
205205
reversed order (based on localization) to avoid that. Tools are ready to be
206206
extended to support the operator mixing, if needed.
207207

208+
### Operator filtering
209+
210+
The Move mutator tool supports operator filtering to control which mutation
211+
operators are applied during the mutation process. This feature allows users to
212+
focus on specific operators or use predefined modes based on [operator
213+
effectiveness](#operator-effectiveness-analysis).
214+
215+
Modes are designed to balance speed and the ability to detect test gaps. Operators
216+
that produce more surviving mutants are more effective at revealing gaps in test
217+
coverage, as surviving mutants indicate untested code paths.
218+
219+
Five predefined modes are available:
220+
- **Light mode**: `binary_operator_swap`, `break_continue_replacement`, `delete_statement` (3 operators)
221+
- **Medium mode**: Light + `literal_replacement` (4 operators)
222+
- **Medium-only mode**: `literal_replacement` (1 operator - only what's added in medium)
223+
- **Heavy mode** (default): All 7 available operators
224+
- **Heavy-only mode**: `unary_operator_replacement`, `binary_operator_replacement`, `if_else_replacement` (3 operators - only what's added in heavy)
225+
226+
Users can also specify custom operator sets using the `--operators` CLI option,
227+
providing a comma-separated list of operator names. This allows for fine-grained
228+
control over which operators are applied.
229+
230+
Operator filtering is performed during AST traversal in the `mutate.rs` module.
231+
When a potential mutation site is found, the tool checks if the corresponding
232+
operator is enabled in the current mode before creating the mutant. This approach
233+
prevents unnecessary mutant generation, making the process more efficient.
234+
235+
#### Operator effectiveness analysis
236+
237+
The data below was calculated by running the tool on the largest projects in
238+
[Aptos' Move Framework](https://github.com/aptos-labs/aptos-core/tree/main/aptos-move/framework),
239+
testing 22,597 mutants with an overall kill rate of 82.02%.
240+
241+
The "Kill Rate" column shows the percentage of mutants killed by tests. While a
242+
high kill rate indicates good test coverage, operators with **lower kill rates**
243+
(more surviving mutants) are often **more effective at detecting test gaps**, as
244+
surviving mutants reveal untested code paths. This is valuable for identifying
245+
weaknesses in test suites.
246+
247+
```
248+
╭──────┬─────────────────────────────┬────────┬────────┬───────────────┬───────────╮
249+
│ Rank │ Operator │ Tested │ Killed │ Kill Rate │ Survived │
250+
├──────┼─────────────────────────────┼────────┼────────┼───────────────┼───────────┤
251+
│ #1 │ unary_operator_replacement │ 219 │ 219 │ 100.00% │ 0/219 │
252+
├──────┼─────────────────────────────┼────────┼────────┼───────────────┼───────────┤
253+
│ #2 │ delete_statement │ 909 │ 895 │ 98.46% │ 14/909 │
254+
├──────┼─────────────────────────────┼────────┼────────┼───────────────┼───────────┤
255+
│ #3 │ break_continue_replacement │ 26 │ 23 │ 88.46% │ 3/26 │
256+
├──────┼─────────────────────────────┼────────┼────────┼───────────────┼───────────┤
257+
│ #4 │ binary_operator_replacement │ 7081 │ 6207 │ 87.66% │ 874/7081 │
258+
├──────┼─────────────────────────────┼────────┼────────┼───────────────┼───────────┤
259+
│ #5 │ if_else_replacement │ 5310 │ 4579 │ 86.23% │ 731/5310 │
260+
├──────┼─────────────────────────────┼────────┼────────┼───────────────┼───────────┤
261+
│ #6 │ literal_replacement │ 8781 │ 6498 │ 74.00% │ 2283/8781 │
262+
├──────┼─────────────────────────────┼────────┼────────┼───────────────┼───────────┤
263+
│ #7 │ binary_operator_swap │ 271 │ 114 │ 42.07% │ 157/271 │
264+
╰──────┴─────────────────────────────┴────────┴────────┴───────────────┴───────────╯
265+
```
266+
267+
The predefined operator modes balance speed with test gap detection capability:
268+
- **Light mode**: Operators with lower kill rates that efficiently reveal test gaps (3 operators)
269+
- **Medium mode**: Light + operators that generate more comprehensive test coverage analysis (4 operators)
270+
- **Heavy mode**: All operators for maximum test gap detection (7 operators)
271+
208272
The Move mutator tool implements the following mutation operators.
209273

210274
### Binary operator replacement

move-mutator/src/cli.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22
// Copyright © Aptos Foundation
33
// SPDX-License-Identifier: Apache-2.0
44

5-
use clap::Parser;
5+
use clap::{Parser, ValueEnum};
66
use std::{path::PathBuf, str::FromStr};
77

88
pub const DEFAULT_OUTPUT_DIR: &str = "mutants_output";
99

10+
/// Mutation operator mode
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
12+
pub enum OperatorModeArg {
13+
Light,
14+
Medium,
15+
MediumOnly,
16+
Heavy,
17+
HeavyOnly,
18+
}
19+
1020
/// Command line options for mutator
1121
#[derive(Parser, Debug, Clone)]
1222
pub struct CLIOptions {
@@ -41,6 +51,22 @@ pub struct CLIOptions {
4151
/// Use the unit test coverage report to generate mutants for source code with unit test coverage.
4252
#[clap(long = "coverage", conflicts_with = "move_sources")]
4353
pub apply_coverage: bool,
54+
55+
/// Mutation operator mode to balance speed and test gap detection.
56+
///
57+
/// - light: binary_operator_swap, break_continue_replacement, delete_statement
58+
/// - medium: light + literal_replacement
59+
/// - medium-only: literal_replacement (only what's added in medium)
60+
/// - heavy (default): all 7 operators
61+
/// - heavy-only: unary_operator_replacement, binary_operator_replacement, if_else_replacement (only what's added in heavy)
62+
#[clap(long, value_enum, conflicts_with = "operators")]
63+
pub mode: Option<OperatorModeArg>,
64+
65+
/// Custom operator selection to run mutations on (comma-separated).
66+
///
67+
/// Available operators: unary_operator_replacement, delete_statement, break_continue_replacement, binary_operator_replacement, if_else_replacement, literal_replacement, binary_operator_swap
68+
#[clap(long, value_parser, value_delimiter = ',', conflicts_with = "mode")]
69+
pub operators: Option<Vec<String>>,
4470
}
4571

4672
/// Checker for conflicts with CLI arguments.
@@ -82,6 +108,8 @@ impl Default for CLIOptions {
82108
no_overwrite: false,
83109
apply_coverage: false,
84110
downsampling_ratio_percentage: None,
111+
mode: None,
112+
operators: None,
85113
}
86114
}
87115
}

move-mutator/src/configuration.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
// Copyright © Aptos Foundation
33
// SPDX-License-Identifier: Apache-2.0
44

5-
use crate::{cli::CLIOptions, coverage::Coverage};
5+
use crate::{
6+
cli::{CLIOptions, OperatorModeArg},
7+
coverage::Coverage,
8+
operator_filter::OperatorMode,
9+
};
610
use std::path::PathBuf;
711

812
/// Mutator configuration for the Move project.
@@ -14,17 +18,49 @@ pub struct Configuration {
1418
pub project_path: Option<PathBuf>,
1519
/// Coverage report where the optional unit test coverage data is stored.
1620
pub(crate) coverage: Coverage,
21+
/// Operator filter that determines which mutation operators are enabled.
22+
pub operator_mode: OperatorMode,
1723
}
1824

1925
impl Configuration {
2026
/// Creates a new configuration using command line options.
21-
#[must_use]
22-
pub fn new(project: CLIOptions, project_path: Option<PathBuf>) -> Self {
23-
Self {
27+
pub fn new(project: CLIOptions, project_path: Option<PathBuf>) -> anyhow::Result<Self> {
28+
// Parse and validate the operator mode from CLI options
29+
let operator_mode = Self::parse_operator_mode(&project)?;
30+
31+
Ok(Self {
2432
project,
2533
project_path,
2634
// Coverage is disabled by default.
2735
coverage: Coverage::default(),
36+
operator_mode,
37+
})
38+
}
39+
40+
fn parse_operator_mode(project: &CLIOptions) -> anyhow::Result<OperatorMode> {
41+
match (&project.mode, &project.operators) {
42+
// --operators specified
43+
(None, Some(operators)) => {
44+
let parsed_ops = OperatorMode::parse_operators(operators)?;
45+
Ok(OperatorMode::Custom(parsed_ops))
46+
},
47+
// --mode specified
48+
(Some(mode_arg), None) => {
49+
let mode = match mode_arg {
50+
OperatorModeArg::Light => OperatorMode::Light,
51+
OperatorModeArg::Medium => OperatorMode::Medium,
52+
OperatorModeArg::MediumOnly => OperatorMode::MediumOnly,
53+
OperatorModeArg::Heavy => OperatorMode::Heavy,
54+
OperatorModeArg::HeavyOnly => OperatorMode::HeavyOnly,
55+
};
56+
Ok(mode)
57+
},
58+
// neither specified
59+
(None, None) => Ok(OperatorMode::default()),
60+
// both specified - this should be prevented by clap conflicts
61+
(Some(_), Some(_)) => {
62+
unreachable!("Both --mode and --operators specified")
63+
},
2864
}
2965
}
3066
}

move-mutator/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod configuration;
1515
pub(crate) mod coverage;
1616
mod mutant;
1717
mod operator;
18+
pub mod operator_filter;
1819
mod operators;
1920
mod output;
2021
pub mod report;
@@ -74,10 +75,19 @@ pub fn run_move_mutator(
7475
};
7576

7677
let mut mutator_configuration =
77-
Configuration::new(options, Some(original_package_path.to_owned()));
78+
Configuration::new(options, Some(original_package_path.to_owned()))?;
7879

7980
trace!("Mutator configuration: {mutator_configuration:?}");
8081

82+
let enabled_operators = mutator_configuration.operator_mode.get_operators();
83+
println!(
84+
"Operator types being mutated ({}):",
85+
enabled_operators.len()
86+
);
87+
for (i, op) in enabled_operators.iter().enumerate() {
88+
println!(" {}. {}", i + 1, op);
89+
}
90+
8191
let package_path = mutator_configuration
8292
.project_path
8393
.clone()

0 commit comments

Comments
 (0)