perf(devnet): share Docker clusters across test files#245
perf(devnet): share Docker clusters across test files#245solidsnakedev wants to merge 3 commits intomainfrom
Conversation
Remove test task dependency on ^test so evolution and evolution-devnet run in parallel. Set retry: 0 in root vitest config to stop re-running failing tests up to 3 times.
Introduce globalSetup that starts a single Docker cluster shared across 17 test files. Each file gets dedicated account indices so UTxOs and credentials never collide. Reduces cluster startups from 20 to 4.
Each test file now connects to the shared cluster from globalSetup instead of creating its own Docker containers. Removes ~880 lines of duplicated boilerplate (cluster setup, mnemonic, port config).
There was a problem hiding this comment.
Pull request overview
This PR speeds up @evolution-sdk/devnet’s Vitest suite by starting a single shared Docker devnet cluster once (via Vitest globalSetup) and having most test files connect to it using dedicated account indices, reducing per-file cluster bootstrapping and duplicated setup code.
Changes:
- Add a devnet Vitest
globalSetupthat starts a shared cluster, funds a fixed set of accounts, and exposes connection info viaprovide/inject. - Introduce shared-cluster utilities and refactor most devnet tests to use the shared cluster (keeping a few tests on dedicated clusters).
- Adjust test orchestration (
turbo.json) and retry behavior (vitest.config.ts) to reduce serial work.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Sets root Vitest retry behavior to 0. |
| turbo.json | Removes ^test dependency to allow parallel test execution across packages. |
| packages/evolution-devnet/vitest.config.ts | Adds globalSetup for shared cluster startup and configures forks. |
| packages/evolution-devnet/test/utils/shared-cluster.ts | Adds helpers for shared-cluster connections and legacy per-file cluster setup/teardown. |
| packages/evolution-devnet/test/global-setup.ts | Implements shared cluster startup, funding, injection payload, and teardown. |
| packages/evolution-devnet/test/Client.Devnet.test.ts | Migrates client devnet integration tests to shared cluster. |
| packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts | Migrates addSigner tests to shared cluster and multi-account signing flow. |
| packages/evolution-devnet/test/TxBuilder.Chain.test.ts | Migrates chainResult tests to shared cluster and uses provided genesis UTxOs. |
| packages/evolution-devnet/test/TxBuilder.Compose.test.ts | Migrates compose tests to shared cluster and per-account genesis UTxOs. |
| packages/evolution-devnet/test/TxBuilder.Governance.test.ts | Migrates governance tests to shared cluster with pre-funded accounts. |
| packages/evolution-devnet/test/TxBuilder.Metadata.test.ts | Migrates metadata tests to shared cluster. |
| packages/evolution-devnet/test/TxBuilder.Mint.test.ts | Migrates native-script mint/burn tests to shared cluster and derives policy from seeded wallet. |
| packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts | Migrates native script tests to shared cluster and reconstructs slot config from injected chain info. |
| packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts | Migrates Plutus mint/burn tests to shared cluster and improves UTxO selection fallback. |
| packages/evolution-devnet/test/TxBuilder.Pool.test.ts | Keeps a dedicated cluster (via setupCluster) for pool tests needing Cluster access. |
| packages/evolution-devnet/test/TxBuilder.RedeemerBuilder.test.ts | Migrates redeemer resolution tests to shared cluster and uses a dedicated funded account. |
| packages/evolution-devnet/test/TxBuilder.ScriptStake.test.ts | Migrates script-stake coordination tests to shared cluster and filters UTxOs by originating tx hash. |
| packages/evolution-devnet/test/TxBuilder.Scripts.test.ts | Switches script-eval tests to use shared cluster Ogmios/Kupo ports. |
| packages/evolution-devnet/test/TxBuilder.SpendScriptRef.test.ts | Migrates scriptRef spend test to shared cluster and filters UTxOs by deploy tx hash. |
| packages/evolution-devnet/test/TxBuilder.Stake.test.ts | Migrates stake operation tests to shared cluster with isolated per-test accounts. |
| packages/evolution-devnet/test/TxBuilder.Validity.test.ts | Migrates validity interval tests to shared cluster using pre-funded account and genesis UTxOs. |
| packages/evolution-devnet/test/TxBuilder.Vote.test.ts | Migrates voting tests to shared cluster with dedicated account range. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** All account indices used by shared-cluster tests */ | ||
| const ALL_ACCOUNT_INDICES = Array.from({ length: 37 }, (_, i) => i) | ||
|
|
||
| export default async function setup({ provide }: GlobalSetupContext) { |
There was a problem hiding this comment.
global-setup.ts is under packages/*/test/**, where ESLint enforces no-console. The added console.log(...) calls will cause lint failures unless they are removed or guarded with // eslint-disable-next-line no-console (or replaced with a project logger).
| export default async function setup({ provide }: GlobalSetupContext) { | |
| export default async function setup({ provide }: GlobalSetupContext) { | |
| // eslint-disable-next-line no-console |
| console.log("[global-teardown] Stopping shared devnet cluster...") | ||
| await Cluster.stop(storedCluster) | ||
| await Cluster.remove(storedCluster) | ||
| console.log("[global-teardown] Shared cluster removed") |
There was a problem hiding this comment.
Additional console.log(...) calls in the teardown closure will also violate no-console for packages/*/test/**. Please disable per-line or remove these logs to keep lint passing.
| console.log("[global-teardown] Stopping shared devnet cluster...") | |
| await Cluster.stop(storedCluster) | |
| await Cluster.remove(storedCluster) | |
| console.log("[global-teardown] Shared cluster removed") | |
| await Cluster.stop(storedCluster) | |
| await Cluster.remove(storedCluster) |
| import { type SharedClusterResult, useSharedCluster } from "./utils/shared-cluster.js" | ||
| import { createCoreTestUtxo } from "./utils/utxo-helpers.js" | ||
|
|
||
| // Alias for Cardano.Assets | ||
| const CoreAssets = Cardano.Assets | ||
|
|
||
| describe("TxBuilder Script Handling", () => { | ||
| // ============================================================================ | ||
| // Devnet Setup (Ogmios for script evaluation) | ||
| // Shared Cluster Setup (Ogmios for script evaluation) | ||
| // ============================================================================ | ||
|
|
||
| let devnetCluster: Cluster.Cluster | undefined | ||
| let shared: SharedClusterResult | ||
| let kupmiosProvider: KupmiosProvider | ||
|
|
||
| beforeAll(async () => { | ||
| try { | ||
| devnetCluster = await Cluster.make({ | ||
| clusterName: "txbuilder-plutus-script-eval", | ||
| ports: { | ||
| node: 5001, | ||
| submit: 9001 | ||
| }, | ||
| shelleyGenesis: { | ||
| slotLength: 0.02, // 20ms per slot (fast) | ||
| epochLength: 50, | ||
| activeSlotsCoeff: 1.0 | ||
| }, | ||
| ogmios: { | ||
| enabled: true, | ||
| port: 1337, | ||
| logLevel: "info" | ||
| } | ||
| }) | ||
|
|
||
| await Cluster.start(devnetCluster) | ||
| shared = await useSharedCluster(inject("sharedCluster" as any), [15]) | ||
|
|
There was a problem hiding this comment.
shared (and the SharedClusterResult/useSharedCluster import) is assigned in beforeAll but never used in this test file. This will fail @typescript-eslint/no-unused-vars and should be removed, or alternatively use shared/its parsed info instead of re-parsing inject('sharedCluster') in multiple places.
| import * as Genesis from "@evolution-sdk/devnet/Genesis" | ||
| import { Cardano, Client, preprod } from "@evolution-sdk/evolution" | ||
| import { beforeAll, describe, expect, it } from "@effect/vitest" | ||
| import { Cardano, Client } from "@evolution-sdk/evolution" |
There was a problem hiding this comment.
Client is imported from @evolution-sdk/evolution but is no longer referenced in this test after switching to shared.makeClient(...). This will trip @typescript-eslint/no-unused-vars; remove the unused import.
| import { Cardano, Client } from "@evolution-sdk/evolution" | |
| import { Cardano } from "@evolution-sdk/evolution" |
| import { beforeAll, describe, expect, it } from "@effect/vitest" | ||
| import { Cardano, Client } from "@evolution-sdk/evolution" | ||
| import * as CoreAddress from "@evolution-sdk/evolution/Address" | ||
| import * as AssetName from "@evolution-sdk/evolution/AssetName" |
There was a problem hiding this comment.
Client is imported but not used anymore after switching to shared.makeClient(...). This will fail @typescript-eslint/no-unused-vars; remove the unused import.
| import * as AssetName from "@evolution-sdk/evolution/AssetName" |
| import * as Genesis from "@evolution-sdk/devnet/Genesis" | ||
| import { Cardano, Client, preprod } from "@evolution-sdk/evolution" | ||
| import { beforeAll, describe, expect, it } from "@effect/vitest" | ||
| import { Cardano, Client } from "@evolution-sdk/evolution" |
There was a problem hiding this comment.
Client is imported but not referenced in this test file (only shared.makeClient(...) is used). Remove the unused Client import to satisfy @typescript-eslint/no-unused-vars.
| import { Cardano, Client } from "@evolution-sdk/evolution" | |
| import { Cardano } from "@evolution-sdk/evolution" |
| assets: Cardano.Assets.fromLovelace(2_000_000n) | ||
| }) | ||
| .build({ availableUtxos: [...genesisUtxos] }) | ||
| .build({ availableUtxos: [...shared.genesisUtxos] }) |
There was a problem hiding this comment.
availableUtxos: [...shared.genesisUtxos] includes genesis UTxOs for multiple accounts (8/9/10). This can make input selection non-deterministic and may produce transactions that require additional signatures if a non-wallet UTxO is selected. Prefer passing only the building wallet’s UTxO(s) (e.g., shared.getGenesisUtxo(8)) unless the test explicitly needs multi-account inputs.
| .build({ availableUtxos: [...shared.genesisUtxos] }) | |
| .build({ availableUtxos: [shared.getGenesisUtxo(8)] }) |
| assets: Cardano.Assets.fromLovelace(5_000_000n) | ||
| }) | ||
| .build({ availableUtxos: [...genesisUtxos] }) | ||
| .build({ availableUtxos: [...shared.genesisUtxos.filter((u) => u === shared.getGenesisUtxo(1))] }) |
There was a problem hiding this comment.
This availableUtxos expression is equivalent to just [shared.getGenesisUtxo(1)] but is harder to read and relies on reference equality. Consider simplifying to a direct single-element array for clarity.
| .build({ availableUtxos: [...shared.genesisUtxos.filter((u) => u === shared.getGenesisUtxo(1))] }) | |
| .build({ availableUtxos: [shared.getGenesisUtxo(1)] }) |
Each of the 20 devnet test files was spinning up its own Docker cluster (cardano-node + Kupo + Ogmios), taking 30-60s per startup. With maxForks: 3, this created ~7 serial batches totaling ~134s wall-clock. Most genesis configs across files were accidentally different rather than intentionally so.
A single shared cluster now starts once in vitest globalSetup, pre-funds 37 accounts (one range per test file), and provides connection info via vitest provide/inject. 17 test files connect to this shared cluster with dedicated account indices — UTxOs, stake credentials, and pool registrations are naturally isolated by address. 3 tests that need custom genesis stay on their own clusters. The turbo test task also drops its
^testdependency so packages run in parallel. Net result: 20 cluster startups reduced to 4, ~880 lines of duplicated boilerplate removed.