2026-05-10 13:12:44 +08:00

189 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();
}