AI_UIAutomation/scripts/sync-ones-results.ts

161 lines
5.9 KiB
TypeScript
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.

/**
* ONES 测试计划结果同步脚本
*
* 用法:
* npx ts-node scripts/sync-ones-results.ts --plan <PLAN_UUID> [--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 <PLAN_UUID> [--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<number, number> } {
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<number, number>();
for (const [num, c] of Object.entries<any>(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();