Skip to content

perf(devnet): share Docker clusters across test files#245

Open
solidsnakedev wants to merge 3 commits intomainfrom
worktree-noble-puzzling-dream
Open

perf(devnet): share Docker clusters across test files#245
solidsnakedev wants to merge 3 commits intomainfrom
worktree-noble-puzzling-dream

Conversation

@solidsnakedev
Copy link
Copy Markdown
Collaborator

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 ^test dependency so packages run in parallel. Net result: 20 cluster startups reduced to 4, ~880 lines of duplicated boilerplate removed.

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).
Copilot AI review requested due to automatic review settings April 10, 2026 22:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 globalSetup that starts a shared cluster, funds a fixed set of accounts, and exposes connection info via provide/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) {
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
export default async function setup({ provide }: GlobalSetupContext) {
export default async function setup({ provide }: GlobalSetupContext) {
// eslint-disable-next-line no-console

Copilot uses AI. Check for mistakes.
Comment on lines +143 to +146
console.log("[global-teardown] Stopping shared devnet cluster...")
await Cluster.stop(storedCluster)
await Cluster.remove(storedCluster)
console.log("[global-teardown] Shared cluster removed")
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +19 to 35
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])

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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"
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import { Cardano, Client } from "@evolution-sdk/evolution"
import { Cardano } from "@evolution-sdk/evolution"

Copilot uses AI. Check for mistakes.
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"
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client is imported but not used anymore after switching to shared.makeClient(...). This will fail @typescript-eslint/no-unused-vars; remove the unused import.

Suggested change
import * as AssetName from "@evolution-sdk/evolution/AssetName"

Copilot uses AI. Check for mistakes.
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"
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import { Cardano, Client } from "@evolution-sdk/evolution"
import { Cardano } from "@evolution-sdk/evolution"

Copilot uses AI. Check for mistakes.
assets: Cardano.Assets.fromLovelace(2_000_000n)
})
.build({ availableUtxos: [...genesisUtxos] })
.build({ availableUtxos: [...shared.genesisUtxos] })
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
.build({ availableUtxos: [...shared.genesisUtxos] })
.build({ availableUtxos: [shared.getGenesisUtxo(8)] })

Copilot uses AI. Check for mistakes.
assets: Cardano.Assets.fromLovelace(5_000_000n)
})
.build({ availableUtxos: [...genesisUtxos] })
.build({ availableUtxos: [...shared.genesisUtxos.filter((u) => u === shared.getGenesisUtxo(1))] })
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
.build({ availableUtxos: [...shared.genesisUtxos.filter((u) => u === shared.getGenesisUtxo(1))] })
.build({ availableUtxos: [shared.getGenesisUtxo(1)] })

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants