feat(ones-sync): step 级结果回写支持
- buildAnchoredPayloads: 按测试名锚点 [ONES:号(#step)] 精确匹配, step 级结果填入 step.execute_result,并聚合出 case 级结果(fail>skip>pass) - postPayloads: 抽出的批量 POST,syncResultsToOnes 复用 - sync-ones-results.ts: 锚点优先 + LCS 兜底,dry-run 打印完整 payload 供核对 注: 控制必测 2 条超级用例(15974/15975)按 step 回写;step 执行结果字段 用 execute_result(待一次受控 live 写入验证字段被接受)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cf994c1aad
commit
88435b0ffc
|
|
@ -7,14 +7,21 @@
|
|||
* 流程:
|
||||
* 1. 读取 reports/.results.json (自动化执行后的结果)
|
||||
* 2. 从 ONES 拉取测试计划用例列表
|
||||
* 3. 按用例名称匹配
|
||||
* 4. 反写匹配成功的结果到 ONES
|
||||
* 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 { fetchPlanCases, matchResults, syncResultsToOnes } from '../utils/ones-sync';
|
||||
import {
|
||||
fetchPlanCases,
|
||||
matchResults,
|
||||
buildAnchoredPayloads,
|
||||
postPayloads,
|
||||
OnesUpdatePayload,
|
||||
} from '../utils/ones-sync';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const ONES_CLI = '/Users/woan/local/bin/ones';
|
||||
|
|
@ -56,6 +63,9 @@ function loadResults(): TestResult[] {
|
|||
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));
|
||||
|
|
@ -76,29 +86,41 @@ function main() {
|
|||
const planCases = fetchPlanCases(planUUID);
|
||||
console.log(` 计划共 ${planCases.length} 条用例`);
|
||||
|
||||
// 3. 匹配
|
||||
console.log(`\n[3/4] 匹配自动化结果到 ONES 用例 ...`);
|
||||
const matched = matchResults(planCases, testResults);
|
||||
console.log(` 匹配成功: ${matched.size} / ${testResults.length}`);
|
||||
// 3. 匹配:锚点优先(支持 step 级),无锚点回退 LCS
|
||||
console.log(`\n[3/4] 匹配 (锚点优先 + LCS 兜底) ...`);
|
||||
const { payloads: anchored, unanchored } = buildAnchoredPayloads(planCases, testResults, executor);
|
||||
const anchoredUUIDs = new Set(anchored.map(p => p.uuid));
|
||||
const stepCount = anchored.reduce((n, p) => n + p.steps.length, 0);
|
||||
console.log(` 锚点匹配: ${anchored.length} 用例 (含 ${stepCount} 个 step 级结果)`);
|
||||
|
||||
if (matched.size > 0) {
|
||||
console.log('\n 匹配详情:');
|
||||
for (const [caseUUID, { result }] of matched) {
|
||||
const pc = planCases.find(c => c.caseUUID === caseUUID);
|
||||
const icon = result === 'passed' ? '✓' : result === 'failed' ? '✗' : '○';
|
||||
console.log(` ${icon} [${result}] ${pc?.caseName || caseUUID}`);
|
||||
// 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 模式,跳过实际写入`);
|
||||
console.log(` 将会更新 ${matched.size} 条用例结果`);
|
||||
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 configStr = execSync(`${ONES_CLI} config show`, { encoding: 'utf-8' });
|
||||
const config = JSON.parse(configStr);
|
||||
const { success, failed: failCount } = syncResultsToOnes(planUUID, matched, config.user_id);
|
||||
const { success, failed: failCount } = postPayloads(planUUID, payloads);
|
||||
console.log(` 成功: ${success} | 失败: ${failCount}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,34 @@ export interface OnesPlanCase {
|
|||
executor?: string;
|
||||
}
|
||||
|
||||
export interface OnesStepResult {
|
||||
uuid: string;
|
||||
// 执行结果填到 execute_result(step.result 是预期文本,属定义不可覆盖)
|
||||
execute_result: 'passed' | 'failed' | 'skipped';
|
||||
actual_result?: string;
|
||||
}
|
||||
|
||||
export interface OnesUpdatePayload {
|
||||
uuid: string;
|
||||
executor: string;
|
||||
note: string;
|
||||
result: 'passed' | 'failed' | 'skipped' | 'to_do';
|
||||
steps: { uuid: string }[];
|
||||
steps: OnesStepResult[];
|
||||
}
|
||||
|
||||
// 测试名锚点:[ONES:15974] 或 [ONES:15974#<stepUuid>](控制为 step 级)
|
||||
const ONES_ANCHOR = /\[ONES:(\d+)(?:#([A-Za-z0-9_-]+))?\]/;
|
||||
|
||||
type SyncStatus = 'passed' | 'failed' | 'skipped';
|
||||
|
||||
function toStatus(s: TestResult['status']): SyncStatus {
|
||||
return s === 'PASS' ? 'passed' : s === 'FAIL' ? 'failed' : 'skipped';
|
||||
}
|
||||
|
||||
// 聚合多个 step 结果为 case 结果:fail > skip > pass
|
||||
function mergeStatus(a: SyncStatus | undefined, b: SyncStatus): SyncStatus {
|
||||
const rank = (s?: SyncStatus) => (s === 'failed' ? 2 : s === 'skipped' ? 1 : 0);
|
||||
return !a || rank(b) > rank(a) ? b : a;
|
||||
}
|
||||
|
||||
function runOnesGraphQL(query: string): any {
|
||||
|
|
@ -119,52 +141,93 @@ function lcs(a: string, b: string): number {
|
|||
}
|
||||
|
||||
/**
|
||||
* 反写结果到 ONES 测试计划
|
||||
*
|
||||
* API: POST /project/api/project/team/{team_uuid}/testcase/plan/{plan_uuid}/cases/update
|
||||
* Body: [{ uuid, executor, note, result, steps: [{ uuid }] }]
|
||||
* 按测试名锚点构建回写 payload(支持 case 级与 step 级)。
|
||||
* - [ONES:<num>] → case 级结果
|
||||
* - [ONES:<num>#<stepUuid>] → step 级结果,并聚合出 case 级结果
|
||||
* 无锚点的结果不在此处理(交由 matchResults LCS 兜底)。
|
||||
*/
|
||||
export function syncResultsToOnes(
|
||||
planUUID: string,
|
||||
results: Map<string, { caseUUID: string; result: 'passed' | 'failed' | 'skipped' }>,
|
||||
executorUUID: string
|
||||
): { success: number; failed: number } {
|
||||
if (results.size === 0) return { success: 0, failed: 0 };
|
||||
export function buildAnchoredPayloads(
|
||||
planCases: OnesPlanCase[],
|
||||
testResults: TestResult[],
|
||||
executor: string
|
||||
): { payloads: OnesUpdatePayload[]; unanchored: TestResult[] } {
|
||||
const byNumber = new Map<number, OnesPlanCase>();
|
||||
for (const pc of planCases) byNumber.set(pc.caseNumber, pc);
|
||||
|
||||
const payload: OnesUpdatePayload[] = [];
|
||||
for (const [, { caseUUID, result }] of results) {
|
||||
payload.push({
|
||||
uuid: caseUUID,
|
||||
executor: executorUUID,
|
||||
note: '',
|
||||
result,
|
||||
steps: [],
|
||||
const caseResult = new Map<string, SyncStatus>();
|
||||
const caseSteps = new Map<string, Map<string, OnesStepResult>>();
|
||||
const unanchored: TestResult[] = [];
|
||||
|
||||
for (const tr of testResults) {
|
||||
const m = ONES_ANCHOR.exec(tr.name);
|
||||
if (!m) {
|
||||
unanchored.push(tr);
|
||||
continue;
|
||||
}
|
||||
const pc = byNumber.get(parseInt(m[1], 10));
|
||||
if (!pc) continue; // 锚点用例不在本计划内
|
||||
const status = toStatus(tr.status);
|
||||
caseResult.set(pc.caseUUID, mergeStatus(caseResult.get(pc.caseUUID), status));
|
||||
if (m[2]) {
|
||||
if (!caseSteps.has(pc.caseUUID)) caseSteps.set(pc.caseUUID, new Map());
|
||||
caseSteps.get(pc.caseUUID)!.set(m[2], {
|
||||
uuid: m[2],
|
||||
execute_result: status,
|
||||
actual_result: (tr.detail || '').slice(0, 500),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 分批提交 (每批最多 50 条)
|
||||
const payloads: OnesUpdatePayload[] = [];
|
||||
for (const [caseUUID, result] of caseResult) {
|
||||
const steps = caseSteps.has(caseUUID) ? Array.from(caseSteps.get(caseUUID)!.values()) : [];
|
||||
payloads.push({ uuid: caseUUID, executor, note: '', result, steps });
|
||||
}
|
||||
return { payloads, unanchored };
|
||||
}
|
||||
|
||||
/**
|
||||
* 分批 POST 回写 payload 到 ONES 测试计划。
|
||||
*/
|
||||
export function postPayloads(
|
||||
planUUID: string,
|
||||
payload: OnesUpdatePayload[]
|
||||
): { success: number; failed: number } {
|
||||
if (payload.length === 0) return { success: 0, failed: 0 };
|
||||
const batchSize = 50;
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (let i = 0; i < payload.length; i += batchSize) {
|
||||
const batch = payload.slice(i, i + batchSize);
|
||||
try {
|
||||
const bodyJson = JSON.stringify(batch);
|
||||
const cmd = `${ONES_CLI} graphql --raw-post "/testcase/plan/${planUUID}/cases/update" '${bodyJson.replace(/'/g, "'\\''")}'`;
|
||||
// 由于 ones CLI 可能不支持 raw-post,直接用 curl
|
||||
const curlCmd = buildCurlCommand(planUUID, batch);
|
||||
execSync(curlCmd, { encoding: 'utf-8', timeout: 30000 });
|
||||
execSync(buildCurlCommand(planUUID, batch), { encoding: 'utf-8', timeout: 30000 });
|
||||
success += batch.length;
|
||||
} catch (e: any) {
|
||||
console.error(`ONES sync batch failed: ${e.message}`);
|
||||
failed += batch.length;
|
||||
}
|
||||
}
|
||||
|
||||
return { success, failed };
|
||||
}
|
||||
|
||||
/**
|
||||
* 反写结果到 ONES 测试计划
|
||||
*
|
||||
* API: POST /project/api/project/team/{team_uuid}/testcase/plan/{plan_uuid}/cases/update
|
||||
* Body: { cases: [{ uuid, executor, note, result, steps: [{ uuid, execute_result }] }] }
|
||||
*/
|
||||
export function syncResultsToOnes(
|
||||
planUUID: string,
|
||||
results: Map<string, { caseUUID: string; result: 'passed' | 'failed' | 'skipped' }>,
|
||||
executorUUID: string
|
||||
): { success: number; failed: number } {
|
||||
const payload: OnesUpdatePayload[] = [];
|
||||
for (const [, { caseUUID, result }] of results) {
|
||||
payload.push({ uuid: caseUUID, executor: executorUUID, note: '', result, steps: [] });
|
||||
}
|
||||
return postPayloads(planUUID, payload);
|
||||
}
|
||||
|
||||
function buildCurlCommand(planUUID: string, payload: OnesUpdatePayload[]): string {
|
||||
const configStr = execSync(`${ONES_CLI} config show`, { encoding: 'utf-8' });
|
||||
const config = JSON.parse(configStr);
|
||||
|
|
|
|||
Loading…
Reference in New Issue