Files
KDL_WORK/kdl-wasm/web/scripts/verify-demo-package.mjs
2026-06-28 20:58:18 +08:00

210 lines
7.8 KiB
JavaScript

import { execFileSync } from "node:child_process";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { dirname, extname, 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 deployRoot = join(webRoot, "test-results", "deploy");
const zipPath = resolve(process.argv[2] ?? latestZipPath());
const extractRoot = join(deployRoot, "_verify", zipBasename(zipPath));
assertSafeVerifyPath(extractRoot);
rmSync(extractRoot, { recursive: true, force: true });
mkdirSync(extractRoot, { recursive: true });
expandZip(zipPath, extractRoot);
const manifest = readJson(join(extractRoot, "demo-manifest.json"));
const report = readJson(join(extractRoot, manifest.report_json));
const programManifest = readJson(join(extractRoot, manifest.program_manifest));
const grlFiles = walkFiles(join(extractRoot, "spec-programs")).filter((file) => extname(file).toLowerCase() === ".grl");
const requiredFiles = [
"index.html",
"demo-manifest.json",
"README.md",
"app/virtual-controller.html",
"app/virtual-controller.css",
"app/virtual-controller.js",
"spec-programs/manifest.json",
manifest.job_json,
manifest.report_json,
manifest.report_html,
manifest.screenshots.desktop,
manifest.screenshots.mobile,
manifest.screenshots.evidence,
"docs/GRL功能语法逻辑与程序创建使用手册.docx",
"docs/通用机器人离线编程系统测试文档.docx",
"docs/working2/README.md",
"docs/working2/05-验收证据.md"
];
const checks = [];
for (const file of requiredFiles) {
checks.push(checkFile(file));
}
checks.push(check("program_count", manifest.program_count === 19, `program_count=${manifest.program_count}`));
checks.push(check("manifest_programs", programManifest.programs.length === 19, `manifest programs=${programManifest.programs.length}`));
checks.push(check("grl_files", grlFiles.length === 19, `grl files=${grlFiles.length}`));
checks.push(check("summary_fail", report.summary.fail === 0, `summary.fail=${report.summary.fail}`));
checks.push(check("report_programs", report.summary.programs === 19, `report programs=${report.summary.programs}`));
checks.push(check("runtime_programs", manifest.runtime_programs === 12, `runtime=${manifest.runtime_programs}`));
checks.push(check("static_programs", manifest.static_programs === 4, `static=${manifest.static_programs}`));
checks.push(check("post_programs", manifest.post_programs === 3, `post=${manifest.post_programs}`));
checks.push(check("missing_sections", Array.isArray(report.coverage.missingSections) && report.coverage.missingSections.length === 0, `missing=${report.coverage.missingSections?.join(",") ?? "n/a"}`));
checks.push(check("relative_urls", allManifestUrlsAreRelative(manifest), "manifest URLs are relative"));
for (const program of manifest.programs) {
checks.push(checkFile(program.source));
checks.push(checkFile(program.artifacts.compile));
}
for (const doc of manifest.docs ?? []) {
checks.push(checkFile(doc.href));
}
const runtimeFramePrograms = manifest.programs.filter((program) => program.coverage_level === "Runtime");
let frameProgramCount = 0;
for (const program of runtimeFramePrograms) {
const trajectory = readJson(join(extractRoot, program.artifacts.trajectory));
const frames = Array.isArray(trajectory.frames) ? trajectory.frames : [];
const hasFrameState = frames.some((frame) =>
Array.isArray(frame.joints) &&
frame.joints.length === 6 &&
Array.isArray(frame.tcp?.position) &&
frame.tcp.position.length === 3 &&
Array.isArray(frame.tcp?.quaternion) &&
frame.tcp.quaternion.length === 4
);
if (hasFrameState) frameProgramCount += 1;
}
checks.push(check(
"runtime_trajectory_frames",
frameProgramCount > 0,
`${frameProgramCount}/${runtimeFramePrograms.length} runtime programs include frame joints and TCP`
));
const failed = checks.filter((item) => !item.ok);
const summary = {
zip: zipPath,
extract_root: extractRoot,
release_id: manifest.release_id,
job_id: manifest.job_id,
program_count: manifest.program_count,
grl_files: grlFiles.length,
summary_fail: report.summary.fail,
checks,
ok: failed.length === 0
};
writeFileSync(join(deployRoot, `${zipBasename(zipPath)}.verify.json`), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
console.log(JSON.stringify(summary, null, 2));
if (failed.length > 0) {
throw new Error(`Demo package verification failed: ${failed.map((item) => item.name).join(", ")}`);
}
function checkFile(relativePath) {
const file = join(extractRoot, relativePath);
const exists = existsSync(file);
return check(`file:${relativePath}`, exists && statSync(file).isFile(), exists ? `${statSync(file).size} bytes` : "missing");
}
function check(name, ok, details) {
return { name, ok: Boolean(ok), details };
}
function allManifestUrlsAreRelative(value) {
const urls = [];
collectUrls(value, urls);
return urls.every((url) => !/^[a-z]+:\/\//i.test(url) && !/^[a-z]:[\\/]/i.test(url) && !url.startsWith("/") && !url.includes("\\"));
}
function collectUrls(value, urls) {
if (Array.isArray(value)) {
value.forEach((item) => collectUrls(item, urls));
return;
}
if (!value || typeof value !== "object") return;
for (const [key, item] of Object.entries(value)) {
if (typeof item === "string" && (key === "href" || key.includes("json") || key.includes("html") || key.includes("entry") || key.includes("manifest") || key.includes("source") || key.includes("screenshot") || key.includes("report") || key.includes("compile") || key.includes("controller") || key.includes("trace") || key.includes("trajectory") || key.includes("roundtrip") || key.includes("io") || key.includes("queue"))) {
urls.push(item);
} else {
collectUrls(item, urls);
}
}
}
function expandZip(source, destination) {
const script = [
"Add-Type -AssemblyName System.IO.Compression.FileSystem",
`$src = ${psSingleQuoted(source)}`,
`$dst = ${psSingleQuoted(destination)}`,
"if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Recurse -Force }",
"New-Item -ItemType Directory -Force -Path $dst | Out-Null",
"[System.IO.Compression.ZipFile]::ExtractToDirectory($src, $dst)"
].join("\n");
execFileSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
stdio: "inherit"
});
}
function latestZipPath() {
if (!existsSync(deployRoot)) {
throw new Error(`No deploy output directory found: ${deployRoot}`);
}
const zips = readdirSync(deployRoot, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.startsWith("kdl-olp-demo-") && entry.name.endsWith(".zip"))
.map((entry) => ({
path: join(deployRoot, entry.name),
mtimeMs: statSync(join(deployRoot, entry.name)).mtimeMs
}))
.sort((left, right) => right.mtimeMs - left.mtimeMs);
if (zips.length === 0) {
throw new Error(`No kdl-olp-demo-*.zip files found in ${deployRoot}`);
}
return zips[0].path;
}
function walkFiles(dir) {
const files = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...walkFiles(fullPath));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files;
}
function readJson(file) {
return JSON.parse(readFileSync(file, "utf8"));
}
function zipBasename(file) {
return file.split(/[\\/]/).at(-1).replace(/\.zip$/i, "");
}
function psSingleQuoted(value) {
return `'${String(value).replaceAll("'", "''")}'`;
}
function assertSafeVerifyPath(path) {
const allowedRoot = resolve(join(deployRoot, "_verify")) + sep;
const resolved = resolve(path);
if (!resolved.startsWith(allowedRoot)) {
throw new Error(`Refusing to modify path outside verify root: ${resolved}`);
}
}