import { exec, spawn, ChildProcess } from 'child_process'; import { promisify } from 'util'; import * as fs from 'fs'; import * as path from 'path'; import { logger } from '../utils/logger'; import { TestEnvironment } from '../models/test-result'; const execAsync = promisify(exec); interface ServiceConfig { name: string; command: string; cwd?: string; healthCheckUrl?: string; healthCheckCommand?: string; port?: number; startupTimeout: number; env?: Record; } export class TestEnvironmentManager { private processes: Map = new Map(); private projectRoot: string; private testRoot: string; private environment: TestEnvironment | null = null; constructor() { this.testRoot = process.cwd(); this.projectRoot = path.dirname(this.testRoot); } async prepare(): Promise { logger.section('环境准备'); logger.info('检查系统环境...'); await this.checkSystemRequirements(); logger.info('检查并安装依赖...'); await this.installDependencies(); logger.info('收集环境信息...'); this.environment = await this.collectEnvironmentInfo(); logger.info('环境准备完成', { environment: this.environment }); return this.environment; } private async checkSystemRequirements(): Promise { const requirements = [ { name: 'Node.js', command: 'node --version', minVersion: '18.0.0' }, { name: 'npm', command: 'npm --version', minVersion: '9.0.0' } ]; for (const req of requirements) { try { const { stdout } = await execAsync(req.command); const version = stdout.trim(); logger.debug(`${req.name} 版本: ${version}`); } catch (error) { throw new Error(`${req.name} 未安装或版本过低,最低要求: ${req.minVersion}`); } } } private async installDependencies(): Promise { const nodeModulesPath = path.join(this.testRoot, 'node_modules'); if (!fs.existsSync(nodeModulesPath)) { logger.info('安装 npm 依赖...'); await execAsync('npm install', { cwd: this.testRoot }); } const playwrightPath = path.join(nodeModulesPath, '@playwright'); if (!fs.existsSync(playwrightPath)) { logger.info('安装 Playwright 浏览器...'); await execAsync('npx playwright install', { cwd: this.testRoot }); } } private async collectEnvironmentInfo(): Promise { const { stdout: nodeVersion } = await execAsync('node --version'); let browserVersions: Record = {}; try { const { stdout } = await execAsync('npx playwright --version', { cwd: this.testRoot }); browserVersions['playwright'] = stdout.trim(); } catch { browserVersions['playwright'] = 'unknown'; } return { nodeVersion: nodeVersion.trim(), os: process.platform, browserVersions, apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:8080', adminBaseUrl: process.env.ADMIN_BASE_URL || 'http://localhost:5174', uniappBaseUrl: process.env.UNIAPP_BASE_URL || 'http://localhost:8081', databaseType: process.env.DB_TYPE || 'h2', timestamp: new Date() }; } async startAPIService(): Promise { logger.section('启动 API 服务'); const apiProjectPath = path.join(this.projectRoot, 'everything-is-suitable-api'); if (!fs.existsSync(apiProjectPath)) { logger.warn('API 项目目录不存在,跳过 API 服务启动'); return; } const serviceConfig: ServiceConfig = { name: 'api', command: 'mvn', cwd: apiProjectPath, port: 8080, startupTimeout: 120000, env: { SPRING_PROFILES_ACTIVE: 'test', SPRING_DATASOURCE_URL: 'jdbc:h2:mem:testdb', SPRING_DATASOURCE_DRIVER_CLASS_NAME: 'org.h2.Driver', SPRING_JPA_DATABASE_PLATFORM: 'org.hibernate.dialect.H2Dialect' } }; await this.startService(serviceConfig, ['spring-boot:run', '-Dspring-boot.run.profiles=test']); } async startAdminService(): Promise { logger.section('启动 Admin 服务'); const adminProjectPath = path.join(this.projectRoot, 'everything-is-suitable-admin'); if (!fs.existsSync(adminProjectPath)) { logger.warn('Admin 项目目录不存在,跳过 Admin 服务启动'); return; } const serviceConfig: ServiceConfig = { name: 'admin', command: 'npm', cwd: adminProjectPath, port: 5174, startupTimeout: 60000, env: {} }; if (!fs.existsSync(path.join(adminProjectPath, 'node_modules'))) { logger.info('安装 Admin 项目依赖...'); await execAsync('npm install', { cwd: adminProjectPath }); } await this.startService(serviceConfig, ['run', 'dev']); } async startUniappService(): Promise { logger.section('启动 Uniapp 服务'); const uniappProjectPath = path.join(this.projectRoot, 'everything-is-suitable-uniapp'); if (!fs.existsSync(uniappProjectPath)) { logger.warn('Uniapp 项目目录不存在,跳过 Uniapp 服务启动'); return; } const serviceConfig: ServiceConfig = { name: 'uniapp', command: 'npm', cwd: uniappProjectPath, port: 8081, startupTimeout: 60000, env: {} }; if (!fs.existsSync(path.join(uniappProjectPath, 'node_modules'))) { logger.info('安装 Uniapp 项目依赖...'); await execAsync('npm install', { cwd: uniappProjectPath }); } await this.startService(serviceConfig, ['run', 'dev:h5']); } private async startService(config: ServiceConfig, args: string[]): Promise { logger.info(`启动 ${config.name} 服务...`); const env = { ...process.env, ...config.env }; const childProcess = spawn(config.command, args, { cwd: config.cwd, env, stdio: ['ignore', 'pipe', 'pipe'], detached: false }); this.processes.set(config.name, childProcess); childProcess.stdout?.on('data', (data) => { const output = data.toString(); logger.debug(`[${config.name}] ${output.trim()}`); }); childProcess.stderr?.on('data', (data) => { const output = data.toString(); logger.debug(`[${config.name}] ERROR: ${output.trim()}`); }); childProcess.on('error', (error) => { logger.error(`${config.name} 服务启动失败`, error); }); await this.waitForServiceReady(config); logger.info(`${config.name} 服务已启动`); } private async waitForServiceReady(config: ServiceConfig): Promise { const startTime = Date.now(); const timeout = config.startupTimeout; while (Date.now() - startTime < timeout) { try { if (config.port) { const isReady = await this.checkPort(config.port); if (isReady) { return; } } await this.sleep(1000); } catch (error) { logger.debug(`等待 ${config.name} 服务就绪...`); } } throw new Error(`${config.name} 服务启动超时 (${timeout}ms)`); } private async checkPort(port: number): Promise { try { const { stdout } = await execAsync(`lsof -i :${port} -t`); return stdout.trim().length > 0; } catch { return false; } } async checkServiceHealth(name: string, url: string): Promise { try { const { stdout } = await execAsync(`curl -s -o /dev/null -w "%{http_code}" ${url}`); const statusCode = parseInt(stdout.trim(), 10); return statusCode >= 200 && statusCode < 300; } catch { return false; } } async stopAllServices(): Promise { logger.section('停止所有服务'); const entries = Array.from(this.processes.entries()); for (const [name, process] of entries) { logger.info(`停止 ${name} 服务...`); try { process.kill('SIGTERM'); logger.info(`${name} 服务已停止`); } catch (error) { logger.warn(`停止 ${name} 服务时出错`, { error }); } } this.processes.clear(); } async cleanup(): Promise { logger.section('清理环境'); await this.stopAllServices(); logger.info('环境清理完成'); } private sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } getEnvironment(): TestEnvironment | null { return this.environment; } }