fix(ones-sync): step 回写按 ONES 形态列全量 step
经实测确认 ONES 写接口形态:case result 可保持 to_do,只在跑过的 step 上 填 execute_result,未跑 step 仅列 uuid,且需列出该用例全部 step。 - OnesStepResult.execute_result 改为可选(未跑 step 只带 uuid) - buildAnchoredPayloads 支持 fullStepsByNumber:列全量 step, 全部跑完才聚合 case 结果(passed/failed),否则保持 to_do - fetchCaseSteps: 取用例全部 step uuid - sync 脚本对含 step 锚点的用例先拉全量 step 列表再回写 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
88435b0ffc
commit
fda826446a
|
|
@ -20,6 +20,7 @@ import {
|
||||||
matchResults,
|
matchResults,
|
||||||
buildAnchoredPayloads,
|
buildAnchoredPayloads,
|
||||||
postPayloads,
|
postPayloads,
|
||||||
|
fetchCaseSteps,
|
||||||
OnesUpdatePayload,
|
OnesUpdatePayload,
|
||||||
} from '../utils/ones-sync';
|
} from '../utils/ones-sync';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
|
|
@ -88,9 +89,23 @@ function main() {
|
||||||
|
|
||||||
// 3. 匹配:锚点优先(支持 step 级),无锚点回退 LCS
|
// 3. 匹配:锚点优先(支持 step 级),无锚点回退 LCS
|
||||||
console.log(`\n[3/4] 匹配 (锚点优先 + LCS 兜底) ...`);
|
console.log(`\n[3/4] 匹配 (锚点优先 + LCS 兜底) ...`);
|
||||||
const { payloads: anchored, unanchored } = buildAnchoredPayloads(planCases, testResults, executor);
|
// 含 step 锚点的用例需列全量 step → 先取其完整 step 列表
|
||||||
|
const stepNums = new Set<number>();
|
||||||
|
for (const tr of testResults) {
|
||||||
|
const m = /\[ONES:(\d+)#/.exec(tr.name);
|
||||||
|
if (m) stepNums.add(parseInt(m[1], 10));
|
||||||
|
}
|
||||||
|
const fullStepsByNumber = new Map<number, string[]>();
|
||||||
|
for (const num of stepNums) fullStepsByNumber.set(num, fetchCaseSteps(num));
|
||||||
|
|
||||||
|
const { payloads: anchored, unanchored } = buildAnchoredPayloads(
|
||||||
|
planCases,
|
||||||
|
testResults,
|
||||||
|
executor,
|
||||||
|
{ fullStepsByNumber }
|
||||||
|
);
|
||||||
const anchoredUUIDs = new Set(anchored.map(p => p.uuid));
|
const anchoredUUIDs = new Set(anchored.map(p => p.uuid));
|
||||||
const stepCount = anchored.reduce((n, p) => n + p.steps.length, 0);
|
const stepCount = anchored.reduce((n, p) => n + p.steps.filter(s => s.execute_result).length, 0);
|
||||||
console.log(` 锚点匹配: ${anchored.length} 用例 (含 ${stepCount} 个 step 级结果)`);
|
console.log(` 锚点匹配: ${anchored.length} 用例 (含 ${stepCount} 个 step 级结果)`);
|
||||||
|
|
||||||
// LCS 兜底:仅补充锚点未覆盖的用例
|
// LCS 兜底:仅补充锚点未覆盖的用例
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,9 @@ export interface OnesPlanCase {
|
||||||
|
|
||||||
export interface OnesStepResult {
|
export interface OnesStepResult {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
// 执行结果填到 execute_result(step.result 是预期文本,属定义不可覆盖)
|
// 执行结果填到 execute_result(step.result 是预期文本,属定义不可覆盖)。
|
||||||
execute_result: 'passed' | 'failed' | 'skipped';
|
// 未执行的 step 只列 uuid、不带 execute_result。
|
||||||
|
execute_result?: 'passed' | 'failed' | 'skipped';
|
||||||
actual_result?: string;
|
actual_result?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,22 +141,43 @@ function lcs(a: string, b: string): number {
|
||||||
return dp[m][n];
|
return dp[m][n];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取某用例的全部 step uuid(按定义顺序),用于回写时列全量 step。
|
||||||
|
*/
|
||||||
|
export function fetchCaseSteps(caseNumber: number): string[] {
|
||||||
|
try {
|
||||||
|
const out = execSync(`${ONES_CLI} testcase case search --key ${caseNumber}`, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
const c = JSON.parse(out).cases?.[0];
|
||||||
|
return (c?.steps || []).map((s: any) => s.uuid);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按测试名锚点构建回写 payload(支持 case 级与 step 级)。
|
* 按测试名锚点构建回写 payload(支持 case 级与 step 级)。
|
||||||
* - [ONES:<num>] → case 级结果
|
* - [ONES:<num>] → case 级结果
|
||||||
* - [ONES:<num>#<stepUuid>] → step 级结果,并聚合出 case 级结果
|
* - [ONES:<num>#<stepUuid>] → step 级结果
|
||||||
* 无锚点的结果不在此处理(交由 matchResults LCS 兜底)。
|
* 无锚点的结果不在此处理(交由 matchResults LCS 兜底)。
|
||||||
|
*
|
||||||
|
* opts.fullStepsByNumber: 用例号 → 全部 step uuid。提供时按 ONES 要求列全量 step,
|
||||||
|
* 只给跑过的填 execute_result,未跑的仅列 uuid;全部跑完才聚合 case 结果,否则保持 to_do。
|
||||||
*/
|
*/
|
||||||
export function buildAnchoredPayloads(
|
export function buildAnchoredPayloads(
|
||||||
planCases: OnesPlanCase[],
|
planCases: OnesPlanCase[],
|
||||||
testResults: TestResult[],
|
testResults: TestResult[],
|
||||||
executor: string
|
executor: string,
|
||||||
|
opts: { fullStepsByNumber?: Map<number, string[]> } = {}
|
||||||
): { payloads: OnesUpdatePayload[]; unanchored: TestResult[] } {
|
): { payloads: OnesUpdatePayload[]; unanchored: TestResult[] } {
|
||||||
const byNumber = new Map<number, OnesPlanCase>();
|
const byNumber = new Map<number, OnesPlanCase>();
|
||||||
for (const pc of planCases) byNumber.set(pc.caseNumber, pc);
|
for (const pc of planCases) byNumber.set(pc.caseNumber, pc);
|
||||||
|
|
||||||
const caseResult = new Map<string, SyncStatus>();
|
const caseLevel = new Map<string, SyncStatus>(); // caseUUID -> result (add/feature)
|
||||||
const caseSteps = new Map<string, Map<string, OnesStepResult>>();
|
const runSteps = new Map<string, Map<string, OnesStepResult>>(); // caseUUID -> stepUuid -> result
|
||||||
|
const numberByUUID = new Map<string, number>();
|
||||||
const unanchored: TestResult[] = [];
|
const unanchored: TestResult[] = [];
|
||||||
|
|
||||||
for (const tr of testResults) {
|
for (const tr of testResults) {
|
||||||
|
|
@ -166,23 +188,47 @@ export function buildAnchoredPayloads(
|
||||||
}
|
}
|
||||||
const pc = byNumber.get(parseInt(m[1], 10));
|
const pc = byNumber.get(parseInt(m[1], 10));
|
||||||
if (!pc) continue; // 锚点用例不在本计划内
|
if (!pc) continue; // 锚点用例不在本计划内
|
||||||
|
numberByUUID.set(pc.caseUUID, pc.caseNumber);
|
||||||
const status = toStatus(tr.status);
|
const status = toStatus(tr.status);
|
||||||
caseResult.set(pc.caseUUID, mergeStatus(caseResult.get(pc.caseUUID), status));
|
|
||||||
if (m[2]) {
|
if (m[2]) {
|
||||||
if (!caseSteps.has(pc.caseUUID)) caseSteps.set(pc.caseUUID, new Map());
|
if (!runSteps.has(pc.caseUUID)) runSteps.set(pc.caseUUID, new Map());
|
||||||
caseSteps.get(pc.caseUUID)!.set(m[2], {
|
runSteps.get(pc.caseUUID)!.set(m[2], {
|
||||||
uuid: m[2],
|
uuid: m[2],
|
||||||
execute_result: status,
|
execute_result: status,
|
||||||
actual_result: (tr.detail || '').slice(0, 500),
|
actual_result: (tr.detail || '').slice(0, 500),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
caseLevel.set(pc.caseUUID, mergeStatus(caseLevel.get(pc.caseUUID), status));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloads: OnesUpdatePayload[] = [];
|
const payloads: OnesUpdatePayload[] = [];
|
||||||
for (const [caseUUID, result] of caseResult) {
|
|
||||||
const steps = caseSteps.has(caseUUID) ? Array.from(caseSteps.get(caseUUID)!.values()) : [];
|
// step 级用例:列全量 step(未跑的仅 uuid),全部跑完才聚合 case 结果
|
||||||
|
for (const [caseUUID, stepMap] of runSteps) {
|
||||||
|
const num = numberByUUID.get(caseUUID)!;
|
||||||
|
const full = opts.fullStepsByNumber?.get(num);
|
||||||
|
let steps: OnesStepResult[];
|
||||||
|
let allRun: boolean;
|
||||||
|
if (full && full.length) {
|
||||||
|
steps = full.map((uuid) => stepMap.get(uuid) ?? { uuid });
|
||||||
|
allRun = full.every((uuid) => stepMap.has(uuid));
|
||||||
|
} else {
|
||||||
|
steps = Array.from(stepMap.values());
|
||||||
|
allRun = true; // 无全量列表时,以已有结果为准
|
||||||
|
}
|
||||||
|
let agg: SyncStatus | undefined;
|
||||||
|
for (const s of stepMap.values()) if (s.execute_result) agg = mergeStatus(agg, s.execute_result);
|
||||||
|
const result: OnesUpdatePayload['result'] = allRun ? agg ?? 'to_do' : 'to_do';
|
||||||
payloads.push({ uuid: caseUUID, executor, note: '', result, steps });
|
payloads.push({ uuid: caseUUID, executor, note: '', result, steps });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// case 级用例(添加/功能,无 step 结果)
|
||||||
|
for (const [caseUUID, result] of caseLevel) {
|
||||||
|
if (runSteps.has(caseUUID)) continue;
|
||||||
|
payloads.push({ uuid: caseUUID, executor, note: '', result, steps: [] });
|
||||||
|
}
|
||||||
|
|
||||||
return { payloads, unanchored };
|
return { payloads, unanchored };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue