import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { testMapping, moduleToTests } from '../config/test-mapping.config'; export interface TestSelectionResult { selectedTests: string[]; affectedModules: string[]; changedFiles: string[]; analysisReport: string; } export class SmartTestSelector { private projectRoot: string; constructor(projectRoot: string = process.cwd()) { this.projectRoot = projectRoot; } /** * 根据代码变更选择测试用例 */ selectTestsByChanges( changedFiles: string[], options: { includeRelated?: boolean; priority?: 'high' | 'medium' | 'low' | 'all'; testLevel?: 'smoke' | 'functional' | 'all'; } = {} ): TestSelectionResult { const { includeRelated = true, priority = 'all', testLevel = 'all', } = options; const selectedTests = new Set(); const affectedModules = new Set(); // 分析每个变更文件 for (const file of changedFiles) { const normalizedPath = this.normalizePath(file); const mapping = this.findMapping(normalizedPath); if (mapping) { // 添加直接关联的测试 mapping.tests.forEach(test => selectedTests.add(test)); mapping.modules.forEach(module => affectedModules.add(module)); // 如果启用关联分析,添加相关模块的测试 if (includeRelated) { this.addRelatedTests(mapping.modules, selectedTests, affectedModules); } } } // 根据优先级过滤 const filteredTests = this.filterByPriority( Array.from(selectedTests), priority ); // 根据测试级别过滤 const finalTests = this.filterByTestLevel(filteredTests, testLevel); // 生成分析报告 const analysisReport = this.generateAnalysisReport({ changedFiles, affectedModules: Array.from(affectedModules), selectedTests: finalTests, }); return { selectedTests: finalTests, affectedModules: Array.from(affectedModules), changedFiles, analysisReport, }; } /** * 从Git获取变更文件 */ getChangedFilesFromGit( baseBranch: string = 'origin/main', headBranch: string = 'HEAD' ): string[] { try { const output = execSync( `git diff --name-only ${baseBranch}...${headBranch}`, { encoding: 'utf-8', cwd: this.projectRoot } ); return output .split('\n') .filter(file => file.trim() && this.isSourceFile(file)); } catch (error) { console.error('Failed to get changed files from git:', error); return []; } } /** * 规范化文件路径 */ private normalizePath(filePath: string): string { return filePath.replace(/\\/g, '/').replace(/^\.\//, ''); } /** * 查找文件对应的测试映射 */ private findMapping(normalizedPath: string) { // 精确匹配 if (testMapping[normalizedPath]) { return testMapping[normalizedPath]; } // 模糊匹配(支持通配符) for (const [pattern, mapping] of Object.entries(testMapping)) { if (this.matchPattern(normalizedPath, pattern)) { return mapping; } } return null; } /** * 简单的模式匹配 */ private matchPattern(path: string, pattern: string): boolean { const regex = new RegExp( '^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$' ); return regex.test(path); } /** * 添加相关测试 */ private addRelatedTests( modules: string[], selectedTests: Set, affectedModules: Set ): void { for (const module of modules) { const relatedTests = moduleToTests[module]; if (relatedTests) { relatedTests.forEach(test => selectedTests.add(test)); } } } /** * 根据优先级过滤测试 */ private filterByPriority( tests: string[], priority: 'high' | 'medium' | 'low' | 'all' ): string[] { if (priority === 'all') { return tests; } const priorityMap = { high: ['@p0', '@smoke'], medium: ['@p1', '@functional'], low: ['@p2', '@edge'], }; const targetTags = priorityMap[priority]; return tests.filter(test => targetTags.some(tag => test.includes(tag)) ); } /** * 根据测试级别过滤 */ private filterByTestLevel( tests: string[], level: 'smoke' | 'functional' | 'all' ): string[] { if (level === 'all') { return tests; } const levelMap = { smoke: '@smoke', functional: '@functional', }; const targetTag = levelMap[level]; return tests.filter(test => test.includes(targetTag)); } /** * 判断是否为源代码文件 */ private isSourceFile(filePath: string): boolean { const extensions = ['.ts', '.tsx', '.js', '.jsx', '.vue', '.java']; return extensions.some(ext => filePath.endsWith(ext)); } /** * 生成分析报告 */ private generateAnalysisReport(data: { changedFiles: string[]; affectedModules: string[]; selectedTests: string[]; }): string { return ` # 智能测试选择分析报告 ## 变更文件 (${data.changedFiles.length}个) ${data.changedFiles.map(f => `- ${f}`).join('\n')} ## 受影响模块 (${data.affectedModules.length}个) ${data.affectedModules.map(m => `- ${m}`).join('\n')} ## 选中测试用例 (${data.selectedTests.length}个) ${data.selectedTests.map(t => `- ${t}`).join('\n')} ## 执行建议 - 优先执行冒烟测试(@smoke标签) - 然后执行功能测试(@functional标签) - 最后执行边缘场景测试(@edge标签) `.trim(); } }