mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## Implement E2E Testing Framework with Isolated Networks ### Summary Refactors the existing E2E testing infrastructure to provide isolated test environments with parallel execution support. Each test suite now runs in its own network namespace, preventing resource conflicts. ### Key Changes - **New Testing Framework** (`test/framework/`): Base classes for test lifecycle management with automatic setup/teardown - **Launcher Module** (`test/launcher/`): Extracted network orchestration logic from CLI handlers for reusability - **Parallel Execution**: Added `test-parallel.ts` script with concurrency limits to prevent resource exhaustion - **Test Isolation**: Each suite gets unique network IDs (format: `suiteName-timestamp`) and Docker networks - **Improved Test Organization**: Migrated tests to new framework, deprecated old test structure ### Test Improvements - Added 4 new test suites demonstrating framework usage. : - `contracts.test.ts` - Smart contract deployment/interaction - `datahaven-substrate.test.ts` - Substrate API operations - `cross-chain.test.ts` - Snowbridge cross-chain messaging - `ethereum-basic.test.ts` - Ethereum network operations > [!WARNING] The test suites themselves are bad and shouldn't be consider examples of good tests. They were AI generated just to test the concurrency of test runners ### Documentation - Added comprehensive framework overview (`E2E_FRAMEWORK_OVERVIEW.md`) - Updated README with parallel testing commands - Added test patterns and best practices ### Breaking Changes - Old test suites moved to `e2e - DEPRECATED/` directory - Test execution now requires extending `BaseTestSuite` class ### Testing Run tests with: `bun test:e2e` or `bun test:e2e:parallel` (with concurrency limits) ### TODO - [ ] Implement good test examples. - [ ] Implement useful test utils (like waiting for an event to show up in DataHaven or Ethereum). - [ ] Enforce tests with CI (currently cannot be done due to intermittent error when sending a transaction with PAPI). --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: undercover-cactus <lola@moonsonglabs.com>
101 lines
2.6 KiB
TypeScript
101 lines
2.6 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { spawn } from "bun";
|
|
import { logger } from "./logger";
|
|
|
|
export type LogLevel = "info" | "debug" | "error" | "warn";
|
|
|
|
export const runShellCommandWithLogger = async (
|
|
command: string,
|
|
options?: {
|
|
cwd?: string;
|
|
env?: object;
|
|
logLevel?: LogLevel;
|
|
waitFor?: (...args: unknown[]) => Promise<void>;
|
|
}
|
|
) => {
|
|
const { cwd = ".", env = {}, logLevel = "info" as LogLevel } = options || {};
|
|
|
|
try {
|
|
if (!existsSync(cwd)) {
|
|
logger.error("❌ CWD does not exist:", cwd);
|
|
throw new Error("❌ CWD does not exist");
|
|
}
|
|
|
|
const proc = spawn(["sh", "-c", command], {
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: {
|
|
...process.env,
|
|
...env
|
|
}
|
|
});
|
|
|
|
const stdoutReader = proc.stdout.getReader();
|
|
const stderrReader = proc.stderr.getReader();
|
|
|
|
let stderrBuffer = "";
|
|
|
|
const readStream = async (
|
|
reader: typeof stdoutReader,
|
|
streamName: string,
|
|
logLevel: LogLevel
|
|
) => {
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
const text = new TextDecoder().decode(value);
|
|
const trimmedText = text.trim();
|
|
if (trimmedText) {
|
|
logger[logLevel](
|
|
trimmedText.includes("\n") ? `>_ \n${trimmedText}` : `>_ ${trimmedText}`
|
|
);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
logger.error(`Error reading from ${streamName} stream:`, err);
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
};
|
|
|
|
const readStderr = async () => {
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await stderrReader.read();
|
|
if (done) break;
|
|
stderrBuffer += new TextDecoder().decode(value);
|
|
}
|
|
} catch (err) {
|
|
logger.error("Error reading from stderr stream:", err);
|
|
} finally {
|
|
stderrReader.releaseLock();
|
|
}
|
|
};
|
|
|
|
await Promise.all([readStream(stdoutReader, "stdout", logLevel), readStderr()]);
|
|
|
|
if (options?.waitFor) {
|
|
await options.waitFor();
|
|
}
|
|
|
|
const exitCode = await proc.exited;
|
|
|
|
// Only log stderr if the command failed
|
|
if (exitCode !== 0) {
|
|
logger.error("❌ Command failed with exit code:", exitCode);
|
|
const trimmedStderr = stderrBuffer.trim();
|
|
if (trimmedStderr) {
|
|
logger.error("Stderr:");
|
|
logger.error(
|
|
trimmedStderr.includes("\n") ? `>_ \n${trimmedStderr}` : `>_ ${trimmedStderr}`
|
|
);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
logger.error("❌ Error running shell command:", command, "in", cwd);
|
|
logger.error(err);
|
|
throw err;
|
|
}
|
|
};
|