Add ABB120 demo suite and docs
This commit is contained in:
209
kdl-wasm/web/scripts/verify-demo-package.mjs
Normal file
209
kdl-wasm/web/scripts/verify-demo-package.mjs
Normal file
@@ -0,0 +1,209 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user