@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.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/index.ts +37 -10
  3. 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 10 parallel copies of your project
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 raw = await readFile("gambit_out/gambit_results.json", "utf-8");
105
- return JSON.parse(raw);
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 = process.argv.slice(2);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godzillaba/mutest",
3
- "version": "1.3.5",
3
+ "version": "1.4.1",
4
4
  "bin": {
5
5
  "mutest": "./index.ts"
6
6
  },