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.
240 lines
9.5 KiB
JavaScript
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();
|