webmystran-wasm 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/PATCHING.md +358 -0
- package/README.md +177 -0
- package/SOURCES.md +48 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/dist/mystran.js +14 -0
- package/dist/mystran.wasm +0 -0
- package/index.js +3 -0
- package/package.json +54 -0
- package/scripts/build.ps1 +19 -0
- package/scripts/fetch_deps.ps1 +157 -0
- package/scripts/smoke_test.ps1 +19 -0
- package/src/mystran_input.js +117 -0
- package/src/output_parser.js +57 -0
- package/src/web_mystran.js +247 -0
- package/tools/build.js +4840 -0
- package/tools/smoke_test.js +978 -0
- package/tools/wasm_runner.js +349 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { WebMYSTRAN } from "../index.js";
|
|
7
|
+
|
|
8
|
+
const RUNTIME_DECK_FILE_NAME = "WEBMYSTRAN.DAT";
|
|
9
|
+
const RUNTIME_DECK_BASE_NAME = "WEBMYSTRAN";
|
|
10
|
+
const RESULT_EXTENSIONS = ["F06", "PCH", "F04", "NEU", "OP2", "ANS", "BUG", "ERR"];
|
|
11
|
+
const PASS_OUTCOMES = new Set(["success", "solver_error"]);
|
|
12
|
+
const BUILD_IMAGE = "ghcr.io/r-wasm/flang-wasm:v20.1.4";
|
|
13
|
+
const RUNTIME_ABORT_REGEX =
|
|
14
|
+
/fatal Fortran runtime error|RuntimeError:\s*Aborted|Aborted\(\)|RuntimeError:\s*unreachable|RuntimeError:\s*memory access out of bounds|RuntimeError:\s*table index is out of bounds|out of memory/i;
|
|
15
|
+
const SOLVER_FAILURE_REGEX = /\*ERROR\s+[0-9A-Z]+|PROCESSING STOPPED|Processing terminated|CHECK F06 OUTPUT FILE/i;
|
|
16
|
+
|
|
17
|
+
function fileExists(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tailLines(text, count) {
|
|
27
|
+
const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
|
|
28
|
+
return lines.slice(Math.max(0, lines.length - count)).join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toPosixPath(value) {
|
|
32
|
+
return String(value || "").replace(/\\/g, "/");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function quoteBash(value) {
|
|
36
|
+
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runDocker(args, options = {}) {
|
|
40
|
+
return spawnSync("docker", args, {
|
|
41
|
+
encoding: options.encoding || "utf8",
|
|
42
|
+
timeout: options.timeout,
|
|
43
|
+
maxBuffer: options.maxBuffer,
|
|
44
|
+
cwd: options.cwd,
|
|
45
|
+
env: options.env || process.env
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createNativeRunnerContext(repoRoot) {
|
|
50
|
+
const binaryPath = path.join(repoRoot, "sources", "MYSTRAN_native_ref", "Binaries", "mystran");
|
|
51
|
+
if (!fileExists(binaryPath)) {
|
|
52
|
+
throw new Error(`Missing native reference binary: ${binaryPath}. Run scripts/build.ps1 first.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const dockerVersion = runDocker(["version"], { timeout: 30000, maxBuffer: 1024 * 1024 });
|
|
56
|
+
if (dockerVersion.error || dockerVersion.status !== 0) {
|
|
57
|
+
const detail = dockerVersion.error ? ` (${dockerVersion.error.message})` : "";
|
|
58
|
+
throw new Error(`Docker engine is required for native compare smoke${detail}.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { repoRoot };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function listDeckFilesRecursive(baseDir) {
|
|
65
|
+
const all = [];
|
|
66
|
+
|
|
67
|
+
function walk(currentDir) {
|
|
68
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const full = path.join(currentDir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
walk(full);
|
|
73
|
+
} else if (entry.isFile() && /\.dat$/i.test(entry.name)) {
|
|
74
|
+
all.push(full);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
walk(baseDir);
|
|
80
|
+
return all.sort((a, b) => a.localeCompare(b));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toBenchRelativePath(benchDeckDir, deckAbsPath) {
|
|
84
|
+
return path.relative(benchDeckDir, deckAbsPath).split(path.sep).join("/");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseFullSmokeArgs(args) {
|
|
88
|
+
const options = {
|
|
89
|
+
decks: [],
|
|
90
|
+
timeoutSeconds: 1800
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
94
|
+
const token = args[index];
|
|
95
|
+
const normalized = token.toLowerCase();
|
|
96
|
+
|
|
97
|
+
if (normalized === "-decks" || normalized === "--decks") {
|
|
98
|
+
let captured = false;
|
|
99
|
+
while (index + 1 < args.length && !args[index + 1].startsWith("-")) {
|
|
100
|
+
index += 1;
|
|
101
|
+
captured = true;
|
|
102
|
+
const parts = String(args[index])
|
|
103
|
+
.split(",")
|
|
104
|
+
.map((value) => value.trim())
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
options.decks.push(...parts);
|
|
107
|
+
}
|
|
108
|
+
if (!captured) {
|
|
109
|
+
throw new Error("Missing value for -Decks");
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (normalized === "-timeoutseconds" || normalized === "--timeoutseconds") {
|
|
115
|
+
if (index + 1 >= args.length) {
|
|
116
|
+
throw new Error("Missing value for -TimeoutSeconds");
|
|
117
|
+
}
|
|
118
|
+
const parsed = Number.parseInt(args[index + 1], 10);
|
|
119
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
120
|
+
throw new Error(`Invalid -TimeoutSeconds value: ${args[index + 1]}`);
|
|
121
|
+
}
|
|
122
|
+
options.timeoutSeconds = parsed;
|
|
123
|
+
index += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (token.startsWith("-")) {
|
|
128
|
+
throw new Error(`Unknown smoke option: ${token}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
options.decks.push(token);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return options;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseDeckWorkerArgs(args) {
|
|
138
|
+
const options = {
|
|
139
|
+
deckPath: "",
|
|
140
|
+
moduleJs: "",
|
|
141
|
+
moduleWasm: "",
|
|
142
|
+
workDir: "/work"
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
146
|
+
const token = args[index];
|
|
147
|
+
const normalized = token.toLowerCase();
|
|
148
|
+
|
|
149
|
+
if (normalized === "--deck-path") {
|
|
150
|
+
options.deckPath = args[index + 1] || "";
|
|
151
|
+
index += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (normalized === "--module-js") {
|
|
155
|
+
options.moduleJs = args[index + 1] || "";
|
|
156
|
+
index += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (normalized === "--module-wasm") {
|
|
160
|
+
options.moduleWasm = args[index + 1] || "";
|
|
161
|
+
index += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (normalized === "--work-dir") {
|
|
165
|
+
options.workDir = args[index + 1] || "/work";
|
|
166
|
+
index += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!options.deckPath || !options.moduleJs || !options.moduleWasm) {
|
|
172
|
+
throw new Error("Deck worker requires --deck-path, --module-js, and --module-wasm");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return options;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function runDeckWorker(args) {
|
|
179
|
+
const options = parseDeckWorkerArgs(args);
|
|
180
|
+
const deckPath = path.resolve(options.deckPath);
|
|
181
|
+
|
|
182
|
+
let solver = null;
|
|
183
|
+
try {
|
|
184
|
+
const startedAt = process.hrtime.bigint();
|
|
185
|
+
const deckText = fs.readFileSync(deckPath, "utf8");
|
|
186
|
+
solver = await WebMYSTRAN.load({
|
|
187
|
+
moduleUrl: pathToFileURL(path.resolve(options.moduleJs)).href,
|
|
188
|
+
wasmUrl: pathToFileURL(path.resolve(options.moduleWasm)).href
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const runResult = solver.run(deckText, {
|
|
192
|
+
workDir: options.workDir || "/work",
|
|
193
|
+
deckFileName: RUNTIME_DECK_FILE_NAME
|
|
194
|
+
});
|
|
195
|
+
const elapsedSec = Number(process.hrtime.bigint() - startedAt) / 1e9;
|
|
196
|
+
|
|
197
|
+
const stdout = runResult.raw?.stdout || "";
|
|
198
|
+
const stderr = runResult.raw?.stderr || "";
|
|
199
|
+
const consoleText = runResult.output?.text || `${stdout}${stderr}`;
|
|
200
|
+
const f06Text = runResult.outputs?.F06 ? solver.readOutput(runResult, "F06", "utf8") || "" : "";
|
|
201
|
+
const resultArtifacts = Array.isArray(runResult.files)
|
|
202
|
+
? runResult.files.map((entry) => ({
|
|
203
|
+
extension: String(entry.extension || "").toUpperCase(),
|
|
204
|
+
length: Number(entry.size || 0)
|
|
205
|
+
}))
|
|
206
|
+
: [];
|
|
207
|
+
const nonEmptyResultFileCount = resultArtifacts.filter((entry) => entry.length > 0).length;
|
|
208
|
+
const hasNormalTermination =
|
|
209
|
+
/MYSTRAN terminated normally/i.test(f06Text) || /MYSTRAN terminated normally/i.test(consoleText);
|
|
210
|
+
const runtimeAbort = RUNTIME_ABORT_REGEX.test(consoleText);
|
|
211
|
+
const solverFailure = SOLVER_FAILURE_REGEX.test(f06Text) || SOLVER_FAILURE_REGEX.test(consoleText);
|
|
212
|
+
const errorCode = getFirstErrorCode(f06Text) || getFirstErrorCode(consoleText);
|
|
213
|
+
const f06Length = resultArtifacts.find((entry) => entry.extension === "F06")?.length || 0;
|
|
214
|
+
|
|
215
|
+
process.stdout.write(
|
|
216
|
+
`${JSON.stringify({
|
|
217
|
+
exitCode: Number.isInteger(runResult.raw?.exitCode) ? runResult.raw.exitCode : 1,
|
|
218
|
+
elapsedSec,
|
|
219
|
+
stdout,
|
|
220
|
+
stderr,
|
|
221
|
+
consoleText,
|
|
222
|
+
hasNormalTermination,
|
|
223
|
+
runtimeAbort,
|
|
224
|
+
solverFailure,
|
|
225
|
+
errorCode,
|
|
226
|
+
f06Present: Boolean(runResult.outputs?.F06),
|
|
227
|
+
f06Length,
|
|
228
|
+
nonEmptyResultFileCount,
|
|
229
|
+
resultArtifacts
|
|
230
|
+
})}\n`
|
|
231
|
+
);
|
|
232
|
+
process.exit(0);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const message = error && error.message ? error.message : String(error);
|
|
235
|
+
process.stdout.write(
|
|
236
|
+
`${JSON.stringify({
|
|
237
|
+
exitCode: 1,
|
|
238
|
+
elapsedSec: 0,
|
|
239
|
+
stdout: "",
|
|
240
|
+
stderr: `${message}\n`,
|
|
241
|
+
consoleText: `${message}\n`,
|
|
242
|
+
hasNormalTermination: false,
|
|
243
|
+
runtimeAbort: false,
|
|
244
|
+
solverFailure: false,
|
|
245
|
+
errorCode: null,
|
|
246
|
+
f06Present: false,
|
|
247
|
+
f06Length: 0,
|
|
248
|
+
nonEmptyResultFileCount: 0,
|
|
249
|
+
resultArtifacts: [],
|
|
250
|
+
workerError: message
|
|
251
|
+
})}\n`
|
|
252
|
+
);
|
|
253
|
+
process.exit(0);
|
|
254
|
+
} finally {
|
|
255
|
+
if (solver) {
|
|
256
|
+
solver.destroy();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function resolveDeckPath(deckSpec, benchDeckDir, allDeckAbs) {
|
|
262
|
+
if (!deckSpec) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const direct = path.isAbsolute(deckSpec) ? deckSpec : path.join(benchDeckDir, deckSpec);
|
|
267
|
+
if (fileExists(direct)) {
|
|
268
|
+
return path.resolve(direct);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const deckSpecNormalized = deckSpec.replace(/\\/g, "/").toLowerCase();
|
|
272
|
+
const exactRelative = allDeckAbs.find((candidate) => {
|
|
273
|
+
const relative = toBenchRelativePath(benchDeckDir, candidate).toLowerCase();
|
|
274
|
+
return relative === deckSpecNormalized;
|
|
275
|
+
});
|
|
276
|
+
if (exactRelative) {
|
|
277
|
+
return exactRelative;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const byBaseName = allDeckAbs.find((candidate) => {
|
|
281
|
+
return path.basename(candidate).toLowerCase() === path.basename(deckSpec).toLowerCase();
|
|
282
|
+
});
|
|
283
|
+
if (byBaseName) {
|
|
284
|
+
return byBaseName;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getFirstErrorCode(text) {
|
|
291
|
+
if (!text || !String(text).trim()) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const match = String(text).match(/\*ERROR\s+([0-9A-Z]+)/);
|
|
295
|
+
return match ? match[1] : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function parseDeckWorkerPayload(stdoutText) {
|
|
299
|
+
const lines = String(stdoutText || "")
|
|
300
|
+
.split(/\r?\n/)
|
|
301
|
+
.map((line) => line.trim())
|
|
302
|
+
.filter(Boolean);
|
|
303
|
+
|
|
304
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
305
|
+
try {
|
|
306
|
+
const parsed = JSON.parse(lines[index]);
|
|
307
|
+
if (parsed && typeof parsed === "object") {
|
|
308
|
+
return parsed;
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// Keep scanning trailing lines for JSON payload.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function normalizeResultArtifacts(artifacts) {
|
|
319
|
+
if (!Array.isArray(artifacts)) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
return artifacts
|
|
323
|
+
.map((entry) => {
|
|
324
|
+
const extension = String(entry.extension || "").toUpperCase();
|
|
325
|
+
const length = Number(entry.length ?? entry.size ?? 0);
|
|
326
|
+
if (!extension) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
extension,
|
|
331
|
+
length: Number.isFinite(length) && length > 0 ? length : 0
|
|
332
|
+
};
|
|
333
|
+
})
|
|
334
|
+
.filter(Boolean);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function evaluateRunStatus(deck, workerResult, processExitCode, timedOut, observedConsoleText) {
|
|
338
|
+
const exitCode = timedOut
|
|
339
|
+
? 124
|
|
340
|
+
: Number.isInteger(workerResult?.exitCode)
|
|
341
|
+
? workerResult.exitCode
|
|
342
|
+
: processExitCode;
|
|
343
|
+
const consoleText = String(workerResult?.consoleText || observedConsoleText || "");
|
|
344
|
+
const resultArtifacts = normalizeResultArtifacts(workerResult?.resultArtifacts);
|
|
345
|
+
const f06Length = Number.isFinite(workerResult?.f06Length) ? Math.max(0, workerResult.f06Length) : 0;
|
|
346
|
+
const nonEmptyResultFileCount = Number.isFinite(workerResult?.nonEmptyResultFileCount)
|
|
347
|
+
? Math.max(0, workerResult.nonEmptyResultFileCount)
|
|
348
|
+
: resultArtifacts.filter((entry) => entry.length > 0).length;
|
|
349
|
+
const f06Present = typeof workerResult?.f06Present === "boolean" ? workerResult.f06Present : false;
|
|
350
|
+
const hasNormalTermination =
|
|
351
|
+
typeof workerResult?.hasNormalTermination === "boolean"
|
|
352
|
+
? workerResult.hasNormalTermination
|
|
353
|
+
: /MYSTRAN terminated normally/i.test(consoleText);
|
|
354
|
+
const runtimeAbort =
|
|
355
|
+
typeof workerResult?.runtimeAbort === "boolean"
|
|
356
|
+
? workerResult.runtimeAbort || RUNTIME_ABORT_REGEX.test(consoleText)
|
|
357
|
+
: RUNTIME_ABORT_REGEX.test(consoleText);
|
|
358
|
+
const solverFailure =
|
|
359
|
+
typeof workerResult?.solverFailure === "boolean"
|
|
360
|
+
? workerResult.solverFailure || SOLVER_FAILURE_REGEX.test(consoleText)
|
|
361
|
+
: SOLVER_FAILURE_REGEX.test(consoleText);
|
|
362
|
+
const errorCode = workerResult?.errorCode || getFirstErrorCode(consoleText);
|
|
363
|
+
|
|
364
|
+
let outcome = "unknown";
|
|
365
|
+
if (timedOut) {
|
|
366
|
+
outcome = "timeout";
|
|
367
|
+
} else if (runtimeAbort) {
|
|
368
|
+
outcome = "runtime_abort";
|
|
369
|
+
} else if (exitCode !== 0) {
|
|
370
|
+
outcome = "process_error";
|
|
371
|
+
} else if (!f06Present) {
|
|
372
|
+
outcome = "missing_f06";
|
|
373
|
+
} else if (f06Length === 0) {
|
|
374
|
+
outcome = "empty_f06";
|
|
375
|
+
} else if (nonEmptyResultFileCount === 0) {
|
|
376
|
+
outcome = "missing_outputs";
|
|
377
|
+
} else if (solverFailure) {
|
|
378
|
+
outcome = "solver_error";
|
|
379
|
+
} else if (hasNormalTermination) {
|
|
380
|
+
outcome = "success";
|
|
381
|
+
} else {
|
|
382
|
+
outcome = "no_success_marker";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
deck,
|
|
387
|
+
outcome,
|
|
388
|
+
exitCode,
|
|
389
|
+
timedOut,
|
|
390
|
+
runtimeAbort,
|
|
391
|
+
hasNormalTermination,
|
|
392
|
+
solverFailure,
|
|
393
|
+
errorCode,
|
|
394
|
+
workerError: workerResult?.workerError || null,
|
|
395
|
+
f06Length,
|
|
396
|
+
nonEmptyResultFileCount,
|
|
397
|
+
resultArtifacts
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function runWasmDeckCase(deckPath, runnerScript, timeoutSeconds, distJs, distWasm) {
|
|
402
|
+
const result = spawnSync(
|
|
403
|
+
"node",
|
|
404
|
+
[
|
|
405
|
+
runnerScript,
|
|
406
|
+
"__run_deck",
|
|
407
|
+
"--deck-path",
|
|
408
|
+
deckPath,
|
|
409
|
+
"--module-js",
|
|
410
|
+
distJs,
|
|
411
|
+
"--module-wasm",
|
|
412
|
+
distWasm,
|
|
413
|
+
"--work-dir",
|
|
414
|
+
"/work"
|
|
415
|
+
],
|
|
416
|
+
{
|
|
417
|
+
cwd: path.dirname(deckPath),
|
|
418
|
+
encoding: "utf8",
|
|
419
|
+
timeout: timeoutSeconds * 1000,
|
|
420
|
+
maxBuffer: 16 * 1024 * 1024
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const timedOut = Boolean(result.error && result.error.code === "ETIMEDOUT");
|
|
425
|
+
const processExitCode = timedOut ? 124 : Number.isInteger(result.status) ? result.status : 1;
|
|
426
|
+
const workerResult = parseDeckWorkerPayload(result.stdout || "");
|
|
427
|
+
const elapsedSec = Number.isFinite(workerResult?.elapsedSec) ? workerResult.elapsedSec : 0;
|
|
428
|
+
const stdout = workerResult ? String(workerResult.stdout || "") : String(result.stdout || "");
|
|
429
|
+
let stderr = String(workerResult?.stderr || result.stderr || "");
|
|
430
|
+
|
|
431
|
+
if (result.error && !timedOut) {
|
|
432
|
+
stderr = `${stderr}${stderr.endsWith("\n") || stderr.length === 0 ? "" : "\n"}${result.error.message}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const consoleText = String(workerResult?.consoleText || `${stdout}${stderr}`);
|
|
436
|
+
const exitCode =
|
|
437
|
+
timedOut || !workerResult
|
|
438
|
+
? processExitCode
|
|
439
|
+
: Number.isInteger(workerResult.exitCode)
|
|
440
|
+
? workerResult.exitCode
|
|
441
|
+
: processExitCode;
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
exitCode,
|
|
445
|
+
timedOut,
|
|
446
|
+
elapsedSec,
|
|
447
|
+
consoleText,
|
|
448
|
+
workerResult
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function createNativeDeckCommand(deckContainerPath, timeoutSeconds) {
|
|
453
|
+
const deckPathLiteral = quoteBash(deckContainerPath);
|
|
454
|
+
const timeoutSec = Math.max(1, Number.parseInt(String(timeoutSeconds), 10) || 1);
|
|
455
|
+
const abortPattern =
|
|
456
|
+
"fatal Fortran runtime error|RuntimeError:[[:space:]]*Aborted|Aborted\\(\\)|RuntimeError:[[:space:]]*unreachable|RuntimeError:[[:space:]]*memory access out of bounds|RuntimeError:[[:space:]]*table index is out of bounds|out of memory";
|
|
457
|
+
const solverPattern = "\\*ERROR[[:space:]]+[0-9A-Z]+|PROCESSING STOPPED|Processing terminated|CHECK F06 OUTPUT FILE";
|
|
458
|
+
|
|
459
|
+
return `
|
|
460
|
+
set -euo pipefail
|
|
461
|
+
deck_path=${deckPathLiteral}
|
|
462
|
+
native_bin='/work/sources/MYSTRAN_native_ref/Binaries/mystran'
|
|
463
|
+
|
|
464
|
+
if [ ! -f "$deck_path" ]; then
|
|
465
|
+
echo '{"exitCode":1,"elapsedSec":0,"timedOut":false,"stdout":"","stderr":"","consoleText":"","hasNormalTermination":false,"runtimeAbort":false,"solverFailure":false,"errorCode":null,"f06Present":false,"f06Length":0,"nonEmptyResultFileCount":0,"resultArtifacts":[],"workerError":"Deck not found in container"}'
|
|
466
|
+
exit 0
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
if [ ! -f "$native_bin" ]; then
|
|
470
|
+
echo '{"exitCode":1,"elapsedSec":0,"timedOut":false,"stdout":"","stderr":"","consoleText":"","hasNormalTermination":false,"runtimeAbort":false,"solverFailure":false,"errorCode":null,"f06Present":false,"f06Length":0,"nonEmptyResultFileCount":0,"resultArtifacts":[],"workerError":"Missing native reference binary"}'
|
|
471
|
+
exit 0
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
chmod +x "$native_bin" || true
|
|
475
|
+
tmpdir=$(mktemp -d /tmp/webmystran-native-smoke-XXXXXX)
|
|
476
|
+
cleanup() { rm -rf "$tmpdir"; }
|
|
477
|
+
trap cleanup EXIT
|
|
478
|
+
|
|
479
|
+
cp "$deck_path" "$tmpdir/${RUNTIME_DECK_FILE_NAME}"
|
|
480
|
+
: > "$tmpdir/MYSTRAN.INI"
|
|
481
|
+
printf '${RUNTIME_DECK_FILE_NAME}\\n' > "$tmpdir/stdin.txt"
|
|
482
|
+
: > "$tmpdir/native.stdout.log"
|
|
483
|
+
: > "$tmpdir/native.stderr.log"
|
|
484
|
+
|
|
485
|
+
start_ns=$(date +%s%N)
|
|
486
|
+
set +e
|
|
487
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
488
|
+
(cd "$tmpdir" && timeout ${timeoutSec}s "$native_bin" < stdin.txt > native.stdout.log 2> native.stderr.log)
|
|
489
|
+
run_exit=$?
|
|
490
|
+
else
|
|
491
|
+
(cd "$tmpdir" && "$native_bin" < stdin.txt > native.stdout.log 2> native.stderr.log)
|
|
492
|
+
run_exit=$?
|
|
493
|
+
fi
|
|
494
|
+
set -e
|
|
495
|
+
end_ns=$(date +%s%N)
|
|
496
|
+
elapsed_sec=$(awk "BEGIN { printf \\"%.6f\\", ($end_ns-$start_ns)/1000000000 }")
|
|
497
|
+
|
|
498
|
+
timed_out=false
|
|
499
|
+
if [ "$run_exit" -eq 124 ] || [ "$run_exit" -eq 137 ] || [ "$run_exit" -eq 143 ]; then
|
|
500
|
+
timed_out=true
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
f06_path="$tmpdir/${RUNTIME_DECK_BASE_NAME}.F06"
|
|
504
|
+
f06_present=false
|
|
505
|
+
f06_length=0
|
|
506
|
+
if [ -f "$f06_path" ]; then
|
|
507
|
+
f06_present=true
|
|
508
|
+
f06_length=$(wc -c < "$f06_path" | tr -d '[:space:]')
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
has_normal=false
|
|
512
|
+
if [ -f "$f06_path" ] && grep -Eiq 'MYSTRAN terminated normally' "$f06_path"; then
|
|
513
|
+
has_normal=true
|
|
514
|
+
elif grep -Eiq 'MYSTRAN terminated normally' "$tmpdir/native.stdout.log" "$tmpdir/native.stderr.log"; then
|
|
515
|
+
has_normal=true
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
runtime_abort=false
|
|
519
|
+
if grep -Eiq '${abortPattern}' "$tmpdir/native.stdout.log" "$tmpdir/native.stderr.log"; then
|
|
520
|
+
runtime_abort=true
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
solver_failure=false
|
|
524
|
+
if [ -f "$f06_path" ] && grep -Eiq '${solverPattern}' "$f06_path"; then
|
|
525
|
+
solver_failure=true
|
|
526
|
+
elif grep -Eiq '${solverPattern}' "$tmpdir/native.stdout.log" "$tmpdir/native.stderr.log"; then
|
|
527
|
+
solver_failure=true
|
|
528
|
+
fi
|
|
529
|
+
|
|
530
|
+
error_code=$( (grep -Eo '\\*ERROR[[:space:]]+[0-9A-Z]+' "$f06_path" "$tmpdir/native.stdout.log" "$tmpdir/native.stderr.log" 2>/dev/null || true) | head -n 1 | awk '{print $2}' )
|
|
531
|
+
error_json=null
|
|
532
|
+
if [ -n "$error_code" ]; then
|
|
533
|
+
error_json="\\"$error_code\\""
|
|
534
|
+
fi
|
|
535
|
+
|
|
536
|
+
artifacts_json=""
|
|
537
|
+
non_empty_count=0
|
|
538
|
+
for ext in ${RESULT_EXTENSIONS.join(" ")}; do
|
|
539
|
+
candidate="$tmpdir/${RUNTIME_DECK_BASE_NAME}.$ext"
|
|
540
|
+
if [ -f "$candidate" ]; then
|
|
541
|
+
length=$(wc -c < "$candidate" | tr -d '[:space:]')
|
|
542
|
+
if [ -z "$length" ]; then
|
|
543
|
+
length=0
|
|
544
|
+
fi
|
|
545
|
+
if [ "$length" -gt 0 ] 2>/dev/null; then
|
|
546
|
+
non_empty_count=$((non_empty_count + 1))
|
|
547
|
+
fi
|
|
548
|
+
entry="{\\"extension\\":\\"$ext\\",\\"length\\":$length}"
|
|
549
|
+
if [ -z "$artifacts_json" ]; then
|
|
550
|
+
artifacts_json="$entry"
|
|
551
|
+
else
|
|
552
|
+
artifacts_json="$artifacts_json,$entry"
|
|
553
|
+
fi
|
|
554
|
+
fi
|
|
555
|
+
done
|
|
556
|
+
|
|
557
|
+
echo "{\\"exitCode\\":$run_exit,\\"elapsedSec\\":$elapsed_sec,\\"timedOut\\":$timed_out,\\"stdout\\":\\"\\",\\"stderr\\":\\"\\",\\"consoleText\\":\\"\\",\\"hasNormalTermination\\":$has_normal,\\"runtimeAbort\\":$runtime_abort,\\"solverFailure\\":$solver_failure,\\"errorCode\\":$error_json,\\"f06Present\\":$f06_present,\\"f06Length\\":$f06_length,\\"nonEmptyResultFileCount\\":$non_empty_count,\\"resultArtifacts\\":[$artifacts_json]}"
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function runNativeDeckCase(nativeContext, deckPath, timeoutSeconds) {
|
|
562
|
+
const deckRelPath = toPosixPath(path.relative(nativeContext.repoRoot, deckPath));
|
|
563
|
+
const deckContainerPath = `/work/${deckRelPath}`;
|
|
564
|
+
const command = createNativeDeckCommand(deckContainerPath, timeoutSeconds);
|
|
565
|
+
|
|
566
|
+
const result = runDocker(
|
|
567
|
+
[
|
|
568
|
+
"run",
|
|
569
|
+
"--rm",
|
|
570
|
+
"--volume",
|
|
571
|
+
`${nativeContext.repoRoot}:/work`,
|
|
572
|
+
"--workdir",
|
|
573
|
+
"/work",
|
|
574
|
+
BUILD_IMAGE,
|
|
575
|
+
"/bin/bash",
|
|
576
|
+
"-lc",
|
|
577
|
+
command
|
|
578
|
+
],
|
|
579
|
+
{
|
|
580
|
+
encoding: "utf8",
|
|
581
|
+
timeout: (Math.max(1, timeoutSeconds) + 120) * 1000,
|
|
582
|
+
maxBuffer: 16 * 1024 * 1024
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
const workerResult = parseDeckWorkerPayload(result.stdout || "");
|
|
587
|
+
const timedOut =
|
|
588
|
+
Boolean(workerResult?.timedOut) || Boolean(result.error && result.error.code === "ETIMEDOUT");
|
|
589
|
+
const processExitCode = timedOut ? 124 : Number.isInteger(result.status) ? result.status : 1;
|
|
590
|
+
const exitCode =
|
|
591
|
+
timedOut || !workerResult
|
|
592
|
+
? processExitCode
|
|
593
|
+
: Number.isInteger(workerResult.exitCode)
|
|
594
|
+
? workerResult.exitCode
|
|
595
|
+
: processExitCode;
|
|
596
|
+
const elapsedSec = Number.isFinite(workerResult?.elapsedSec) ? workerResult.elapsedSec : 0;
|
|
597
|
+
const consoleText = String(workerResult?.consoleText || `${result.stdout || ""}${result.stderr || ""}`);
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
exitCode,
|
|
601
|
+
timedOut,
|
|
602
|
+
elapsedSec,
|
|
603
|
+
consoleText,
|
|
604
|
+
workerResult
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function hasNonEmptyArtifact(status, extension) {
|
|
609
|
+
const normalized = String(extension || "").toUpperCase();
|
|
610
|
+
if (!normalized) {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
return status.resultArtifacts.some(
|
|
614
|
+
(entry) => String(entry.extension || "").toUpperCase() === normalized && Number(entry.length || 0) > 0
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function evaluateNativeCompare(wasmStatus, nativeStatus) {
|
|
619
|
+
const wasmPass = PASS_OUTCOMES.has(wasmStatus.outcome);
|
|
620
|
+
const nativePass = PASS_OUTCOMES.has(nativeStatus.outcome);
|
|
621
|
+
const wasmOutcome = String(wasmStatus.outcome || "");
|
|
622
|
+
const nativeOutcome = String(nativeStatus.outcome || "");
|
|
623
|
+
const bothSuccess = wasmOutcome === "success" && nativeOutcome === "success";
|
|
624
|
+
const bothSolverError = wasmOutcome === "solver_error" && nativeOutcome === "solver_error";
|
|
625
|
+
|
|
626
|
+
if (wasmPass !== nativePass) {
|
|
627
|
+
return { match: false, reason: "pass_mismatch" };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (wasmPass && nativePass) {
|
|
631
|
+
if (bothSolverError) {
|
|
632
|
+
return { match: true, reason: null };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!bothSuccess) {
|
|
636
|
+
return { match: false, reason: "pass_outcome_mismatch" };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const wasmErrorCode = wasmStatus.errorCode || null;
|
|
640
|
+
const nativeErrorCode = nativeStatus.errorCode || null;
|
|
641
|
+
if (wasmErrorCode !== nativeErrorCode) {
|
|
642
|
+
return { match: false, reason: "error_code_mismatch" };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (wasmStatus.hasNormalTermination !== nativeStatus.hasNormalTermination) {
|
|
646
|
+
return { match: false, reason: "termination_marker_mismatch" };
|
|
647
|
+
}
|
|
648
|
+
const wasmHasOp2 = hasNonEmptyArtifact(wasmStatus, "OP2");
|
|
649
|
+
const nativeHasOp2 = hasNonEmptyArtifact(nativeStatus, "OP2");
|
|
650
|
+
if (wasmHasOp2 !== nativeHasOp2) {
|
|
651
|
+
return { match: false, reason: "op2_presence_mismatch" };
|
|
652
|
+
}
|
|
653
|
+
} else if (wasmOutcome !== nativeOutcome) {
|
|
654
|
+
return { match: false, reason: "outcome_mismatch" };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return { match: true, reason: null };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function runQuickSmoke(repoRoot) {
|
|
661
|
+
const distJs = path.join(repoRoot, "dist", "mystran.js");
|
|
662
|
+
const distWasm = path.join(repoRoot, "dist", "mystran.wasm");
|
|
663
|
+
const deckLabel = "Bush_Bar/bar_01.DAT";
|
|
664
|
+
const deckPath = path.join(
|
|
665
|
+
repoRoot,
|
|
666
|
+
"sources",
|
|
667
|
+
"MYSTRAN_Benchmark",
|
|
668
|
+
"Benchmark_Decks",
|
|
669
|
+
"Bush_Bar",
|
|
670
|
+
"bar_01.DAT"
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
if (!fileExists(distJs)) {
|
|
674
|
+
console.error("[smoke] Missing dist/mystran.js. Build first:");
|
|
675
|
+
console.error(" scripts\\build.ps1");
|
|
676
|
+
process.exit(2);
|
|
677
|
+
}
|
|
678
|
+
if (!fileExists(distWasm)) {
|
|
679
|
+
console.error("[smoke] Missing dist/mystran.wasm. Build first:");
|
|
680
|
+
console.error(" scripts\\build.ps1");
|
|
681
|
+
process.exit(2);
|
|
682
|
+
}
|
|
683
|
+
if (!fileExists(deckPath)) {
|
|
684
|
+
console.error("[smoke] Missing benchmark deck:");
|
|
685
|
+
console.error(` ${deckPath}`);
|
|
686
|
+
console.error(" Run scripts\\fetch_deps.ps1 first.");
|
|
687
|
+
process.exit(2);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const nativeContext = createNativeRunnerContext(repoRoot);
|
|
691
|
+
let mystran = null;
|
|
692
|
+
let combinedText = "";
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
const deckText = fs.readFileSync(deckPath, "utf8");
|
|
696
|
+
const startedAt = process.hrtime.bigint();
|
|
697
|
+
mystran = await WebMYSTRAN.load({
|
|
698
|
+
moduleUrl: pathToFileURL(distJs).href,
|
|
699
|
+
wasmUrl: pathToFileURL(distWasm).href
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const result = mystran.run(deckText, {
|
|
703
|
+
workDir: "/work",
|
|
704
|
+
deckFileName: RUNTIME_DECK_FILE_NAME
|
|
705
|
+
});
|
|
706
|
+
const wasmElapsedSec = Number(process.hrtime.bigint() - startedAt) / 1e9;
|
|
707
|
+
|
|
708
|
+
const problems = [];
|
|
709
|
+
const { raw, output } = result;
|
|
710
|
+
combinedText = output.text || "";
|
|
711
|
+
|
|
712
|
+
if (raw.exitCode !== 0) {
|
|
713
|
+
problems.push(`exit code ${raw.exitCode}`);
|
|
714
|
+
}
|
|
715
|
+
if (!output.hasNormalTermination) {
|
|
716
|
+
problems.push("missing normal termination marker");
|
|
717
|
+
}
|
|
718
|
+
if (output.errorCodes.length > 0) {
|
|
719
|
+
problems.push(`solver error code(s): ${output.errorCodes.join(", ")}`);
|
|
720
|
+
}
|
|
721
|
+
if (!result.outputs.F06) {
|
|
722
|
+
problems.push("missing F06 output");
|
|
723
|
+
}
|
|
724
|
+
if (!result.outputs.OP2) {
|
|
725
|
+
problems.push("missing OP2 output");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
let f06Text = "";
|
|
729
|
+
if (result.outputs.F06) {
|
|
730
|
+
f06Text = mystran.readOutput(result, "F06", "utf8");
|
|
731
|
+
if (!/MYSTRAN END/i.test(f06Text)) {
|
|
732
|
+
problems.push("F06 missing MYSTRAN END marker");
|
|
733
|
+
}
|
|
734
|
+
if (!/Total cpu time/i.test(f06Text)) {
|
|
735
|
+
problems.push("F06 missing total CPU time marker");
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const resultArtifacts = Array.isArray(result.files)
|
|
740
|
+
? result.files.map((entry) => ({
|
|
741
|
+
extension: String(entry.extension || "").toUpperCase(),
|
|
742
|
+
length: Number(entry.size || 0)
|
|
743
|
+
}))
|
|
744
|
+
: [];
|
|
745
|
+
const wasmWorkerResult = {
|
|
746
|
+
exitCode: Number.isInteger(raw.exitCode) ? raw.exitCode : 1,
|
|
747
|
+
elapsedSec: wasmElapsedSec,
|
|
748
|
+
stdout: raw.stdout || "",
|
|
749
|
+
stderr: raw.stderr || "",
|
|
750
|
+
consoleText: output.text || "",
|
|
751
|
+
hasNormalTermination: Boolean(output.hasNormalTermination),
|
|
752
|
+
runtimeAbort: Boolean(output.hasRuntimeAbort),
|
|
753
|
+
solverFailure: Boolean(output.hasSolverError),
|
|
754
|
+
errorCode: output.firstErrorCode || null,
|
|
755
|
+
f06Present: Boolean(result.outputs.F06),
|
|
756
|
+
f06Length: f06Text.length,
|
|
757
|
+
nonEmptyResultFileCount: resultArtifacts.filter((entry) => entry.length > 0).length,
|
|
758
|
+
resultArtifacts
|
|
759
|
+
};
|
|
760
|
+
const wasmStatus = evaluateRunStatus(deckLabel, wasmWorkerResult, raw.exitCode, false, output.text || "");
|
|
761
|
+
|
|
762
|
+
const nativeRun = runNativeDeckCase(nativeContext, deckPath, 1800);
|
|
763
|
+
const nativeStatus = evaluateRunStatus(
|
|
764
|
+
deckLabel,
|
|
765
|
+
nativeRun.workerResult,
|
|
766
|
+
nativeRun.exitCode,
|
|
767
|
+
nativeRun.timedOut,
|
|
768
|
+
nativeRun.consoleText
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
const comparison = evaluateNativeCompare(wasmStatus, nativeStatus);
|
|
772
|
+
if (!PASS_OUTCOMES.has(nativeStatus.outcome)) {
|
|
773
|
+
problems.push(`native outcome ${nativeStatus.outcome}`);
|
|
774
|
+
}
|
|
775
|
+
if (!comparison.match) {
|
|
776
|
+
problems.push(`native compare mismatch (${comparison.reason})`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const isPass = problems.length === 0;
|
|
780
|
+
const lineState = isPass ? "PASS" : "FAIL";
|
|
781
|
+
const cmpToken = comparison.match ? "match" : `mismatch:${comparison.reason}`;
|
|
782
|
+
const codeToken =
|
|
783
|
+
wasmStatus.errorCode || nativeStatus.errorCode
|
|
784
|
+
? ` error=${wasmStatus.errorCode ?? "none"}/${nativeStatus.errorCode ?? "none"}`
|
|
785
|
+
: "";
|
|
786
|
+
console.log(
|
|
787
|
+
`[smoke][0001/0001] ${lineState} deck=${deckLabel} wasm=${wasmStatus.outcome} native=${nativeStatus.outcome} cmp=${cmpToken} exit=${wasmStatus.exitCode}/${nativeStatus.exitCode} time=${wasmElapsedSec.toFixed(3)}s/${nativeRun.elapsedSec.toFixed(3)}s f06=${wasmStatus.f06Length}/${nativeStatus.f06Length} files=${wasmStatus.nonEmptyResultFileCount}/${nativeStatus.nonEmptyResultFileCount}${codeToken}`
|
|
788
|
+
);
|
|
789
|
+
console.log(
|
|
790
|
+
`Smoke summary: total=1 pass=${isPass ? 1 : 0} fail=${isPass ? 0 : 1} wasmWall=${wasmElapsedSec.toFixed(3)}s nativeWall=${nativeRun.elapsedSec.toFixed(3)}s`
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
if (!isPass) {
|
|
794
|
+
for (const problem of problems) {
|
|
795
|
+
console.error(` - ${problem}`);
|
|
796
|
+
}
|
|
797
|
+
console.error("");
|
|
798
|
+
console.error("--- tail of wasm combined console output ---");
|
|
799
|
+
console.error(tailLines(combinedText, 120));
|
|
800
|
+
console.error("-------------------------------------------");
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
} finally {
|
|
804
|
+
if (mystran) {
|
|
805
|
+
mystran.destroy();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function runFullSmoke(repoRoot, forwardedArgs) {
|
|
811
|
+
const options = parseFullSmokeArgs(forwardedArgs);
|
|
812
|
+
const distDir = path.join(repoRoot, "dist");
|
|
813
|
+
const benchDeckDir = path.join(repoRoot, "sources", "MYSTRAN_Benchmark", "Benchmark_Decks");
|
|
814
|
+
const runnerScript = fileURLToPath(import.meta.url);
|
|
815
|
+
|
|
816
|
+
const distJs = path.join(distDir, "mystran.js");
|
|
817
|
+
const webmystranWasm = path.join(distDir, "mystran.wasm");
|
|
818
|
+
for (const requiredPath of [distJs, webmystranWasm, benchDeckDir]) {
|
|
819
|
+
if (!fileExists(requiredPath)) {
|
|
820
|
+
throw new Error(`Missing required path: ${requiredPath}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const allDeckAbs = listDeckFilesRecursive(benchDeckDir);
|
|
825
|
+
const deckSpecs =
|
|
826
|
+
options.decks.length > 0
|
|
827
|
+
? options.decks
|
|
828
|
+
: allDeckAbs.map((deckPath) => toBenchRelativePath(benchDeckDir, deckPath));
|
|
829
|
+
|
|
830
|
+
if (deckSpecs.length === 0) {
|
|
831
|
+
throw new Error(`No benchmark decks found under ${benchDeckDir}`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const nativeContext = createNativeRunnerContext(repoRoot);
|
|
835
|
+
const runnerFailures = [];
|
|
836
|
+
const outcomeFailures = [];
|
|
837
|
+
let pairPassCount = 0;
|
|
838
|
+
let wasmPassCount = 0;
|
|
839
|
+
let nativePassCount = 0;
|
|
840
|
+
let ignoredBothNonSuccessCount = 0;
|
|
841
|
+
let wasmTotalWallSec = 0;
|
|
842
|
+
let nativeTotalWallSec = 0;
|
|
843
|
+
|
|
844
|
+
let deckIndex = 0;
|
|
845
|
+
const totalDecks = deckSpecs.length;
|
|
846
|
+
for (const deckSpec of deckSpecs) {
|
|
847
|
+
deckIndex += 1;
|
|
848
|
+
const deckPrefix = `[smoke][${String(deckIndex).padStart(4, "0")}/${String(totalDecks).padStart(4, "0")}]`;
|
|
849
|
+
const deckPath = resolveDeckPath(deckSpec, benchDeckDir, allDeckAbs);
|
|
850
|
+
if (!deckPath) {
|
|
851
|
+
const message = `Deck not found: ${deckSpec}`;
|
|
852
|
+
runnerFailures.push(message);
|
|
853
|
+
console.log(`${deckPrefix} FAIL deck=${deckSpec} wasm=deck_not_found native=deck_not_found cmp=mismatch:deck_not_found exit=1/1 time=0.000s/0.000s f06=0/0 files=0/0`);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const deckLabel = toBenchRelativePath(benchDeckDir, deckPath);
|
|
858
|
+
const wasmRun = runWasmDeckCase(
|
|
859
|
+
deckPath,
|
|
860
|
+
runnerScript,
|
|
861
|
+
options.timeoutSeconds,
|
|
862
|
+
distJs,
|
|
863
|
+
webmystranWasm
|
|
864
|
+
);
|
|
865
|
+
const nativeRun = runNativeDeckCase(nativeContext, deckPath, options.timeoutSeconds);
|
|
866
|
+
wasmTotalWallSec += wasmRun.elapsedSec;
|
|
867
|
+
nativeTotalWallSec += nativeRun.elapsedSec;
|
|
868
|
+
|
|
869
|
+
const wasmStatus = evaluateRunStatus(
|
|
870
|
+
deckLabel,
|
|
871
|
+
wasmRun.workerResult,
|
|
872
|
+
wasmRun.exitCode,
|
|
873
|
+
wasmRun.timedOut,
|
|
874
|
+
wasmRun.consoleText
|
|
875
|
+
);
|
|
876
|
+
const nativeStatus = evaluateRunStatus(
|
|
877
|
+
deckLabel,
|
|
878
|
+
nativeRun.workerResult,
|
|
879
|
+
nativeRun.exitCode,
|
|
880
|
+
nativeRun.timedOut,
|
|
881
|
+
nativeRun.consoleText
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
const comparison = evaluateNativeCompare(wasmStatus, nativeStatus);
|
|
885
|
+
const wasmPass = PASS_OUTCOMES.has(wasmStatus.outcome);
|
|
886
|
+
const nativePass = PASS_OUTCOMES.has(nativeStatus.outcome);
|
|
887
|
+
if (wasmPass) {
|
|
888
|
+
wasmPassCount += 1;
|
|
889
|
+
}
|
|
890
|
+
if (nativePass) {
|
|
891
|
+
nativePassCount += 1;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const bothNonSuccess = wasmStatus.outcome !== "success" && nativeStatus.outcome !== "success";
|
|
895
|
+
const isPass = bothNonSuccess || (wasmPass && nativePass && comparison.match);
|
|
896
|
+
if (isPass) {
|
|
897
|
+
pairPassCount += 1;
|
|
898
|
+
if (bothNonSuccess) {
|
|
899
|
+
ignoredBothNonSuccessCount += 1;
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
outcomeFailures.push(
|
|
903
|
+
`${deckLabel} failed: wasm=${wasmStatus.outcome} native=${nativeStatus.outcome} cmp=${comparison.match ? "match" : comparison.reason} wasmExit=${wasmStatus.exitCode} nativeExit=${nativeStatus.exitCode} wasmError=${wasmStatus.errorCode ?? "none"} nativeError=${nativeStatus.errorCode ?? "none"}`
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const lineState = isPass ? "PASS" : "FAIL";
|
|
908
|
+
const cmpToken = bothNonSuccess
|
|
909
|
+
? "ignored:both_non_success"
|
|
910
|
+
: comparison.match
|
|
911
|
+
? "match"
|
|
912
|
+
: `mismatch:${comparison.reason}`;
|
|
913
|
+
const lineError =
|
|
914
|
+
wasmStatus.errorCode || nativeStatus.errorCode
|
|
915
|
+
? ` error=${wasmStatus.errorCode ?? "none"}/${nativeStatus.errorCode ?? "none"}`
|
|
916
|
+
: "";
|
|
917
|
+
console.log(
|
|
918
|
+
`${deckPrefix} ${lineState} deck=${deckLabel} wasm=${wasmStatus.outcome} native=${nativeStatus.outcome} cmp=${cmpToken} exit=${wasmStatus.exitCode}/${nativeStatus.exitCode} time=${wasmRun.elapsedSec.toFixed(3)}s/${nativeRun.elapsedSec.toFixed(3)}s f06=${wasmStatus.f06Length}/${nativeStatus.f06Length} files=${wasmStatus.nonEmptyResultFileCount}/${nativeStatus.nonEmptyResultFileCount}${lineError}`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const hasFailures = runnerFailures.length > 0 || outcomeFailures.length > 0;
|
|
923
|
+
if (runnerFailures.length > 0) {
|
|
924
|
+
console.log("");
|
|
925
|
+
console.log("Run infrastructure failures:");
|
|
926
|
+
for (const failure of runnerFailures) {
|
|
927
|
+
console.log(` - ${failure}`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (outcomeFailures.length > 0) {
|
|
931
|
+
console.log("");
|
|
932
|
+
console.log("Outcome failures:");
|
|
933
|
+
for (const failure of outcomeFailures) {
|
|
934
|
+
console.log(` - ${failure}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const failedCount = deckSpecs.length - pairPassCount;
|
|
939
|
+
console.log("");
|
|
940
|
+
console.log(
|
|
941
|
+
`Smoke summary: total=${deckSpecs.length} pass=${pairPassCount} fail=${failedCount} wasmPass=${wasmPassCount} nativePass=${nativePassCount} ignoredBothNonSuccess=${ignoredBothNonSuccessCount} wasmWall=${Number(wasmTotalWallSec.toFixed(3))}s nativeWall=${Number(nativeTotalWallSec.toFixed(3))}s`
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
if (hasFailures) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
`Smoke test failed (pairPass=${pairPassCount}, pairFail=${failedCount}, wasmPass=${wasmPassCount}, nativePass=${nativePassCount}, ignoredBothNonSuccess=${ignoredBothNonSuccessCount}).`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async function main() {
|
|
952
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
953
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
954
|
+
const args = process.argv.slice(2);
|
|
955
|
+
const mode = (args[0] || "quick").toLowerCase();
|
|
956
|
+
|
|
957
|
+
if (mode === "__run_deck") {
|
|
958
|
+
await runDeckWorker(args.slice(1));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (mode === "quick") {
|
|
963
|
+
await runQuickSmoke(repoRoot);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (mode === "full") {
|
|
967
|
+
runFullSmoke(repoRoot, args.slice(1));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
throw new Error(`Unknown command "${args[0]}". Supported: quick, full`);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
main().catch((error) => {
|
|
975
|
+
console.error("[smoke] FAIL");
|
|
976
|
+
console.error(error && error.message ? error.message : error);
|
|
977
|
+
process.exit(1);
|
|
978
|
+
});
|