datahaven/test/utils/shell.ts
Facundo Farall 9b311e00ef
test: 🏗️ Setup e2e testing framework (#104)
## 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>
2025-07-16 18:51:07 +02:00

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;
}
};