// Headless check of the run pipeline in Node against the local numbl build: // stages the vendored +mr tree + the FID example exactly the way the // numbl/browser worker does for an idle boot (cwd /project, relative // workspace-file names, execute-by-name with mainScriptPath 'repl' and no // search paths), runs it, collects *.seq via the same dir() snippet the // runner uses, and byte-compares the result against the MATLAB golden. import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { executeCode, VirtualFileSystem, BrowserFileIOAdapter, BrowserSystemAdapter, } from 'numbl' const here = path.dirname(fileURLToPath(import.meta.url)) const repoRoot = path.join(here, '..') const mrRoot = path.join(repoRoot, 'src', 'engine', 'pulseq') const MD5_OVERRIDE = `function digest = md5(message, noBuiltIn) digest = hash('MD5', char(message)); end ` const COLLECT_SNIPPET = `seqlab_d = dir('*.seq'); for seqlab_i = 1:numel(seqlab_d) fprintf('SEQLAB_FILE:%s\\n', seqlab_d(seqlab_i).name); end ` // Boot files, as mrFiles.ts builds them (paths relative to /project). function bootFiles() { const files = [] for (const entry of fs.readdirSync(path.join(mrRoot, '+mr'), { recursive: true })) { const rel = String(entry) const abs = path.join(mrRoot, '+mr', rel) if (!fs.statSync(abs).isFile() || !rel.endsWith('.m')) continue const vfsPath = `+mr/${rel}` files.push({ path: vfsPath, content: vfsPath === '+mr/+aux/md5.m' ? MD5_OVERRIDE : fs.readFileSync(abs, 'utf8'), }) } return files } const script = fs.readFileSync(path.join(repoRoot, 'src', 'examples', 'fid.m'), 'utf8') const files = [...bootFiles(), { path: 'main.m', content: script }] // Mirror of worker.ts boot (idle) + execute('main'). const enc = new TextEncoder() const vfs = new VirtualFileSystem() for (const f of files) vfs.writeFile('/project/' + f.path, enc.encode(f.content)) vfs.setCwd('/project') const workspaceFiles = files.map((f) => ({ name: f.path, source: f.content })) const workerOptions = { onOutput: (text) => process.stdout.write(`[numbl] ${text}`), displayResults: true, maxIterations: 1e9, optimization: '1', fileIO: new BrowserFileIOAdapter(vfs), system: new BrowserSystemAdapter(vfs), } const t0 = Date.now() let result try { result = executeCode('main;', workerOptions, workspaceFiles, 'repl', []) } catch (err) { console.error('run failed:', err.message ?? err) if (err.file) console.error(` at ${err.file}:${err.line}`) process.exit(1) } console.log(`run finished in ${((Date.now() - t0) / 1000).toFixed(1)} s`) // Collect .seq files through the same follow-up execute the runner performs. const collected = [] const collectResult = executeCode( COLLECT_SNIPPET, { ...workerOptions, onOutput: (text) => collected.push(text), initialVariableValues: result.variableValues, initialHoldState: result.holdState, implicitCwdPath: result.implicitCwdPath, }, workspaceFiles, 'repl', result.searchPaths ?? [], ) if (!collectResult) process.exit(1) const names = [...collected.join('').matchAll(/^SEQLAB_FILE:(.+)$/gm)].map((m) => m[1]) console.log('collected .seq files:', names) if (names.length !== 1 || names[0] !== 'fid.seq') { console.error('FAIL: expected exactly [fid.seq]') process.exit(1) } const produced = Buffer.from(vfs.readFile('/project/fid.seq')) const golden = fs.readFileSync(path.join(repoRoot, 'test-data', 'golden', 'fid.seq')) if (produced.equals(golden)) { console.log(`PASS: fid.seq is byte-identical to the MATLAB golden (${golden.length} bytes)`) } else { console.error(`FAIL: fid.seq differs from golden (${produced.length} vs ${golden.length} bytes)`) const a = produced.toString('utf8').split('\n') const b = golden.toString('utf8').split('\n') for (let i = 0; i < Math.max(a.length, b.length); i++) { if (a[i] !== b[i]) { console.error(` first diff at line ${i + 1}:`) console.error(` produced: ${a[i]}`) console.error(` golden: ${b[i]}`) break } } process.exit(1) }