Skip to content

Commit cb9bca7

Browse files
authored
Merge pull request #66 from OffchainLabs/gambit-tool
Add mutation testing
2 parents eebc6ac + f607a91 commit cb9bca7

File tree

9 files changed

+575
-3
lines changed

9 files changed

+575
-3
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ test/storage/*-old
1919

2020
# local deployment files
2121
network.json
22+
23+
# Gambit (mutation test) files
24+
gambit_out/
25+
test-mutation/mutant_test_env/

docs/mutation_testing.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Mutation testing using Certora's Gambit framework
2+
3+
[Gambit tool](https://docs.certora.com/en/latest/docs/gambit/gambit.html) takes Solidity file or list of files as input and it will generate "mutants" as output. Those are copies of original file, but each with an atomic modification of original file, ie. flipped operator, mutated 'if' condition etc. Next step is running the test suite against the mutant. If all tests still pass, mutant survived, that's bad - either there's faulty test or there is missing coverage. If some test(s) fail, mutant's been killed, that's good.
4+
5+
## How to install Gambit
6+
To build from source (assumes Rust and Cargo are installed), clone the Gambit repo
7+
```
8+
git clone git@github.com:Certora/gambit.git
9+
```
10+
11+
Then run
12+
```
13+
cargo install --path .
14+
```
15+
16+
Alternatively, prebuilt binaries can be used - more info [here](https://docs.certora.com/en/latest/docs/gambit/gambit.html#installation).
17+
18+
19+
## Using Gambit
20+
CLI command `gambit mutate` is used to generated mutants. It can take single file as an input, ie. let's say we want to generate mutants for file `L1ERC20Gateway.sol`:
21+
```
22+
gambit mutate --solc_remappings "@openzeppelin=node_modules/@openzeppelin" "@arbitrum=node_modules/@arbitrum" -f contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol
23+
```
24+
25+
Command will output `gambit_out` folder containing list of mutants (modified Solidity files) and `gambit_results.json` which contains info about all the generated mutants. Another way to view the mutant info is to run:
26+
```
27+
gambit summary
28+
```
29+
30+
Gambit can also take set of Solidity files as input in JSON format, ie:
31+
```
32+
gambit mutate --json test-mutation/config.json
33+
```
34+
35+
List of all configuration options for the JSON file can be found [here](https://docs.certora.com/en/latest/docs/gambit/gambit.html#configuration-files).
36+
37+
Gambit only generates the mutants, it will not execute any tests. To check if mutant gets killed or survives, we need to copy modified Solidity file over the original one, re-compile the project and re-run the test suite. This process has been automated and described in next chapter.
38+
39+
## Gambit integration with Foundry
40+
To get the most benefits from Gambit, we have integrated it with Foundry and automated testing and reporting process. This is how `gambitTester` script works:
41+
- generates mutants for files specified in test-mutation/config.json
42+
- for each mutant do in parallel (in batches):
43+
- replace original file with mutant
44+
- re-compile and run foundry suite
45+
- track results
46+
- report results
47+
48+
Here are practical steps to run mutation test over the set of Arbitrum token bridge contracts. First we need to update `test-mutation/config.json` with the list of solidity files to be tested. In this case we use config file that was prepared in advance:
49+
```
50+
cp test-mutation/all-configs/config.tokenbridge-ethereum.json test-mutation/config.json
51+
```
52+
53+
Now run the script, it will generate mutants and start compiling/testing them in parallel:
54+
```
55+
yarn run test:mutation
56+
```
57+
58+
It will take some time for script to execute (depends on the underlying HW).
59+
60+
Script output looks like this:
61+
```
62+
❯ yarn run test:mutation
63+
64+
yarn run v1.22.19
65+
$ ts-node test-mutation/gambitTester.ts
66+
====== Generating mutants
67+
Generated 209 mutants in gambit_out/
68+
69+
====== Test mutants
70+
Testing mutant batch 0..7
71+
Testing mutant batch 7..14
72+
Testing mutant batch 14..21
73+
Testing mutant batch 21..28
74+
Testing mutant batch 28..35
75+
Testing mutant batch 35..42
76+
Testing mutant batch 42..49
77+
Testing mutant batch 49..56
78+
Testing mutant batch 56..63
79+
Testing mutant batch 63..70
80+
Testing mutant batch 70..77
81+
Testing mutant batch 77..84
82+
Testing mutant batch 84..91
83+
Testing mutant batch 91..98
84+
Testing mutant batch 98..105
85+
Testing mutant batch 105..112
86+
Testing mutant batch 112..119
87+
Testing mutant batch 119..126
88+
Testing mutant batch 126..133
89+
Testing mutant batch 133..140
90+
Testing mutant batch 140..147
91+
Testing mutant batch 147..154
92+
Testing mutant batch 154..161
93+
Testing mutant batch 161..168
94+
Testing mutant batch 168..175
95+
Testing mutant batch 175..182
96+
Testing mutant batch 182..189
97+
Testing mutant batch 189..196
98+
Testing mutant batch 196..203
99+
Testing mutant batch 203..210
100+
101+
====== Results
102+
103+
Mutant ID | File Name | Status
104+
----------------------------------------------
105+
----------------------------------------------
106+
1 | L1ArbitrumMessenger.sol | SURVIVED
107+
2 | L1ArbitrumMessenger.sol | SURVIVED
108+
3 | L1ArbitrumMessenger.sol | KILLED
109+
----------------------------------------------
110+
4 | L1ArbitrumExtendedGateway.sol | KILLED
111+
5 | L1ArbitrumExtendedGateway.sol | KILLED
112+
6 | L1ArbitrumExtendedGateway.sol | KILLED
113+
7 | L1ArbitrumExtendedGateway.sol | KILLED
114+
8 | L1ArbitrumExtendedGateway.sol | KILLED
115+
9 | L1ArbitrumExtendedGateway.sol | KILLED
116+
10 | L1ArbitrumExtendedGateway.sol | KILLED
117+
11 | L1ArbitrumExtendedGateway.sol | KILLED
118+
119+
...
120+
121+
205 | L1WethGateway.sol | SURVIVED
122+
206 | L1WethGateway.sol | SURVIVED
123+
207 | L1WethGateway.sol | SURVIVED
124+
208 | L1WethGateway.sol | SURVIVED
125+
209 | L1WethGateway.sol | SURVIVED
126+
----------------------------------------------
127+
Total Mutants: 209
128+
Killed: 133 (63.64%)
129+
Survived: 76 (36.36%)
130+
131+
====== Done in 20.91 min
132+
```
133+
134+
We're insterested to analyze the mutants which survived. The 1st column in output, `Mutant ID`, can be used to find the exact mutation that was applied by looking into the matching entry in the `gambit_results.json` file.
135+
136+
## Other considerations
137+
Mutation testing script is time-intensive due to all the re-compiling work. For that reason, list of input files should be optimized to give the most benefit for the limited time period available for testing. Ie. single Solidity files can be targeted and tested manually, and the broader scope of files can be tested overnight.
138+
139+
Other params that can be adjusted are type of mutations to use, number of mutants to be generated, randomness seed to use in generation, etc.
140+

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"test:unit": "forge test",
2121
"test:e2e:local-env": "yarn hardhat test test-e2e/*",
2222
"test:storage": "./scripts/storage_layout_test.bash",
23+
"test:mutation": "ts-node test-mutation/gambitTester.ts",
2324
"deploy:local:token-bridge": "ts-node ./scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts",
2425
"deploy:token-bridge-creator": "ts-node ./scripts/deployment/deployTokenBridgeCreator.ts",
2526
"create:token-bridge": "ts-node ./scripts/deployment/createTokenBridge.ts",
@@ -51,6 +52,7 @@
5152
"@typechain/ethers-v5": "^7.0.1",
5253
"@typechain/hardhat": "^2.3.0",
5354
"@types/chai": "^4.2.15",
55+
"@types/fs-extra": "^11.0.4",
5456
"@types/mocha": "^9.0.0",
5557
"@types/node": "^14.14.28",
5658
"@types/prompts": "^2.0.14",
@@ -66,6 +68,7 @@
6668
"eslint-plugin-prettier": "^4.0.0",
6769
"ethereum-waffle": "^3.2.0",
6870
"ethers": "^5.4.5",
71+
"fs-extra": "^11.2.0",
6972
"hardhat": "2.17.3",
7073
"hardhat-contract-sizer": "^2.10.0",
7174
"hardhat-deploy": "^0.9.1",

remappings.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/
22
forge-std/=lib/forge-std/src/
33
@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/
44
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
5-
6-
7-
5+
@arbitrum=node_modules/@arbitrum
6+
@offchainlabs=node_modules/@offchainlabs
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol",
4+
"sourceroot": "..",
5+
"solc_remappings": [
6+
"@openzeppelin=../node_modules/@openzeppelin",
7+
"@arbitrum=../node_modules/@arbitrum"
8+
]
9+
}
10+
]
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
[
2+
{
3+
"filename": "../contracts/tokenbridge/ethereum/L1ArbitrumMessenger.sol",
4+
"sourceroot": "..",
5+
"solc_remappings": [
6+
"@openzeppelin=../node_modules/@openzeppelin",
7+
"@arbitrum=../node_modules/@arbitrum"
8+
]
9+
},
10+
{
11+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1ArbitrumExtendedGateway.sol",
12+
"sourceroot": "..",
13+
"solc_remappings": [
14+
"@openzeppelin=../node_modules/@openzeppelin",
15+
"@arbitrum=../node_modules/@arbitrum"
16+
]
17+
},
18+
{
19+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol",
20+
"sourceroot": "..",
21+
"solc_remappings": [
22+
"@openzeppelin=../node_modules/@openzeppelin",
23+
"@arbitrum=../node_modules/@arbitrum"
24+
]
25+
},
26+
{
27+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol",
28+
"sourceroot": "..",
29+
"solc_remappings": [
30+
"@openzeppelin=../node_modules/@openzeppelin",
31+
"@arbitrum=../node_modules/@arbitrum"
32+
]
33+
},
34+
{
35+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol",
36+
"sourceroot": "..",
37+
"solc_remappings": [
38+
"@openzeppelin=../node_modules/@openzeppelin",
39+
"@arbitrum=../node_modules/@arbitrum"
40+
]
41+
},
42+
{
43+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1ForceOnlyReverseCustomGateway.sol",
44+
"sourceroot": "..",
45+
"solc_remappings": [
46+
"@openzeppelin=../node_modules/@openzeppelin",
47+
"@arbitrum=../node_modules/@arbitrum"
48+
]
49+
},
50+
{
51+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol",
52+
"sourceroot": "..",
53+
"solc_remappings": [
54+
"@openzeppelin=../node_modules/@openzeppelin",
55+
"@arbitrum=../node_modules/@arbitrum"
56+
]
57+
},
58+
{
59+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol",
60+
"sourceroot": "..",
61+
"solc_remappings": [
62+
"@openzeppelin=../node_modules/@openzeppelin",
63+
"@arbitrum=../node_modules/@arbitrum"
64+
]
65+
},
66+
{
67+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1OrbitERC20Gateway.sol",
68+
"sourceroot": "..",
69+
"solc_remappings": [
70+
"@openzeppelin=../node_modules/@openzeppelin",
71+
"@arbitrum=../node_modules/@arbitrum"
72+
]
73+
},
74+
{
75+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol",
76+
"sourceroot": "..",
77+
"solc_remappings": [
78+
"@openzeppelin=../node_modules/@openzeppelin",
79+
"@arbitrum=../node_modules/@arbitrum"
80+
]
81+
},
82+
{
83+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1OrbitReverseCustomGateway.sol",
84+
"sourceroot": "..",
85+
"solc_remappings": [
86+
"@openzeppelin=../node_modules/@openzeppelin",
87+
"@arbitrum=../node_modules/@arbitrum"
88+
]
89+
},
90+
{
91+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1ReverseCustomGateway.sol",
92+
"sourceroot": "..",
93+
"solc_remappings": [
94+
"@openzeppelin=../node_modules/@openzeppelin",
95+
"@arbitrum=../node_modules/@arbitrum"
96+
]
97+
},
98+
{
99+
"filename": "../contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol",
100+
"sourceroot": "..",
101+
"solc_remappings": [
102+
"@openzeppelin=../node_modules/@openzeppelin",
103+
"@arbitrum=../node_modules/@arbitrum"
104+
]
105+
}
106+
]

0 commit comments

Comments
 (0)