# Foundry Mutation Testing An overview of integrating mutation testing into Foundry based off the [Gambit](https://github.com/Certora/gambit) framework. ## Command The suggestion is to use subcommand `forge mutate --sample-seed ..<...>`. Another suggestion is to use a flag `forge test --mutate` but that's not recommended because it would introduce the mutation test flags into test and could become confusing for developers. ```rust= pub struct MutationTestArgs { /// Output mutation test results in JSON format. #[clap(long, short, help_heading = "Display options")] json: bool, #[clap(flatten)] filter: MutationTestFilterArgs, /// Stop running mutation tests after the first failure #[clap(long)] pub fail_fast: bool, /// List mutation tests instead of running them #[clap(long, short, help_heading = "Display options")] list: bool, #[clap(flatten)] evm_opts: EvmArgs, #[clap(flatten)] opts: CoreBuildArgs, #[clap(value_enum, default_value = "function")] test_mode: TestMode, } ``` ## Configuration ### **Filter** We aim to support 8 types of mutation filters - Random Sample - Function pattern - Function pattern inverse - Contract pattern - Contract pattern inverse - Path pattern - Path pattern inverse - Mutation rules ```rust= pub struct MutationTestFilterArgs { /// Only run randomly chosen mutation based on this seed #[clap(long = "sample-seed", visible_alias = "ms", value_name = "number")] pub sample_seed: Option<u64>, /// Only run mutations on functions matching the specified regex pattern. #[clap(long = "match-function", visible_alias = "mf", value_name = "REGEX")] pub function_pattern: Option<regex::Regex>, /// Only run mutations on functions that do not match the specified regex pattern. #[clap( long = "no-match-function", visible_alias = "nmf", value_name = "REGEX" )] pub function_pattern_inverse: Option<regex::Regex>, /// Only run mutations on functions in contracts matching the specified regex pattern. #[clap(long = "match-contract", visible_alias = "mc", value_name = "REGEX")] pub contract_pattern: Option<regex::Regex>, /// Only run mutations in contracts that do not match the specified regex pattern. #[clap( long = "no-match-contract", visible_alias = "nmc", value_name = "REGEX" )] pub contract_pattern_inverse: Option<regex::Regex>, /// Only run mutations on source files matching the specified glob pattern. #[clap(long = "match-path", visible_alias = "mp", value_name = "GLOB")] pub path_pattern: Option<GlobMatcher>, /// Only run mutations on source files that do not match the specified glob pattern. #[clap( name = "no-match-path", long = "no-match-path", visible_alias = "nmp", value_name = "GLOB" )] pub path_pattern_inverse: Option<GlobMatcher>, /// Only use this select mutation rules #[clap(long = "mutation-rules", visible_alias = "mtr", value_name = "String")] pub mutation_rules: Vec<String> } ``` ### **TestMode** Mutation testing can take quite a lot of time, this aims to support different testing mode that enable faster development and better developer UX. ```rust= pub enum TestMode { /// Only run tests matching the contract file name /// e.g. for Counter.sol mutations run tests in Counter.t.sol File, /// Only run tests matching similar function names /// e.g. for Counter.sol:addNumber mutation run tests in test suite /// matching a regex test_addNumber, testAddNumber, test_AddNumber, /// testFuzz_[a|A]ddNumber, testFail_addNumber Function, /// Run the entire test suite. This should be used in a CI /// environment as it would take quite sometime Full } ``` ### Future Feature Configurations - Support loading of custom mutation rules ## **Implementation** #### **Mutation Rules** We support similar mutation rules as in Gambit ```rust= pub enum MutationRules { AssignmentMutation, BinaryOpMutation, DeleteExpressionMutation, ElimDelegateMutation, FunctionCallMutation, IfStatementMutation, RequireMutation, SwapArgumentsFunctionMutation, SwapArgumentsOperatorMutation, UnaryOperatorMutation, } ``` #### **Mutation Test Output** Output of running mutation tests ##### **CLI Summary Output** ```shell= | File | Killed | Survived | Equivalent | |--------------------|-----------|------------|----------------| | src/Counter.sol | 100 | 50 | 3 | | Total | 100 | 50 | 4 | ``` ##### **CLI Detailed Output** ```shell= | File | Mutation | Diff | Result | |--------------------|------------------|-----------------------------|----------------| | src/Counter.sol:10 | BinaryOpMutation | number -= 1 | Killed | ``` #### **Json Output** This is similar to Gambit JSON output with the addition of "result" property. ```jsonld= [ { "id": "1", "name": "BinaryOpMutation.sol", "description": "BinaryOpMutation", "diff": "--- original\n+++ mutant\n@@ -4,7 +4,8 @@\n \n contract BinaryOpMutation {\n function myAddition(uint256 x, uint256 y) public pure returns (uint256) {\n-\treturn x + y;\n+\t/// BinaryOpMutation(`+` |==> `-`) of: `return x + y;`\n+\treturn x-y;\n }\n \n function mySubtraction(uint256 x, uint256 y) public pure returns (uint256) {\n", "original": "BinaryOpMutation/BinaryOpMutation.sol", "result": "killed" } ] ```