611 lines
31 KiB
JavaScript
611 lines
31 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
||
import {
|
||
copyFileSync,
|
||
existsSync,
|
||
mkdirSync,
|
||
readdirSync,
|
||
readFileSync,
|
||
rmSync,
|
||
statSync,
|
||
writeFileSync
|
||
} from "node:fs";
|
||
import { dirname, join, resolve, sep } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const root = resolve(fileURLToPath(new URL("../../..", import.meta.url)));
|
||
const webRoot = join(root, "kdl-wasm", "web");
|
||
const resultRoot = join(webRoot, "test-results", "abb120-spec");
|
||
const specRoot = join(webRoot, "tests", "fixtures", "abb120", "spec-programs");
|
||
const virtualControllerEvidencePath = join(webRoot, "test-results", "virtual-controller", "evidence.json");
|
||
const outputPath = join(root, "work", "doc", "通用机器人离线编程系统测试文档.docx");
|
||
const stagingRoot = join(webRoot, "test-results", "docx-build");
|
||
|
||
const jobId = process.argv[2] ?? latestJobId();
|
||
const jobDir = join(resultRoot, jobId);
|
||
const job = readJson(join(jobDir, "job.json"));
|
||
const report = readJson(join(jobDir, "report.json"));
|
||
const manifest = readJson(join(jobDir, "manifest.json"));
|
||
const uiEvidence = readJson(virtualControllerEvidencePath);
|
||
const generatedAt = formatShanghaiTime(new Date());
|
||
|
||
const descriptionByProgram = {
|
||
W2_00_MinimalModule: "最小模块、language/module/proc main、常量 speed/zone、joint_target、MoveJ 基础执行。",
|
||
W2_10_DataTargetToolFrame: "数据类型、tool、frame、pose、poseq、joint_target、pose_target、工具和工件坐标切换。",
|
||
W2_20_ExpressionMath: "算术、比较、逻辑、括号、单位后缀、pi/e、三角函数和编译期求值。",
|
||
W2_30_MotionAllTypes: "MoveJ、MoveL、MoveC、set_tool、set_frame、set_speed、set_zone 和 inline 参数优先级。",
|
||
W2_40_PathEvents: "path defaults/source、point、event before/after/at distance、run_path。",
|
||
W2_50_OperationProcess: "operation kind/process/start_action/end_action、run_operation。",
|
||
W2_60_IOWaitPulse: "io.di/do/ai/ao/gi/go/ri/ro、wait、timeout、on_timeout、pulse 和边沿条件。",
|
||
W2_70_ControlFlow: "if/elseif/else、while、for、switch、break、continue 流程控制。",
|
||
W2_80_ProcFuncCall: "proc、in/out/inout、call、return、func 返回值和作用域。",
|
||
W2_90_ExceptionAlarm: "alarm、raise、try/catch/finally、trap/task P1 边界诊断。",
|
||
W2_99_FullSpecExample: "完整示例,覆盖 tool/frame、path、operation、IO event、run_operation 和回 home。",
|
||
W2_E10_ExpressionDiagnostics: "表达式负向测试:未知符号、未知函数、参数数量、数学域、除零、单位不匹配。",
|
||
W2_E20_SemanticDiagnostics: "语义负向测试:重复符号、out/inout 非左值、缺失调用目标、函数副作用、缺失目标。",
|
||
W2_E30_MotionDiagnostics: "运动负向测试:关节限位、不可达 pose、空 path 等 KDL/Path 诊断。",
|
||
W2_E40_ControlFlowDiagnostics: "流程控制负向测试:非法 break/continue、jump、重复 case、非常量 case、缺失 label。",
|
||
W2_E50_RuntimeTimeout: "运行时超时负向测试:wait timeout、on_timeout、报警 source map。",
|
||
W2_P10_BrandHints: "品牌提示后处理测试:post_hint、@brand.* 消费或报告。",
|
||
W2_P20_CrossBrandMotion: "跨品牌运动后处理:ABB/FANUC/KUKA motion、speed、zone、IO、wait 映射。",
|
||
W2_P30_Roundtrip: "后处理和导入回读测试:post 后 ABB import roundtrip,记录差异和警告。"
|
||
};
|
||
|
||
const commandResults = [
|
||
{
|
||
command: "npm run typecheck",
|
||
scope: "TypeScript 静态类型检查",
|
||
process: "执行 tsc -p kdl-wasm/web/tsconfig.json --noEmit,检查 Web、GRL、runtime、post 和 suite 代码类型。",
|
||
result: "通过,未输出 TypeScript 错误。"
|
||
},
|
||
{
|
||
command: "npm test",
|
||
scope: "全量 Vitest 自动化测试",
|
||
process: "执行 vitest run --config kdl-wasm/web/vitest.config.ts,覆盖 KDL、GRL、runtime、controller、post/import、workspace、report、docs 和 ABB120 integration。",
|
||
result: "通过,41 个 test files、155 个 tests 全部 passed。"
|
||
},
|
||
{
|
||
command: "npm run suite:abb120-spec",
|
||
scope: "ABB120 GRL 规范程序套件",
|
||
process: "读取 manifest.json 和 19 个 GRL 程序,逐项执行 parse、semantic compile、runtime/static/post 检查,并输出 job/report/program artifacts。",
|
||
result: `通过,生成 ${job.job_id};programs=${report.summary.programs},pass=${report.summary.pass},expected diagnostic=${report.summary.diagnostic},fail=${report.summary.fail},missingSections=${report.coverage.missingSections.length}。`
|
||
},
|
||
{
|
||
command: "npm run verify:virtual-controller",
|
||
scope: "虚拟控制器 UI 和截图验证",
|
||
process: "使用 Chrome DevTools Protocol 打开 virtual-controller.html,分别验证 desktop/mobile 视口、点击 load/run/DI1、检查主要面板、状态、wait 和横向溢出,并截图。",
|
||
result: `通过,desktop=${uiEvidence.desktop.screenshotBytes} bytes,mobile=${uiEvidence.mobile.screenshotBytes} bytes;state=${uiEvidence.desktop.state}/${uiEvidence.mobile.state},wait=${uiEvidence.desktop.wait}/${uiEvidence.mobile.wait}。`
|
||
}
|
||
];
|
||
|
||
const programs = job.programs.map((program) => {
|
||
const sourcePath = join(specRoot, program.file);
|
||
return {
|
||
...program,
|
||
manifestEntry: manifest.programs.find((entry) => entry.program_id === program.program_id),
|
||
sourcePath,
|
||
source: readFileSync(sourcePath, "utf8")
|
||
};
|
||
});
|
||
|
||
buildDocx();
|
||
console.log(JSON.stringify({ outputPath, jobId, programs: programs.length }, null, 2));
|
||
|
||
function buildDocx() {
|
||
assertSafeStaging(stagingRoot);
|
||
rmSync(stagingRoot, { recursive: true, force: true });
|
||
mkdirSync(join(stagingRoot, "_rels"), { recursive: true });
|
||
mkdirSync(join(stagingRoot, "docProps"), { recursive: true });
|
||
mkdirSync(join(stagingRoot, "word", "_rels"), { recursive: true });
|
||
mkdirSync(join(stagingRoot, "word", "media"), { recursive: true });
|
||
|
||
const images = copyImages();
|
||
writeFileSync(join(stagingRoot, "[Content_Types].xml"), contentTypesXml(), "utf8");
|
||
writeFileSync(join(stagingRoot, "_rels", ".rels"), packageRelsXml(), "utf8");
|
||
writeFileSync(join(stagingRoot, "docProps", "core.xml"), corePropsXml(), "utf8");
|
||
writeFileSync(join(stagingRoot, "docProps", "app.xml"), appPropsXml(), "utf8");
|
||
writeFileSync(join(stagingRoot, "word", "styles.xml"), stylesXml(), "utf8");
|
||
writeFileSync(join(stagingRoot, "word", "_rels", "document.xml.rels"), documentRelsXml(images), "utf8");
|
||
writeFileSync(join(stagingRoot, "word", "document.xml"), documentXml(images), "utf8");
|
||
|
||
if (existsSync(outputPath)) {
|
||
rmSync(outputPath, { force: true });
|
||
}
|
||
zipDirectory(stagingRoot, outputPath);
|
||
}
|
||
|
||
function copyImages() {
|
||
const images = [
|
||
{
|
||
id: "rIdImage1",
|
||
fileName: "virtual-controller-desktop.png",
|
||
source: uiEvidence.desktop.screenshot,
|
||
title: "虚拟控制器桌面端截图",
|
||
cx: 5943600,
|
||
cy: 3962400
|
||
},
|
||
{
|
||
id: "rIdImage2",
|
||
fileName: "virtual-controller-mobile.png",
|
||
source: uiEvidence.mobile.screenshot,
|
||
title: "虚拟控制器移动端截图",
|
||
cx: 2743200,
|
||
cy: 5943600
|
||
}
|
||
];
|
||
for (const image of images) {
|
||
copyFileSync(image.source, join(stagingRoot, "word", "media", image.fileName));
|
||
}
|
||
return images;
|
||
}
|
||
|
||
function documentXml(images) {
|
||
const body = [];
|
||
body.push(heading("通用机器人离线编程系统测试文档", 1));
|
||
body.push(p(`生成时间:${generatedAt}`));
|
||
body.push(p(`测试对象:通用机器人离线编程系统 / ABB IRB120 虚拟控制器 / GRL 规范程序套件`));
|
||
body.push(p(`工作区:${root}`));
|
||
body.push(p(`最新测试 job:${job.job_id}`));
|
||
|
||
body.push(heading("一、测试结论", 1));
|
||
body.push(table([
|
||
["项目", "结果"],
|
||
["测试结论", "通过。本轮未发现非预期失败;5 个 diagnostic 程序为预期负向测试。"],
|
||
["规范覆盖", `覆盖章节 ${report.coverage.coveredSections.join("、")};缺失章节:${report.coverage.missingSections.length === 0 ? "无" : report.coverage.missingSections.join("、")}`],
|
||
["程序统计", `共 ${report.summary.programs} 个 GRL 规范程序;pass=${report.summary.pass},expected diagnostic=${report.summary.diagnostic},fail=${report.summary.fail}`],
|
||
["测试层级", `Runtime=${report.coverage.runtimePrograms},Static=${report.coverage.staticPrograms},Post=${report.coverage.postPrograms}`],
|
||
["主要证据", `${relative(jobDir)};${relative(dirname(uiEvidence.desktop.screenshot))}`]
|
||
]));
|
||
|
||
body.push(heading("二、测试范围", 1));
|
||
body.push(p("本次测试覆盖离线编程系统的语法解析、语义编译、KDL 运动规划接口、虚拟控制器状态机、IO/wait/pulse、路径和工艺操作、异常报警、三品牌后处理、导入回读、工作台 UI 响应和截图证据。"));
|
||
body.push(table([
|
||
["范围", "覆盖内容"],
|
||
["GRL 语言", "词法、顶层结构、类型系统、表达式、目标点、速度、zone、motion、path、operation、IO、流程控制、proc/func、异常和 P1 边界。"],
|
||
["运动与 KDL", "ABB IRB120 fixture、MoveJ/MoveL/MoveC、path planning、joint limit、unreachable target、trajectory summary。"],
|
||
["虚拟控制器", "loadProgram、start、stepInto、hold、resume、stepMotion、stop、resetFault、runtime trace、motion queue、IO image。"],
|
||
["后处理与导入", "ABB/FANUC/KUKA 输出文件、post report、ABB import roundtrip。"],
|
||
["用户界面", "virtual-controller.html 桌面端和移动端渲染、交互、主要面板可见性和截图。"]
|
||
]));
|
||
|
||
body.push(heading("三、测试命令与执行过程", 1));
|
||
body.push(table([
|
||
["命令", "测试内容", "执行过程", "执行结果"],
|
||
...commandResults.map((item) => [item.command, item.scope, item.process, item.result])
|
||
]));
|
||
|
||
body.push(heading("四、截图证据", 1));
|
||
body.push(p(`截图验证应用:${uiEvidence.app}`));
|
||
body.push(p(`Chrome:${uiEvidence.chrome}`));
|
||
body.push(heading("4.1 桌面端截图", 2));
|
||
body.push(p(`视口:${uiEvidence.desktop.viewport.width}x${uiEvidence.desktop.viewport.height};state=${uiEvidence.desktop.state};wait=${uiEvidence.desktop.wait};trace=${uiEvidence.desktop.trace};截图:${relative(uiEvidence.desktop.screenshot)}`));
|
||
body.push(imageParagraph(images[0], 1));
|
||
body.push(heading("4.2 移动端截图", 2));
|
||
body.push(p(`视口:${uiEvidence.mobile.viewport.width}x${uiEvidence.mobile.viewport.height},deviceScaleFactor=${uiEvidence.mobile.viewport.deviceScaleFactor};state=${uiEvidence.mobile.state};wait=${uiEvidence.mobile.wait};trace=${uiEvidence.mobile.trace};截图:${relative(uiEvidence.mobile.screenshot)}`));
|
||
body.push(imageParagraph(images[1], 2));
|
||
|
||
body.push(heading("五、规范覆盖矩阵", 1));
|
||
body.push(table([
|
||
["规范章节", "覆盖层级", "程序"],
|
||
...manifest.coverage_matrix.map((item) => [
|
||
String(item.spec_section),
|
||
item.coverage_level,
|
||
item.program_ids.join("、")
|
||
])
|
||
]));
|
||
|
||
body.push(heading("六、全部测试程序明细与源码", 1));
|
||
for (const [index, program] of programs.entries()) {
|
||
appendProgramSection(body, program, index + 1);
|
||
}
|
||
|
||
body.push(heading("七、主要产物索引", 1));
|
||
body.push(table([
|
||
["产物", "路径"],
|
||
["job.json", relative(join(jobDir, "job.json"))],
|
||
["report.json", relative(join(jobDir, "report.json"))],
|
||
["report.html", relative(join(jobDir, "report.html"))],
|
||
["manifest.json", relative(join(jobDir, "manifest.json"))],
|
||
["桌面端截图", relative(uiEvidence.desktop.screenshot)],
|
||
["移动端截图", relative(uiEvidence.mobile.screenshot)],
|
||
["UI evidence.json", relative(virtualControllerEvidencePath)]
|
||
]));
|
||
|
||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
|
||
xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex"
|
||
xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex"
|
||
xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex"
|
||
xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex"
|
||
xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex"
|
||
xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex"
|
||
xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex"
|
||
xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex"
|
||
xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex"
|
||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||
xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink"
|
||
xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d"
|
||
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||
xmlns:oel="http://schemas.microsoft.com/office/2019/extlst"
|
||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
|
||
xmlns:v="urn:schemas-microsoft-com:vml"
|
||
xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
|
||
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||
xmlns:w10="urn:schemas-microsoft-com:office:word"
|
||
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
|
||
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
|
||
xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex"
|
||
xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"
|
||
xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml"
|
||
xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du"
|
||
xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash"
|
||
xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex"
|
||
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
|
||
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
|
||
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
|
||
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
|
||
mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16du wp14">
|
||
<w:body>
|
||
${body.join("\n")}
|
||
<w:sectPr>
|
||
<w:pgSz w:w="11906" w:h="16838"/>
|
||
<w:pgMar w:top="1134" w:right="850" w:bottom="1134" w:left="850" w:header="708" w:footer="708" w:gutter="0"/>
|
||
<w:cols w:space="708"/>
|
||
<w:docGrid w:linePitch="312"/>
|
||
</w:sectPr>
|
||
</w:body>
|
||
</w:document>`;
|
||
}
|
||
|
||
function appendProgramSection(body, program, index) {
|
||
const expectedDiagnostics = program.manifestEntry?.expected_diagnostics ?? [];
|
||
const actualCodes = [...new Set(program.diagnostics.map((diagnostic) => diagnostic.code))];
|
||
body.push(heading(`6.${index} ${program.program_id}`, 2));
|
||
body.push(table([
|
||
["字段", "内容"],
|
||
["测试内容", descriptionByProgram[program.program_id] ?? "按 manifest 执行规范测试。"],
|
||
["源码文件", relative(program.sourcePath)],
|
||
["覆盖层级", program.coverage_level],
|
||
["覆盖章节", program.spec_sections.join("、")],
|
||
["预期状态", program.expected_status],
|
||
["实际状态", program.status],
|
||
["预期诊断", expectedDiagnostics.length === 0 ? "无" : expectedDiagnostics.join("、")],
|
||
["实际诊断代码", actualCodes.length === 0 ? "无" : actualCodes.join("、")],
|
||
["输入哈希", program.input_hash],
|
||
["源码行数", String(program.source.split(/\r?\n/).length)]
|
||
]));
|
||
|
||
body.push(heading("测试过程", 3));
|
||
for (const step of programSteps(program)) {
|
||
body.push(p(step));
|
||
}
|
||
|
||
body.push(heading("执行结果摘要", 3));
|
||
body.push(table(programResultRows(program)));
|
||
|
||
if (program.controller?.commands?.length) {
|
||
body.push(heading("虚拟控制器命令轨迹", 3));
|
||
body.push(table([
|
||
["命令", "前状态", "后状态", "结果", "诊断"],
|
||
...program.controller.commands.map((command) => [
|
||
command.command,
|
||
command.before ?? "",
|
||
command.after ?? "",
|
||
command.result ?? "",
|
||
command.diagnostics.length === 0 ? "无" : command.diagnostics.map((diagnostic) => diagnostic.code).join("、")
|
||
])
|
||
]));
|
||
}
|
||
|
||
body.push(heading("证据文件", 3));
|
||
body.push(table([
|
||
["类型", "路径"],
|
||
...Object.entries(program.artifacts).map(([key, value]) => [key, relative(join(resultRoot, value))])
|
||
]));
|
||
|
||
body.push(heading("程序源码", 3));
|
||
body.push(codeBlock(program.source));
|
||
}
|
||
|
||
function programSteps(program) {
|
||
const steps = [
|
||
`1. 读取源码 ${relative(program.sourcePath)},加载 program_id、spec_sections、coverage_level、expected_status 和 expected_diagnostics。`,
|
||
"2. 执行 parseGrl,完成词法、语法、模块结构和 source map 解析。",
|
||
"3. 执行 compileSemanticProgram,生成语义 IR、符号表、procedure/path/operation、KDL motion/path request 和诊断列表。"
|
||
];
|
||
if (program.coverage_level === "Runtime") {
|
||
steps.push(`4. 加载 IR 到 VirtualController,按 ${manifest.runtime_commands.join(" -> ")} 执行控制器命令,采集 controller snapshot、motion queue、runtime trace、IO image 和 trajectory summary。`);
|
||
steps.push("5. 对比实际状态与 manifest 预期状态;对比实际诊断代码与 expected_diagnostics;写入 compile/controller/motion-queue/trace/io/trajectory/post/roundtrip 证据。");
|
||
} else if (program.coverage_level === "Static") {
|
||
steps.push("4. 执行 static preflight diagnostics,覆盖表达式、语义、运动、path、流程控制等负向检查;不要求进入 happy-path 运行。");
|
||
steps.push("5. 对比实际诊断代码与 expected_diagnostics,确认 diagnostic 状态为预期负向结果,并写入 compile 及各类空/摘要证据文件。");
|
||
} else {
|
||
steps.push("4. 执行 postProcessAllBrands,生成 ABB/FANUC/KUKA 输出文件名和 post report。");
|
||
steps.push("5. 执行 ABB import roundtrip 摘要检查,记录 pass/warn/fail 与导入诊断,并写入 post-report/roundtrip 证据。");
|
||
}
|
||
return steps;
|
||
}
|
||
|
||
function programResultRows(program) {
|
||
const rows = [
|
||
["项目", "结果"],
|
||
["解析", program.compile.parsed ? "parsed=true" : "parsed=false"],
|
||
["模块名", program.compile.moduleName ?? ""],
|
||
["sourceMapEntries", String(program.compile.sourceMapEntries)],
|
||
["symbol/procedure/path/operation", `${program.compile.symbolCount}/${program.compile.procedureCount}/${program.compile.pathCount}/${program.compile.operationCount}`],
|
||
["KDL motion/path requests", `${program.compile.kdlMotionRequests}/${program.compile.kdlPathRequests}`],
|
||
["状态", program.status]
|
||
];
|
||
if (program.controller) {
|
||
rows.push(["controller final state", program.controller.snapshot.state.state]);
|
||
rows.push(["runtime trace events", String(program.trace?.length ?? 0)]);
|
||
rows.push(["motion queue items", String(program.motionQueue?.items?.length ?? 0)]);
|
||
rows.push(["IO image count", String(Object.keys(program.io?.image ?? {}).length)]);
|
||
}
|
||
if (program.trajectory) {
|
||
rows.push(["trajectory sample/duration", `${program.trajectory.sampleCount}/${program.trajectory.duration}`]);
|
||
}
|
||
if (program.post) {
|
||
rows.push(["post filenames", program.post.filenames.join("、")]);
|
||
rows.push(["post report codes", program.post.report.length === 0 ? "无" : [...new Set(program.post.report.map((issue) => issue.code))].join("、")]);
|
||
}
|
||
if (program.roundtrip) {
|
||
rows.push(["roundtrip", `${program.roundtrip.status}; ${program.roundtrip.diagnostics.map((diagnostic) => diagnostic.code).join("、") || "无诊断"}`]);
|
||
}
|
||
rows.push(["诊断数量", String(program.diagnostics.length)]);
|
||
return rows;
|
||
}
|
||
|
||
function heading(text, level) {
|
||
return paragraph(text, `Heading${Math.min(level, 3)}`);
|
||
}
|
||
|
||
function p(text) {
|
||
return paragraph(text, "BodyText");
|
||
}
|
||
|
||
function paragraph(text, style) {
|
||
return `<w:p><w:pPr><w:pStyle w:val="${style}"/></w:pPr>${run(text)}</w:p>`;
|
||
}
|
||
|
||
function run(text) {
|
||
return `<w:r><w:t xml:space="preserve">${escapeXml(String(text))}</w:t></w:r>`;
|
||
}
|
||
|
||
function codeBlock(text) {
|
||
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||
const lines = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
|
||
return lines.map((line) =>
|
||
`<w:p><w:pPr><w:pStyle w:val="Code"/></w:pPr><w:r><w:t xml:space="preserve">${escapeXml(line === "" ? " " : line)}</w:t></w:r></w:p>`
|
||
).join("\n");
|
||
}
|
||
|
||
function table(rows) {
|
||
const width = Math.floor(9000 / Math.max(1, rows[0]?.length ?? 1));
|
||
return `<w:tbl>
|
||
<w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/><w:tblLook w:val="04A0"/></w:tblPr>
|
||
${rows.map((row, rowIndex) => `<w:tr>${row.map((cell) => tableCell(cell, width, rowIndex === 0)).join("")}</w:tr>`).join("\n")}
|
||
</w:tbl>`;
|
||
}
|
||
|
||
function tableCell(text, width, header) {
|
||
return `<w:tc>
|
||
<w:tcPr><w:tcW w:w="${width}" w:type="dxa"/></w:tcPr>
|
||
<w:p><w:pPr>${header ? "<w:pStyle w:val=\"TableHeader\"/>" : ""}</w:pPr>${run(text)}</w:p>
|
||
</w:tc>`;
|
||
}
|
||
|
||
function imageParagraph(image, docPrId) {
|
||
return `<w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:drawing>
|
||
<wp:inline distT="0" distB="0" distL="0" distR="0">
|
||
<wp:extent cx="${image.cx}" cy="${image.cy}"/>
|
||
<wp:effectExtent l="0" t="0" r="0" b="0"/>
|
||
<wp:docPr id="${docPrId}" name="${escapeXml(image.title)}"/>
|
||
<wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>
|
||
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
||
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||
<pic:nvPicPr><pic:cNvPr id="${docPrId}" name="${escapeXml(image.fileName)}"/><pic:cNvPicPr/></pic:nvPicPr>
|
||
<pic:blipFill><a:blip r:embed="${image.id}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
|
||
<pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${image.cx}" cy="${image.cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>
|
||
</pic:pic>
|
||
</a:graphicData>
|
||
</a:graphic>
|
||
</wp:inline>
|
||
</w:drawing></w:r></w:p>`;
|
||
}
|
||
|
||
function contentTypesXml() {
|
||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Default Extension="png" ContentType="image/png"/>
|
||
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
|
||
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
|
||
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
|
||
</Types>`;
|
||
}
|
||
|
||
function packageRelsXml() {
|
||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
|
||
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
|
||
</Relationships>`;
|
||
}
|
||
|
||
function documentRelsXml(images) {
|
||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||
${images.map((image) => `<Relationship Id="${image.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${image.fileName}"/>`).join("\n")}
|
||
</Relationships>`;
|
||
}
|
||
|
||
function corePropsXml() {
|
||
const iso = new Date().toISOString();
|
||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
|
||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||
xmlns:dcterms="http://purl.org/dc/terms/"
|
||
xmlns:dcmitype="http://purl.org/dc/dcmitype/"
|
||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||
<dc:title>通用机器人离线编程系统测试文档</dc:title>
|
||
<dc:creator>Codex</dc:creator>
|
||
<cp:lastModifiedBy>Codex</cp:lastModifiedBy>
|
||
<dcterms:created xsi:type="dcterms:W3CDTF">${iso}</dcterms:created>
|
||
<dcterms:modified xsi:type="dcterms:W3CDTF">${iso}</dcterms:modified>
|
||
</cp:coreProperties>`;
|
||
}
|
||
|
||
function appPropsXml() {
|
||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
|
||
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
|
||
<Application>Codex</Application>
|
||
<DocSecurity>0</DocSecurity>
|
||
<ScaleCrop>false</ScaleCrop>
|
||
<Company>KDL Work</Company>
|
||
<LinksUpToDate>false</LinksUpToDate>
|
||
<SharedDoc>false</SharedDoc>
|
||
<HyperlinksChanged>false</HyperlinksChanged>
|
||
<AppVersion>16.0000</AppVersion>
|
||
</Properties>`;
|
||
}
|
||
|
||
function stylesXml() {
|
||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||
<w:style w:type="paragraph" w:default="1" w:styleId="Normal">
|
||
<w:name w:val="Normal"/>
|
||
<w:qFormat/>
|
||
<w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="21"/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="BodyText">
|
||
<w:name w:val="Body Text"/>
|
||
<w:basedOn w:val="Normal"/>
|
||
<w:pPr><w:spacing w:after="120" w:line="276" w:lineRule="auto"/></w:pPr>
|
||
<w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="21"/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="Heading1">
|
||
<w:name w:val="heading 1"/>
|
||
<w:basedOn w:val="Normal"/>
|
||
<w:next w:val="BodyText"/>
|
||
<w:qFormat/>
|
||
<w:pPr><w:keepNext/><w:spacing w:before="360" w:after="180"/></w:pPr>
|
||
<w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="32"/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="Heading2">
|
||
<w:name w:val="heading 2"/>
|
||
<w:basedOn w:val="Normal"/>
|
||
<w:next w:val="BodyText"/>
|
||
<w:qFormat/>
|
||
<w:pPr><w:keepNext/><w:spacing w:before="280" w:after="140"/></w:pPr>
|
||
<w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="27"/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="Heading3">
|
||
<w:name w:val="heading 3"/>
|
||
<w:basedOn w:val="Normal"/>
|
||
<w:next w:val="BodyText"/>
|
||
<w:qFormat/>
|
||
<w:pPr><w:keepNext/><w:spacing w:before="180" w:after="100"/></w:pPr>
|
||
<w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="23"/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="Code">
|
||
<w:name w:val="Code"/>
|
||
<w:basedOn w:val="Normal"/>
|
||
<w:pPr><w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/></w:pPr>
|
||
<w:rPr><w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:eastAsia="Consolas"/><w:sz w:val="16"/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="TableHeader">
|
||
<w:name w:val="Table Header"/>
|
||
<w:basedOn w:val="Normal"/>
|
||
<w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="20"/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="table" w:default="1" w:styleId="TableNormal">
|
||
<w:name w:val="Normal Table"/>
|
||
<w:tblPr><w:tblInd w:w="0" w:type="dxa"/><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="108" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="108" w:type="dxa"/></w:tblCellMar></w:tblPr>
|
||
</w:style>
|
||
<w:style w:type="table" w:styleId="TableGrid">
|
||
<w:name w:val="Table Grid"/>
|
||
<w:basedOn w:val="TableNormal"/>
|
||
<w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:left w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:right w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/></w:tblBorders></w:tblPr>
|
||
</w:style>
|
||
</w:styles>`;
|
||
}
|
||
|
||
function latestJobId() {
|
||
const jobs = readdirSync(resultRoot, { withFileTypes: true })
|
||
.filter((entry) => entry.isDirectory() && existsSync(join(resultRoot, entry.name, "job.json")))
|
||
.map((entry) => ({
|
||
name: entry.name,
|
||
mtimeMs: statSync(join(resultRoot, entry.name, "job.json")).mtimeMs
|
||
}))
|
||
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||
if (jobs.length === 0) {
|
||
throw new Error(`No abb120 spec job found in ${resultRoot}`);
|
||
}
|
||
return jobs[0].name;
|
||
}
|
||
|
||
function readJson(path) {
|
||
return JSON.parse(readFileSync(path, "utf8"));
|
||
}
|
||
|
||
function relative(path) {
|
||
return path.replace(`${root}${sep}`, "").replaceAll("\\", "/");
|
||
}
|
||
|
||
function escapeXml(value) {
|
||
return String(value)
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll("\"", """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function psSingleQuoted(value) {
|
||
return `'${String(value).replaceAll("'", "''")}'`;
|
||
}
|
||
|
||
function zipDirectory(sourceDir, destinationFile) {
|
||
const script = [
|
||
"Add-Type -AssemblyName System.IO.Compression.FileSystem",
|
||
`$src = ${psSingleQuoted(sourceDir)}`,
|
||
`$dst = ${psSingleQuoted(destinationFile)}`,
|
||
"if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force }",
|
||
"[System.IO.Compression.ZipFile]::CreateFromDirectory($src, $dst)"
|
||
].join("\n");
|
||
execFileSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
||
stdio: "inherit"
|
||
});
|
||
}
|
||
|
||
function assertSafeStaging(path) {
|
||
const allowedRoot = join(webRoot, "test-results") + sep;
|
||
const resolved = resolve(path);
|
||
if (!resolved.startsWith(allowedRoot)) {
|
||
throw new Error(`Refusing to clear staging path outside test-results: ${resolved}`);
|
||
}
|
||
}
|
||
|
||
function formatShanghaiTime(date) {
|
||
const parts = new Intl.DateTimeFormat("zh-CN", {
|
||
timeZone: "Asia/Shanghai",
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
hour12: false
|
||
}).formatToParts(date);
|
||
const value = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
||
return `${value.year}-${value.month}-${value.day} ${value.hour}:${value.minute}:${value.second} Asia/Shanghai`;
|
||
}
|