Setup & Configuration
Mutation testing with Mull consists of at most three steps:
- mutant generation
- mutant execution
- report generation (optional, can be done during execution)
Let’s reuse the same code from the Quick Primer:
#include <assert.h>
// Returns 1 if value is in the closed interval [min, max], 0 otherwiseint in_range(int value, int min, int max) { return value >= min && value < max;}
int main(void) { assert(in_range(5, 1, 10) == 1); // mid-range: both conditions true assert(in_range(0, 1, 10) == 0); // below min: first condition false assert(in_range(15, 1, 10) == 0); // above max: second condition false return 0;}Mutant Generation
Section titled “Mutant Generation”Mull is implemented as a Clang/LLVM plugin, so mutants are generated at compile time.
In the Quick Primer example we’ve seen that enabling Mull was as easy as adding a few compiler flags to the compile command:
# mutant generationclang-22 \ -fpass-plugin=/usr/lib/mull-ir-frontend-22 \ -g -grecord-command-line \ main.c -o range_testsMore specifically, the important bits are:
-fpass-plugin=/usr/lib/mull-ir-frontend-22this is the core of Mull, the plugin version must match the Clang version used to compile the code-g -grecord-command-lineto function properly, Mull needs debug information such that mutants generated at the very low-level can be mapped back to the high-level source code.
That’s all it takes, but Mull uses some default configuration which is a good start, but your project likely needs special treatment.
The plugin is configured using mull.yml file placed at the current working directory, alternatively you can pass the path to the file using environment variable MULL_CONFIG=path-to-file.yml:
# create some configstouch mull.ymltouch mull-ci.yml
# uses mull.yml by defaultclang-22 \ -fpass-plugin=/usr/lib/mull-ir-frontend-22 \ -g -grecord-command-line \ main.c -o range_tests
# uses mull-ci.ymlenv MULL_CONFIG=mull-ci.yml clang-22 \ -fpass-plugin=/usr/lib/mull-ir-frontend-22 \ -g -grecord-command-line \ main.c -o range_testsThere are quite a few options of what goes into the config, but you’ll normally just need a couple of those. See the full reference here: YAML Configuration.
Mutant Execution
Section titled “Mutant Execution”Once the binary is compiled we can run mutation analysis using mull-runner.
This time let’s look at both killed and survived mutants (using -ide-reporter-show-killed):
# mutant generation> clang-22 \ -fpass-plugin=/usr/lib/mull-ir-frontend-22 \ -g -grecord-command-line \ main.c -o range_tests# mutant execution> mull-runner-22 -ide-reporter-show-killed range_tests[info] Using config /workspaces/mull/demo/mull.yml[warning] Could not find dynamic library: libc.so.6[info] Warm up run (threads: 1) [################################] 1/1. Finished in 1ms[info] Baseline run (threads: 1) [################################] 1/1. Finished in 0ms[info] Running mutants (threads: 7) [################################] 7/7. Finished in 3ms[info] Killed mutants (5/7):/workspaces/mull/demo/main.c:5:16: warning: Killed: Replaced >= with < [cxx_ge_to_lt] return value >= min && value < max; ^/workspaces/mull/demo/main.c:5:32: warning: Killed: Replaced < with >= [cxx_lt_to_ge] return value >= min && value < max; ^/workspaces/mull/demo/main.c:9:30: warning: Killed: Replaced with != [cxx_eq_to_ne] assert(in_range(5, 1, 10) == 1); // mid-range: both conditions true ^/workspaces/mull/demo/main.c:10:30: warning: Killed: Replaced with != [cxx_eq_to_ne] assert(in_range(0, 1, 10) == 0); // below min: first condition false ^/workspaces/mull/demo/main.c:11:30: warning: Killed: Replaced with != [cxx_eq_to_ne] assert(in_range(15, 1, 10) == 0); // above max: second condition false ^[info] Survived mutants (2/7):/workspaces/mull/demo/main.c:5:16: warning: Survived: Replaced >= with > [cxx_ge_to_gt] return value >= min && value < max; ^/workspaces/mull/demo/main.c:5:32: warning: Survived: Replaced < with <= [cxx_lt_to_le] return value >= min && value < max; ^[info] Mutation score: 71%[info] Surviving mutants: 2[info] Total execution time: 9msIn total, Mull generated 7 mutants, 5 of which were killed.
Strictly speaking, the cxx_eq_to_ne mutants belong to the test suite and usually you want to avoid mutating the test suite itself.
There are several ways to disable/ignore those mutants (see Filters), but for this example we are just going to disable mutation operators producing those mutants.
Modify mull.yml as follows:
mutators: - cxx_boundary # all mutators from the group - cxx_comparison # all mutators from the groupignoreMutators: - cxx_eq_to_ne # single mutatorRebuild & rerun:
# mutant generation> clang-22 \ -fpass-plugin=/usr/lib/mull-ir-frontend-22 \ -g -grecord-command-line \ main.c -o range_tests# mutant execution> mull-runner-22 -ide-reporter-show-killed range_tests[info] Using config /workspaces/mull/demo/mull.yml[warning] Could not find dynamic library: libc.so.6[info] Warm up run (threads: 1) [################################] 1/1. Finished in 1ms[info] Baseline run (threads: 1) [################################] 1/1. Finished in 0ms[info] Running mutants (threads: 4) [################################] 4/4. Finished in 2ms[info] Killed mutants (2/4):/workspaces/mull/demo/main.c:5:16: warning: Killed: Replaced >= with < [cxx_ge_to_lt] return value >= min && value < max; ^/workspaces/mull/demo/main.c:5:32: warning: Killed: Replaced < with >= [cxx_lt_to_ge] return value >= min && value < max; ^[info] Survived mutants (2/4):/workspaces/mull/demo/main.c:5:16: warning: Survived: Replaced >= with > [cxx_ge_to_gt] return value >= min && value < max; ^/workspaces/mull/demo/main.c:5:32: warning: Survived: Replaced < with <= [cxx_lt_to_le] return value >= min && value < max; ^[info] Mutation score: 50%[info] Surviving mutants: 2[info] Total execution time: 6msNow we only have relevant mutants.
The full list of support mutation operators can be found here: Mutation Operators.
Report Generation
Section titled “Report Generation”For simple cases mull-runner has all you need. In fact, it supports all the Reporters and combines mutant execution and report generation in a single tool.
However, there are cases when you may want to aggregate data across several runs, several binaries, or even several machines.
To do so, one needs to “export” mutant execution state and then use mull-reporter to generate a combined report.
This is especially useful for cases with Multiple Test Targets.
The usage is simple:
# mutant execution> mull-runner-22 -reporters SQLite -report-name range_tests range_tests[info] Using config /workspaces/mull/demo/mull.yml[warning] Could not find dynamic library: libc.so.6[info] Warm up run (threads: 1) [################################] 1/1. Finished in 1ms[info] Baseline run (threads: 1) [################################] 1/1. Finished in 2ms[info] Running mutants (threads: 4) [################################] 4/4. Finished in 32ms[info] Results can be found at './range_tests.sqlite'[info] Surviving mutants: 2[info] Total execution time: 75ms
# report generation> mull-reporter-22 -ide-reporter-show-killed range_tests.sqlite[info] Using config /workspaces/mull/demo/mull.yml[info] Killed mutants (2/4):/workspaces/mull/demo/main.c:5:16: warning: Killed: Replaced >= with < [cxx_ge_to_lt] return value >= min && value < max; ^/workspaces/mull/demo/main.c:5:32: warning: Killed: Replaced < with >= [cxx_lt_to_ge] return value >= min && value < max; ^[info] Survived mutants (2/4):/workspaces/mull/demo/main.c:5:16: warning: Survived: Replaced >= with > [cxx_ge_to_gt] return value >= min && value < max; ^/workspaces/mull/demo/main.c:5:32: warning: Survived: Replaced < with <= [cxx_lt_to_le] return value >= min && value < max; ^[info] Mutation score: 50%[info] Surviving mutants: 2[info] Total reporting time: 1msNow you have everything you need to apply mutation testing to your project.
For more use cases, consider looking at the guides and references.