189 lines
6.4 KiB
JavaScript
189 lines
6.4 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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();
|
|||
|
|
}
|