/** * ONES 测试计划结果同步脚本 * * 用法: * npx ts-node scripts/sync-ones-results.ts --plan [--dry-run] * * 流程: * 1. 读取 reports/.results.json (自动化执行后的结果) * 2. 从本地参数文件 test-plan/ones-writeback-params.json 取用例(号→uuid)+ 步骤总数 * —— 不再读 ONES(用例固定仅新增,刷新参数用 npm run gen:writeback-params) * 3. 按测试名锚点 [ONES:号(#step)] 精确匹配(支持 step 级);无锚点回退用例名 LCS * 4. 反写结果到 ONES (dry-run 仅打印 payload) */ import * as fs from 'fs'; import * as path from 'path'; import { TestResult } from '../utils/test-reporter'; import { matchResults, buildAnchoredPayloads, postPayloads, OnesPlanCase, OnesUpdatePayload, } from '../utils/ones-sync'; import { execSync } from 'child_process'; const ONES_CLI = '/Users/woan/local/bin/ones'; const RESULTS_FILE = path.resolve(__dirname, '../reports/.results.json'); const PARAMS_FILE = path.resolve(__dirname, '../test-plan/ones-writeback-params.json'); function parseArgs() { const args = process.argv.slice(2); let planUUID = ''; let dryRun = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--plan' && args[i + 1]) { planUUID = args[i + 1]; i++; } else if (args[i] === '--dry-run') { dryRun = true; } } if (!planUUID) { console.error('Usage: npx ts-node scripts/sync-ones-results.ts --plan [--dry-run]'); process.exit(1); } return { planUUID, dryRun }; } function loadResults(): TestResult[] { if (!fs.existsSync(RESULTS_FILE)) { console.error(`结果文件不存在: ${RESULTS_FILE}`); console.error('请先运行自动化测试以生成结果文件'); process.exit(1); } const data = JSON.parse(fs.readFileSync(RESULTS_FILE, 'utf-8')); return data.results || []; } /** 从本地参数文件取 用例(OnesPlanCase[]) + 控制用例步骤总数(号→step数)。不读 ONES。 */ function loadParams(): { planCases: OnesPlanCase[]; totalStepsByNumber: Map } { if (!fs.existsSync(PARAMS_FILE)) { console.error(`参数文件不存在: ${PARAMS_FILE}`); console.error('请先运行: npm run gen:writeback-params'); process.exit(1); } const p = JSON.parse(fs.readFileSync(PARAMS_FILE, 'utf-8')); const planCases: OnesPlanCase[] = (p.cases || []).map((c: any) => ({ key: '', caseUUID: c.uuid, caseName: c.name, caseNumber: c.number, currentResult: 'to_do', })); const totalStepsByNumber = new Map(); for (const [num, c] of Object.entries(p.controlCases || {})) { totalStepsByNumber.set(Number(num), (c.steps || []).length); } return { planCases, totalStepsByNumber }; } function main() { const { planUUID, dryRun } = parseArgs(); const config = JSON.parse(execSync(`${ONES_CLI} config show`, { encoding: 'utf-8' })); const executor = config.user_id; console.log('='.repeat(60)); console.log(' ONES 测试计划结果同步'); console.log('='.repeat(60)); console.log(` 计划UUID: ${planUUID}`); console.log(` 模式: ${dryRun ? '预览 (dry-run)' : '实际写入'}`); console.log('-'.repeat(60)); // 1. 加载自动化结果 const testResults = loadResults(); console.log(`\n[1/4] 加载自动化结果: ${testResults.length} 条`); const passed = testResults.filter(r => r.status === 'PASS').length; const failed = testResults.filter(r => r.status === 'FAIL').length; const skipped = testResults.filter(r => r.status === 'SKIP').length; console.log(` PASS: ${passed} | FAIL: ${failed} | SKIP: ${skipped}`); // 2. 从本地参数文件取用例 + 步骤总数(不读 ONES) console.log(`\n[2/4] 读取本地回写参数 ...`); const { planCases, totalStepsByNumber } = loadParams(); console.log(` 参数: ${planCases.length} 用例, ${totalStepsByNumber.size} 个控制用例步骤数`); // 3. 匹配:锚点优先(支持 step 级),无锚点回退 LCS console.log(`\n[3/4] 匹配 (锚点优先 + LCS 兜底) ...`); const { payloads: anchored, unanchored } = buildAnchoredPayloads( planCases, testResults, executor, { totalStepsByNumber } ); const anchoredUUIDs = new Set(anchored.map(p => p.uuid)); const stepCount = anchored.reduce((n, p) => n + p.steps.filter(s => s.execute_result).length, 0); console.log(` 锚点匹配: ${anchored.length} 用例 (含 ${stepCount} 个 step 级结果)`); // LCS 兜底:仅补充锚点未覆盖的用例 const lcs = matchResults(planCases, unanchored); const lcsPayloads: OnesUpdatePayload[] = []; for (const [, { caseUUID, result }] of lcs) { if (anchoredUUIDs.has(caseUUID)) continue; lcsPayloads.push({ uuid: caseUUID, executor, note: '', result, steps: [] }); } console.log(` LCS 兜底: ${lcsPayloads.length} 用例 (未锚点剩余 ${unanchored.length} 条)`); const payloads = [...anchored, ...lcsPayloads]; if (payloads.length > 0) { console.log('\n 回写预览:'); for (const p of payloads) { const pc = planCases.find(c => c.caseUUID === p.uuid); const icon = p.result === 'passed' ? '✓' : p.result === 'failed' ? '✗' : '○'; const stepInfo = p.steps.length ? ` (${p.steps.length} steps)` : ''; console.log(` ${icon} [${p.result}] ${pc?.caseName || p.uuid}${stepInfo}`); } } // 4. 反写 if (dryRun) { console.log(`\n[4/4] DRY-RUN:将更新 ${payloads.length} 条用例,跳过实际写入`); console.log(' 完整 payload (供核对 step 字段格式):'); console.log(JSON.stringify({ cases: payloads }, null, 2)); } else { console.log(`\n[4/4] 反写结果到 ONES ...`); const { success, failed: failCount } = postPayloads(planUUID, payloads); console.log(` 成功: ${success} | 失败: ${failCount}`); } console.log('\n' + '='.repeat(60)); console.log(' 同步完成'); console.log('='.repeat(60)); } main();