Compare commits

..

1 Commits

Author SHA1 Message Date
Charles Kerr
604a7dde05 test: set timeout limit for ESM fixture processes 2026-05-01 10:31:42 -05:00
4 changed files with 90 additions and 147 deletions

View File

@@ -72,17 +72,8 @@ if (process.platform === 'win32') {
}
}
// Map process.exit to app.exit, which quits gracefully. When called without
// an explicit code, fall back to process.exitCode like Node.js does.
process.exit = ((code: number | string | undefined | null) => {
// Refs https://github.com/nodejs/node/blob/fc192ee030ee076b948ce7d9d72cba6c101989b8/lib/internal/process/per_thread.js#L229-L252
if (code !== undefined) {
// Node.js handles any string to number conversion here for us
process.exitCode = code;
}
app.exit(process.exitCode || 0);
}) as typeof process.exit;
// Map process.exit to app.exit, which quits gracefully.
process.exit = app.exit as () => never;
// Load the RPC server.
require('@electron/internal/browser/rpc-server');

View File

@@ -2,35 +2,22 @@ import { BrowserWindow } from 'electron';
import { expect } from 'chai';
import * as cp from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { stripVTControlCharacters } from 'node:util';
import { spawnAndWait } from './lib/spec-helpers';
const fixtureTimeout = 20000;
const fixtureKillTimeout = 5000;
const runFixture = async (appPath: string, args: string[] = []) => {
const result = cp.spawn(process.execPath, [appPath, ...args], {
stdio: 'pipe'
return await spawnAndWait(process.execPath, [appPath, ...args], {
timeout: fixtureTimeout,
killTimeout: fixtureKillTimeout,
stripOutput: true
});
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
result.stdout.on('data', (chunk) => stdout.push(chunk));
result.stderr.on('data', (chunk) => stderr.push(chunk));
const [code, signal] = await new Promise<[number | null, NodeJS.Signals | null]>((resolve) => {
result.on('close', (code, signal) => {
resolve([code, signal]);
});
});
return {
code,
signal,
stdout: stripVTControlCharacters(Buffer.concat(stdout).toString().trim()),
stderr: stripVTControlCharacters(Buffer.concat(stderr).toString().trim())
};
};
const fixturePath = path.resolve(__dirname, 'fixtures', 'esm');

View File

@@ -11,6 +11,7 @@ import * as net from 'node:net';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import * as url from 'node:url';
import { stripVTControlCharacters } from 'node:util';
import * as v8 from 'node:v8';
const addOnly = <T>(fn: Function): T => {
@@ -109,6 +110,77 @@ export async function startRemoteControlApp(extraArgs: string[] = [], options?:
return new RemoteControlApp(appProcess, port);
}
export interface SpawnAndWaitResult {
code: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
}
export interface SpawnAndWaitOptions extends childProcess.SpawnOptionsWithoutStdio {
timeout: number;
killTimeout?: number;
stripOutput?: boolean;
}
function formatSpawnOutput(stdout: Buffer[], stderr: Buffer[], stripOutput = false) {
const normalize = (chunks: Buffer[]) => {
const output = Buffer.concat(chunks).toString().trim();
return stripOutput ? stripVTControlCharacters(output) : output;
};
return {
stdout: normalize(stdout),
stderr: normalize(stderr)
};
}
export async function spawnAndWait(
command: string,
args: string[],
options: SpawnAndWaitOptions
): Promise<SpawnAndWaitResult> {
const { timeout, killTimeout = 5000, stripOutput, ...spawnOptions } = options;
const child = childProcess.spawn(command, args, spawnOptions);
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
child.stdout.on('data', (chunk) => stdout.push(chunk));
child.stderr.on('data', (chunk) => stderr.push(chunk));
let timedOut = false;
let killTimeoutId: NodeJS.Timeout | undefined;
const timeoutId = globalThis.setTimeout(() => {
timedOut = true;
child.kill();
killTimeoutId = globalThis.setTimeout(() => {
child.kill('SIGKILL');
}, killTimeout);
}, timeout);
const [code, signal] = await new Promise<[number | null, NodeJS.Signals | null]>((resolve, reject) => {
child.on('error', reject);
child.on('close', (code, signal) => {
resolve([code, signal]);
});
}).finally(() => {
globalThis.clearTimeout(timeoutId);
if (killTimeoutId) globalThis.clearTimeout(killTimeoutId);
});
const output = formatSpawnOutput(stdout, stderr, stripOutput);
if (timedOut) {
throw new Error(
`Timed out after ${timeout}ms waiting for ${command} ${args.join(' ')} to exit. Process closed with code ${code} and signal ${signal}.\n\nstdout:\n${output.stdout}\n\nstderr:\n${output.stderr}`
);
}
return {
code,
signal,
...output
};
}
export function waitUntil(callback: () => boolean | Promise<boolean>, opts: { rate?: number; timeout?: number } = {}) {
const { rate = 10, timeout = 10000 } = opts;
return (async () => {

View File

@@ -17,14 +17,7 @@ import {
spawn
} from './lib/codesign-helpers';
import { withTempDirectory } from './lib/fs-helpers';
import {
getRemoteContext,
ifdescribe,
ifit,
itremote,
startRemoteControlApp,
useRemoteContext
} from './lib/spec-helpers';
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
const mainFixturesPath = path.resolve(__dirname, 'fixtures');
@@ -593,115 +586,9 @@ describe('node feature', () => {
});
});
describe('process.exit', () => {
it('exits with exit code zero when called without an argument', async () => {
const rc = await startRemoteControlApp();
rc.remotely(() => {
setImmediate(() => process.exit());
}).catch(() => {});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(0);
});
it('uses process.exitCode when called without an argument', async () => {
const rc = await startRemoteControlApp();
rc.remotely(() => {
process.exitCode = 42;
setImmediate(() => process.exit());
}).catch(() => {});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(42);
});
it('overrides process.exitCode when called with an argument', async () => {
const rc = await startRemoteControlApp();
rc.remotely(() => {
process.exitCode = 42;
setImmediate(() => process.exit(11));
}).catch(() => {});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(11);
});
it('can be called with a null argument', async () => {
const rc = await startRemoteControlApp();
rc.remotely(() => {
setImmediate(() => process.exit(null));
}).catch(() => {});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(0);
});
it('can be called with a number argument', async () => {
const rc = await startRemoteControlApp();
rc.remotely(() => {
setImmediate(() => process.exit(7));
}).catch(() => {});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(7);
});
it('throws with an invalid number argument', async () => {
const rc = await startRemoteControlApp();
let stdout = '';
rc.process.stdout!.on('data', (d) => {
stdout += d.toString();
});
rc.remotely(() => {
setImmediate(() => {
try {
process.exit(4.2);
} catch (err) {
console.log(err);
process.exit(99);
}
});
}).catch(() => {});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(99);
expect(stdout).to.match(
/RangeError \[ERR_OUT_OF_RANGE\]: The value of "code" is out of range. It must be an integer./
);
});
it('can be called with a string argument', async () => {
const rc = await startRemoteControlApp();
rc.remotely(() => {
setImmediate(() => process.exit('12'));
}).catch(() => {});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(12);
});
it('throws with an invalid string argument', async () => {
const rc = await startRemoteControlApp();
let stdout = '';
rc.process.stdout!.on('data', (d) => {
stdout += d.toString();
});
rc.remotely(() => {
setImmediate(() => {
try {
process.exit('invalid');
} catch (err) {
console.log(err);
process.exit(99);
}
});
}).catch(() => {});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(99);
expect(stdout).to.match(/TypeError \[ERR_INVALID_ARG_TYPE\]/);
});
});
describe('process.stdout', () => {
useRemoteContext();
it('is a real Node stream', () => {
expect((process.stdout as any)._type).to.not.be.undefined();
});
itremote('does not throw an exception when accessed', () => {
expect(() => process.stdout).to.not.throw();
});
@@ -1036,6 +923,12 @@ describe('node feature', () => {
});
});
describe('process.stdout', () => {
it('is a real Node stream', () => {
expect((process.stdout as any)._type).to.not.be.undefined();
});
});
describe('fs.readFile', () => {
it('can accept a FileHandle as the Path argument', async () => {
const filePathForHandle = path.resolve(mainFixturesPath, 'dogs-running.txt');