Files
cnc_wams/qa/web-rtcp-5axis-site-test/generate-toolpath-preview-docx-report.mjs

253 lines
8.9 KiB
JavaScript
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.
import fs from "node:fs/promises";
import path from "node:path";
import {
AlignmentType,
Document,
HeadingLevel,
ImageRun,
Packer,
Paragraph,
Table,
TableCell,
TableRow,
TextRun,
WidthType,
} from "docx";
const ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
const OUTPUT_DIR = path.join(ROOT, "output");
const reportJsonPath = path.join(OUTPUT_DIR, "toolpath-preview-cases.json");
const report = JSON.parse(await fs.readFile(reportJsonPath, "utf8"));
const generated = new Date(report.generatedAt || Date.now());
const ymd = generated.toISOString().slice(0, 10);
const docxPath = path.join(OUTPUT_DIR, `web-rtcp-5axis-toolpath-preview-report-${ymd}.docx`);
const counts = report.cases.reduce((acc, testCase) => {
acc[testCase.status] = (acc[testCase.status] || 0) + 1;
return acc;
}, {});
const children = [
new Paragraph({
text: "刀具预览与 G-code 执行刀具轨迹专项测试报告",
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
}),
centered(`测试对象:${report.targetUrl}`),
centered(`项目路径:${report.projectPath || "/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/index.html"}`),
centered(`生成时间:${formatDateTime(generated)}`),
blank(),
heading("1. 测试结论"),
para(`本次专项覆盖 6 个刀具预览/刀具轨迹场景PASS ${counts.PASS || 0}FAIL ${counts.FAIL || 0} 项。`),
para("已验证首屏预览、矩形 G-code、圆弧 G-code、清空预览、LinuxCNC vendored impeller 长路径、RTCP 运行态执行轨迹。截图中机床参考模型、TCP 球、刀轴线和刀路均有可见性证据。"),
para("发现 1 项关键问题vendored impeller 程序在预览态为 RTCP on但进入 G-code 运行态后 canvas dataset 中 RTCP 状态变为 off。"),
summaryTable(),
heading("2. 测试范围与判定口径"),
kvTable([
["预览对象", "五轴机床参考模型、工作台、XYZ 坐标轴、旋转轴、刀具/TCP 球、刀轴线"],
["轨迹对象", "G-code canonical motion 预览路径、rapid/feed/arc 分层、已执行轨迹、当前段高亮"],
["运行反馈", "LinuxCNC interpreter WASM、TP/runtime samples、task/HAL runtime feedback"],
["可见性口径", "canvas dataset ready + WebGL renderer + scene objects + pixel luminance/non-black ratio + 截图"],
["语义边界", "可视化层只消费 runtime/canonical motion/task-HAL feedback不生成 G-code/CNC 语义"],
]),
heading("3. 场景结果总表"),
caseSummaryTable(report.cases),
heading("4. 失败/风险项"),
issueTable(report.cases.filter((testCase) => testCase.status !== "PASS")),
heading("5. 场景明细与截图"),
];
for (const testCase of report.cases) {
children.push(...(await caseBlock(testCase)));
}
children.push(
heading("6. 原始证据"),
kvTable([
["原始 JSON", reportJsonPath],
["截图数量", String(Object.keys(report.screenshots || {}).length)],
["Console error", String((report.consoleErrors || []).length)],
["Chrome", report.chromePath || "-"],
["Word 报告", docxPath],
["项目路径", report.projectPath || "-"],
]),
);
if ((report.consoleErrors || []).length > 0) {
children.push(
heading("7. Console 记录"),
...report.consoleErrors.map((entry) => para(entry)),
);
}
const doc = new Document({
sections: [{ properties: {}, children: children.flat() }],
});
await fs.writeFile(docxPath, await Packer.toBuffer(doc));
console.log(`toolpath_docx_report=${docxPath}`);
function heading(text) {
return new Paragraph({
text,
heading: HeadingLevel.HEADING_1,
spacing: { before: 240, after: 120 },
});
}
function centered(text) {
return new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun(String(text))],
});
}
function blank() {
return new Paragraph({ text: "" });
}
function para(text) {
return new Paragraph({
children: [new TextRun(String(text))],
spacing: { after: 100 },
});
}
function summaryTable() {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [
cell("场景总数", true),
cell(String(report.cases.length)),
cell("PASS", true),
cell(String(counts.PASS || 0)),
cell("FAIL", true),
cell(String(counts.FAIL || 0)),
],
}),
],
});
}
function kvTable(rows) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({ children: [cell("项目", true), cell("内容", true)] }),
...rows.map(([key, value]) => new TableRow({ children: [cell(key), cell(value)] })),
],
});
}
function caseSummaryTable(cases) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [cell("场景", true), cell("结果", true), cell("路径点", true), cell("执行点", true), cell("RTCP", true), cell("可见性", true)],
}),
...cases.map((testCase) => new TableRow({
children: [
cell(`${testCase.name}\n${testCase.summary}`),
cell(testCase.status),
cell(testCase.dataset.threePathPoints),
cell(testCase.dataset.threeExecutedPathPoints),
cell(testCase.dataset.threeRtcpState),
cell(`luma=${testCase.pixelStats?.averageLuminance ?? "-"}\nnonBlack=${testCase.pixelStats?.nonBlackRatio ?? "-"}`),
],
})),
],
});
}
function issueTable(failedCases) {
if (failedCases.length === 0) return para("未发现失败场景。");
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({ children: [cell("场景", true), cell("失败检查", true), cell("影响", true), cell("建议", true)] }),
...failedCases.map((testCase) => new TableRow({
children: [
cell(testCase.name),
cell(testCase.checks.filter((item) => !item.pass).map((item) => `${item.name}: ${item.detail}`).join("\n")),
cell("G-code 运行态下 RTCP/TCP 轨迹状态与预览态不一致,可能误导操作者判断刀具姿态和 TCP 执行轨迹。"),
cell("检查 RUN/task-HAL status 合并逻辑,避免运行反馈将 RTCP/kinesType 从已加载程序的 TCP 状态回退到 identity/off。"),
],
})),
],
});
}
async function caseBlock(testCase) {
const screenshotPath = report.screenshots[testCase.name];
const blocks = [
new Paragraph({
text: `${testCase.name} - ${testCase.status}`,
heading: HeadingLevel.HEADING_2,
spacing: { before: 180, after: 120 },
}),
para(testCase.summary),
kvTable([
["activeProgram", testCase.state.activeProgram],
["programSource", testCase.state.programSource],
["programExecutionSourceMode", testCase.state.programExecutionSourceMode],
["runState", testCase.state.runState],
["rtcpState / kinsType", `${testCase.state.rtcpState} / ${testCase.state.kinsType}`],
["motion/sample index", `${testCase.state.programExecutionMotionIndex} / ${testCase.state.programExecutionSampleIndex}`],
["runtime feedback", testCase.state.programRuntimeFeedbackSource || "-"],
["path/executed/rapid/feed/arc", `${testCase.dataset.threePathPoints}/${testCase.dataset.threeExecutedPathPoints}/${testCase.dataset.threeRapidPathPoints}/${testCase.dataset.threeFeedPathPoints}/${testCase.dataset.threeArcPathPoints}`],
["renderer/model", `${testCase.dataset.threeRenderer} / ${testCase.dataset.threeMachineReferenceModel}`],
["pixel stats", `averageLuminance=${testCase.pixelStats?.averageLuminance ?? "-"}, nonBlackRatio=${testCase.pixelStats?.nonBlackRatio ?? "-"}`],
]),
new Paragraph({
text: "检查项",
heading: HeadingLevel.HEADING_3,
spacing: { before: 120, after: 80 },
}),
checksTable(testCase.checks),
];
if (screenshotPath) {
const image = await fs.readFile(screenshotPath);
blocks.push(
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new ImageRun({
data: image,
type: "png",
transformation: { width: 520, height: 390 },
}),
],
}),
para(`截图文件:${screenshotPath}`),
);
}
return blocks;
}
function checksTable(checks) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({ children: [cell("检查", true), cell("结果", true), cell("证据", true)] }),
...checks.map((item) => new TableRow({
children: [cell(item.name), cell(item.pass ? "PASS" : "FAIL"), cell(item.detail)],
})),
],
});
}
function cell(text, bold = false) {
return new TableCell({
width: { size: 25, type: WidthType.PERCENTAGE },
children: String(text ?? "-").split("\n").map((line) => new Paragraph({
children: [new TextRun({ text: line, bold })],
})),
});
}
function formatDateTime(date) {
return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
}