integration tests
This commit is contained in:
189
tests/integration/harness/docker-manager.ts
Normal file
189
tests/integration/harness/docker-manager.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Docker Manager for Integration Tests
|
||||
* Manages Docker Compose services for integration testing
|
||||
*/
|
||||
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
|
||||
export interface DockerServiceConfig {
|
||||
name: string;
|
||||
port: number;
|
||||
healthCheck: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export class DockerManager {
|
||||
private static instance: DockerManager;
|
||||
private services: Map<string, boolean> = new Map();
|
||||
private composeProject = 'gridpilot-test';
|
||||
private composeFile = 'docker-compose.test.yml';
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): DockerManager {
|
||||
if (!DockerManager.instance) {
|
||||
DockerManager.instance = new DockerManager();
|
||||
}
|
||||
return DockerManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Docker services are already running
|
||||
*/
|
||||
isRunning(): boolean {
|
||||
try {
|
||||
const output = execSync(
|
||||
`docker-compose -p ${this.composeProject} -f ${this.composeFile} ps -q 2>/dev/null || true`,
|
||||
{ encoding: 'utf8' }
|
||||
).trim();
|
||||
return output.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Docker services with dependency checking
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
console.log('[DockerManager] Starting test environment...');
|
||||
|
||||
if (this.isRunning()) {
|
||||
console.log('[DockerManager] Services already running, checking health...');
|
||||
await this.waitForServices();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start services
|
||||
execSync(
|
||||
`COMPOSE_PARALLEL_LIMIT=1 docker-compose -p ${this.composeProject} -f ${this.composeFile} up -d ready api`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
|
||||
console.log('[DockerManager] Services starting, waiting for health...');
|
||||
await this.waitForServices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all services to be healthy using polling
|
||||
*/
|
||||
async waitForServices(): Promise<void> {
|
||||
const services: DockerServiceConfig[] = [
|
||||
{
|
||||
name: 'db',
|
||||
port: 5433,
|
||||
healthCheck: 'pg_isready -U gridpilot_test_user -d gridpilot_test',
|
||||
timeout: 60000
|
||||
},
|
||||
{
|
||||
name: 'api',
|
||||
port: 3101,
|
||||
healthCheck: 'curl -f http://localhost:3101/health',
|
||||
timeout: 90000
|
||||
}
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
await this.waitForService(service);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a single service to be healthy
|
||||
*/
|
||||
async waitForService(config: DockerServiceConfig): Promise<void> {
|
||||
const timeout = config.timeout || 30000;
|
||||
const startTime = Date.now();
|
||||
|
||||
console.log(`[DockerManager] Waiting for ${config.name}...`);
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
// Try health check command
|
||||
if (config.name === 'db') {
|
||||
// For DB, check if it's ready to accept connections
|
||||
try {
|
||||
execSync(
|
||||
`docker exec ${this.composeProject}-${config.name}-1 ${config.healthCheck} 2>/dev/null`,
|
||||
{ stdio: 'pipe' }
|
||||
);
|
||||
console.log(`[DockerManager] ✓ ${config.name} is healthy`);
|
||||
return;
|
||||
} catch {}
|
||||
} else {
|
||||
// For API, check HTTP endpoint
|
||||
const response = await fetch(`http://localhost:${config.port}/health`);
|
||||
if (response.ok) {
|
||||
console.log(`[DockerManager] ✓ ${config.name} is healthy`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Service not ready yet, continue waiting
|
||||
}
|
||||
|
||||
await setTimeout(1000);
|
||||
}
|
||||
|
||||
throw new Error(`[DockerManager] ${config.name} failed to become healthy within ${timeout}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Docker services
|
||||
*/
|
||||
stop(): void {
|
||||
console.log('[DockerManager] Stopping test environment...');
|
||||
try {
|
||||
execSync(
|
||||
`docker-compose -p ${this.composeProject} -f ${this.composeFile} down --remove-orphans`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[DockerManager] Warning: Failed to stop services cleanly:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up volumes and containers
|
||||
*/
|
||||
clean(): void {
|
||||
console.log('[DockerManager] Cleaning up test environment...');
|
||||
try {
|
||||
execSync(
|
||||
`docker-compose -p ${this.composeProject} -f ${this.composeFile} down -v --remove-orphans --volumes`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[DockerManager] Warning: Failed to clean up cleanly:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command in a service container
|
||||
*/
|
||||
execInService(service: string, command: string): string {
|
||||
try {
|
||||
return execSync(
|
||||
`docker exec ${this.composeProject}-${service}-1 ${command}`,
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to execute command in ${service}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service logs
|
||||
*/
|
||||
getLogs(service: string): string {
|
||||
try {
|
||||
return execSync(
|
||||
`docker logs ${this.composeProject}-${service}-1 --tail 100`,
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
} catch (error) {
|
||||
return `Failed to get logs: ${error}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user