Files
TRIMSP/tests/wsl-runtime.test.js
salman b3e75b2300 Ship the WSL2 runtime in a Squirrel Windows .exe
The Windows installer now bundles the WSL2 engine runtime so the installed
app works out of the box.

- forge.config.js: configure the Squirrel maker (setupExe name, setup icon) and
  require the rootfs for any Windows build, throwing a clear error if
  packaging/wsl2/out/ is empty so the .exe can never ship without its engine.
- wsl-runtime.js: rootfsCandidates now checks the flat resources/ path first,
  which is where forge's extraResource copies the archive (the wsl-runtime/
  subdirectory and the dev-checkout path remain as fallbacks).
- package.json: add package:win / make:win scripts that build the static engine,
  the rootfs, then package/make for win32.
- tests: cover the flat-first rootfs lookup.
- README/CHANGELOG: document the .exe build, its Wine+Mono cross-build
  prerequisite on Linux, and the required-rootfs guard.
2026-10-20 09:42:47 +02:00

240 lines
9.5 KiB
JavaScript

// Tests for the Windows/WSL2 bundled-engine runtime plumbing.
//
// Two layers are covered:
// 1. the pure main-process module wsl-runtime.js, driven with an injected
// spawner so no Windows host is required;
// 2. the renderer-side launch translation and runtime gate in TrimSPelec.js,
// lifted out of the shipped source and run against minimal stubs.
//
// Run with: node tests/wsl-runtime.test.js
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const wsl = require('../wsl-runtime');
// --- helpers ---------------------------------------------------------------
function makeRun(responses) {
const calls = [];
const run = function (args, onOutput) {
calls.push(args);
const next = responses.shift() || { status: 0, stdout: '', stderr: '' };
if (typeof onOutput === 'function' && next.stderr) onOutput(next.stderr);
return Promise.resolve(next);
};
run.calls = calls;
return run;
}
function makeRootfs() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trimsp-wsl-'));
const file = path.join(dir, wsl.ROOTFS_BASENAME);
fs.writeFileSync(file, 'rootfs');
return { dir, file };
}
// --- 1. pure module --------------------------------------------------------
function testDecodeWslOutput() {
assert.strictEqual(wsl.decodeWslOutput(Buffer.from('abc', 'utf16le')), 'abc');
assert.strictEqual(wsl.decodeWslOutput(Buffer.from('abc', 'utf8')), 'abc');
assert.strictEqual(wsl.decodeWslOutput('plain'), 'plain');
assert.strictEqual(wsl.decodeWslOutput(null), '');
assert.strictEqual(wsl.decodeWslOutput(Buffer.alloc(0)), '');
}
function testParseDistroList() {
assert.deepStrictEqual(wsl.parseDistroList('a\r\nb\r\n'), ['a', 'b']);
assert.deepStrictEqual(wsl.parseDistroList(' a \n\n b '), ['a', 'b']);
assert.deepStrictEqual(wsl.parseDistroList('a\u0000\nb\u0000'), ['a', 'b']);
assert.deepStrictEqual(wsl.parseDistroList(''), []);
assert.deepStrictEqual(wsl.parseDistroList(null), []);
}
function testDistroIsRegistered() {
assert.strictEqual(wsl.distroIsRegistered('Ubuntu\ntrimspNL-runtime\n', wsl.DEFAULT_DISTRO), true);
assert.strictEqual(wsl.distroIsRegistered('TRIMSPNL-RUNTIME\n', wsl.DEFAULT_DISTRO), true);
assert.strictEqual(wsl.distroIsRegistered('Ubuntu\n', wsl.DEFAULT_DISTRO), false);
}
function testArgumentBuilders() {
assert.deepStrictEqual(wsl.listArgs(), ['--list', '--quiet']);
assert.deepStrictEqual(
wsl.importArgs('/tmp/r.tar.gz', 'C:\\dir', 'trimspNL-runtime'),
['--import', 'trimspNL-runtime', 'C:\\dir', '/tmp/r.tar.gz', '--version', '2']);
assert.strictEqual(
wsl.distroInstallDir('C:\\Users\\x\\AppData\\Local', 'trimspNL-runtime'),
path.join('C:\\Users\\x\\AppData\\Local', 'TrimSP', 'trimspNL-runtime'));
}
function testFindRootfsTarball() {
assert.strictEqual(wsl.findRootfsTarball('/nonexistent'), '');
// forge copies extraResource entries flat into resources/, so the flat path
// is the primary candidate.
const flat = fs.mkdtempSync(path.join(os.tmpdir(), 'trimsp-flat-'));
fs.writeFileSync(path.join(flat, wsl.ROOTFS_BASENAME), 'rootfs');
assert.strictEqual(wsl.findRootfsTarball(flat), path.join(flat, wsl.ROOTFS_BASENAME));
// A wsl-runtime/ subdirectory is also honoured.
const { dir, file } = makeRootfs();
const sub = path.join(dir, 'wsl-runtime');
fs.mkdirSync(sub);
fs.renameSync(file, path.join(sub, wsl.ROOTFS_BASENAME));
assert.strictEqual(wsl.findRootfsTarball(dir), path.join(sub, wsl.ROOTFS_BASENAME));
assert.ok(wsl.rootfsCandidates('/x')[0].endsWith(wsl.ROOTFS_BASENAME));
}
async function testNotWindows() {
const run = makeRun([]);
const result = await wsl.ensureWSLRuntime({ platform: 'linux', run });
assert.strictEqual(result.ok, true);
assert.strictEqual(result.status, 'skipped');
assert.strictEqual(result.reason, 'not-windows');
assert.strictEqual(run.calls.length, 0);
}
async function testMissingRootfs() {
const run = makeRun([]);
const result = await wsl.ensureWSLRuntime({
platform: 'win32', resourcesPath: '/nonexistent', run
});
assert.strictEqual(result.ok, false);
assert.strictEqual(result.status, 'missing-rootfs');
}
async function testAlreadyPresentSkipsImport() {
const { file } = makeRootfs();
const run = makeRun([{ status: 0, stdout: wsl.DEFAULT_DISTRO + '\r\n', stderr: '' }]);
const result = await wsl.ensureWSLRuntime({
platform: 'win32', rootfsPath: file, appDataPath: '/tmp/appdata', run
});
assert.strictEqual(result.status, 'already-present');
assert.strictEqual(run.calls.length, 1);
}
async function testImportSucceeds() {
const { file } = makeRootfs();
const phases = [];
const run = makeRun([
{ status: 0, stdout: 'No installed distributions\r\n', stderr: '' },
{ status: 0, stdout: '', stderr: 'importing...' }
]);
const result = await wsl.ensureWSLRuntime({
platform: 'win32', rootfsPath: file, appDataPath: '/tmp/appdata', run,
onProgress: function (p) { phases.push(p.phase); }
});
assert.strictEqual(result.ok, true);
assert.strictEqual(result.status, 'imported');
assert.strictEqual(result.target, wsl.distroInstallDir('/tmp/appdata', wsl.DEFAULT_DISTRO));
assert.deepStrictEqual(run.calls[1], wsl.importArgs(file, result.target, wsl.DEFAULT_DISTRO));
assert.strictEqual(phases[0], 'checking');
assert.ok(phases.includes('importing'));
assert.strictEqual(phases[phases.length - 1], 'ready');
}
async function testImportFailure() {
const { file } = makeRootfs();
const run = makeRun([
{ status: 0, stdout: '', stderr: '' },
{ status: 1, stdout: '', stderr: 'boom' }
]);
const result = await wsl.ensureWSLRuntime({
platform: 'win32', rootfsPath: file, appDataPath: '/tmp/appdata', run
});
assert.strictEqual(result.ok, false);
assert.strictEqual(result.status, 'import-failed');
assert.ok(result.error.includes('boom'));
}
// --- 2. renderer translation + gate ---------------------------------------
function testRendererTranslation() {
const src = path.join(__dirname, '..', 'TrimSPelec.js');
const srcText = fs.readFileSync(src, 'utf8');
// Extract just the translation helpers by satisfying their free variables.
const bundled = 'C:\\app\\resources\\bin\\trimspNL';
const fakeProcess = { platform: 'win32', resourcesPath: 'C:\\app\\resources' };
const fakePath = path.win32;
const fileExists = function (p) { return p === bundled; };
function grab(name) {
const re = new RegExp('function ' + name + '\\s*\\([^)]*\\)\\s*\\{[\\s\\S]*?\\n\\}', 'm');
const m = srcText.match(re);
if (!m) throw new Error('function not found: ' + name);
return m[0];
}
const body = [
'var TRIMSP_WSL_DISTRO = ' + JSON.stringify('trimspNL-runtime') + ';',
'var TRIMSP_WSL_PREFIX = ' + JSON.stringify('/opt/trimsp-runtime') + ';',
grab('bundledTrimBinaryPath'),
grab('isWslRuntimeHost'),
grab('windowsPathToWsl'),
grab('prepareTrimspLaunch'),
'return { bundledTrimBinaryPath, isWslRuntimeHost, windowsPathToWsl, prepareTrimspLaunch };'
].join('\n');
const api = new Function('process', 'path', 'fileExists', body)(
fakeProcess, fakePath, fileExists);
assert.strictEqual(api.isWslRuntimeHost(), true);
assert.strictEqual(api.windowsPathToWsl('C:\\work\\job.inp'), '/mnt/c/work/job.inp');
assert.strictEqual(api.windowsPathToWsl('D:/data/x.rge'), '/mnt/d/data/x.rge');
assert.strictEqual(api.windowsPathToWsl('pcg32'), 'pcg32');
const launch = api.prepareTrimspLaunch(bundled, ['job', 'pcg32']);
assert.strictEqual(launch.command, 'wsl.exe');
assert.strictEqual(launch.viaWsl, true);
assert.deepStrictEqual(launch.args,
['-d', 'trimspNL-runtime', '--', '/opt/trimsp-runtime/bin/trimspNL', 'job', 'pcg32']);
// A user-configured Windows-native binary must pass through untouched.
const native = api.prepareTrimspLaunch('C:\\tools\\trimspNL.exe', ['job']);
assert.strictEqual(native.command, 'C:\\tools\\trimspNL.exe');
assert.strictEqual(native.viaWsl, false);
// On Linux the translation is inert.
fakeProcess.platform = 'linux';
assert.strictEqual(api.isWslRuntimeHost(), false);
const inert = api.prepareTrimspLaunch(bundled, ['job']);
assert.strictEqual(inert.viaWsl, false);
assert.strictEqual(inert.command, bundled);
}
function run() {
const tests = [
['decodeWslOutput', testDecodeWslOutput],
['parseDistroList', testParseDistroList],
['distroIsRegistered', testDistroIsRegistered],
['argumentBuilders', testArgumentBuilders],
['findRootfsTarball', testFindRootfsTarball],
['notWindows', testNotWindows],
['missingRootfs', testMissingRootfs],
['alreadyPresent', testAlreadyPresentSkipsImport],
['importSucceeds', testImportSucceeds],
['importFailure', testImportFailure],
['rendererTranslation', testRendererTranslation]
];
let failed = 0;
(async function () {
for (const [name, fn] of tests) {
try {
await fn();
console.log('PASS ' + name);
} catch (error) {
failed++;
console.log('FAIL ' + name + ' ' + (error && error.message));
}
}
console.log('\n' + (tests.length - failed) + ' passed, ' + failed + ' failed');
process.exit(failed ? 1 : 0);
})();
}
run();