datahaven/test/framework/manager.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

83 lines
2.3 KiB
TypeScript

import { logger } from "utils";
export interface TestSuiteRegistry {
suiteId: string;
networkId: string;
startTime: number;
status: "running" | "completed" | "failed";
}
/**
* Manager for tracking running test suites and ensuring cleanup
*/
export class TestSuiteManager {
private static instance: TestSuiteManager;
private suites: Map<string, TestSuiteRegistry> = new Map();
private constructor() {
// Set up process exit handlers to ensure cleanup
process.on("exit", () => this.cleanupAll());
process.on("SIGINT", () => this.cleanupAll());
process.on("SIGTERM", () => this.cleanupAll());
}
static getInstance(): TestSuiteManager {
if (!TestSuiteManager.instance) {
TestSuiteManager.instance = new TestSuiteManager();
}
return TestSuiteManager.instance;
}
registerSuite(suiteId: string, networkId: string): void {
if (this.suites.has(suiteId)) {
throw new Error(`Test suite ${suiteId} is already registered`);
}
this.suites.set(suiteId, {
suiteId,
networkId,
startTime: Date.now(),
status: "running"
});
logger.debug(`Registered test suite: ${suiteId} with network: ${networkId}`);
}
completeSuite(suiteId: string): void {
const suite = this.suites.get(suiteId);
if (suite) {
suite.status = "completed";
const duration = ((Date.now() - suite.startTime) / 1000).toFixed(1);
logger.debug(`Test suite ${suiteId} completed in ${duration}s`);
}
}
failSuite(suiteId: string): void {
const suite = this.suites.get(suiteId);
if (suite) {
suite.status = "failed";
logger.debug(`Test suite ${suiteId} failed`);
}
}
getRunningCount(): number {
return Array.from(this.suites.values()).filter((s) => s.status === "running").length;
}
getRunningNetworkIds(): string[] {
return Array.from(this.suites.values())
.filter((s) => s.status === "running")
.map((s) => s.networkId);
}
private cleanupAll(): void {
const runningSuites = Array.from(this.suites.values()).filter((s) => s.status === "running");
if (runningSuites.length > 0) {
logger.warn(`⚠️ Process exiting with ${runningSuites.length} test suite(s) still running`);
runningSuites.forEach((suite) => {
logger.warn(` - ${suite.suiteId} (network: ${suite.networkId})`);
});
}
}
}