210 lines
7.8 KiB
JavaScript
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}`);
|
|
}
|
|
}
|