@godzillaba/mutest 1.3.5 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/index.ts +37 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npx @godzillaba/mutest src/Counter.sol
|
|
|
10
10
|
|
|
11
11
|
Pass one or more Solidity files. Mutest will:
|
|
12
12
|
|
|
13
|
-
1. Create
|
|
13
|
+
1. Create parallel copies of your project (8 by default)
|
|
14
14
|
2. Generate mutants with Gambit (e.g. `++` -> `--`, assignments replaced)
|
|
15
15
|
3. Run `forge test` against each mutant across the worker copies
|
|
16
16
|
4. Report which mutants were killed (tests caught them) or survived (coverage gap)
|
|
@@ -27,6 +27,14 @@ npx @godzillaba/mutest
|
|
|
27
27
|
|
|
28
28
|
This reads `gambit_out/survivors.json` (or falls back to `gambit_out/gambit_results.json`) and re-runs the test suite against those mutants. Useful after improving your tests to check if previously surviving mutants are now caught.
|
|
29
29
|
|
|
30
|
+
### Options
|
|
31
|
+
|
|
32
|
+
`--workers <n>` / `-w <n>` — number of parallel workers (default: 8).
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npx @godzillaba/mutest --workers 4 src/Counter.sol
|
|
36
|
+
```
|
|
37
|
+
|
|
30
38
|
## Requirements
|
|
31
39
|
|
|
32
40
|
- [Foundry](https://getfoundry.sh/) (`forge`)
|
package/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
import { execFile as execFileCb } from "child_process";
|
|
3
|
-
import { readFile, writeFile, cp, rm } from "fs/promises";
|
|
3
|
+
import { readFile, writeFile, cp, rm, readdir } from "fs/promises";
|
|
4
4
|
import { promisify } from "util";
|
|
5
|
+
import { join } from "path";
|
|
5
6
|
|
|
6
7
|
const execFile = promisify(execFileCb);
|
|
7
8
|
|
|
@@ -28,7 +29,7 @@ async function setupWorkers(workerCount: number): Promise<string> {
|
|
|
28
29
|
return root;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
async function runGambit(solFiles: string[]): Promise<Mutant[]> {
|
|
32
|
+
async function runGambit(solFiles: string[], concurrency: number): Promise<Mutant[]> {
|
|
32
33
|
await rm("gambit_out", { recursive: true, force: true });
|
|
33
34
|
const { stdout: remappingsRaw } = await execFile("forge", ["remappings"]);
|
|
34
35
|
const remappings = remappingsRaw.trim().replaceAll('/=', '=').split("\n").filter(Boolean);
|
|
@@ -36,7 +37,6 @@ async function runGambit(solFiles: string[]): Promise<Mutant[]> {
|
|
|
36
37
|
|
|
37
38
|
const results: Mutant[] = [];
|
|
38
39
|
const pending = [...solFiles];
|
|
39
|
-
const concurrency = 10;
|
|
40
40
|
|
|
41
41
|
async function worker() {
|
|
42
42
|
while (pending.length > 0) {
|
|
@@ -47,7 +47,6 @@ async function runGambit(solFiles: string[]): Promise<Mutant[]> {
|
|
|
47
47
|
]);
|
|
48
48
|
const raw = await readFile(`${outdir}/gambit_results.json`, "utf-8");
|
|
49
49
|
const mutants: Mutant[] = JSON.parse(raw);
|
|
50
|
-
for (const m of mutants) m.name = `${file}/${m.name}`;
|
|
51
50
|
results.push(...mutants);
|
|
52
51
|
}
|
|
53
52
|
}
|
|
@@ -74,7 +73,7 @@ async function processMutants(
|
|
|
74
73
|
const dest = `${workerDir}/${mutant.original}`;
|
|
75
74
|
const backup = `${dest}.orig`;
|
|
76
75
|
await cp(dest, backup);
|
|
77
|
-
await cp(`gambit_out/${mutant.name}`, dest);
|
|
76
|
+
await cp(`gambit_out/${mutant.original}/${mutant.name}`, dest);
|
|
78
77
|
try {
|
|
79
78
|
await execFile("forge", ["test", "--optimize", "false", "--root", workerDir]);
|
|
80
79
|
survivors.push(mutant);
|
|
@@ -92,6 +91,16 @@ async function processMutants(
|
|
|
92
91
|
console.log(`Wrote survivors.json`);
|
|
93
92
|
}
|
|
94
93
|
|
|
94
|
+
async function findJsonFiles(dir: string, name: string): Promise<string[]> {
|
|
95
|
+
const results: string[] = [];
|
|
96
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
97
|
+
const full = join(dir, entry.name);
|
|
98
|
+
if (entry.isDirectory()) results.push(...await findJsonFiles(full, name));
|
|
99
|
+
else if (entry.name === name) results.push(full);
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
async function loadExistingMutants(): Promise<Mutant[]> {
|
|
96
105
|
try {
|
|
97
106
|
const raw = await readFile("gambit_out/survivors.json", "utf-8");
|
|
@@ -101,13 +110,31 @@ async function loadExistingMutants(): Promise<Mutant[]> {
|
|
|
101
110
|
return survivors;
|
|
102
111
|
}
|
|
103
112
|
} catch {}
|
|
104
|
-
const
|
|
105
|
-
|
|
113
|
+
const files = await findJsonFiles("gambit_out", "gambit_results.json");
|
|
114
|
+
const all: Mutant[] = [];
|
|
115
|
+
for (const f of files) {
|
|
116
|
+
const raw = await readFile(f, "utf-8");
|
|
117
|
+
all.push(...JSON.parse(raw));
|
|
118
|
+
}
|
|
119
|
+
return all;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseArgs() {
|
|
123
|
+
const args = process.argv.slice(2);
|
|
124
|
+
let workers = 8;
|
|
125
|
+
const solFiles: string[] = [];
|
|
126
|
+
for (let i = 0; i < args.length; i++) {
|
|
127
|
+
if (args[i] === "--workers" || args[i] === "-w") {
|
|
128
|
+
workers = parseInt(args[++i], 10);
|
|
129
|
+
} else {
|
|
130
|
+
solFiles.push(args[i]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { solFiles, workers };
|
|
106
134
|
}
|
|
107
135
|
|
|
108
136
|
async function main() {
|
|
109
|
-
const solFiles =
|
|
110
|
-
const workerCount = 10;
|
|
137
|
+
const { solFiles, workers: workerCount } = parseArgs();
|
|
111
138
|
console.log(`Setting up ${workerCount} workers...`);
|
|
112
139
|
const tempDir = await setupWorkers(workerCount);
|
|
113
140
|
|
|
@@ -115,7 +142,7 @@ async function main() {
|
|
|
115
142
|
let mutants: Mutant[];
|
|
116
143
|
if (solFiles.length > 0) {
|
|
117
144
|
console.log(`Running gambit on ${solFiles.join(", ")}...`);
|
|
118
|
-
mutants = await runGambit(solFiles);
|
|
145
|
+
mutants = await runGambit(solFiles, workerCount);
|
|
119
146
|
} else {
|
|
120
147
|
console.log("No files specified, using existing gambit_out/...");
|
|
121
148
|
mutants = await loadExistingMutants();
|