#!/usr/bin/env node /** * 测试趋势分析工具 * 收集和分析历史测试数据,识别测试质量变化趋势 */ const fs = require('fs'); const path = require('path'); const os = require('os'); class TestTrendAnalyzer { constructor() { this.trendDataPath = path.join(process.cwd(), 'test-results', 'trends.json'); this.historyDataPath = path.join(process.cwd(), 'test-results', 'history'); this.trendData = this.loadTrendData(); } loadTrendData() { try { if (fs.existsSync(this.trendDataPath)) { return JSON.parse(fs.readFileSync(this.trendDataPath, 'utf-8')); } } catch (error) { console.warn('加载趋势数据失败:', error.message); } return { runs: [], summary: { totalRuns: 0, avgPassRate: 0, avgDuration: 0, trend: 'stable', lastUpdated: null, }, }; } saveTrendData() { const dir = path.dirname(this.trendDataPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(this.trendDataPath, JSON.stringify(this.trendData, null, 2), 'utf-8'); } addTestRun(testResults) { const run = { timestamp: new Date().toISOString(), summary: { total: testResults.summary?.total || 0, passed: testResults.summary?.passed || 0, failed: testResults.summary?.failed || 0, skipped: testResults.summary?.skipped || 0, flaky: testResults.summary?.flaky || 0, passRate: testResults.summary?.passRate || 0, failRate: testResults.summary?.failRate || 0, skipRate: testResults.summary?.skipRate || 0, flakyRate: testResults.summary?.flakyRate || 0, totalDuration: testResults.summary?.totalDuration || 0, avgDuration: testResults.summary?.avgDuration || 0, }, failedTests: testResults.failedTests || [], slowestTests: testResults.slowestTests || [], environment: this.getEnvironmentInfo(), }; this.trendData.runs.push(run); this.updateSummary(); this.saveTrendData(); this.saveHistory(run); return run; } updateSummary() { const runs = this.trendData.runs; const recentRuns = runs.slice(-10); this.trendData.summary.totalRuns = runs.length; this.trendData.summary.avgPassRate = this.calculateAverage(recentRuns, 'passRate'); this.trendData.summary.avgDuration = this.calculateAverage(recentRuns, 'totalDuration'); this.trendData.summary.trend = this.analyzeTrend(); this.trendData.summary.lastUpdated = new Date().toISOString(); } calculateAverage(runs, field) { if (runs.length === 0) return 0; const sum = runs.reduce((acc, run) => acc + (run.summary[field] || 0), 0); return sum / runs.length; } analyzeTrend() { const runs = this.trendData.runs; if (runs.length < 3) return 'stable'; const recentPassRates = runs.slice(-5).map(r => r.summary.passRate); const avgPassRate = recentPassRates.reduce((a, b) => a + b, 0) / recentPassRates.length; const latestPassRate = recentPassRates[recentPassRates.length - 1]; if (latestPassRate < avgPassRate - 5) { return 'degrading'; } else if (latestPassRate > avgPassRate + 5) { return 'improving'; } else { return 'stable'; } } saveHistory(run) { const dir = this.historyDataPath; if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const filename = `run-${Date.now()}.json`; const filepath = path.join(dir, filename); fs.writeFileSync(filepath, JSON.stringify(run, null, 2), 'utf-8'); } getEnvironmentInfo() { return { platform: os.platform(), arch: os.arch(), nodeVersion: process.version, hostname: os.hostname(), cpus: os.cpus().length, totalMemory: os.totalmem(), freeMemory: os.freemem(), }; } generateTrendReport() { const runs = this.trendData.runs; const summary = this.trendData.summary; if (runs.length === 0) { console.log('暂无测试数据'); return; } console.log(''); console.log('═══════════════════════════════════════════'); console.log('📈 测试趋势分析报告'); console.log('═══════════════════════════════════════════'); console.log(''); console.log(`📊 总运行次数: ${summary.totalRuns}`); console.log(`📈 平均通过率: ${summary.avgPassRate.toFixed(2)}%`); console.log(`⏱️ 平均耗时: ${this.formatDuration(summary.avgDuration)}`); console.log(`📉 趋势: ${this.getTrendEmoji(summary.trend)} ${summary.trend.toUpperCase()}`); console.log(''); const recentRuns = runs.slice(-10); console.log('📅 最近10次运行:'); recentRuns.forEach((run, index) => { const date = new Date(run.timestamp); const dateStr = date.toLocaleString('zh-CN'); const passRate = run.summary.passRate.toFixed(2); const duration = this.formatDuration(run.summary.totalDuration); console.log(` ${index + 1}. ${dateStr} - 通过率: ${passRate}% - 耗时: ${duration}`); }); console.log(''); this.analyzeFlakyTests(); this.analyzeSlowTests(); this.analyzeFailedTests(); this.generateRecommendations(); } analyzeFlakyTests() { const runs = this.trendData.runs; const flakyTestMap = new Map(); runs.forEach(run => { run.failedTests.forEach(test => { const key = `${test.title}`; if (!flakyTestMap.has(key)) { flakyTestMap.set(key, { title: test.title, failures: 0, runs: 0, }); } flakyTestMap.get(key).failures++; }); flakyTestMap.forEach(test => { test.runs++; }); }); const flakyTests = Array.from(flakyTestMap.values()) .filter(test => test.failures >= 2) .sort((a, b) => b.failures - a.failures) .slice(0, 10); if (flakyTests.length > 0) { console.log('🔄 不稳定测试 (失败2次以上):'); flakyTests.forEach((test, index) => { const failRate = ((test.failures / test.runs) * 100).toFixed(2); console.log(` ${index + 1}. ${test.title} - 失败: ${test.failures}/${test.runs} (${failRate}%)`); }); console.log(''); } } analyzeSlowTests() { const runs = this.trendData.runs; const slowTestMap = new Map(); runs.forEach(run => { run.slowestTests.forEach(test => { const key = `${test.title}`; if (!slowTestMap.has(key)) { slowTestMap.set(key, { title: test.title, durations: [], }); } slowTestMap.get(key).durations.push(test.duration); }); }); const slowTests = Array.from(slowTestMap.values()) .map(test => ({ title: test.title, avgDuration: test.durations.reduce((a, b) => a + b, 0) / test.durations.length, maxDuration: Math.max(...test.durations), runs: test.durations.length, })) .sort((a, b) => b.avgDuration - a.avgDuration) .slice(0, 10); if (slowTests.length > 0) { console.log('🐌 最慢的测试 (平均耗时):'); slowTests.forEach((test, index) => { console.log(` ${index + 1}. ${test.title} - 平均: ${this.formatDuration(test.avgDuration)} - 最大: ${this.formatDuration(test.maxDuration)}`); }); console.log(''); } } analyzeFailedTests() { const runs = this.trendData.runs; const failedTestMap = new Map(); runs.forEach(run => { run.failedTests.forEach(test => { const key = `${test.title}`; if (!failedTestMap.has(key)) { failedTestMap.set(key, { title: test.title, failures: 0, lastFailure: null, errorMessages: new Set(), }); } failedTestMap.get(key).failures++; failedTestMap.get(key).lastFailure = run.timestamp; if (test.error) { failedTestMap.get(key).errorMessages.add(test.error); } }); }); const failedTests = Array.from(failedTestMap.values()) .sort((a, b) => b.failures - a.failures) .slice(0, 10); if (failedTests.length > 0) { console.log('❌ 最常失败的测试:'); failedTests.forEach((test, index) => { const lastFailure = new Date(test.lastFailure).toLocaleString('zh-CN'); console.log(` ${index + 1}. ${test.title} - 失败: ${test.failures}次 - 最后失败: ${lastFailure}`); }); console.log(''); } } generateRecommendations() { const summary = this.trendData.summary; const runs = this.trendData.runs; const recommendations = []; if (summary.trend === 'degrading') { recommendations.push('⚠️ 测试通过率呈下降趋势,建议检查最近的代码变更'); } const recentFlakyRate = runs.slice(-5).reduce((sum, run) => sum + run.summary.flakyRate, 0) / 5; if (recentFlakyRate > 10) { recommendations.push('🔄 不稳定测试比例较高,建议优化测试稳定性'); } const recentAvgDuration = runs.slice(-5).reduce((sum, run) => sum + run.summary.totalDuration, 0) / 5; if (recentAvgDuration > 300000) { recommendations.push('⏱️ 测试执行时间较长,建议优化测试性能'); } if (recommendations.length > 0) { console.log('💡 改进建议:'); recommendations.forEach(rec => { console.log(` ${rec}`); }); console.log(''); } } getTrendEmoji(trend) { switch (trend) { case 'improving': return '📈'; case 'degrading': return '📉'; default: return '➡️'; } } formatDuration(ms) { if (ms < 1000) { return `${ms}ms`; } else if (ms < 60000) { return `${(ms / 1000).toFixed(1)}s`; } else { return `${(ms / 60000).toFixed(1)}m`; } } } // 命令行接口 if (require.main === module) { const analyzer = new TestTrendAnalyzer(); const command = process.argv[2]; switch (command) { case 'add': const resultsFile = process.argv[3]; if (resultsFile && fs.existsSync(resultsFile)) { const testResults = JSON.parse(fs.readFileSync(resultsFile, 'utf-8')); analyzer.addTestRun(testResults); console.log('✅ 测试数据已添加'); } else { console.error('❌ 错误: 请提供有效的测试结果文件'); process.exit(1); } break; case 'report': analyzer.generateTrendReport(); break; case 'export': const exportFile = process.argv[3] || 'test-trends.json'; fs.writeFileSync(exportFile, JSON.stringify(analyzer.trendData, null, 2), 'utf-8'); console.log(`✅ 趋势数据已导出到: ${exportFile}`); break; default: console.log('测试趋势分析工具'); console.log(''); console.log('用法:'); console.log(' node testTrendAnalyzer.js add - 添加测试结果'); console.log(' node testTrendAnalyzer.js report - 生成趋势报告'); console.log(' node testTrendAnalyzer.js export [file.json] - 导出趋势数据'); console.log(''); break; } } module.exports = TestTrendAnalyzer;