212 lines
7.9 KiB
JavaScript
212 lines
7.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
import { createFullLinuxCncExecutionBoundary } from "../../app/src/runtime/full-execution-boundary.js";
|
|
import { createNativeTaskHalReadinessAudit } from "../../app/src/runtime/native-task-hal-audit.js";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const projectDir = resolve(__dirname, "../..");
|
|
const rootDir = resolve(projectDir, "..");
|
|
const phase0Script = resolve(rootDir, "wasm-port/tests/native/verify_task_hal_phase0.sh");
|
|
const manifestLog = resolve(rootDir, "wasm-port/build/task-hal/verify_task_hal_source_manifest.stdout.log");
|
|
const probeLog = resolve(rootDir, "wasm-port/build/task-hal/probe_trt_task_hal_runtime.stdout.log");
|
|
const manifestReport = resolve(rootDir, "wasm-port/build/task-hal/task-hal-source-manifest.tsv");
|
|
const artifactDir = resolve(projectDir, "build/readiness");
|
|
const artifactPath = resolve(artifactDir, "native-task-hal-readiness.json");
|
|
|
|
run(phase0Script);
|
|
|
|
const sourceManifest = parseKeyValueFile(manifestLog);
|
|
const nativeProbe = parseKeyValueFile(probeLog);
|
|
const fullExecutionBoundary = createFullLinuxCncExecutionBoundary(createPromotedSimulationState());
|
|
const audit = createNativeTaskHalReadinessAudit({
|
|
sourceManifest,
|
|
nativeProbe,
|
|
fullExecutionBoundary,
|
|
taskHalRuntimeReadiness: {
|
|
taskRuntimeReady: true,
|
|
motionRuntimeReady: true,
|
|
halRuntimeReady: true,
|
|
halSyncReady: true,
|
|
},
|
|
taskHalStatus: {
|
|
summary: {
|
|
taskRuntimeReady: true,
|
|
motionRuntimeReady: true,
|
|
halRuntimeReady: true,
|
|
halSyncReady: true,
|
|
taskHalComparisonReady: true,
|
|
},
|
|
},
|
|
generatedAt: "2026-06-22T00:00:00.000Z",
|
|
artifactPaths: {
|
|
readinessJson: relativeFromRoot(artifactPath),
|
|
sourceManifestLog: relativeFromRoot(manifestLog),
|
|
sourceManifestReport: relativeFromRoot(manifestReport),
|
|
nativeProbeLog: relativeFromRoot(probeLog),
|
|
},
|
|
});
|
|
|
|
assert.equal(audit.status, "ok");
|
|
assert.equal(audit.taskHalWebSimulationBoundaryConsistent, true);
|
|
assert.equal(audit.webSimulation.promoted, true);
|
|
assert.equal(audit.webSimulation.nativeTaskReady, true);
|
|
assert.equal(audit.webSimulation.nativeHalSyncReady, true);
|
|
assert.equal(audit.webSimulation.fullLinuxCncProgramExecutionReady, true);
|
|
assert.equal(audit.nativeHostAndHardware.nativeProbe, "ok");
|
|
assert.match(audit.nativeHostAndHardware.nativeProbeStatus, /^(passed|ready_disabled_by_default|skipped_missing_host_runtime)$/);
|
|
assert.equal(audit.nativeHostAndHardware.nativePromotionAllowed, false);
|
|
assert.equal(audit.nativeHostAndHardware.hardwareDrive, false);
|
|
assert.equal(audit.nativeHostAndHardware.hostRealtimeKernel, false);
|
|
assert.equal(audit.nativeHostAndHardware.externalUserMProcessReady, false);
|
|
assert.equal(audit.nativeHostAndHardware.toolDbProcessReady, false);
|
|
assert.equal(audit.nativeHostAndHardware.hostExternalUserMProcessReady, false);
|
|
assert.equal(audit.nativeHostAndHardware.hostToolDbProcessReady, false);
|
|
assert.equal(audit.nativeHostAndHardware.arbitraryUserMExecution, false);
|
|
assert.equal(audit.toolUserWebSimulation.externalUserMProcessReady, true);
|
|
assert.equal(audit.toolUserWebSimulation.externalUserMProcessScope, "web_simulation_only");
|
|
assert.equal(audit.toolUserWebSimulation.toolDbProcessReady, true);
|
|
assert.equal(audit.toolUserWebSimulation.toolDbProcessScope, "web_simulation_only");
|
|
assert.equal(audit.toolUserWebSimulation.ready, true);
|
|
assert.equal(audit.sourceManifest.ready, true);
|
|
assert.equal(audit.sourceManifest.taskSourceCount > 0, true);
|
|
assert.equal(audit.sourceManifest.halSourceCount > 0, true);
|
|
assert.equal(audit.sourceManifest.motionSourceCount > 0, true);
|
|
assert.equal(audit.gates.task_hal_web_simulation_boundary_consistent, 1);
|
|
assert.equal(audit.gates.hardware_drive, 0);
|
|
assert.equal(audit.gates.host_realtime_kernel, 0);
|
|
assert.equal(audit.gates.external_user_m_process_ready, 1);
|
|
assert.equal(audit.gates.tool_db_process_ready, 1);
|
|
assert.equal(audit.gates.host_external_user_m_process_ready, 0);
|
|
assert.equal(audit.gates.host_tool_db_process_ready, 0);
|
|
assert.equal(audit.gates.promotion_scope, "web_simulation_only");
|
|
|
|
mkdirSync(artifactDir, { recursive: true });
|
|
writeFileSync(artifactPath, `${JSON.stringify(audit, null, 2)}\n`);
|
|
|
|
const saved = JSON.parse(readFileSync(artifactPath, "utf8"));
|
|
assert.equal(saved.apiName, "web-rtcp-5axis-native-task-hal-readiness-audit");
|
|
assert.equal(saved.status, "ok");
|
|
|
|
console.log("native_task_hal_source_artifact_audit=ok");
|
|
console.log(`native_task_hal_readiness_artifact=${relativeFromRoot(artifactPath)}`);
|
|
console.log("task_hal_web_simulation_boundary_consistent=1");
|
|
console.log(`native_task_hal_host_probe_status=${audit.nativeHostAndHardware.nativeProbeStatus}`);
|
|
console.log("hardware_drive=0");
|
|
console.log("host_realtime_kernel=0");
|
|
console.log("external_user_m_process_ready=1");
|
|
console.log("tool_db_process_ready=1");
|
|
console.log("host_external_user_m_process_ready=0");
|
|
console.log("host_tool_db_process_ready=0");
|
|
console.log("promotion_scope=web_simulation_only");
|
|
|
|
function createPromotedSimulationState() {
|
|
const programExecution = {
|
|
sourceMode: "linuxcnc-interpreter-wasm",
|
|
plannerTiming: {
|
|
plannerRuntimeReady: true,
|
|
semanticBoundary: "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
|
},
|
|
summary: {
|
|
motionEventCount: 3,
|
|
canonicalEventCount: 8,
|
|
plannerRuntimeReady: true,
|
|
},
|
|
};
|
|
|
|
return {
|
|
machineProfile: "xyzac-trt",
|
|
linuxCncBoundaryAdapter: {
|
|
linuxCncKinematicsReady: true,
|
|
linuxCncInterpreterReady: true,
|
|
},
|
|
rtcpFrame: {
|
|
semanticBoundary: "linuxcnc_kinematics_wasm_c_abi",
|
|
readiness: { linuxCncKinematicsReady: true },
|
|
},
|
|
interpreterRuntimeReadiness: {
|
|
loaded: true,
|
|
semanticBoundary: "linuxcnc_interpreter_wasm_canonical_events",
|
|
},
|
|
programExecution,
|
|
machineFileStaging: {
|
|
status: "staged",
|
|
fileCount: 12,
|
|
},
|
|
machineFileExecution: {
|
|
sourceMode: "linuxcnc-machine-file-remap-wasm",
|
|
summary: { machineFileExecutionReady: true },
|
|
resultText: [
|
|
"fiveaxis_ini_open=1",
|
|
"fiveaxis_remaps_ready=1",
|
|
"fiveaxis_file_reached_exit=1",
|
|
"fiveaxis_hal_switchkins: rc=0 found=1 value=1",
|
|
].join("\n"),
|
|
},
|
|
taskHalRuntimeReadiness: {
|
|
taskRuntimeReady: true,
|
|
motionRuntimeReady: true,
|
|
halRuntimeReady: true,
|
|
halSyncReady: true,
|
|
},
|
|
taskHalStatus: {
|
|
summary: {
|
|
taskRuntimeReady: true,
|
|
motionRuntimeReady: true,
|
|
halRuntimeReady: true,
|
|
halSyncReady: true,
|
|
taskHalComparisonReady: true,
|
|
},
|
|
ui: {
|
|
taskCycle: 2,
|
|
servoCycle: 20,
|
|
motionQueueDepth: 0,
|
|
halChangedPinCount: 4,
|
|
},
|
|
},
|
|
toolDbReadiness: {
|
|
toolDbProcessReady: true,
|
|
toolDbProcessScope: "web_simulation_only",
|
|
hostToolDbProcessReady: false,
|
|
toolCount: 10,
|
|
},
|
|
controlledUserMReadiness: {
|
|
externalUserMProcessReady: true,
|
|
externalUserMProcessScope: "web_simulation_only",
|
|
hostExternalUserMProcessReady: false,
|
|
arbitraryUserMExecution: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
function run(scriptPath) {
|
|
const result = spawnSync("bash", [scriptPath], {
|
|
cwd: rootDir,
|
|
encoding: "utf8",
|
|
});
|
|
if (result.status !== 0) {
|
|
process.stdout.write(result.stdout || "");
|
|
process.stderr.write(result.stderr || "");
|
|
throw new Error(`${scriptPath} failed with status ${result.status}`);
|
|
}
|
|
}
|
|
|
|
function parseKeyValueFile(path) {
|
|
const fields = {};
|
|
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
if (!line || line.startsWith("#")) continue;
|
|
const equals = line.indexOf("=");
|
|
if (equals <= 0) continue;
|
|
fields[line.slice(0, equals)] = line.slice(equals + 1);
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
function relativeFromRoot(path) {
|
|
return path.startsWith(`${rootDir}/`) ? path.slice(rootDir.length + 1) : path;
|
|
}
|