189 lines
6.4 KiB
JavaScript
Raw Normal View History

/**
* GMP-Agent 测试报告生成器
* test-runner.js · GH-GMP-004 · 录册A02
*
* 功能
* - 自动发现 test/ 目录下的测试文件test-*.js
* - 运行每个测试文件捕获结果
* - 生成结构化JSON测试报告
* - 输出人类可读的摘要
* - 退出码反映测试结果0=全通过, 1=有失败
*
* 环境要求Node.js 20+ · 纯标准库 · 无第三方依赖
*
* 用法
* node gmp-agent/src/test-runner.js [--dir=test/] [--report=test-report.json] [--verbose]
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
// ─── 配置 ───────────────────────────────────────────────
const DEFAULT_TEST_DIR = path.join(__dirname, '..', 'test');
const DEFAULT_REPORT_FILE = path.join(__dirname, '..', 'test-report.json');
// ─── 测试发现 ──────────────────────────────────────────
/**
* 发现测试文件
* @param {string} testDir - 测试目录
* @returns {string[]} 测试文件路径列表
*/
function discoverTests(testDir) {
if (!fs.existsSync(testDir)) {
console.warn(`[test-runner] Test directory not found: ${testDir}`);
return [];
}
return fs.readdirSync(testDir)
.filter(f => f.startsWith('test-') && f.endsWith('.js'))
.map(f => path.join(testDir, f))
.sort();
}
// ─── 测试执行 ──────────────────────────────────────────
/**
* 运行单个测试文件
* @param {string} testFile - 测试文件路径
* @param {boolean} verbose - 是否输出详细信息
* @returns {object} { file, name, status, duration, output, error? }
*/
function runTest(testFile, verbose = false) {
const name = path.basename(testFile, '.js');
const startTime = Date.now();
const result = {
file: testFile,
name,
status: 'pass', // pass | fail | error | skip
duration: 0,
output: '',
error: null,
};
try {
const output = execFileSync('node', [testFile], {
timeout: 30000, // 30秒超时
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, GMP_TEST_MODE: 'true' },
});
result.output = output.trim();
result.status = 'pass';
} catch (err) {
result.status = err.killed ? 'error' : 'fail';
result.output = (err.stdout || '').trim();
result.error = (err.stderr || err.message || '').trim();
}
result.duration = Date.now() - startTime;
if (verbose) {
const icon = result.status === 'pass' ? '✅' : result.status === 'fail' ? '❌' : '⚠️';
console.log(` ${icon} ${name} (${result.duration}ms)`);
if (result.error) {
console.log(` Error: ${result.error.split('\n')[0]}`);
}
}
return result;
}
// ─── 报告生成 ──────────────────────────────────────────
/**
* 生成测试报告
* @param {Array<object>} results - 测试结果列表
* @returns {object} 结构化测试报告
*/
function generateReport(results) {
const summary = {
total: results.length,
pass: results.filter(r => r.status === 'pass').length,
fail: results.filter(r => r.status === 'fail').length,
error: results.filter(r => r.status === 'error').length,
skip: results.filter(r => r.status === 'skip').length,
totalDuration: results.reduce((sum, r) => sum + r.duration, 0),
};
return {
reportVersion: '1.0.0',
generator: 'gmp-agent/test-runner · LC-A02',
timestamp: new Date().toISOString(),
environment: {
node: process.version,
platform: process.platform,
arch: process.arch,
},
summary,
verdict: summary.fail === 0 && summary.error === 0 ? 'PASS' : 'FAIL',
results,
};
}
/**
* 打印人类可读摘要
*/
function printSummary(report) {
console.log('\n══════════════════════════════════════');
console.log(' GMP-Agent Test Report');
console.log('══════════════════════════════════════');
console.log(` Timestamp : ${report.timestamp}`);
console.log(` Node : ${report.environment.node}`);
console.log(` Total : ${report.summary.total}`);
console.log(` Pass : ${report.summary.pass}`);
console.log(` Fail : ${report.summary.fail}`);
console.log(` Error : ${report.summary.error}`);
console.log(` Skip : ${report.summary.skip}`);
console.log(` Duration : ${report.summary.totalDuration}ms`);
console.log(` Verdict : ${report.verdict}`);
console.log('══════════════════════════════════════\n');
}
// ─── 主流程 ──────────────────────────────────────────
function main() {
const args = process.argv.slice(2);
const verbose = args.includes('--verbose');
const testDirArg = args.find(a => a.startsWith('--dir='));
const reportArg = args.find(a => a.startsWith('--report='));
const testDir = testDirArg ? testDirArg.split('=')[1] : DEFAULT_TEST_DIR;
const reportFile = reportArg ? reportArg.split('=')[1] : DEFAULT_REPORT_FILE;
console.log(`[test-runner] Discovering tests in: ${testDir}`);
const testFiles = discoverTests(testDir);
if (testFiles.length === 0) {
console.log('[test-runner] No test files found.');
process.exit(0);
}
console.log(`[test-runner] Found ${testFiles.length} test file(s)\n`);
const results = testFiles.map(f => runTest(f, verbose));
const report = generateReport(results);
// 写报告文件
try {
fs.writeFileSync(reportFile, JSON.stringify(report, null, 2), 'utf-8');
console.log(`[test-runner] Report saved to: ${reportFile}`);
} catch (err) {
console.error(`[test-runner] Failed to save report: ${err.message}`);
}
printSummary(report);
// 退出码
process.exit(report.verdict === 'PASS' ? 0 : 1);
}
// ─── 导出(供其他模块调用)+ CLI入口 ────────────────────
module.exports = { discoverTests, runTest, generateReport, printSummary };
if (require.main === module) {
main();
}