Files
gex/test/gexcc/programs.test.js
velzie 0ac6bbbfa8 test: the unix rebind, terminal polling, /proc usage, pool flavour, empty exec argument, spare Worker and dense table checks in run.mjs; filelocks and peercred whatever the load
The checks run as gexcc/dl-NAME, each on both arches but pool flavour,
which is the AArch64 shim's. A script's leg that must have run (its
browser runtime one) is named by the test, and a skip line after a
check fails it.

filelocks waited fixed times for a child to take a lock or to start
waiting, and a loaded machine made it late: a child now says over a
pipe when it holds a lock or is about to wait, a holder keeps its lock
until told, a waiter checks that the release was announced before its
wait ended, the deadlock's two orders print the same, and the signal
that ends a wait comes again until the wait has begun. peercred's
connecting child printed beside its parent, and two processes' lines on
one stdout keep no order on the page: the child's line goes to the
parent over the connection. More programs at once (GEX_TEST_INNER,
default 4).

run.mjs: a node-host test's last messages could come after its exit,
which read as an early exit; the exit is taken once the channel has
closed too. A test's work directories go at its end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_e0aa53aa95eb4b86910519b4d14812a5
(cherry picked from commit 9e3b6b809a0bfcf29db216059e1051cd85319512)
2026-11-05 20:28:35 -05:00

94 lines
4.9 KiB
JavaScript

// gexcc programs against the host: each fixtures/gexcc/programs/NAME.c
// (NAME.cpp) built with cc (c++) and with gexcc (gexc++), and run three
// ways: natively, natively under this arch's gexc, and on the page's shim
// (hosts/browser/js/run-node.mjs, the page's shared build under Node). The
// output and the exit status must be the host's on each.
//
// NAME.sh, when there is one, runs the program (given its path) in place of
// running it directly: host FIFOs, host stdin. Those are native only, as is
// any `system()` call without BUSYBOX (a gexcc busybox for the guest's
// /bin/sh). A source with `gexcc-test: as root` runs as root on both sides,
// in a user namespace with root and 65535 other ids mapped.
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { FIXTURES, gexcc, pool } from '../lib/gexcc.mjs';
export const meta = { hosts: ['node'], timeout: 1800 };
const DIR = join(FIXTURES, 'programs');
const ROOT = join(import.meta.dirname, '..', '..');
/** The guest's lines of run-node.mjs's stdout, less the runtime's own. */
const guestOf = (out) => out.split('\n').filter((l) => !/^\[host\]|^\[gex\] |^\[worker\] |^\[gex-console/.test(l)).join('\n').replace(/\n+$/, '');
export default async function (t) {
const c = gexcc(t);
const x86 = t.arch === 'x86';
const seed = join(ROOT, 'page/public/gex', `seed${x86 ? '.x86' : ''}.shared.wasm`);
const page = existsSync(seed) && existsSync(join(ROOT, `page/public/gex/gex_browser${x86 ? '.x86' : ''}.shared.wasm`));
if (!page) t.say(`no ${seed} (make assets): the page legs are not run`);
const busybox = process.env.BUSYBOX && existsSync(process.env.BUSYBOX) ? process.env.BUSYBOX : null;
const srcs = readdirSync(DIR).filter((f) => /\.(c|cpp)$/.test(f)).sort();
const asRoot = ['unshare', '--map-auto', '--map-root-user'];
await pool(srcs, async (f) => {
const cxx = f.endsWith('.cpp');
const name = f.replace(/\.(c|cpp)$/, '');
const src = join(DIR, f);
const code = readFileSync(src, 'utf8');
const d = c.dir(name);
try {
await c.hostBuild(['-O1', '-w', '-o', `${d}/${name}.native`, src, '-lpthread'], { cxx });
// function pointer cast emulation is static-only
await c.build(['-O2', '-w', ...(name.startsWith('fpcast') ? ['-fgex-fpcast-emu'] : []), '-o', `${d}/${name}`, src], { cxx });
} catch (e) {
t.check(`${name}: builds`, false, e.message);
return;
}
const root = code.includes('gexcc-test: as root') ? asRoot : [];
const driver = join(DIR, `${name}.sh`);
const go = (prog) => c.run(existsSync(driver) ? ['sh', driver, prog] : [...root, prog], { cwd: d });
const native = await go(`./${name}.native`);
const gex = await go(`./${name}`);
t.check(`${name}`, native.out === gex.out && native.code === gex.code,
native.code !== gex.code ? `native exit ${native.code}, gex exit ${gex.code}: ${gex.out.split('\n').slice(-3).join(' / ')}` : diff(native.out, gex.out));
if (!page || existsSync(driver)) return;
const bb = code.includes('system(') ? busybox : undefined;
if (code.includes('system(') && !bb) return t.say(`${name} (page): needs BUSYBOX, not run`);
const web = await c.run([...root, process.execPath, join(ROOT, 'hosts/browser/js/run-node.mjs'), ...(bb ? ['--busybox', bb] : []),
'--file', `/t/${name}=${d}/${name}`, '--cwd', '/t', '--argv0', `./${name}`, seed, `${d}/${name}`],
{ cwd: d, stderr: false, env: { GEX_ARCH: x86 ? 'x86' : 'aarch64', GEX_SHARED: '1' } });
const out = guestOf(web.out);
t.check(`${name} (page)`, native.out === out && native.code === web.code,
native.code !== web.code ? `native exit ${native.code}, page exit ${web.code}` : diff(native.out, out));
});
// A program linked against libc.so starts (gex's loader fills libc's
// registry of shared libraries, which a musl without it does not have), in
// C and in C++ with a throw caught.
const d = c.dir('shared');
writeFileSync(join(d, 'sh.c'), '#include <stdio.h>\nint main(void){puts("shared c");return 0;}\n');
writeFileSync(join(d, 'shx.cpp'), '#include <cstdio>\nint main(){try{throw 7;}catch(int e){std::printf("shared c++ %d\\n",e);}return 0;}\n');
for (const [src, out, want, cxx] of [['sh.c', 'sh', 'shared c', false], ['shx.cpp', 'shx', 'shared c++ 7', true]]) {
try {
await c.build(['-O2', '-o', out, src], { cxx, shared: true, cwd: d });
const r = await c.run([`./${out}`], { cwd: d, env: { LD_LIBRARY_PATH: c.libdir } });
t.eq(`shared-${out}`, r.out, want);
} catch (e) {
t.check(`shared-${out}: builds`, false, e.message);
}
}
c.clean();
}
/** The first lines where `a` and `b` differ, for a FAIL's detail. */
function diff(a, b) {
if (a === b) return '';
const x = a.split('\n');
const y = b.split('\n');
const i = x.findIndex((l, k) => l !== y[k]);
const k = i < 0 ? x.length : i;
return `line ${k + 1}: host ${JSON.stringify(x[k] ?? '(end)')}, gex ${JSON.stringify(y[k] ?? '(end)')}`;
}