mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## Summary - fixes the untrusted CI failure in `e2e-tests / E2E Tests with Kurtosis Ethereum Network` - keeps validator-set-submitter startup actionable by avoiding test-only contract config imports during container startup - improves submitter readiness diagnostics by capturing both stdout and stderr from container logs and making streamed log matching robust to chunked UTF-8 output - reduces validator-set-submitter Docker build time in CI by building from `test/` and adding a tight `test/.dockerignore` - makes local arm64 E2E runs use a native local Snowbridge relayer image instead of forcing `linux/amd64` emulation - auto-builds the local relayer image when needed for `:local` tags ## Why The original failing untrusted test started as a submitter container startup problem, but the branch now also addresses a second timeout path that showed up while debugging: - the submitter image was being built from the repository root with a large Docker context, which made the `validator-set-update` suite spend most of its hook timeout budget inside `docker build` - on Apple Silicon, forcing `datahavenxyz/snowbridge-relay:latest` through `linux/amd64` caused `generate-beacon-checkpoint` to segfault during local runs These changes make the submitter failure actionable, cut the CI Docker build context down substantially, and keep local E2E runs reliable on arm64. ## Validation - `cd test && bun fmt` - `cd test && bun x tsc --noEmit` - `bun test e2e/suites/validator-set-update.test.ts --timeout 900000` - `cd test && docker build -f tools/validator-set-submitter/Dockerfile -t datahavenxyz/validator-set-submitter:local .`
193 lines
6.5 KiB
TypeScript
193 lines
6.5 KiB
TypeScript
import { afterAll, beforeAll } from "bun:test";
|
|
import readline from "node:readline";
|
|
import { isCI } from "launcher/network";
|
|
import { logger } from "utils";
|
|
import { launchNetwork } from "../../launcher";
|
|
import { getDefaultRelayerImageTag } from "../../launcher/network";
|
|
import type { LaunchNetworkResult } from "../../launcher/types";
|
|
import { ConnectorFactory, type TestConnectors } from "./connectors";
|
|
import { TestSuiteManager } from "./manager";
|
|
|
|
export interface TestSuiteOptions {
|
|
/** Unique name for the test suite */
|
|
suiteName: string;
|
|
/** Network configuration options */
|
|
networkOptions?: {
|
|
/** Slot time in milliseconds for the network */
|
|
slotTime?: number;
|
|
/** Enable Blockscout explorer for the network */
|
|
blockscout?: boolean;
|
|
/** Build DataHaven runtime from source, needed to reflect local changes */
|
|
buildDatahaven?: boolean;
|
|
/** Docker image tag for DataHaven node */
|
|
datahavenImageTag?: string;
|
|
/** Docker image tag for Snowbridge relayer */
|
|
relayerImageTag?: string;
|
|
};
|
|
/** Keep network running after tests complete for debugging */
|
|
keepAlive?: boolean;
|
|
}
|
|
|
|
export abstract class BaseTestSuite {
|
|
protected networkId: string;
|
|
protected connectors?: LaunchNetworkResult;
|
|
protected testConnectors?: TestConnectors;
|
|
private connectorFactory?: ConnectorFactory;
|
|
private options: TestSuiteOptions;
|
|
private manager: TestSuiteManager;
|
|
|
|
constructor(options: TestSuiteOptions) {
|
|
this.options = options;
|
|
// Generate unique network ID using suite name and timestamp
|
|
this.networkId = `${options.suiteName}-${Date.now()}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
this.manager = TestSuiteManager.getInstance();
|
|
}
|
|
|
|
protected setupHooks(): void {
|
|
beforeAll(async () => {
|
|
logger.info(`🧪 Setting up test suite: ${this.options.suiteName}`);
|
|
logger.info(`📝 Network ID: ${this.networkId}`);
|
|
|
|
try {
|
|
// Register suite with manager
|
|
this.manager.registerSuite(this.options.suiteName, this.networkId);
|
|
|
|
// Launch the network
|
|
this.connectors = await launchNetwork({
|
|
networkId: this.networkId,
|
|
datahavenImageTag:
|
|
this.options.networkOptions?.datahavenImageTag || "datahavenxyz/datahaven:local",
|
|
relayerImageTag:
|
|
this.options.networkOptions?.relayerImageTag || getDefaultRelayerImageTag(),
|
|
buildDatahaven: false, // default to false in the test suite so we can speed up the CI
|
|
...this.options.networkOptions
|
|
});
|
|
|
|
// Create test connectors
|
|
this.connectorFactory = new ConnectorFactory(this.connectors);
|
|
this.testConnectors = await this.connectorFactory.createTestConnectors();
|
|
|
|
// Allow derived classes to perform additional setup
|
|
await this.onSetup();
|
|
|
|
logger.success(`Test suite setup complete: ${this.options.suiteName}`);
|
|
} catch (error) {
|
|
logger.error(`Failed to setup test suite: ${this.options.suiteName}`, error);
|
|
this.manager.failSuite(this.options.suiteName);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
logger.info(`🧹 Tearing down test suite: ${this.options.suiteName}`);
|
|
|
|
try {
|
|
if (this.options.keepAlive && !isCI) {
|
|
this.printNetworkInfo();
|
|
await this.waitForEnter();
|
|
}
|
|
|
|
// Allow derived classes to perform cleanup
|
|
await this.onTeardown();
|
|
|
|
// Cleanup test connectors
|
|
if (this.testConnectors && this.connectorFactory) {
|
|
await this.connectorFactory.cleanup(this.testConnectors);
|
|
}
|
|
|
|
// Cleanup the network
|
|
if (this.connectors?.cleanup) {
|
|
await this.connectors.cleanup();
|
|
}
|
|
|
|
// Mark suite as completed
|
|
this.manager.completeSuite(this.options.suiteName);
|
|
|
|
logger.success(`Test suite teardown complete: ${this.options.suiteName}`);
|
|
} catch (error) {
|
|
logger.error(`Error during test suite teardown: ${this.options.suiteName}`, error);
|
|
this.manager.failSuite(this.options.suiteName);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Override this method to perform additional setup after network launch
|
|
*/
|
|
protected async onSetup(): Promise<void> {
|
|
// Default implementation does nothing
|
|
}
|
|
|
|
/**
|
|
* Override this method to perform cleanup before network teardown
|
|
*/
|
|
protected async onTeardown(): Promise<void> {
|
|
// Default implementation does nothing
|
|
}
|
|
|
|
/**
|
|
* Get network connectors - throws if not initialized
|
|
*/
|
|
protected getConnectors(): LaunchNetworkResult {
|
|
if (!this.connectors) {
|
|
throw new Error("Network connectors not initialized. Did you call setupHooks()?");
|
|
}
|
|
return this.connectors;
|
|
}
|
|
|
|
/**
|
|
* Get test connectors - throws if not initialized
|
|
*/
|
|
public getTestConnectors(): TestConnectors {
|
|
if (!this.testConnectors) {
|
|
throw new Error("Test connectors not initialized. Did you call setupHooks()?");
|
|
}
|
|
return this.testConnectors;
|
|
}
|
|
|
|
/**
|
|
* Get connector factory - throws if not initialized
|
|
*/
|
|
public getConnectorFactory(): ConnectorFactory {
|
|
if (!this.connectorFactory) {
|
|
throw new Error("Connector factory not initialized. Did you call setupHooks()?");
|
|
}
|
|
return this.connectorFactory;
|
|
}
|
|
|
|
private printNetworkInfo(): void {
|
|
try {
|
|
const connectors = this.getConnectors();
|
|
const ln = connectors.launchedNetwork;
|
|
logger.info("🛠 Keep-alive mode enabled. Network will remain running until you press Enter.");
|
|
logger.info("📡 Network info:");
|
|
logger.info(` • Network ID: ${ln.networkId}`);
|
|
logger.info(` • Network Name: ${ln.networkName}`);
|
|
logger.info(` • DataHaven RPC: ${connectors.dataHavenRpcUrl}`);
|
|
logger.info(` • Ethereum RPC: ${connectors.ethereumRpcUrl}`);
|
|
logger.info(` • Ethereum CL: ${connectors.ethereumClEndpoint}`);
|
|
const containers = ln.containers || [];
|
|
if (containers.length > 0) {
|
|
logger.info(" • Containers:");
|
|
for (const c of containers) {
|
|
const pubPorts = Object.entries(c.publicPorts || {})
|
|
.map(([k, v]) => `${k}:${v}`)
|
|
.join(", ");
|
|
logger.info(` - ${c.name} [${pubPorts}]`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logger.warn("Could not print network info", e as Error);
|
|
}
|
|
}
|
|
|
|
private async waitForEnter(): Promise<void> {
|
|
return await new Promise<void>((resolve) => {
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
rl.question("\nPress Enter to teardown and cleanup... ", () => {
|
|
rl.close();
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
}
|