Skip to content

Commit 7f7752e

Browse files
Merge pull request #507 from pimlicolabs/claude/adoring-nash
perf(test): share test infrastructure per worker and use unique keys
2 parents 4b24bdb + 49640ae commit 7f7752e

20 files changed

Lines changed: 914 additions & 382 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ temp.ts
2121
.env.test.local
2222
.env.production.local
2323
.envrc
24-
.vscode
24+
.vscode
25+
.claude

.size-limit.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
"name": "permissionless (esm)",
44
"path": "./packages/permissionless/_esm/index.js",
5-
"limit": "60 kB",
5+
"limit": "250 kB",
66
"import": "*"
77
},
88
{

bun.lock

Lines changed: 143 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/testing/01-architecture.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,15 @@ bun run test
4646
│ describe.each(getCoreSmartAccounts())(...) ← matrix over accounts
4747
│ testWithRpc("...", async ({ rpc }) => { ... })
4848
49-
└─ per test() invocation:
50-
1. testWithRpc fixture allocates 3 ports
51-
2. spawns anvil → setupContracts → alto → mock-paymaster
52-
3. awaits the test body with { rpc: { anvilRpc, altoRpc, paymasterRpc } }
53-
4. stops all 3 instances, releases ports
49+
└─ per worker (shared across all tests in that worker):
50+
1. first testWithRpc fixture call triggers getSharedRig()
51+
2. spawns anvil → setupContracts → alto → mock-paymaster (once)
52+
3. tops up paymaster deposits (1000 ETH per EntryPoint)
53+
per test:
54+
4. clears alto mempool + resets base fee
55+
5. awaits the test body with { rpc: { anvilRpc, altoRpc, paymasterRpc } }
56+
worker exit:
57+
6. stops all 3 instances (Promise.all)
5458
```
5559

5660
Fixture mechanics are detailed in [02-lifecycle.md](./02-lifecycle.md); process details in [03-infrastructure.md](./03-infrastructure.md).
@@ -99,31 +103,32 @@ Field-by-field:
99103
| `coverage.reporter` | `CI ? ["lcov"] : ["text","json","html"]` | Local dev gets HTML + text output; CI produces only lcov (Codecov-friendly). |
100104
| `coverage.include` | `["**/permissionless/**"]` | Measures coverage of the published package only. |
101105
| `coverage.exclude` | test files, generated `_cjs`/`_esm`/`_types`, test infra | Keeps coverage numbers honest. |
102-
| `sequence.concurrent` | `false` | Tests inside one file run **sequentially**. Multiple parallel Anvil stacks from one file would risk port exhaustion and noisy logs. |
103-
| `fileParallelism` | `true` | Separate `.test.ts` files DO run in parallel (in separate workers). |
106+
| `sequence.concurrent` | `false` | Tests inside one file run **sequentially**. This is essential for the shared-rig model: tests in a worker share the same anvil/alto/paymaster trio and clear state between runs. |
107+
| `fileParallelism` | `true` | Separate `.test.ts` files DO run in parallel (in separate workers). Each worker gets its own shared rig. |
104108
| `environment` | `"node"` | No jsdom; tests run in raw Node. |
105109
| `testTimeout` | `60_000` (60s) | User operations + bundling + receipts are slow; 60s is the per-test cap. |
106-
| `hookTimeout` | `45_000` (45s) | The `testWithRpc` fixture setup (anvil + contracts + alto + paymaster) can take tens of seconds cold. |
110+
| `hookTimeout` | `45_000` (45s) | The `testWithRpc` fixture setup (on first test: anvil + contracts + alto + paymaster) can take tens of seconds cold. |
107111
| `include` | `join(__dirname, "./**/*.test.ts")` | Discovery is rooted at `packages/permissionless/` — not the repo root. Tests elsewhere (e.g. `packages/mock-paymaster/`) are **not** picked up. |
108112
| `env` | `loadEnv("test", process.cwd())` | Reads `.env.test`, `.env.test.local`, `.env`, `.env.local` from the CWD and exposes `VITE_*` vars to tests. |
109113

110114
### Parallelism model
111115

112-
- **File-level:** parallel. Each `.test.ts` file runs in its own worker, which means each has its own module-level state (including the `ports: number[]` tracker in `testWithRpc.ts:80`). Workers do not share a port list, but `get-port` asks the OS for a free port, so real collisions are rare.
113-
- **Test-level (inside a file):** serial. Each `testWithRpc(...)` inside a file waits for the previous one to tear down before the next starts. That means **within one file, there is never more than one Anvil/Alto/paymaster trio alive at once**.
116+
- **File-level:** parallel. Each `.test.ts` file runs in its own worker, which means each has its own shared rig (anvil + alto + paymaster trio). Workers do not share processes or ports.
117+
- **Test-level (inside a file):** serial. Each `testWithRpc(...)` inside a file waits for the previous one to complete before the next starts. This is critical because tests share the same anvil/alto/paymaster trio within a worker.
114118

115119
Combined: at any instant, concurrency ≈ number of `.test.ts` files currently in flight. With Vitest's default worker count (≈ CPU cores), you typically have 4–8 parallel stacks.
116120

117121
### Why `hookTimeout` is 45s
118122

119-
`testWithRpc` setup does, in order:
123+
The first `testWithRpc` call in a worker triggers `getSharedRig()`, which does:
120124
1. Allocate 3 free ports (ms).
121125
2. Start Anvil (~1–3s cold).
122126
3. Call `setupContracts(anvilRpc)` which issues **~80+ transactions** to the deterministic deployer and a few impersonation + `setCode` calls. With all txns batched via `Promise.all`, this typically lands in 2–8s but can spike.
123-
4. Start Alto (subprocess, waits for `"Server listening at"` message; ~2–5s).
127+
4. Start Alto with debug endpoints enabled (subprocess, waits for `"Server listening at"` message; ~2–5s).
124128
5. Start the paymaster (Fastify boot; ~1s).
129+
6. Top up paymaster deposits (~1s).
125130

126-
On a slow machine or noisy CI runner, cumulative setup > 30s is possible; 45s is the margin.
131+
On a slow machine or noisy CI runner, cumulative setup > 30s is possible; 45s is the margin. Subsequent tests in the same worker skip this setup entirely (the rig is already running), so only the first test pays the cold-start cost.
127132

128133
## File layout
129134

@@ -152,7 +157,7 @@ Non-exhaustive — see `packages/permissionless/**/*.test.ts` for the full set.
152157

153158
| Directory / file | Purpose |
154159
| ----------------------------------------- | ----------------------------------------------------------------------- |
155-
| `src/testWithRpc.ts` | The `testWithRpc` fixture — starts/stops anvil + alto + paymaster. |
160+
| `src/testWithRpc.ts` | The `testWithRpc` fixture — shared per-worker rig, per-test reset, `createAutoBundleTransport`. |
156161
| `src/utils.ts` | All viem/Pimlico/smart-account helpers. Re-read this often. |
157162
| `src/types.ts` | `AAParamType<entryPointVersion>`. |
158163
| `mock-aa-infra/alto/instance.ts` | `alto(...)` prool instance factory. |
@@ -171,7 +176,7 @@ It is imported from test files by **relative path** (e.g. `"../../../permissionl
171176
| You want to… | Open |
172177
| ---------------------------------------- | ------------------------------------------------------------------------- |
173178
| Change how tests are discovered / timed out | `packages/permissionless/vitest.config.ts` |
174-
| Change the per-test setup/teardown | `packages/permissionless-test/src/testWithRpc.ts` |
179+
| Change the per-worker setup or per-test reset | `packages/permissionless-test/src/testWithRpc.ts` |
175180
| Add a new viem client helper | `packages/permissionless-test/src/utils.ts` |
176181
| Add a new smart account type | `packages/permissionless-test/src/utils.ts` + `mock-aa-infra/alto/constants/accounts/` + `mock-aa-infra/alto/index.ts` |
177182
| Change how the bundler is started | `packages/permissionless-test/mock-aa-infra/alto/instance.ts` |

0 commit comments

Comments
 (0)