08ea5fbe98
添加用户管理视图、API和状态管理文件
286 lines
8.3 KiB
TypeScript
286 lines
8.3 KiB
TypeScript
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<string, string>;
|
|
}
|
|
|
|
export class TestEnvironmentManager {
|
|
private processes: Map<string, ChildProcess> = 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<TestEnvironment> {
|
|
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<void> {
|
|
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<void> {
|
|
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<TestEnvironment> {
|
|
const { stdout: nodeVersion } = await execAsync('node --version');
|
|
|
|
let browserVersions: Record<string, string> = {};
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<boolean> {
|
|
try {
|
|
const { stdout } = await execAsync(`lsof -i :${port} -t`);
|
|
return stdout.trim().length > 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async checkServiceHealth(name: string, url: string): Promise<boolean> {
|
|
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<void> {
|
|
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<void> {
|
|
logger.section('清理环境');
|
|
await this.stopAllServices();
|
|
logger.info('环境清理完成');
|
|
}
|
|
|
|
private sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
getEnvironment(): TestEnvironment | null {
|
|
return this.environment;
|
|
}
|
|
}
|