Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

1167
test/common/README.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
'use strict';
const { Stream } = require('stream');
function noop() {}
// A stream to push an array into a REPL
function ArrayStream() {
this.run = function(data) {
data.forEach((line) => {
this.emit('data', `${line}\n`);
});
};
}
Object.setPrototypeOf(ArrayStream.prototype, Stream.prototype);
Object.setPrototypeOf(ArrayStream, Stream);
ArrayStream.prototype.readable = true;
ArrayStream.prototype.writable = true;
ArrayStream.prototype.pause = noop;
ArrayStream.prototype.resume = noop;
ArrayStream.prototype.write = noop;
module.exports = ArrayStream;

View File

@ -0,0 +1,112 @@
'use strict';
const common = require('.');
const path = require('node:path');
const test = require('node:test');
const fs = require('node:fs/promises');
const assert = require('node:assert/strict');
const stackFramesRegexp = /(?<=\n)(\s+)((.+?)\s+\()?(?:\(?(.+?):(\d+)(?::(\d+))?)\)?(\s+\{)?(\[\d+m)?(\n|$)/g;
const windowNewlineRegexp = /\r/g;
function replaceNodeVersion(str) {
return str.replaceAll(process.version, '*');
}
function replaceStackTrace(str, replacement = '$1*$7$8\n') {
return str.replace(stackFramesRegexp, replacement);
}
function replaceInternalStackTrace(str) {
return str.replaceAll(/(\W+).*node:internal.*/g, '$1*');
}
function replaceWindowsLineEndings(str) {
return str.replace(windowNewlineRegexp, '');
}
function replaceWindowsPaths(str) {
return common.isWindows ? str.replaceAll(path.win32.sep, path.posix.sep) : str;
}
function transformProjectRoot(replacement = '') {
const projectRoot = path.resolve(__dirname, '../..');
return (str) => {
return str.replaceAll('\\\'', "'").replaceAll(projectRoot, replacement);
};
}
function transform(...args) {
return (str) => args.reduce((acc, fn) => fn(acc), str);
}
function getSnapshotPath(filename) {
const { name, dir } = path.parse(filename);
return path.resolve(dir, `${name}.snapshot`);
}
async function assertSnapshot(actual, filename = process.argv[1]) {
const snapshot = getSnapshotPath(filename);
if (process.env.NODE_REGENERATE_SNAPSHOTS) {
await fs.writeFile(snapshot, actual);
} else {
let expected;
try {
expected = await fs.readFile(snapshot, 'utf8');
} catch (e) {
if (e.code === 'ENOENT') {
console.log(
'Snapshot file does not exist. You can create a new one by running the test with NODE_REGENERATE_SNAPSHOTS=1',
);
}
throw e;
}
assert.strictEqual(actual, replaceWindowsLineEndings(expected));
}
}
/**
* Spawn a process and assert its output against a snapshot.
* if you want to automatically update the snapshot, run tests with NODE_REGENERATE_SNAPSHOTS=1
* transform is a function that takes the output and returns a string that will be compared against the snapshot
* this is useful for normalizing output such as stack traces
* there are some predefined transforms in this file such as replaceStackTrace and replaceWindowsLineEndings
* both of which can be used as an example for writing your own
* compose multiple transforms by passing them as arguments to the transform function:
* assertSnapshot.transform(assertSnapshot.replaceStackTrace, assertSnapshot.replaceWindowsLineEndings)
* @param {string} filename
* @param {function(string): string} [transform]
* @param {object} [options] - control how the child process is spawned
* @param {boolean} [options.tty] - whether to spawn the process in a pseudo-tty
* @returns {Promise<void>}
*/
async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ...options } = {}) {
if (tty && common.isWindows) {
test({ skip: 'Skipping pseudo-tty tests, as pseudo terminals are not available on Windows.' });
return;
}
let flags = common.parseTestFlags(filename);
if (options.flags) {
flags = [...options.flags, ...flags];
}
const executable = tty ? (process.env.PYTHON || 'python3') : process.execPath;
const args =
tty ?
[path.join(__dirname, '../..', 'tools/pseudo-tty.py'), process.execPath, ...flags, filename] :
[...flags, filename];
const { stdout, stderr } = await common.spawnPromisified(executable, args, options);
await assertSnapshot(transform(`${stdout}${stderr}`), filename);
}
module.exports = {
assertSnapshot,
getSnapshotPath,
replaceNodeVersion,
replaceStackTrace,
replaceInternalStackTrace,
replaceWindowsLineEndings,
replaceWindowsPaths,
spawnAndAssert,
transform,
transformProjectRoot,
};

54
test/common/benchmark.js Normal file
View File

@ -0,0 +1,54 @@
'use strict';
const assert = require('assert');
const fork = require('child_process').fork;
const path = require('path');
const runjs = path.join(__dirname, '..', '..', 'benchmark', 'run.js');
function runBenchmark(name, env) {
const argv = ['test'];
argv.push(name);
const mergedEnv = { ...process.env, ...env };
const child = fork(runjs, argv, {
env: mergedEnv,
stdio: ['inherit', 'pipe', 'inherit', 'ipc'],
});
child.stdout.setEncoding('utf8');
let stdout = '';
child.stdout.on('data', (line) => {
stdout += line;
});
child.on('exit', (code, signal) => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
// This bit makes sure that each benchmark file is being sent settings such
// that the benchmark file runs just one set of options. This helps keep the
// benchmark tests from taking a long time to run. Therefore, stdout should be composed as follows:
// The first and last lines should be empty.
// Each test should be separated by a blank line.
// The first line of each test should contain the test's name.
// The second line of each test should contain the configuration for the test.
// If the test configuration is not a group, there should be exactly two lines.
// Otherwise, it is possible to have more than two lines.
const splitTests = stdout.split(/\n\s*\n/);
for (let testIdx = 1; testIdx < splitTests.length - 1; testIdx++) {
const lines = splitTests[testIdx].split('\n');
assert.ok(/.+/.test(lines[0]));
if (!lines[1].includes('group="')) {
assert.strictEqual(lines.length, 2, `benchmark file not running exactly one configuration in test: ${stdout}`);
}
}
});
}
module.exports = runBenchmark;

View File

@ -0,0 +1,156 @@
'use strict';
const assert = require('assert');
const { spawnSync, execFileSync } = require('child_process');
const common = require('./');
const util = require('util');
// Workaround for Windows Server 2008R2
// When CMD is used to launch a process and CMD is killed too quickly, the
// process can stay behind running in suspended state, never completing.
function cleanupStaleProcess(filename) {
if (!common.isWindows) {
return;
}
process.once('beforeExit', () => {
const basename = filename.replace(/.*[/\\]/g, '');
try {
execFileSync(`${process.env.SystemRoot}\\System32\\wbem\\WMIC.exe`, [
'process',
'where',
`commandline like '%${basename}%child'`,
'delete',
'/nointeractive',
]);
} catch {
// Ignore failures, there might not be any stale process to clean up.
}
});
}
// This should keep the child process running long enough to expire
// the timeout.
const kExpiringChildRunTime = common.platformTimeout(20 * 1000);
const kExpiringParentTimer = 1;
assert(kExpiringChildRunTime > kExpiringParentTimer);
function logAfterTime(time) {
setTimeout(() => {
// The following console statements are part of the test.
console.log('child stdout');
console.error('child stderr');
}, time);
}
function checkOutput(str, check) {
if ((check instanceof RegExp && !check.test(str)) ||
(typeof check === 'string' && check !== str)) {
return { passed: false, reason: `did not match ${util.inspect(check)}` };
}
if (typeof check === 'function') {
try {
check(str);
} catch (error) {
return {
passed: false,
reason: `did not match expectation, checker throws:\n${util.inspect(error)}`,
};
}
}
return { passed: true };
}
function expectSyncExit(caller, spawnArgs, {
status,
signal,
stderr: stderrCheck,
stdout: stdoutCheck,
trim = false,
}) {
const child = spawnSync(...spawnArgs);
const failures = [];
let stderrStr, stdoutStr;
if (status !== undefined && child.status !== status) {
failures.push(`- process terminated with status ${child.status}, expected ${status}`);
}
if (signal !== undefined && child.signal !== signal) {
failures.push(`- process terminated with signal ${child.signal}, expected ${signal}`);
}
function logAndThrow() {
const tag = `[process ${child.pid}]:`;
console.error(`${tag} --- stderr ---`);
console.error(stderrStr === undefined ? child.stderr.toString() : stderrStr);
console.error(`${tag} --- stdout ---`);
console.error(stdoutStr === undefined ? child.stdout.toString() : stdoutStr);
console.error(`${tag} status = ${child.status}, signal = ${child.signal}`);
const error = new Error(`${failures.join('\n')}`);
if (spawnArgs[2]) {
error.options = spawnArgs[2];
}
let command = spawnArgs[0];
if (Array.isArray(spawnArgs[1])) {
command += ' ' + spawnArgs[1].join(' ');
}
error.command = command;
Error.captureStackTrace(error, caller);
throw error;
}
// If status and signal are not matching expectations, fail early.
if (failures.length !== 0) {
logAndThrow();
}
if (stderrCheck !== undefined) {
stderrStr = child.stderr.toString();
const { passed, reason } = checkOutput(trim ? stderrStr.trim() : stderrStr, stderrCheck);
if (!passed) {
failures.push(`- stderr ${reason}`);
}
}
if (stdoutCheck !== undefined) {
stdoutStr = child.stdout.toString();
const { passed, reason } = checkOutput(trim ? stdoutStr.trim() : stdoutStr, stdoutCheck);
if (!passed) {
failures.push(`- stdout ${reason}`);
}
}
if (failures.length !== 0) {
logAndThrow();
}
return { child, stderr: stderrStr, stdout: stdoutStr };
}
function spawnSyncAndExit(...args) {
const spawnArgs = args.slice(0, args.length - 1);
const expectations = args[args.length - 1];
return expectSyncExit(spawnSyncAndExit, spawnArgs, expectations);
}
function spawnSyncAndExitWithoutError(...args) {
return expectSyncExit(spawnSyncAndExitWithoutError, [...args], {
status: 0,
signal: null,
});
}
function spawnSyncAndAssert(...args) {
const expectations = args.pop();
return expectSyncExit(spawnSyncAndAssert, [...args], {
status: 0,
signal: null,
...expectations,
});
}
module.exports = {
cleanupStaleProcess,
logAfterTime,
kExpiringChildRunTime,
kExpiringParentTimer,
spawnSyncAndAssert,
spawnSyncAndExit,
spawnSyncAndExitWithoutError,
};

28
test/common/countdown.js Normal file
View File

@ -0,0 +1,28 @@
'use strict';
const assert = require('assert');
const kLimit = Symbol('limit');
const kCallback = Symbol('callback');
const common = require('./');
class Countdown {
constructor(limit, cb) {
assert.strictEqual(typeof limit, 'number');
assert.strictEqual(typeof cb, 'function');
this[kLimit] = limit;
this[kCallback] = common.mustCall(cb);
}
dec() {
assert(this[kLimit] > 0, 'Countdown expired');
if (--this[kLimit] === 0)
this[kCallback]();
return this[kLimit];
}
get remaining() {
return this[kLimit];
}
}
module.exports = Countdown;

50
test/common/cpu-prof.js Normal file
View File

@ -0,0 +1,50 @@
'use strict';
require('./');
const fs = require('fs');
const path = require('path');
const assert = require('assert');
function getCpuProfiles(dir) {
const list = fs.readdirSync(dir);
return list
.filter((file) => file.endsWith('.cpuprofile'))
.map((file) => path.join(dir, file));
}
function getFrames(file, suffix) {
const data = fs.readFileSync(file, 'utf8');
const profile = JSON.parse(data);
const frames = profile.nodes.filter((i) => {
const frame = i.callFrame;
return frame.url.endsWith(suffix);
});
return { frames, nodes: profile.nodes };
}
function verifyFrames(output, file, suffix) {
const { frames, nodes } = getFrames(file, suffix);
if (frames.length === 0) {
// Show native debug output and the profile for debugging.
console.log(output.stderr.toString());
console.log(nodes);
}
assert.notDeepStrictEqual(frames, []);
}
// We need to set --cpu-interval to a smaller value to make sure we can
// find our workload in the samples. 50us should be a small enough sampling
// interval for this.
const kCpuProfInterval = 50;
const env = {
...process.env,
NODE_DEBUG_NATIVE: 'INSPECTOR_PROFILER',
};
module.exports = {
getCpuProfiles,
kCpuProfInterval,
env,
getFrames,
verifyFrames,
};

164
test/common/crypto.js Normal file
View File

@ -0,0 +1,164 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
const assert = require('assert');
const crypto = require('crypto');
const {
createSign,
createVerify,
publicEncrypt,
privateDecrypt,
sign,
verify,
} = crypto;
// The values below (modp2/modp2buf) are for a 1024 bits long prime from
// RFC 2412 E.2, see https://tools.ietf.org/html/rfc2412. */
const modp2buf = Buffer.from([
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f,
0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b,
0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22,
0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, 0xef, 0x95,
0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d,
0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51,
0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6,
0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff,
0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, 0xee, 0x38, 0x6b, 0xfb,
0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b,
0x1f, 0xe6, 0x49, 0x28, 0x66, 0x51, 0xec, 0xe6, 0x53, 0x81,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
]);
// Asserts that the size of the given key (in chars or bytes) is within 10% of
// the expected size.
function assertApproximateSize(key, expectedSize) {
const u = typeof key === 'string' ? 'chars' : 'bytes';
const min = Math.floor(0.9 * expectedSize);
const max = Math.ceil(1.1 * expectedSize);
assert(key.length >= min,
`Key (${key.length} ${u}) is shorter than expected (${min} ${u})`);
assert(key.length <= max,
`Key (${key.length} ${u}) is longer than expected (${max} ${u})`);
}
// Tests that a key pair can be used for encryption / decryption.
function testEncryptDecrypt(publicKey, privateKey) {
const message = 'Hello Node.js world!';
const plaintext = Buffer.from(message, 'utf8');
for (const key of [publicKey, privateKey]) {
const ciphertext = publicEncrypt(key, plaintext);
const received = privateDecrypt(privateKey, ciphertext);
assert.strictEqual(received.toString('utf8'), message);
}
}
// Tests that a key pair can be used for signing / verification.
function testSignVerify(publicKey, privateKey) {
const message = Buffer.from('Hello Node.js world!');
function oldSign(algo, data, key) {
return createSign(algo).update(data).sign(key);
}
function oldVerify(algo, data, key, signature) {
return createVerify(algo).update(data).verify(key, signature);
}
for (const signFn of [sign, oldSign]) {
const signature = signFn('SHA256', message, privateKey);
for (const verifyFn of [verify, oldVerify]) {
for (const key of [publicKey, privateKey]) {
const okay = verifyFn('SHA256', message, key, signature);
assert(okay);
}
}
}
}
// Constructs a regular expression for a PEM-encoded key with the given label.
function getRegExpForPEM(label, cipher) {
const head = `\\-\\-\\-\\-\\-BEGIN ${label}\\-\\-\\-\\-\\-`;
const rfc1421Header = cipher == null ? '' :
`\nProc-Type: 4,ENCRYPTED\nDEK-Info: ${cipher},[^\n]+\n`;
const body = '([a-zA-Z0-9\\+/=]{64}\n)*[a-zA-Z0-9\\+/=]{1,64}';
const end = `\\-\\-\\-\\-\\-END ${label}\\-\\-\\-\\-\\-`;
return new RegExp(`^${head}${rfc1421Header}\n${body}\n${end}\n$`);
}
const pkcs1PubExp = getRegExpForPEM('RSA PUBLIC KEY');
const pkcs1PrivExp = getRegExpForPEM('RSA PRIVATE KEY');
const pkcs1EncExp = (cipher) => getRegExpForPEM('RSA PRIVATE KEY', cipher);
const spkiExp = getRegExpForPEM('PUBLIC KEY');
const pkcs8Exp = getRegExpForPEM('PRIVATE KEY');
const pkcs8EncExp = getRegExpForPEM('ENCRYPTED PRIVATE KEY');
const sec1Exp = getRegExpForPEM('EC PRIVATE KEY');
const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher);
// Synthesize OPENSSL_VERSION_NUMBER format with the layout 0xMNN00PPSL
const opensslVersionNumber = (major = 0, minor = 0, patch = 0) => {
assert(major >= 0 && major <= 0xf);
assert(minor >= 0 && minor <= 0xff);
assert(patch >= 0 && patch <= 0xff);
return (major << 28) | (minor << 20) | (patch << 4);
};
let OPENSSL_VERSION_NUMBER;
const hasOpenSSL = (major = 0, minor = 0, patch = 0) => {
if (!common.hasCrypto) return false;
if (OPENSSL_VERSION_NUMBER === undefined) {
const regexp = /(?<m>\d+)\.(?<n>\d+)\.(?<p>\d+)/;
const { m, n, p } = process.versions.openssl.match(regexp).groups;
OPENSSL_VERSION_NUMBER = opensslVersionNumber(m, n, p);
}
return OPENSSL_VERSION_NUMBER >= opensslVersionNumber(major, minor, patch);
};
let opensslCli = null;
module.exports = {
modp2buf,
assertApproximateSize,
testEncryptDecrypt,
testSignVerify,
pkcs1PubExp,
pkcs1PrivExp,
pkcs1EncExp, // used once
spkiExp,
pkcs8Exp, // used once
pkcs8EncExp, // used once
sec1Exp,
sec1EncExp,
hasOpenSSL,
get hasOpenSSL3() {
return hasOpenSSL(3);
},
// opensslCli defined lazily to reduce overhead of spawnSync
get opensslCli() {
if (opensslCli !== null) return opensslCli;
if (process.config.variables.node_shared_openssl) {
// Use external command
opensslCli = 'openssl';
} else {
const path = require('path');
// Use command built from sources included in Node.js repository
opensslCli = path.join(path.dirname(process.execPath), 'openssl-cli');
}
if (exports.isWindows) opensslCli += '.exe';
const { spawnSync } = require('child_process');
const opensslCmd = spawnSync(opensslCli, ['version']);
if (opensslCmd.status !== 0 || opensslCmd.error !== undefined) {
// OpenSSL command cannot be executed
opensslCli = false;
}
return opensslCli;
},
};

184
test/common/debugger.js Normal file
View File

@ -0,0 +1,184 @@
'use strict';
const common = require('../common');
const spawn = require('child_process').spawn;
const BREAK_MESSAGE = new RegExp('(?:' + [
'assert', 'break', 'break on start', 'debugCommand',
'exception', 'other', 'promiseRejection', 'step',
].join('|') + ') in', 'i');
// Some macOS machines require more time to receive the outputs from the client.
let TIMEOUT = common.platformTimeout(10000);
if (common.isWindows) {
// Some of the windows machines in the CI need more time to receive
// the outputs from the client.
// https://github.com/nodejs/build/issues/3014
TIMEOUT = common.platformTimeout(15000);
}
function isPreBreak(output) {
return /Break on start/.test(output) && /1 \(function \(exports/.test(output);
}
function startCLI(args, flags = [], spawnOpts = {}) {
let stderrOutput = '';
const child =
spawn(process.execPath, [...flags, 'inspect', ...args], spawnOpts);
const outputBuffer = [];
function bufferOutput(chunk) {
if (this === child.stderr) {
stderrOutput += chunk;
}
outputBuffer.push(chunk);
}
function getOutput() {
return outputBuffer.join('\n').replaceAll('\b', '');
}
child.stdout.setEncoding('utf8');
child.stdout.on('data', bufferOutput);
child.stderr.setEncoding('utf8');
child.stderr.on('data', bufferOutput);
if (process.env.VERBOSE === '1') {
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
}
return {
flushOutput() {
const output = this.output;
outputBuffer.length = 0;
return output;
},
waitFor(pattern) {
function checkPattern(str) {
if (Array.isArray(pattern)) {
return pattern.every((p) => p.test(str));
}
return pattern.test(str);
}
return new Promise((resolve, reject) => {
function checkOutput() {
if (checkPattern(getOutput())) {
tearDown();
resolve();
}
}
function onChildClose(code, signal) {
tearDown();
let message = 'Child exited';
if (code) {
message += `, code ${code}`;
}
if (signal) {
message += `, signal ${signal}`;
}
message += ` while waiting for ${pattern}; found: ${this.output}`;
if (stderrOutput) {
message += `\n STDERR: ${stderrOutput}`;
}
reject(new Error(message));
}
const timer = setTimeout(() => {
tearDown();
reject(new Error([
`Timeout (${TIMEOUT}) while waiting for ${pattern}`,
`found: ${this.output}`,
].join('; ')));
}, TIMEOUT);
function tearDown() {
clearTimeout(timer);
child.stdout.removeListener('data', checkOutput);
child.removeListener('close', onChildClose);
}
child.on('close', onChildClose);
child.stdout.on('data', checkOutput);
checkOutput();
});
},
waitForPrompt() {
return this.waitFor(/>\s+$/);
},
async waitForInitialBreak() {
await this.waitFor(/break (?:on start )?in/i);
if (isPreBreak(this.output)) {
await this.command('next', false);
return this.waitFor(/break in/);
}
},
get breakInfo() {
const output = this.output;
const breakMatch =
output.match(/(step |break (?:on start )?)in ([^\n]+):(\d+)\n/i);
if (breakMatch === null) {
throw new Error(
`Could not find breakpoint info in ${JSON.stringify(output)}`);
}
return { filename: breakMatch[2], line: +breakMatch[3] };
},
ctrlC() {
return this.command('.interrupt');
},
get output() {
return getOutput();
},
get rawOutput() {
return outputBuffer.join('').toString();
},
parseSourceLines() {
return getOutput().split('\n')
.map((line) => line.match(/(?:\*|>)?\s*(\d+)/))
.filter((match) => match !== null)
.map((match) => +match[1]);
},
writeLine(input, flush = true) {
if (flush) {
this.flushOutput();
}
if (process.env.VERBOSE === '1') {
process.stderr.write(`< ${input}\n`);
}
child.stdin.write(input);
child.stdin.write('\n');
},
command(input, flush = true) {
this.writeLine(input, flush);
return this.waitForPrompt();
},
stepCommand(input) {
this.writeLine(input, true);
return this
.waitFor(BREAK_MESSAGE)
.then(() => this.waitForPrompt());
},
quit() {
return new Promise((resolve) => {
child.stdin.end();
child.on('close', resolve);
});
},
};
}
module.exports = startCLI;

341
test/common/dns.js Normal file
View File

@ -0,0 +1,341 @@
'use strict';
const assert = require('assert');
const os = require('os');
const { isIP } = require('net');
const types = {
A: 1,
AAAA: 28,
NS: 2,
CNAME: 5,
SOA: 6,
PTR: 12,
MX: 15,
TXT: 16,
ANY: 255,
CAA: 257,
};
const classes = {
IN: 1,
};
// Naïve DNS parser/serializer.
function readDomainFromPacket(buffer, offset) {
assert.ok(offset < buffer.length);
const length = buffer[offset];
if (length === 0) {
return { nread: 1, domain: '' };
} else if ((length & 0xC0) === 0) {
offset += 1;
const chunk = buffer.toString('ascii', offset, offset + length);
// Read the rest of the domain.
const { nread, domain } = readDomainFromPacket(buffer, offset + length);
return {
nread: 1 + length + nread,
domain: domain ? `${chunk}.${domain}` : chunk,
};
}
// Pointer to another part of the packet.
assert.strictEqual(length & 0xC0, 0xC0);
// eslint-disable-next-line @stylistic/js/space-infix-ops, @stylistic/js/space-unary-ops
const pointeeOffset = buffer.readUInt16BE(offset) &~ 0xC000;
return {
nread: 2,
domain: readDomainFromPacket(buffer, pointeeOffset),
};
}
function parseDNSPacket(buffer) {
assert.ok(buffer.length > 12);
const parsed = {
id: buffer.readUInt16BE(0),
flags: buffer.readUInt16BE(2),
};
const counts = [
['questions', buffer.readUInt16BE(4)],
['answers', buffer.readUInt16BE(6)],
['authorityAnswers', buffer.readUInt16BE(8)],
['additionalRecords', buffer.readUInt16BE(10)],
];
let offset = 12;
for (const [ sectionName, count ] of counts) {
parsed[sectionName] = [];
for (let i = 0; i < count; ++i) {
const { nread, domain } = readDomainFromPacket(buffer, offset);
offset += nread;
const type = buffer.readUInt16BE(offset);
const rr = {
domain,
cls: buffer.readUInt16BE(offset + 2),
};
offset += 4;
for (const name in types) {
if (types[name] === type)
rr.type = name;
}
if (sectionName !== 'questions') {
rr.ttl = buffer.readInt32BE(offset);
const dataLength = buffer.readUInt16BE(offset);
offset += 6;
switch (type) {
case types.A:
assert.strictEqual(dataLength, 4);
rr.address = `${buffer[offset + 0]}.${buffer[offset + 1]}.` +
`${buffer[offset + 2]}.${buffer[offset + 3]}`;
break;
case types.AAAA:
assert.strictEqual(dataLength, 16);
rr.address = buffer.toString('hex', offset, offset + 16)
.replace(/(.{4}(?!$))/g, '$1:');
break;
case types.TXT:
{
let position = offset;
rr.entries = [];
while (position < offset + dataLength) {
const txtLength = buffer[offset];
rr.entries.push(buffer.toString('utf8',
position + 1,
position + 1 + txtLength));
position += 1 + txtLength;
}
assert.strictEqual(position, offset + dataLength);
break;
}
case types.MX:
{
rr.priority = buffer.readInt16BE(buffer, offset);
offset += 2;
const { nread, domain } = readDomainFromPacket(buffer, offset);
rr.exchange = domain;
assert.strictEqual(nread, dataLength);
break;
}
case types.NS:
case types.CNAME:
case types.PTR:
{
const { nread, domain } = readDomainFromPacket(buffer, offset);
rr.value = domain;
assert.strictEqual(nread, dataLength);
break;
}
case types.SOA:
{
const mname = readDomainFromPacket(buffer, offset);
const rname = readDomainFromPacket(buffer, offset + mname.nread);
rr.nsname = mname.domain;
rr.hostmaster = rname.domain;
const trailerOffset = offset + mname.nread + rname.nread;
rr.serial = buffer.readUInt32BE(trailerOffset);
rr.refresh = buffer.readUInt32BE(trailerOffset + 4);
rr.retry = buffer.readUInt32BE(trailerOffset + 8);
rr.expire = buffer.readUInt32BE(trailerOffset + 12);
rr.minttl = buffer.readUInt32BE(trailerOffset + 16);
assert.strictEqual(trailerOffset + 20, dataLength);
break;
}
default:
throw new Error(`Unknown RR type ${rr.type}`);
}
offset += dataLength;
}
parsed[sectionName].push(rr);
assert.ok(offset <= buffer.length);
}
}
assert.strictEqual(offset, buffer.length);
return parsed;
}
function writeIPv6(ip) {
const parts = ip.replace(/^:|:$/g, '').split(':');
const buf = Buffer.alloc(16);
let offset = 0;
for (const part of parts) {
if (part === '') {
offset += 16 - 2 * (parts.length - 1);
} else {
buf.writeUInt16BE(parseInt(part, 16), offset);
offset += 2;
}
}
return buf;
}
function writeDomainName(domain) {
return Buffer.concat(domain.split('.').map((label) => {
assert(label.length < 64);
return Buffer.concat([
Buffer.from([label.length]),
Buffer.from(label, 'ascii'),
]);
}).concat([Buffer.alloc(1)]));
}
function writeDNSPacket(parsed) {
const buffers = [];
const kStandardResponseFlags = 0x8180;
buffers.push(new Uint16Array([
parsed.id,
parsed.flags ?? kStandardResponseFlags,
parsed.questions?.length,
parsed.answers?.length,
parsed.authorityAnswers?.length,
parsed.additionalRecords?.length,
]));
for (const q of parsed.questions) {
assert(types[q.type]);
buffers.push(writeDomainName(q.domain));
buffers.push(new Uint16Array([
types[q.type],
q.cls === undefined ? classes.IN : q.cls,
]));
}
for (const rr of [].concat(parsed.answers,
parsed.authorityAnswers,
parsed.additionalRecords)) {
if (!rr) continue;
assert(types[rr.type]);
buffers.push(writeDomainName(rr.domain));
buffers.push(new Uint16Array([
types[rr.type],
rr.cls === undefined ? classes.IN : rr.cls,
]));
buffers.push(new Int32Array([rr.ttl]));
const rdLengthBuf = new Uint16Array(1);
buffers.push(rdLengthBuf);
switch (rr.type) {
case 'A':
rdLengthBuf[0] = 4;
buffers.push(new Uint8Array(rr.address.split('.')));
break;
case 'AAAA':
rdLengthBuf[0] = 16;
buffers.push(writeIPv6(rr.address));
break;
case 'TXT': {
const total = rr.entries.map((s) => s.length).reduce((a, b) => a + b);
// Total length of all strings + 1 byte each for their lengths.
rdLengthBuf[0] = rr.entries.length + total;
for (const txt of rr.entries) {
buffers.push(new Uint8Array([Buffer.byteLength(txt)]));
buffers.push(Buffer.from(txt));
}
break;
}
case 'MX':
rdLengthBuf[0] = 2;
buffers.push(new Uint16Array([rr.priority]));
// fall through
case 'NS':
case 'CNAME':
case 'PTR':
{
const domain = writeDomainName(rr.exchange || rr.value);
rdLengthBuf[0] += domain.length;
buffers.push(domain);
break;
}
case 'SOA':
{
const mname = writeDomainName(rr.nsname);
const rname = writeDomainName(rr.hostmaster);
rdLengthBuf[0] = mname.length + rname.length + 20;
buffers.push(mname, rname);
buffers.push(new Uint32Array([
rr.serial, rr.refresh, rr.retry, rr.expire, rr.minttl,
]));
break;
}
case 'CAA':
{
rdLengthBuf[0] = 5 + rr.issue.length + 2;
buffers.push(Buffer.from([Number(rr.critical)]));
buffers.push(Buffer.from([Number(5)]));
buffers.push(Buffer.from('issue' + rr.issue));
break;
}
default:
throw new Error(`Unknown RR type ${rr.type}`);
}
}
return Buffer.concat(buffers.map((typedArray) => {
const buf = Buffer.from(typedArray.buffer,
typedArray.byteOffset,
typedArray.byteLength);
if (os.endianness() === 'LE') {
if (typedArray.BYTES_PER_ELEMENT === 2) buf.swap16();
if (typedArray.BYTES_PER_ELEMENT === 4) buf.swap32();
}
return buf;
}));
}
const mockedErrorCode = 'ENOTFOUND';
const mockedSysCall = 'getaddrinfo';
function errorLookupMock(code = mockedErrorCode, syscall = mockedSysCall) {
return function lookupWithError(hostname, dnsopts, cb) {
const err = new Error(`${syscall} ${code} ${hostname}`);
err.code = code;
err.errno = code;
err.syscall = syscall;
err.hostname = hostname;
cb(err);
};
}
function createMockedLookup(...addresses) {
addresses = addresses.map((address) => ({ address: address, family: isIP(address) }));
// Create a DNS server which replies with a AAAA and a A record for the same host
return function lookup(hostname, options, cb) {
if (options.all === true) {
process.nextTick(() => {
cb(null, addresses);
});
return;
}
process.nextTick(() => {
cb(null, addresses[0].address, addresses[0].family);
});
};
}
module.exports = {
types,
classes,
writeDNSPacket,
parseDNSPacket,
errorLookupMock,
mockedErrorCode,
mockedSysCall,
createMockedLookup,
};

59
test/common/fixtures.js Normal file
View File

@ -0,0 +1,59 @@
'use strict';
const path = require('path');
const fs = require('fs');
const { pathToFileURL } = require('url');
const fixturesDir = path.join(__dirname, '..', 'fixtures');
function fixturesPath(...args) {
return path.join(fixturesDir, ...args);
}
function fixturesFileURL(...args) {
return pathToFileURL(fixturesPath(...args));
}
function readFixtureSync(args, enc) {
if (Array.isArray(args))
return fs.readFileSync(fixturesPath(...args), enc);
return fs.readFileSync(fixturesPath(args), enc);
}
function readFixtureKey(name, enc) {
return fs.readFileSync(fixturesPath('keys', name), enc);
}
function readFixtureKeys(enc, ...names) {
return names.map((name) => readFixtureKey(name, enc));
}
// This should be in sync with test/fixtures/utf8_test_text.txt.
// We copy them here as a string because this is supposed to be used
// in fs API tests.
const utf8TestText = '永和九年,嵗在癸丑,暮春之初,會於會稽山隂之蘭亭,脩稧事也。' +
'羣賢畢至,少長咸集。此地有崇山峻領,茂林脩竹;又有清流激湍,' +
'暎帶左右。引以為流觴曲水,列坐其次。雖無絲竹管弦之盛,一觴一詠,' +
'亦足以暢敘幽情。是日也,天朗氣清,恵風和暢;仰觀宇宙之大,' +
'俯察品類之盛;所以遊目騁懐,足以極視聽之娛,信可樂也。夫人之相與,' +
'俯仰一世,或取諸懐抱,悟言一室之內,或因寄所託,放浪形骸之外。' +
'雖趣舎萬殊,靜躁不同,當其欣扵所遇,暫得扵己,怏然自足,' +
'不知老之將至。及其所之既惓,情隨事遷,感慨係之矣。向之所欣,' +
'俛仰之閒以為陳跡,猶不能不以之興懐;況脩短隨化,終期扵盡。' +
'古人云:「死生亦大矣。」豈不痛哉!每攬昔人興感之由,若合一契,' +
'未嘗不臨文嗟悼,不能喻之扵懐。固知一死生為虛誕,齊彭殤為妄作。' +
'後之視今,亦由今之視昔,悲夫!故列敘時人,錄其所述,雖世殊事異,' +
'所以興懐,其致一也。後之攬者,亦將有感扵斯文。';
module.exports = {
fixturesDir,
path: fixturesPath,
fileURL: fixturesFileURL,
readSync: readFixtureSync,
readKey: readFixtureKey,
readKeys: readFixtureKeys,
utf8TestText,
get utf8TestTextPath() {
return fixturesPath('utf8_test_text.txt');
},
};

17
test/common/fixtures.mjs Normal file
View File

@ -0,0 +1,17 @@
import fixtures from './fixtures.js';
const {
fixturesDir,
path,
fileURL,
readSync,
readKey,
} = fixtures;
export {
fixturesDir,
path,
fileURL,
readSync,
readKey,
};

188
test/common/gc.js Normal file
View File

@ -0,0 +1,188 @@
'use strict';
const wait = require('timers/promises').setTimeout;
const assert = require('assert');
const common = require('../common');
// TODO(joyeecheung): rewrite checkIfCollectable to use this too.
const { setImmediate: setImmediatePromisified } = require('timers/promises');
const gcTrackerMap = new WeakMap();
const gcTrackerTag = 'NODE_TEST_COMMON_GC_TRACKER';
/**
* Installs a garbage collection listener for the specified object.
* Uses async_hooks for GC tracking, which may affect test functionality.
* A full setImmediate() invocation passes between a global.gc() call and the listener being invoked.
* @param {object} obj - The target object to track for garbage collection.
* @param {object} gcListener - The listener object containing the ongc callback.
* @param {Function} gcListener.ongc - The function to call when the target object is garbage collected.
*/
function onGC(obj, gcListener) {
const async_hooks = require('async_hooks');
const onGcAsyncHook = async_hooks.createHook({
init: common.mustCallAtLeast(function(id, type) {
if (this.trackedId === undefined) {
assert.strictEqual(type, gcTrackerTag);
this.trackedId = id;
}
}),
destroy(id) {
assert.notStrictEqual(this.trackedId, -1);
if (id === this.trackedId) {
this.gcListener.ongc();
onGcAsyncHook.disable();
}
},
}).enable();
onGcAsyncHook.gcListener = gcListener;
gcTrackerMap.set(obj, new async_hooks.AsyncResource(gcTrackerTag));
obj = null;
}
/**
* Repeatedly triggers garbage collection until a specified condition is met or a maximum number of attempts is reached.
* This utillity must be run in a Node.js instance that enables --expose-gc.
* @param {string|Function} [name] - Optional name, used in the rejection message if the condition is not met.
* @param {Function} condition - A function that returns true when the desired condition is met.
* @param {number} maxCount - Maximum number of garbage collections that should be tried.
* @param {object} gcOptions - Options to pass into the global gc() function.
* @returns {Promise} A promise that resolves when the condition is met, or rejects after 10 failed attempts.
*/
async function gcUntil(name, condition, maxCount = 10, gcOptions) {
for (let count = 0; count < maxCount; ++count) {
await setImmediatePromisified();
if (gcOptions) {
await global.gc(gcOptions);
} else {
await global.gc(); // Passing in undefined is not the same as empty.
}
if (condition()) {
return;
}
}
throw new Error(`Test ${name} failed`);
}
// This function can be used to check if an object factor leaks or not,
// but it needs to be used with care:
// 1. The test should be set up with an ideally small
// --max-old-space-size or --max-heap-size, which combined with
// the maxCount parameter can reproduce a leak of the objects
// created by fn().
// 2. This works under the assumption that if *none* of the objects
// created by fn() can be garbage-collected, the test would crash due
// to OOM.
// 3. If *any* of the objects created by fn() can be garbage-collected,
// it is considered leak-free. The FinalizationRegistry is used to
// terminate the test early once we detect any of the object is
// garbage-collected to make the test less prone to false positives.
// This may be especially important for memory management relying on
// emphemeron GC which can be inefficient to deal with extremely fast
// heap growth.
// Note that this can still produce false positives. When the test using
// this function still crashes due to OOM, inspect the heap to confirm
// if a leak is present (e.g. using heap snapshots).
// The generateSnapshotAt parameter can be used to specify a count
// interval to create the heap snapshot which may enforce a more thorough GC.
// This can be tried for code paths that require it for the GC to catch up
// with heap growth. However this type of forced GC can be in conflict with
// other logic in V8 such as bytecode aging, and it can slow down the test
// significantly, so it should be used scarcely and only as a last resort.
async function checkIfCollectable(
fn, maxCount = 4096, generateSnapshotAt = Infinity, logEvery = 128) {
let anyFinalized = false;
let count = 0;
const f = new FinalizationRegistry(() => {
anyFinalized = true;
});
async function createObject() {
const obj = await fn();
f.register(obj);
if (count++ < maxCount && !anyFinalized) {
setImmediate(createObject, 1);
}
// This can force a more thorough GC, but can slow the test down
// significantly in a big heap. Use it with care.
if (count % generateSnapshotAt === 0) {
// XXX(joyeecheung): This itself can consume a bit of JS heap memory,
// but the other alternative writeHeapSnapshot can run into disk space
// not enough problems in the CI & be slower depending on file system.
// Just do this for now as long as it works and only invent some
// internal voodoo when we absolutely have no other choice.
require('v8').getHeapSnapshot().pause().read();
console.log(`Generated heap snapshot at ${count}`);
}
if (count % logEvery === 0) {
console.log(`Created ${count} objects`);
}
if (anyFinalized) {
console.log(`Found finalized object at ${count}, stop testing`);
}
}
createObject();
}
// Repeat an operation and give GC some breathing room at every iteration.
async function runAndBreathe(fn, repeat, waitTime = 20) {
for (let i = 0; i < repeat; i++) {
await fn();
await wait(waitTime);
}
}
/**
* This requires --expose-internals.
* This function can be used to check if an object factory leaks or not by
* iterating over the heap and count objects with the specified class
* (which is checked by looking up the prototype chain).
* @param {(i: number) => number} fn The factory receiving iteration count
* and returning number of objects created. The return value should be
* precise otherwise false negatives can be produced.
* @param {Function} ctor The constructor of the objects being counted.
* @param {number} count Number of iterations that this check should be done
* @param {number} waitTime Optional breathing time for GC.
*/
async function checkIfCollectableByCounting(fn, ctor, count, waitTime = 20) {
const { queryObjects } = require('v8');
const { name } = ctor;
const initialCount = queryObjects(ctor, { format: 'count' });
console.log(`Initial count of ${name}: ${initialCount}`);
let totalCreated = 0;
for (let i = 0; i < count; ++i) {
const created = await fn(i);
totalCreated += created;
console.log(`#${i}: created ${created} ${name}, total ${totalCreated}`);
await wait(waitTime); // give GC some breathing room.
const currentCount = queryObjects(ctor, { format: 'count' });
const collected = totalCreated - (currentCount - initialCount);
console.log(`#${i}: counted ${currentCount} ${name}, collected ${collected}`);
if (collected > 0) {
console.log(`Detected ${collected} collected ${name}, finish early`);
return;
}
}
await wait(waitTime); // give GC some breathing room.
const currentCount = queryObjects(ctor, { format: 'count' });
const collected = totalCreated - (currentCount - initialCount);
console.log(`Last count: counted ${currentCount} ${name}, collected ${collected}`);
// Some objects with the prototype can be collected.
if (collected > 0) {
console.log(`Detected ${collected} collected ${name}`);
return;
}
throw new Error(`${name} cannot be collected`);
}
module.exports = {
checkIfCollectable,
runAndBreathe,
checkIfCollectableByCounting,
onGC,
gcUntil,
};

150
test/common/globals.js Normal file
View File

@ -0,0 +1,150 @@
'use strict';
const intrinsics = new Set([
'Object',
'Function',
'Array',
'Number',
'parseFloat',
'parseInt',
'Infinity',
'NaN',
'undefined',
'Boolean',
'String',
'Symbol',
'Date',
'Promise',
'RegExp',
'Error',
'AggregateError',
'EvalError',
'RangeError',
'ReferenceError',
'SyntaxError',
'TypeError',
'URIError',
'globalThis',
'JSON',
'Math',
'Intl',
'ArrayBuffer',
'Uint8Array',
'Int8Array',
'Uint16Array',
'Int16Array',
'Float16Array',
'Uint32Array',
'Int32Array',
'Float32Array',
'Float64Array',
'Uint8ClampedArray',
'BigUint64Array',
'BigInt64Array',
'DataView',
'Map',
'BigInt',
'Set',
'WeakMap',
'WeakSet',
'Proxy',
'Reflect',
'ShadowRealm',
'FinalizationRegistry',
'WeakRef',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'escape',
'unescape',
'eval',
'isFinite',
'isNaN',
'SharedArrayBuffer',
'Atomics',
'WebAssembly',
'Iterator',
'SuppressedError',
'DisposableStack',
'AsyncDisposableStack',
]);
if (global.gc) {
intrinsics.add('gc');
}
// v8 exposes console in the global scope.
intrinsics.add('console');
const webIdlExposedWildcard = new Set([
'DOMException',
'TextEncoder',
'TextDecoder',
'AbortController',
'AbortSignal',
'CustomEvent',
'EventTarget',
'Event',
'URL',
'URLSearchParams',
'ReadableStream',
'ReadableStreamDefaultReader',
'ReadableStreamBYOBReader',
'ReadableStreamBYOBRequest',
'ReadableByteStreamController',
'ReadableStreamDefaultController',
'TransformStream',
'TransformStreamDefaultController',
'WritableStream',
'WritableStreamDefaultWriter',
'WritableStreamDefaultController',
'ByteLengthQueuingStrategy',
'CountQueuingStrategy',
'TextEncoderStream',
'TextDecoderStream',
'CompressionStream',
'DecompressionStream',
]);
const webIdlExposedWindow = new Set([
'console',
'BroadcastChannel',
'queueMicrotask',
'structuredClone',
'MessageChannel',
'MessagePort',
'MessageEvent',
'clearInterval',
'clearTimeout',
'setInterval',
'setTimeout',
'atob',
'btoa',
'Blob',
'Performance',
'performance',
'fetch',
'FormData',
'Headers',
'Request',
'Response',
'WebSocket',
'EventSource',
'CloseEvent',
]);
const nodeGlobals = new Set([
'process',
'global',
'Buffer',
'clearImmediate',
'setImmediate',
]);
module.exports = {
intrinsics,
webIdlExposedWildcard,
webIdlExposedWindow,
nodeGlobals,
};

344
test/common/heap.js Normal file
View File

@ -0,0 +1,344 @@
'use strict';
const assert = require('assert');
const util = require('util');
let _buildEmbedderGraph;
function buildEmbedderGraph() {
if (_buildEmbedderGraph) { return _buildEmbedderGraph(); }
let internalBinding;
try {
internalBinding = require('internal/test/binding').internalBinding;
} catch (e) {
console.error('The test must be run with `--expose-internals`');
throw e;
}
({ buildEmbedderGraph: _buildEmbedderGraph } = internalBinding('heap_utils'));
return _buildEmbedderGraph();
}
const { getHeapSnapshot } = require('v8');
function createJSHeapSnapshot(stream = getHeapSnapshot()) {
stream.pause();
const dump = JSON.parse(stream.read());
const meta = dump.snapshot.meta;
const nodes =
readHeapInfo(dump.nodes, meta.node_fields, meta.node_types, dump.strings);
const edges =
readHeapInfo(dump.edges, meta.edge_fields, meta.edge_types, dump.strings);
for (const node of nodes) {
node.incomingEdges = [];
node.outgoingEdges = [];
}
let fromNodeIndex = 0;
let edgeIndex = 0;
for (const { type, name_or_index, to_node } of edges) {
while (edgeIndex === nodes[fromNodeIndex].edge_count) {
edgeIndex = 0;
fromNodeIndex++;
}
const toNode = nodes[to_node / meta.node_fields.length];
const fromNode = nodes[fromNodeIndex];
const edge = {
type,
to: toNode,
from: fromNode,
name: typeof name_or_index === 'string' ? name_or_index : null,
};
toNode.incomingEdges.push(edge);
fromNode.outgoingEdges.push(edge);
edgeIndex++;
}
for (const node of nodes) {
assert.strictEqual(node.edge_count, node.outgoingEdges.length,
`${node.edge_count} !== ${node.outgoingEdges.length}`);
}
return nodes;
}
function readHeapInfo(raw, fields, types, strings) {
const items = [];
for (let i = 0; i < raw.length; i += fields.length) {
const item = {};
for (let j = 0; j < fields.length; j++) {
const name = fields[j];
let type = types[j];
if (Array.isArray(type)) {
item[name] = type[raw[i + j]];
} else if (name === 'name_or_index') { // type === 'string_or_number'
if (item.type === 'element' || item.type === 'hidden')
type = 'number';
else
type = 'string';
}
if (type === 'string') {
item[name] = strings[raw[i + j]];
} else if (type === 'number' || type === 'node') {
item[name] = raw[i + j];
}
}
items.push(item);
}
return items;
}
function inspectNode(snapshot) {
return util.inspect(snapshot, { depth: 4 });
}
function isEdge(edge, { node_name, edge_name }) {
if (edge_name !== undefined && edge.name !== edge_name) {
return false;
}
// From our internal embedded graph
if (edge.to.value) {
if (edge.to.value.constructor.name !== node_name) {
return false;
}
} else if (edge.to.name !== node_name) {
return false;
}
return true;
}
class State {
constructor(stream) {
this.snapshot = createJSHeapSnapshot(stream);
this.embedderGraph = buildEmbedderGraph();
}
// Validate the v8 heap snapshot
validateSnapshot(rootName, expected, { loose = false } = {}) {
const rootNodes = this.snapshot.filter(
(node) => node.name === rootName && node.type !== 'string');
if (loose) {
assert(rootNodes.length >= expected.length,
`Expect to find at least ${expected.length} '${rootName}', ` +
`found ${rootNodes.length}`);
} else {
assert.strictEqual(
rootNodes.length, expected.length,
`Expect to find ${expected.length} '${rootName}', ` +
`found ${rootNodes.length}`);
}
for (const expectation of expected) {
if (expectation.children) {
for (const expectedEdge of expectation.children) {
const check = typeof expectedEdge === 'function' ? expectedEdge :
(edge) => (isEdge(edge, expectedEdge));
const hasChild = rootNodes.some(
(node) => node.outgoingEdges.some(check),
);
// Don't use assert with a custom message here. Otherwise the
// inspection in the message is done eagerly and wastes a lot of CPU
// time.
if (!hasChild) {
throw new Error(
'expected to find child ' +
`${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`);
}
}
}
if (expectation.detachedness !== undefined) {
const matchedNodes = rootNodes.filter(
(node) => node.detachedness === expectation.detachedness);
if (loose) {
assert(matchedNodes.length >= rootNodes.length,
`Expect to find at least ${rootNodes.length} with ` +
`detachedness ${expectation.detachedness}, ` +
`found ${matchedNodes.length}`);
} else {
assert.strictEqual(
matchedNodes.length, rootNodes.length,
`Expect to find ${rootNodes.length} with detachedness ` +
`${expectation.detachedness}, found ${matchedNodes.length}`);
}
}
}
}
// Validate our internal embedded graph representation
validateGraph(rootName, expected, { loose = false } = {}) {
const rootNodes = this.embedderGraph.filter(
(node) => node.name === rootName,
);
if (loose) {
assert(rootNodes.length >= expected.length,
`Expect to find at least ${expected.length} '${rootName}', ` +
`found ${rootNodes.length}`);
} else {
assert.strictEqual(
rootNodes.length, expected.length,
`Expect to find ${expected.length} '${rootName}', ` +
`found ${rootNodes.length}`);
}
for (const expectation of expected) {
if (expectation.children) {
for (const expectedEdge of expectation.children) {
const check = typeof expectedEdge === 'function' ? expectedEdge :
(edge) => (isEdge(edge, expectedEdge));
// Don't use assert with a custom message here. Otherwise the
// inspection in the message is done eagerly and wastes a lot of CPU
// time.
const hasChild = rootNodes.some(
(node) => node.edges.some(check),
);
if (!hasChild) {
throw new Error(
'expected to find child ' +
`${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`);
}
}
}
}
}
validateSnapshotNodes(rootName, expected, { loose = false } = {}) {
this.validateSnapshot(rootName, expected, { loose });
this.validateGraph(rootName, expected, { loose });
}
}
function recordState(stream = undefined) {
return new State(stream);
}
function validateSnapshotNodes(...args) {
return recordState().validateSnapshotNodes(...args);
}
/**
* A alternative heap snapshot validator that can be used to verify cppgc-managed nodes.
* Modified from
* https://chromium.googlesource.com/v8/v8/+/b00e995fb212737802810384ba2b868d0d92f7e5/test/unittests/heap/cppgc-js/unified-heap-snapshot-unittest.cc#134
* @param {object[]} nodes Snapshot nodes returned by createJSHeapSnapshot() or a subset filtered from it.
* @param {string} rootName Name of the root node. Typically a class name used to filter all native nodes with
* this name. For cppgc-managed objects, this is typically the name configured by
* SET_CPPGC_NAME() prefixed with an additional "Node /" prefix e.g.
* "Node / ContextifyScript"
* @param {[{
* node_name?: string,
* edge_name?: string,
* node_type?: string,
* edge_type?: string,
* }]} retainingPath The retaining path specification to search from the root nodes.
* @param {boolean} allowEmpty Whether the function should fail if no matching nodes can be found.
* @returns {[object]} All the leaf nodes matching the retaining path specification. If none can be found,
* logs the nodes found in the last matching step of the path (if any), and throws an
* assertion error.
*/
function validateByRetainingPathFromNodes(nodes, rootName, retainingPath, allowEmpty = false) {
let haystack = nodes.filter((n) => n.name === rootName && n.type !== 'string');
for (let i = 0; i < retainingPath.length; ++i) {
const expected = retainingPath[i];
const newHaystack = [];
for (const parent of haystack) {
for (let j = 0; j < parent.outgoingEdges.length; j++) {
const edge = parent.outgoingEdges[j];
// The strings are represented as { type: 'string', name: '<string content>' } in the snapshot.
// Ignore them or we'll poke into strings that are just referenced as names of real nodes,
// unless the caller is specifically looking for string nodes via `node_type`.
let match = (edge.to.type !== 'string');
if (expected.node_type) {
match = (edge.to.type === expected.node_type);
}
if (expected.node_name && edge.to.name !== expected.node_name) {
match = false;
}
if (expected.edge_name && edge.name !== expected.edge_name) {
match = false;
}
if (expected.edge_type && edge.type !== expected.type) {
match = false;
}
if (match) {
newHaystack.push(edge.to);
}
}
}
if (newHaystack.length === 0) {
if (allowEmpty) {
return [];
}
const format = (val) => util.inspect(val, { breakLength: 128, depth: 3 });
console.error('#');
console.error('# Retaining path to search for:');
for (let j = 0; j < retainingPath.length; ++j) {
console.error(`# - '${format(retainingPath[j])}'${i === j ? '\t<--- not found' : ''}`);
}
console.error('#\n');
console.error('# Nodes found in the last step include:');
for (let j = 0; j < haystack.length; ++j) {
console.error(`# - '${format(haystack[j])}`);
}
assert.fail(`Could not find target edge ${format(expected)} in the heap snapshot.`);
}
haystack = newHaystack;
}
return haystack;
}
function getHeapSnapshotOptionTests() {
const fixtures = require('../common/fixtures');
const cases = [
{
options: { exposeInternals: true },
expected: [{
children: [
// We don't have anything special to test here yet
// because we don't use cppgc or embedder heap tracer.
{ edge_name: 'nonNumeric', node_name: 'test' },
],
}],
},
{
options: { exposeNumericValues: true },
expected: [{
children: [
{ edge_name: 'numeric', node_name: 'smi number' },
],
}],
},
];
return {
fixtures: fixtures.path('klass-with-fields.js'),
check(snapshot, expected) {
snapshot.validateSnapshot('Klass', expected, { loose: true });
},
cases,
};
}
/**
* Similar to @see {validateByRetainingPathFromNodes} but creates the snapshot from scratch.
*/
function validateByRetainingPath(...args) {
const nodes = createJSHeapSnapshot();
return validateByRetainingPathFromNodes(nodes, ...args);
}
module.exports = {
recordState,
validateSnapshotNodes,
validateByRetainingPath,
validateByRetainingPathFromNodes,
getHeapSnapshotOptionTests,
createJSHeapSnapshot,
};

View File

@ -0,0 +1,32 @@
'use strict';
// Hijack stdout and stderr
const stdWrite = {};
function hijackStdWritable(name, listener) {
const stream = process[name];
const _write = stdWrite[name] = stream.write;
stream.writeTimes = 0;
stream.write = function(data, callback) {
try {
listener(data);
} catch (e) {
process.nextTick(() => { throw e; });
}
_write.call(stream, data, callback);
stream.writeTimes++;
};
}
function restoreWritable(name) {
process[name].write = stdWrite[name];
delete process[name].writeTimes;
}
module.exports = {
hijackStdout: hijackStdWritable.bind(null, 'stdout'),
hijackStderr: hijackStdWritable.bind(null, 'stderr'),
restoreStdout: restoreWritable.bind(null, 'stdout'),
restoreStderr: restoreWritable.bind(null, 'stderr'),
};

129
test/common/http2.js Normal file
View File

@ -0,0 +1,129 @@
'use strict';
// An HTTP/2 testing tool used to create mock frames for direct testing
// of HTTP/2 endpoints.
const kFrameData = Symbol('frame-data');
const FLAG_EOS = 0x1;
const FLAG_ACK = 0x1;
const FLAG_EOH = 0x4;
const FLAG_PADDED = 0x8;
const PADDING = Buffer.alloc(255);
const kClientMagic = Buffer.from('505249202a20485454502f322' +
'e300d0a0d0a534d0d0a0d0a', 'hex');
const kFakeRequestHeaders = Buffer.from('828684410f7777772e65' +
'78616d706c652e636f6d', 'hex');
const kFakeResponseHeaders = Buffer.from('4803333032580770726976617465611d' +
'4d6f6e2c203231204f63742032303133' +
'2032303a31333a323120474d546e1768' +
'747470733a2f2f7777772e6578616d70' +
'6c652e636f6d', 'hex');
function isUint32(val) {
return val >>> 0 === val;
}
function isUint24(val) {
return val >>> 0 === val && val <= 0xFFFFFF;
}
function isUint8(val) {
return val >>> 0 === val && val <= 0xFF;
}
function write32BE(array, pos, val) {
if (!isUint32(val))
throw new RangeError('val is not a 32-bit number');
array[pos++] = (val >> 24) & 0xff;
array[pos++] = (val >> 16) & 0xff;
array[pos++] = (val >> 8) & 0xff;
array[pos++] = val & 0xff;
}
function write24BE(array, pos, val) {
if (!isUint24(val))
throw new RangeError('val is not a 24-bit number');
array[pos++] = (val >> 16) & 0xff;
array[pos++] = (val >> 8) & 0xff;
array[pos++] = val & 0xff;
}
function write8(array, pos, val) {
if (!isUint8(val))
throw new RangeError('val is not an 8-bit number');
array[pos] = val;
}
class Frame {
constructor(length, type, flags, id) {
this[kFrameData] = Buffer.alloc(9);
write24BE(this[kFrameData], 0, length);
write8(this[kFrameData], 3, type);
write8(this[kFrameData], 4, flags);
write32BE(this[kFrameData], 5, id);
}
get data() {
return this[kFrameData];
}
}
class SettingsFrame extends Frame {
constructor(ack = false) {
let flags = 0;
if (ack)
flags |= FLAG_ACK;
super(0, 4, flags, 0);
}
}
class HeadersFrame extends Frame {
constructor(id, payload, padlen = 0, final = false) {
let len = payload.length;
let flags = FLAG_EOH;
if (final) flags |= FLAG_EOS;
const buffers = [payload];
if (padlen > 0) {
buffers.unshift(Buffer.from([padlen]));
buffers.push(PADDING.slice(0, padlen));
len += padlen + 1;
flags |= FLAG_PADDED;
}
super(len, 1, flags, id);
buffers.unshift(this[kFrameData]);
this[kFrameData] = Buffer.concat(buffers);
}
}
class PingFrame extends Frame {
constructor(ack = false) {
const buffers = [Buffer.alloc(8)];
super(8, 6, ack ? 1 : 0, 0);
buffers.unshift(this[kFrameData]);
this[kFrameData] = Buffer.concat(buffers);
}
}
class AltSvcFrame extends Frame {
constructor(size) {
const buffers = [Buffer.alloc(size)];
super(size, 10, 0, 0);
buffers.unshift(this[kFrameData]);
this[kFrameData] = Buffer.concat(buffers);
}
}
module.exports = {
Frame,
AltSvcFrame,
HeadersFrame,
SettingsFrame,
PingFrame,
kFakeRequestHeaders,
kFakeResponseHeaders,
kClientMagic,
};

1017
test/common/index.js Executable file

File diff suppressed because it is too large Load Diff

100
test/common/index.mjs Normal file
View File

@ -0,0 +1,100 @@
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const common = require('./index.js');
const {
allowGlobals,
buildType,
canCreateSymLink,
childShouldThrowAndAbort,
enoughTestMem,
escapePOSIXShell,
expectsError,
expectWarning,
getArrayBufferViews,
getBufferSources,
getTTYfd,
hasCrypto,
hasSQLite,
hasIntl,
hasIPv6,
isAIX,
isAlive,
isFreeBSD,
isIBMi,
isInsideDirWithUnusualChars,
isLinux,
isOpenBSD,
isMacOS,
isSunOS,
isWindows,
localIPv6Hosts,
mustCall,
mustCallAtLeast,
mustNotCall,
mustNotMutateObjectDeep,
mustSucceed,
nodeProcessAborted,
parseTestFlags,
PIPE,
platformTimeout,
printSkipMessage,
runWithInvalidFD,
skip,
skipIf32Bits,
skipIfEslintMissing,
skipIfInspectorDisabled,
skipIfSQLiteMissing,
spawnPromisified,
} = common;
const getPort = () => common.PORT;
export {
allowGlobals,
buildType,
canCreateSymLink,
childShouldThrowAndAbort,
createRequire,
enoughTestMem,
escapePOSIXShell,
expectsError,
expectWarning,
getArrayBufferViews,
getBufferSources,
getPort,
getTTYfd,
hasCrypto,
hasSQLite,
hasIntl,
hasIPv6,
isAIX,
isAlive,
isFreeBSD,
isIBMi,
isInsideDirWithUnusualChars,
isLinux,
isOpenBSD,
isMacOS,
isSunOS,
isWindows,
localIPv6Hosts,
mustCall,
mustCallAtLeast,
mustNotCall,
mustNotMutateObjectDeep,
mustSucceed,
nodeProcessAborted,
parseTestFlags,
PIPE,
platformTimeout,
printSkipMessage,
runWithInvalidFD,
skip,
skipIf32Bits,
skipIfEslintMissing,
skipIfInspectorDisabled,
skipIfSQLiteMissing,
spawnPromisified,
};

View File

@ -0,0 +1,576 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const http = require('http');
const fixtures = require('../common/fixtures');
const { spawn } = require('child_process');
const { URL, pathToFileURL } = require('url');
const { EventEmitter, once } = require('events');
const _MAINSCRIPT = fixtures.path('loop.js');
const DEBUG = false;
const TIMEOUT = common.platformTimeout(15 * 1000);
function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) {
const args = [].concat(inspectorFlags);
if (scriptContents) {
args.push('-e', scriptContents);
} else {
args.push(scriptFile);
}
const child = spawn(process.execPath, args);
const handler = tearDown.bind(null, child);
process.on('exit', handler);
process.on('uncaughtException', handler);
process.on('unhandledRejection', handler);
process.on('SIGINT', handler);
return child;
}
function makeBufferingDataCallback(dataCallback) {
let buffer = Buffer.alloc(0);
return (data) => {
const newData = Buffer.concat([buffer, data]);
const str = newData.toString('utf8');
const lines = str.replace(/\r/g, '').split('\n');
if (str.endsWith('\n'))
buffer = Buffer.alloc(0);
else
buffer = Buffer.from(lines.pop(), 'utf8');
for (const line of lines)
dataCallback(line);
};
}
function tearDown(child, err) {
child.kill();
if (err) {
console.error(err);
process.exit(1);
}
}
function parseWSFrame(buffer) {
// Protocol described in https://tools.ietf.org/html/rfc6455#section-5
let message = null;
if (buffer.length < 2)
return { length: 0, message };
if (buffer[0] === 0x88 && buffer[1] === 0x00) {
return { length: 2, message, closed: true };
}
assert.strictEqual(buffer[0], 0x81);
let dataLen = 0x7F & buffer[1];
let bodyOffset = 2;
if (buffer.length < bodyOffset + dataLen)
return 0;
if (dataLen === 126) {
dataLen = buffer.readUInt16BE(2);
bodyOffset = 4;
} else if (dataLen === 127) {
assert(buffer[2] === 0 && buffer[3] === 0, 'Inspector message too big');
dataLen = buffer.readUIntBE(4, 6);
bodyOffset = 10;
}
if (buffer.length < bodyOffset + dataLen)
return { length: 0, message };
const jsonPayload =
buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8');
try {
message = JSON.parse(jsonPayload);
} catch (e) {
console.error(`JSON.parse() failed for: ${jsonPayload}`);
throw e;
}
if (DEBUG)
console.log('[received]', JSON.stringify(message));
return { length: bodyOffset + dataLen, message };
}
function formatWSFrame(message) {
const messageBuf = Buffer.from(JSON.stringify(message));
const wsHeaderBuf = Buffer.allocUnsafe(16);
wsHeaderBuf.writeUInt8(0x81, 0);
let byte2 = 0x80;
const bodyLen = messageBuf.length;
let maskOffset = 2;
if (bodyLen < 126) {
byte2 = 0x80 + bodyLen;
} else if (bodyLen < 65536) {
byte2 = 0xFE;
wsHeaderBuf.writeUInt16BE(bodyLen, 2);
maskOffset = 4;
} else {
byte2 = 0xFF;
wsHeaderBuf.writeUInt32BE(bodyLen, 2);
wsHeaderBuf.writeUInt32BE(0, 6);
maskOffset = 10;
}
wsHeaderBuf.writeUInt8(byte2, 1);
wsHeaderBuf.writeUInt32BE(0x01020408, maskOffset);
for (let i = 0; i < messageBuf.length; i++)
messageBuf[i] = messageBuf[i] ^ (1 << (i % 4));
return Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]);
}
class InspectorSession {
constructor(socket, instance) {
this._instance = instance;
this._socket = socket;
this._nextId = 1;
this._commandResponsePromises = new Map();
this._unprocessedNotifications = [];
this._notificationCallback = null;
this._scriptsIdsByUrl = new Map();
this._pausedDetails = null;
let buffer = Buffer.alloc(0);
socket.on('data', (data) => {
buffer = Buffer.concat([buffer, data]);
do {
const { length, message, closed } = parseWSFrame(buffer);
if (!length)
break;
if (closed) {
socket.write(Buffer.from([0x88, 0x00])); // WS close frame
}
buffer = buffer.slice(length);
if (message)
this._onMessage(message);
} while (true);
});
this._terminationPromise = new Promise((resolve) => {
socket.once('close', resolve);
});
}
waitForServerDisconnect() {
return this._terminationPromise;
}
async disconnect() {
this._socket.destroy();
return this.waitForServerDisconnect();
}
_onMessage(message) {
if (message.id) {
const { resolve, reject } = this._commandResponsePromises.get(message.id);
this._commandResponsePromises.delete(message.id);
if (message.result)
resolve(message.result);
else
reject(message.error);
} else {
if (message.method === 'Debugger.scriptParsed') {
const { scriptId, url } = message.params;
this._scriptsIdsByUrl.set(scriptId, url);
const fileUrl = url.startsWith('file:') ?
url : pathToFileURL(url).toString();
if (fileUrl === this.scriptURL().toString()) {
this.mainScriptId = scriptId;
}
}
if (message.method === 'Debugger.paused')
this._pausedDetails = message.params;
if (message.method === 'Debugger.resumed')
this._pausedDetails = null;
if (this._notificationCallback) {
// In case callback needs to install another
const callback = this._notificationCallback;
this._notificationCallback = null;
callback(message);
} else {
this._unprocessedNotifications.push(message);
}
}
}
unprocessedNotifications() {
return this._unprocessedNotifications;
}
_sendMessage(message) {
const msg = JSON.parse(JSON.stringify(message)); // Clone!
msg.id = this._nextId++;
if (DEBUG)
console.log('[sent]', JSON.stringify(msg));
const responsePromise = new Promise((resolve, reject) => {
this._commandResponsePromises.set(msg.id, { resolve, reject });
});
return new Promise(
(resolve) => this._socket.write(formatWSFrame(msg), resolve))
.then(() => responsePromise);
}
send(commands) {
if (Array.isArray(commands)) {
// Multiple commands means the response does not matter. There might even
// never be a response.
return Promise
.all(commands.map((command) => this._sendMessage(command)))
.then(() => {});
}
return this._sendMessage(commands);
}
waitForNotification(methodOrPredicate, description) {
const desc = description || methodOrPredicate;
const message = `Timed out waiting for matching notification (${desc})`;
return fires(
this._asyncWaitForNotification(methodOrPredicate), message, TIMEOUT);
}
async _asyncWaitForNotification(methodOrPredicate) {
function matchMethod(notification) {
return notification.method === methodOrPredicate;
}
const predicate =
typeof methodOrPredicate === 'string' ? matchMethod : methodOrPredicate;
let notification = null;
do {
if (this._unprocessedNotifications.length) {
notification = this._unprocessedNotifications.shift();
} else {
notification = await new Promise(
(resolve) => this._notificationCallback = resolve);
}
} while (!predicate(notification));
return notification;
}
_isBreakOnLineNotification(message, line, expectedScriptPath) {
if (message.method === 'Debugger.paused') {
const callFrame = message.params.callFrames[0];
const location = callFrame.location;
const scriptPath = this._scriptsIdsByUrl.get(location.scriptId);
assert.strictEqual(decodeURIComponent(scriptPath),
decodeURIComponent(expectedScriptPath),
`${scriptPath} !== ${expectedScriptPath}`);
assert.strictEqual(location.lineNumber, line);
return true;
}
}
waitForBreakOnLine(line, url) {
return this
.waitForNotification(
(notification) =>
this._isBreakOnLineNotification(notification, line, url),
`break on ${url}:${line}`);
}
waitForPauseOnStart() {
return this
.waitForNotification(
(notification) =>
notification.method === 'Debugger.paused' && notification.params.reason === 'Break on start',
'break on start');
}
pausedDetails() {
return this._pausedDetails;
}
_matchesConsoleOutputNotification(notification, type, values) {
if (!Array.isArray(values))
values = [ values ];
if (notification.method === 'Runtime.consoleAPICalled') {
const params = notification.params;
if (params.type === type) {
let i = 0;
for (const value of params.args) {
if (value.value !== values[i++])
return false;
}
return i === values.length;
}
}
}
waitForConsoleOutput(type, values) {
const desc = `Console output matching ${JSON.stringify(values)}`;
return this.waitForNotification(
(notification) => this._matchesConsoleOutputNotification(notification,
type, values),
desc);
}
async runToCompletion() {
console.log('[test]', 'Verify node waits for the frontend to disconnect');
await this.send({ 'method': 'Debugger.resume' });
await this.waitForNotification((notification) => {
if (notification.method === 'Debugger.paused') {
this.send({ 'method': 'Debugger.resume' });
}
return notification.method === 'Runtime.executionContextDestroyed' &&
notification.params.executionContextId === 1;
});
await this.waitForDisconnect();
}
async waitForDisconnect() {
while ((await this._instance.nextStderrString()) !==
'Waiting for the debugger to disconnect...');
await this.disconnect();
}
scriptPath() {
return this._instance.scriptPath();
}
script() {
return this._instance.script();
}
scriptURL() {
return pathToFileURL(this.scriptPath());
}
}
class NodeInstance extends EventEmitter {
constructor(inspectorFlags = ['--inspect-brk=0', '--expose-internals'],
scriptContents = '',
scriptFile = _MAINSCRIPT,
logger = console) {
super();
this._logger = logger;
this._scriptPath = scriptFile;
this._script = scriptFile ? null : scriptContents;
this._portCallback = null;
this.resetPort();
this._process = spawnChildProcess(inspectorFlags, scriptContents,
scriptFile);
this._running = true;
this._stderrLineCallback = null;
this._unprocessedStderrLines = [];
this._process.stdout.on('data', makeBufferingDataCallback(
(line) => {
this.emit('stdout', line);
this._logger.log('[out]', line);
}));
this._process.stderr.on('data', makeBufferingDataCallback(
(message) => this.onStderrLine(message)));
this._shutdownPromise = new Promise((resolve) => {
this._process.once('exit', (exitCode, signal) => {
if (signal) {
this._logger.error(`[err] child process crashed, signal ${signal}`);
}
resolve({ exitCode, signal });
this._running = false;
});
});
}
get pid() {
return this._process.pid;
}
resetPort() {
this.portPromise = new Promise((resolve) => this._portCallback = resolve);
}
static async startViaSignal(scriptContents) {
const instance = new NodeInstance(
['--expose-internals', '--inspect-port=0'],
`${scriptContents}\nprocess._rawDebug('started');`, undefined);
const msg = 'Timed out waiting for process to start';
while (await fires(instance.nextStderrString(), msg, TIMEOUT) !== 'started');
process._debugProcess(instance._process.pid);
return instance;
}
onStderrLine(line) {
this.emit('stderr', line);
this._logger.log('[err]', line);
if (this._portCallback) {
const matches = line.match(/Debugger listening on ws:\/\/.+:(\d+)\/.+/);
if (matches) {
this._portCallback(matches[1]);
this._portCallback = null;
}
}
if (this._stderrLineCallback) {
this._stderrLineCallback(line);
this._stderrLineCallback = null;
} else {
this._unprocessedStderrLines.push(line);
}
}
httpGet(host, path, hostHeaderValue) {
this._logger.log('[test]', `Testing ${path}`);
const headers = hostHeaderValue ? { 'Host': hostHeaderValue } : null;
return this.portPromise.then((port) => new Promise((resolve, reject) => {
const req = http.get({ host, port, family: 4, path, headers }, (res) => {
let response = '';
res.setEncoding('utf8');
res
.on('data', (data) => response += data.toString())
.on('end', () => {
resolve(response);
});
});
req.on('error', reject);
})).then((response) => {
try {
return JSON.parse(response);
} catch (e) {
e.body = response;
throw e;
}
});
}
async sendUpgradeRequest() {
const response = await this.httpGet(null, '/json/list');
const devtoolsUrl = response[0].webSocketDebuggerUrl;
const port = await this.portPromise;
return http.get({
port,
family: 4,
path: new URL(devtoolsUrl).pathname,
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Version': 13,
'Sec-WebSocket-Key': 'key==',
},
});
}
async connectInspectorSession() {
this._logger.log('[test]', 'Connecting to a child Node process');
const upgradeRequest = await this.sendUpgradeRequest();
return new Promise((resolve) => {
upgradeRequest
.on('upgrade',
(message, socket) => resolve(new InspectorSession(socket, this)))
.on('response', common.mustNotCall('Upgrade was not received'));
});
}
async expectConnectionDeclined() {
this._logger.log('[test]', 'Checking upgrade is not possible');
const upgradeRequest = await this.sendUpgradeRequest();
return new Promise((resolve) => {
upgradeRequest
.on('upgrade', common.mustNotCall('Upgrade was received'))
.on('response', (response) =>
response.on('data', () => {})
.on('end', () => resolve(response.statusCode)));
});
}
expectShutdown() {
return this._shutdownPromise;
}
nextStderrString() {
if (this._unprocessedStderrLines.length)
return Promise.resolve(this._unprocessedStderrLines.shift());
return new Promise((resolve) => this._stderrLineCallback = resolve);
}
write(message) {
this._process.stdin.write(message);
}
kill() {
this._process.kill();
return this.expectShutdown();
}
scriptPath() {
return this._scriptPath;
}
script() {
if (this._script === null)
this._script = fs.readFileSync(this.scriptPath(), 'utf8');
return this._script;
}
}
function onResolvedOrRejected(promise, callback) {
return promise.then((result) => {
callback();
return result;
}, (error) => {
callback();
throw error;
});
}
function timeoutPromise(error, timeoutMs) {
let clearCallback = null;
let done = false;
const promise = onResolvedOrRejected(new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(error), timeoutMs);
clearCallback = () => {
if (done)
return;
clearTimeout(timeout);
resolve();
};
}), () => done = true);
promise.clear = clearCallback;
return promise;
}
// Returns a new promise that will propagate `promise` resolution or rejection
// if that happens within the `timeoutMs` timespan, or rejects with `error` as
// a reason otherwise.
function fires(promise, error, timeoutMs) {
const timeout = timeoutPromise(error, timeoutMs);
return Promise.race([
onResolvedOrRejected(promise, () => timeout.clear()),
timeout,
]);
}
/**
* When waiting for inspector events, there might be no handles on the event
* loop, and leads to process exits.
*
* This function provides a utility to wait until a inspector event for a certain
* time.
*/
function waitUntil(session, eventName, timeout = 1000) {
const resolvers = Promise.withResolvers();
const timer = setTimeout(() => {
resolvers.reject(new Error(`Wait for inspector event ${eventName} timed out`));
}, timeout);
once(session, eventName)
.then((res) => {
resolvers.resolve(res);
clearTimeout(timer);
}, (error) => {
// This should never happen.
resolvers.reject(error);
clearTimeout(timer);
});
return resolvers.promise;
}
module.exports = {
NodeInstance,
waitUntil,
};

60
test/common/internet.js Normal file
View File

@ -0,0 +1,60 @@
'use strict';
// Utilities for internet-related tests
const addresses = {
// A generic host that has registered common DNS records,
// supports both IPv4 and IPv6, and provides basic HTTP/HTTPS services
INET_HOST: 'nodejs.org',
// A host that provides IPv4 services
INET4_HOST: 'nodejs.org',
// A host that provides IPv6 services
INET6_HOST: 'nodejs.org',
// An accessible IPv4 IP,
// defaults to the Google Public DNS IPv4 address
INET4_IP: '8.8.8.8',
// An accessible IPv6 IP,
// defaults to the Google Public DNS IPv6 address
INET6_IP: '2001:4860:4860::8888',
// An invalid host that cannot be resolved
// See https://tools.ietf.org/html/rfc2606#section-2
INVALID_HOST: 'something.invalid',
// A host with MX records registered
MX_HOST: 'nodejs.org',
// On some systems, .invalid returns a server failure/try again rather than
// record not found. Use this to guarantee record not found.
NOT_FOUND: 'come.on.fhqwhgads.test',
// A host with SRV records registered
SRV_HOST: '_caldav._tcp.google.com',
// A host with PTR records registered
PTR_HOST: '8.8.8.8.in-addr.arpa',
// A host with NAPTR records registered
NAPTR_HOST: 'sip2sip.info',
// A host with SOA records registered
SOA_HOST: 'nodejs.org',
// A host with CAA record registered
CAA_HOST: 'google.com',
// A host with CNAME records registered
CNAME_HOST: 'blog.nodejs.org',
// A host with NS records registered
NS_HOST: 'nodejs.org',
// A host with TLSA records registered
TLSA_HOST: '_443._tcp.fedoraproject.org',
// A host with TXT records registered
TXT_HOST: 'nodejs.org',
// An accessible IPv4 DNS server
DNS4_SERVER: '8.8.8.8',
// An accessible IPv4 DNS server
DNS6_SERVER: '2001:4860:4860::8888',
};
for (const key of Object.keys(addresses)) {
const envName = `NODE_TEST_${key}`;
if (process.env[envName]) {
addresses[key] = process.env[envName];
}
}
module.exports = {
addresses,
};

View File

@ -0,0 +1,57 @@
'use strict';
const assert = require('assert');
const common = require('./');
// The formats could change when V8 is updated, then the tests should be
// updated accordingly.
function assertResultShape(result) {
assert.strictEqual(typeof result.jsMemoryEstimate, 'number');
assert.strictEqual(typeof result.jsMemoryRange[0], 'number');
assert.strictEqual(typeof result.jsMemoryRange[1], 'number');
}
function assertSummaryShape(result) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof result.total, 'object');
assertResultShape(result.total);
}
function assertDetailedShape(result, contexts = 0) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof result.total, 'object');
assert.strictEqual(typeof result.current, 'object');
assertResultShape(result.total);
assertResultShape(result.current);
if (contexts === 0) {
assert.deepStrictEqual(result.other, []);
} else {
assert.strictEqual(result.other.length, contexts);
for (const item of result.other) {
assertResultShape(item);
}
}
}
function assertSingleDetailedShape(result) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof result.total, 'object');
assert.strictEqual(typeof result.current, 'object');
assert.deepStrictEqual(result.other, []);
assertResultShape(result.total);
assertResultShape(result.current);
}
function expectExperimentalWarning() {
common.expectWarning(
'ExperimentalWarning',
'vm.measureMemory is an experimental feature and might change at any time',
);
}
module.exports = {
assertSummaryShape,
assertDetailedShape,
assertSingleDetailedShape,
expectExperimentalWarning,
};

33
test/common/net.js Normal file
View File

@ -0,0 +1,33 @@
'use strict';
const net = require('net');
const options = { port: 0, reusePort: true };
function checkSupportReusePort() {
return new Promise((resolve, reject) => {
const server = net.createServer().listen(options);
server.on('listening', () => {
server.close(resolve);
});
server.on('error', (err) => {
console.log('The `reusePort` option is not supported:', err.message);
server.close();
reject(err);
});
});
}
function hasMultiLocalhost() {
const { internalBinding } = require('internal/test/binding');
const { TCP, constants: TCPConstants } = internalBinding('tcp_wrap');
const t = new TCP(TCPConstants.SOCKET);
const ret = t.bind('127.0.0.2', 0);
t.close();
return ret === 0;
}
module.exports = {
checkSupportReusePort,
hasMultiLocalhost,
options,
};

3
test/common/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "commonjs"
}

View File

@ -0,0 +1,138 @@
'use strict';
const assert = require('assert');
function getTestCases(isWorker = false) {
const cases = [];
function exitsOnExitCodeSet() {
process.exitCode = 42;
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 42);
assert.strictEqual(code, 42);
});
}
cases.push({ func: exitsOnExitCodeSet, result: 42 });
function changesCodeViaExit() {
process.exitCode = 99;
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 42);
assert.strictEqual(code, 42);
});
process.exit(42);
}
cases.push({ func: changesCodeViaExit, result: 42 });
function changesCodeZeroExit() {
process.exitCode = 99;
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 0);
assert.strictEqual(code, 0);
});
process.exit(0);
}
cases.push({ func: changesCodeZeroExit, result: 0 });
function exitWithOneOnUncaught() {
process.exitCode = 99;
process.on('exit', (code) => {
// Cannot use assert because it will be uncaughtException -> 1 exit code
// that will render this test useless
if (code !== 1 || process.exitCode !== 1) {
console.log('wrong code! expected 1 for uncaughtException');
process.exit(99);
}
});
throw new Error('ok');
}
cases.push({
func: exitWithOneOnUncaught,
result: 1,
error: /^Error: ok$/,
});
function changeCodeInsideExit() {
process.exitCode = 95;
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 95);
assert.strictEqual(code, 95);
process.exitCode = 99;
});
}
cases.push({ func: changeCodeInsideExit, result: 99 });
function zeroExitWithUncaughtHandler() {
const noop = () => { };
process.on('exit', (code) => {
process.off('uncaughtException', noop);
assert.strictEqual(process.exitCode, undefined);
assert.strictEqual(code, 0);
});
process.on('uncaughtException', noop);
throw new Error('ok');
}
cases.push({ func: zeroExitWithUncaughtHandler, result: 0 });
function changeCodeInUncaughtHandler() {
const modifyExitCode = () => { process.exitCode = 97; };
process.on('exit', (code) => {
process.off('uncaughtException', modifyExitCode);
assert.strictEqual(process.exitCode, 97);
assert.strictEqual(code, 97);
});
process.on('uncaughtException', modifyExitCode);
throw new Error('ok');
}
cases.push({ func: changeCodeInUncaughtHandler, result: 97 });
function changeCodeInExitWithUncaught() {
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 1);
assert.strictEqual(code, 1);
process.exitCode = 98;
});
throw new Error('ok');
}
cases.push({
func: changeCodeInExitWithUncaught,
result: 98,
error: /^Error: ok$/,
});
function exitWithZeroInExitWithUncaught() {
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 1);
assert.strictEqual(code, 1);
process.exitCode = 0;
});
throw new Error('ok');
}
cases.push({
func: exitWithZeroInExitWithUncaught,
result: 0,
error: /^Error: ok$/,
});
function exitWithThrowInUncaughtHandler() {
process.on('uncaughtException', () => {
throw new Error('ok');
});
throw new Error('bad');
}
cases.push({
func: exitWithThrowInUncaughtHandler,
result: isWorker ? 1 : 7,
error: /^Error: ok$/,
});
function exitWithUndefinedFatalException() {
process._fatalException = undefined;
throw new Error('ok');
}
cases.push({
func: exitWithUndefinedFatalException,
result: 6,
});
return cases;
}
exports.getTestCases = getTestCases;

67
test/common/prof.js Normal file
View File

@ -0,0 +1,67 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
function getHeapProfiles(dir) {
const list = fs.readdirSync(dir);
return list
.filter((file) => file.endsWith('.heapprofile'))
.map((file) => path.join(dir, file));
}
function findFirstFrameInNode(root, func) {
const first = root.children.find(
(child) => child.callFrame.functionName === func,
);
if (first) {
return first;
}
for (const child of root.children) {
const first = findFirstFrameInNode(child, func);
if (first) {
return first;
}
}
return undefined;
}
function findFirstFrame(file, func) {
const data = fs.readFileSync(file, 'utf8');
const profile = JSON.parse(data);
const first = findFirstFrameInNode(profile.head, func);
return { frame: first, roots: profile.head.children };
}
function verifyFrames(output, file, func) {
const { frame, roots } = findFirstFrame(file, func);
if (!frame) {
// Show native debug output and the profile for debugging.
console.log(output.stderr.toString());
console.log(roots);
}
assert.notStrictEqual(frame, undefined);
}
// We need to set --heap-prof-interval to a small enough value to make
// sure we can find our workload in the samples, so we need to set
// TEST_ALLOCATION > kHeapProfInterval.
const kHeapProfInterval = 128;
const TEST_ALLOCATION = kHeapProfInterval * 2;
const env = {
...process.env,
TEST_ALLOCATION,
NODE_DEBUG_NATIVE: 'INSPECTOR_PROFILER',
};
// TODO(joyeecheung): share the fixutres with v8 coverage tests
module.exports = {
getHeapProfiles,
verifyFrames,
findFirstFrame,
kHeapProfInterval,
TEST_ALLOCATION,
env,
};

100
test/common/proxy-server.js Normal file
View File

@ -0,0 +1,100 @@
'use strict';
const net = require('net');
const http = require('http');
const assert = require('assert');
function logRequest(logs, req) {
logs.push({
method: req.method,
url: req.url,
headers: { ...req.headers },
});
}
// This creates a minimal proxy server that logs the requests it gets
// to an array before performing proxying.
exports.createProxyServer = function() {
const logs = [];
const proxy = http.createServer();
proxy.on('request', (req, res) => {
logRequest(logs, req);
const [hostname, port] = req.headers.host.split(':');
const targetPort = port || 80;
const options = {
hostname: hostname,
port: targetPort,
path: req.url,
method: req.method,
headers: req.headers,
};
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res, { end: true });
});
proxyReq.on('error', (err) => {
logs.push({ error: err, source: 'proxy request' });
res.writeHead(500);
res.end('Proxy error: ' + err.message);
});
req.pipe(proxyReq, { end: true });
});
proxy.on('connect', (req, res, head) => {
logRequest(logs, req);
const [hostname, port] = req.url.split(':');
const proxyReq = net.connect(port, hostname, () => {
res.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Node.js-Proxy\r\n' +
'\r\n',
);
proxyReq.write(head);
res.pipe(proxyReq);
proxyReq.pipe(res);
});
proxyReq.on('error', (err) => {
logs.push({ error: err, source: 'proxy request' });
res.write('HTTP/1.1 500 Connection Error\r\n\r\n');
res.end('Proxy error: ' + err.message);
});
});
proxy.on('error', (err) => {
logs.push({ error: err, source: 'proxy server' });
});
return { proxy, logs };
};
exports.checkProxiedRequest = async function(envExtension, expectation) {
const { spawnPromisified } = require('./');
const fixtures = require('./fixtures');
const { code, signal, stdout, stderr } = await spawnPromisified(
process.execPath,
[fixtures.path('fetch-and-log.mjs')], {
env: {
...process.env,
...envExtension,
},
});
assert.deepStrictEqual({
stderr: stderr.trim(),
stdout: stdout.trim(),
code,
signal,
}, {
stderr: '',
code: 0,
signal: null,
...expectation,
});
};

346
test/common/report.js Normal file
View File

@ -0,0 +1,346 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const net = require('net');
const os = require('os');
const path = require('path');
const util = require('util');
const cpus = os.cpus();
function findReports(pid, dir) {
// Default filenames are of the form
// report.<date>.<time>.<pid>.<tid>.<seq>.json
const format = '^report\\.\\d+\\.\\d+\\.' + pid + '\\.\\d+\\.\\d+\\.json$';
const filePattern = new RegExp(format);
const files = fs.readdirSync(dir);
const results = [];
files.forEach((file) => {
if (filePattern.test(file))
results.push(path.join(dir, file));
});
return results;
}
function validate(filepath, fields) {
const report = fs.readFileSync(filepath, 'utf8');
if (process.report.compact) {
const end = report.indexOf('\n');
assert.strictEqual(end, report.length - 1);
}
validateContent(JSON.parse(report), fields);
}
function validateContent(report, fields = []) {
if (typeof report === 'string') {
try {
report = JSON.parse(report);
} catch {
throw new TypeError(
'validateContent() expects a JSON string or JavaScript Object');
}
}
try {
_validateContent(report, fields);
} catch (err) {
try {
err.stack += util.format('\n------\nFailing Report:\n%O', report);
} catch {
// Continue regardless of error.
}
throw err;
}
}
function _validateContent(report, fields = []) {
const isWindows = process.platform === 'win32';
const isJavaScriptThreadReport = report.javascriptHeap != null;
// Verify that all sections are present as own properties of the report.
const sections = ['header', 'nativeStack', 'javascriptStack', 'libuv',
'sharedObjects', 'resourceUsage', 'workers'];
if (!process.report.excludeEnv) {
sections.push('environmentVariables');
}
if (!isWindows)
sections.push('userLimits');
if (report.uvthreadResourceUsage)
sections.push('uvthreadResourceUsage');
if (isJavaScriptThreadReport)
sections.push('javascriptHeap');
checkForUnknownFields(report, sections);
sections.forEach((section) => {
assert(Object.hasOwn(report, section));
assert(typeof report[section] === 'object' && report[section] !== null);
});
fields.forEach((field) => {
function checkLoop(actual, rest, expect) {
actual = actual[rest.shift()];
if (rest.length === 0 && actual !== undefined) {
assert.strictEqual(actual, expect);
} else {
assert(actual);
checkLoop(actual, rest, expect);
}
}
let actual, expect;
if (Array.isArray(field)) {
[actual, expect] = field;
} else {
actual = field;
expect = undefined;
}
checkLoop(report, actual.split('.'), expect);
});
// Verify the format of the header section.
const header = report.header;
const headerFields = ['event', 'trigger', 'filename', 'dumpEventTime',
'dumpEventTimeStamp', 'processId', 'commandLine',
'nodejsVersion', 'wordSize', 'arch', 'platform',
'componentVersions', 'release', 'osName', 'osRelease',
'osVersion', 'osMachine', 'cpus', 'host',
'glibcVersionRuntime', 'glibcVersionCompiler', 'cwd',
'reportVersion', 'networkInterfaces', 'threadId'];
checkForUnknownFields(header, headerFields);
assert.strictEqual(header.reportVersion, 5); // Increment as needed.
assert.strictEqual(typeof header.event, 'string');
assert.strictEqual(typeof header.trigger, 'string');
assert(typeof header.filename === 'string' || header.filename === null);
assert.notStrictEqual(new Date(header.dumpEventTime).toString(),
'Invalid Date');
assert(String(+header.dumpEventTimeStamp), header.dumpEventTimeStamp);
assert(Number.isSafeInteger(header.processId));
assert(Number.isSafeInteger(header.threadId) || header.threadId === null);
assert.strictEqual(typeof header.cwd, 'string');
assert(Array.isArray(header.commandLine));
header.commandLine.forEach((arg) => {
assert.strictEqual(typeof arg, 'string');
});
assert.strictEqual(header.nodejsVersion, process.version);
assert(Number.isSafeInteger(header.wordSize));
assert.strictEqual(header.arch, os.arch());
assert.strictEqual(header.platform, os.platform());
assert.deepStrictEqual(header.componentVersions, process.versions);
assert.deepStrictEqual(header.release, process.release);
assert.strictEqual(header.osName, os.type());
assert.strictEqual(header.osRelease, os.release());
assert.strictEqual(typeof header.osVersion, 'string');
assert.strictEqual(typeof header.osMachine, 'string');
assert(Array.isArray(header.cpus));
assert.strictEqual(header.cpus.length, cpus.length);
header.cpus.forEach((cpu) => {
assert.strictEqual(typeof cpu.model, 'string');
assert.strictEqual(typeof cpu.speed, 'number');
assert.strictEqual(typeof cpu.user, 'number');
assert.strictEqual(typeof cpu.nice, 'number');
assert.strictEqual(typeof cpu.sys, 'number');
assert.strictEqual(typeof cpu.idle, 'number');
assert.strictEqual(typeof cpu.irq, 'number');
assert(cpus.some((c) => {
return c.model === cpu.model;
}));
});
assert(Array.isArray(header.networkInterfaces));
header.networkInterfaces.forEach((iface) => {
assert.strictEqual(typeof iface.name, 'string');
assert.strictEqual(typeof iface.internal, 'boolean');
assert.match(iface.mac, /^([0-9A-F][0-9A-F]:){5}[0-9A-F]{2}$/i);
if (iface.family === 'IPv4') {
assert.strictEqual(net.isIPv4(iface.address), true);
assert.strictEqual(net.isIPv4(iface.netmask), true);
assert.strictEqual(iface.scopeid, undefined);
} else if (iface.family === 'IPv6') {
assert.strictEqual(net.isIPv6(iface.address), true);
assert.strictEqual(net.isIPv6(iface.netmask), true);
assert(Number.isInteger(iface.scopeid));
} else {
assert.strictEqual(iface.family, 'unknown');
assert.strictEqual(iface.address, undefined);
assert.strictEqual(iface.netmask, undefined);
assert.strictEqual(iface.scopeid, undefined);
}
});
assert.strictEqual(header.host, os.hostname());
// Verify the format of the nativeStack section.
assert(Array.isArray(report.nativeStack));
report.nativeStack.forEach((frame) => {
assert(typeof frame === 'object' && frame !== null);
checkForUnknownFields(frame, ['pc', 'symbol']);
assert.strictEqual(typeof frame.pc, 'string');
assert.match(frame.pc, /^0x[0-9a-f]+$/);
assert.strictEqual(typeof frame.symbol, 'string');
});
if (isJavaScriptThreadReport) {
// Verify the format of the javascriptStack section.
checkForUnknownFields(report.javascriptStack,
['message', 'stack', 'errorProperties']);
assert.strictEqual(typeof report.javascriptStack.errorProperties,
'object');
assert.strictEqual(typeof report.javascriptStack.message, 'string');
if (report.javascriptStack.stack !== undefined) {
assert(Array.isArray(report.javascriptStack.stack));
report.javascriptStack.stack.forEach((frame) => {
assert.strictEqual(typeof frame, 'string');
});
}
// Verify the format of the javascriptHeap section.
const heap = report.javascriptHeap;
// See `PrintGCStatistics` in node_report.cc
const jsHeapFields = [
'totalMemory',
'executableMemory',
'totalCommittedMemory',
'availableMemory',
'totalGlobalHandlesMemory',
'usedGlobalHandlesMemory',
'usedMemory',
'memoryLimit',
'mallocedMemory',
'externalMemory',
'peakMallocedMemory',
'nativeContextCount',
'detachedContextCount',
'doesZapGarbage',
'heapSpaces',
];
checkForUnknownFields(heap, jsHeapFields);
// Do not check `heapSpaces` here
for (let i = 0; i < jsHeapFields.length - 1; i++) {
assert(
Number.isSafeInteger(heap[jsHeapFields[i]]),
`heap.${jsHeapFields[i]} is not a safe integer`,
);
}
assert(typeof heap.heapSpaces === 'object' && heap.heapSpaces !== null);
const heapSpaceFields = ['memorySize', 'committedMemory', 'capacity',
'used', 'available'];
Object.keys(heap.heapSpaces).forEach((spaceName) => {
const space = heap.heapSpaces[spaceName];
checkForUnknownFields(space, heapSpaceFields);
heapSpaceFields.forEach((field) => {
assert(Number.isSafeInteger(space[field]));
});
});
}
// Verify the format of the resourceUsage section.
const usage = { ...report.resourceUsage };
// Delete it, otherwise checkForUnknownFields will throw error
delete usage.constrained_memory;
const resourceUsageFields = ['userCpuSeconds', 'kernelCpuSeconds',
'cpuConsumptionPercent', 'userCpuConsumptionPercent',
'kernelCpuConsumptionPercent',
'maxRss', 'rss', 'free_memory', 'total_memory',
'available_memory', 'pageFaults', 'fsActivity'];
checkForUnknownFields(usage, resourceUsageFields);
assert.strictEqual(typeof usage.userCpuSeconds, 'number');
assert.strictEqual(typeof usage.kernelCpuSeconds, 'number');
assert.strictEqual(typeof usage.cpuConsumptionPercent, 'number');
assert.strictEqual(typeof usage.userCpuConsumptionPercent, 'number');
assert.strictEqual(typeof usage.kernelCpuConsumptionPercent, 'number');
assert(typeof usage.rss, 'string');
assert(typeof usage.maxRss, 'string');
assert(typeof usage.free_memory, 'string');
assert(typeof usage.total_memory, 'string');
assert(typeof usage.available_memory, 'string');
// This field may not exist
if (report.resourceUsage.constrained_memory) {
assert(typeof report.resourceUsage.constrained_memory, 'string');
}
assert(typeof usage.pageFaults === 'object' && usage.pageFaults !== null);
checkForUnknownFields(usage.pageFaults, ['IORequired', 'IONotRequired']);
assert(Number.isSafeInteger(usage.pageFaults.IORequired));
assert(Number.isSafeInteger(usage.pageFaults.IONotRequired));
assert(typeof usage.fsActivity === 'object' && usage.fsActivity !== null);
checkForUnknownFields(usage.fsActivity, ['reads', 'writes']);
assert(Number.isSafeInteger(usage.fsActivity.reads));
assert(Number.isSafeInteger(usage.fsActivity.writes));
// Verify the format of the uvthreadResourceUsage section, if present.
if (report.uvthreadResourceUsage) {
const usage = report.uvthreadResourceUsage;
const threadUsageFields = ['userCpuSeconds', 'kernelCpuSeconds',
'cpuConsumptionPercent', 'fsActivity',
'userCpuConsumptionPercent',
'kernelCpuConsumptionPercent'];
checkForUnknownFields(usage, threadUsageFields);
assert.strictEqual(typeof usage.userCpuSeconds, 'number');
assert.strictEqual(typeof usage.kernelCpuSeconds, 'number');
assert.strictEqual(typeof usage.cpuConsumptionPercent, 'number');
assert.strictEqual(typeof usage.userCpuConsumptionPercent, 'number');
assert.strictEqual(typeof usage.kernelCpuConsumptionPercent, 'number');
assert(typeof usage.fsActivity === 'object' && usage.fsActivity !== null);
checkForUnknownFields(usage.fsActivity, ['reads', 'writes']);
assert(Number.isSafeInteger(usage.fsActivity.reads));
assert(Number.isSafeInteger(usage.fsActivity.writes));
}
// Verify the format of the libuv section.
assert(Array.isArray(report.libuv));
report.libuv.forEach((resource) => {
assert.strictEqual(typeof resource.type, 'string');
assert.strictEqual(typeof resource.address, 'string');
assert.match(resource.address, /^0x[0-9a-f]+$/);
assert.strictEqual(typeof resource.is_active, 'boolean');
assert.strictEqual(typeof resource.is_referenced,
resource.type === 'loop' ? 'undefined' : 'boolean');
});
if (!process.report.excludeEnv) {
// Verify the format of the environmentVariables section.
for (const [key, value] of Object.entries(report.environmentVariables)) {
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof value, 'string');
}
}
// Verify the format of the userLimits section on non-Windows platforms.
if (!isWindows) {
const userLimitsFields = ['core_file_size_blocks', 'data_seg_size_bytes',
'file_size_blocks', 'max_locked_memory_bytes',
'max_memory_size_bytes', 'open_files',
'stack_size_bytes', 'cpu_time_seconds',
'max_user_processes', 'virtual_memory_bytes'];
checkForUnknownFields(report.userLimits, userLimitsFields);
for (const [type, limits] of Object.entries(report.userLimits)) {
assert.strictEqual(typeof type, 'string');
assert(typeof limits === 'object' && limits !== null);
checkForUnknownFields(limits, ['soft', 'hard']);
assert(typeof limits.soft === 'number' || limits.soft === 'unlimited',
`Invalid ${type} soft limit of ${limits.soft}`);
assert(typeof limits.hard === 'number' || limits.hard === 'unlimited',
`Invalid ${type} hard limit of ${limits.hard}`);
}
}
// Verify the format of the sharedObjects section.
assert(Array.isArray(report.sharedObjects));
report.sharedObjects.forEach((sharedObject) => {
assert.strictEqual(typeof sharedObject, 'string');
});
// Verify the format of the workers section.
assert(Array.isArray(report.workers));
report.workers.forEach((worker) => _validateContent(worker));
}
function checkForUnknownFields(actual, expected) {
Object.keys(actual).forEach((field) => {
assert(expected.includes(field), `'${field}' not expected in ${expected}`);
});
}
module.exports = { findReports, validate, validateContent };

27
test/common/require-as.js Normal file
View File

@ -0,0 +1,27 @@
'use strict';
if (require.main !== module) {
const { spawnSync } = require('child_process');
function runModuleAs(filename, flags, spawnOptions, role) {
return spawnSync(process.execPath,
[...flags, __filename, role, filename], spawnOptions);
}
module.exports = runModuleAs;
return;
}
const { Worker, isMainThread, workerData } = require('worker_threads');
if (isMainThread) {
if (process.argv[2] === 'worker') {
new Worker(__filename, {
workerData: process.argv[3],
});
return;
}
require(process.argv[3]);
} else {
require(workerData);
}

144
test/common/sea.js Normal file
View File

@ -0,0 +1,144 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { inspect } = require('util');
const { readFileSync, copyFileSync, statSync } = require('fs');
const {
spawnSyncAndExitWithoutError,
} = require('../common/child_process');
function skipIfSingleExecutableIsNotSupported() {
if (!process.config.variables.single_executable_application)
common.skip('Single Executable Application support has been disabled.');
if (!['darwin', 'win32', 'linux'].includes(process.platform))
common.skip(`Unsupported platform ${process.platform}.`);
if (process.platform === 'linux' && process.config.variables.is_debug === 1)
common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.');
if (process.config.variables.node_shared)
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2366/sea: error while loading shared libraries: ' +
'libnode.so.112: cannot open shared object file: No such file or directory`.');
if (process.config.variables.icu_gyp_path === 'tools/icu/icu-system.gyp')
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2379/sea: error while loading shared libraries: ' +
'libicui18n.so.71: cannot open shared object file: No such file or directory`.');
if (!process.config.variables.node_use_openssl || process.config.variables.node_shared_openssl)
common.skip('Running the resultant binary fails with `Node.js is not compiled with OpenSSL crypto support`.');
if (process.config.variables.want_separate_host_toolset !== 0)
common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.');
if (process.platform === 'linux') {
const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' });
const isAlpine = /^NAME="Alpine Linux"/m.test(osReleaseText);
if (isAlpine) common.skip('Alpine Linux is not supported.');
if (process.arch === 's390x') {
common.skip('On s390x, postject fails with `memory access out of bounds`.');
}
}
if (process.config.variables.ubsan) {
common.skip('UndefinedBehavior Sanitizer is not supported');
}
try {
readFileSync(process.execPath);
} catch (e) {
if (e.code === 'ERR_FS_FILE_TOO_LARGE') {
common.skip('The Node.js binary is too large to be supported by postject');
}
}
tmpdir.refresh();
// The SEA tests involve making a copy of the executable and writing some fixtures
// to the tmpdir. To be safe, ensure that the disk space has at least a copy of the
// executable and some extra space for blobs and configs is available.
const stat = statSync(process.execPath);
const expectedSpace = stat.size + 10 * 1024 * 1024;
if (!tmpdir.hasEnoughSpace(expectedSpace)) {
common.skip(`Available disk space < ${Math.floor(expectedSpace / 1024 / 1024)} MB`);
}
}
function generateSEA(targetExecutable, sourceExecutable, seaBlob, verifyWorkflow = false) {
try {
copyFileSync(sourceExecutable, targetExecutable);
} catch (e) {
const message = `Cannot copy ${sourceExecutable} to ${targetExecutable}: ${inspect(e)}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
console.log(`Copied ${sourceExecutable} to ${targetExecutable}`);
const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js');
try {
spawnSyncAndExitWithoutError(process.execPath, [
postjectFile,
targetExecutable,
'NODE_SEA_BLOB',
seaBlob,
'--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_SEA' ] : [],
]);
} catch (e) {
const message = `Cannot inject ${seaBlob} into ${targetExecutable}: ${inspect(e)}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
console.log(`Injected ${seaBlob} into ${targetExecutable}`);
if (process.platform === 'darwin') {
try {
spawnSyncAndExitWithoutError('codesign', [ '--sign', '-', targetExecutable ]);
spawnSyncAndExitWithoutError('codesign', [ '--verify', targetExecutable ]);
} catch (e) {
const message = `Cannot sign ${targetExecutable}: ${inspect(e)}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
console.log(`Signed ${targetExecutable}`);
} else if (process.platform === 'win32') {
try {
spawnSyncAndExitWithoutError('where', [ 'signtool' ]);
} catch (e) {
const message = `Cannot find signtool: ${inspect(e)}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
let stderr;
try {
({ stderr } = spawnSyncAndExitWithoutError('signtool', [ 'sign', '/fd', 'SHA256', targetExecutable ]));
spawnSyncAndExitWithoutError('signtool', ['verify', '/pa', 'SHA256', targetExecutable]);
} catch (e) {
const message = `Cannot sign ${targetExecutable}: ${inspect(e)}\n${stderr}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
console.log(`Signed ${targetExecutable}`);
}
}
module.exports = {
skipIfSingleExecutableIsNotSupported,
generateSEA,
};

View File

@ -0,0 +1,50 @@
'use strict';
const common = require('../common');
const path = require('path');
const kNodeShared = Boolean(process.config.variables.node_shared);
const kShlibSuffix = process.config.variables.shlib_suffix;
const kExecPath = path.dirname(process.execPath);
// If node executable is linked to shared lib, need to take care about the
// shared lib path.
function addLibraryPath(env) {
if (!kNodeShared) {
return;
}
env ||= process.env;
env.LD_LIBRARY_PATH =
(env.LD_LIBRARY_PATH ? env.LD_LIBRARY_PATH + path.delimiter : '') +
kExecPath;
// For AIX.
env.LIBPATH =
(env.LIBPATH ? env.LIBPATH + path.delimiter : '') +
kExecPath;
// For macOS.
env.DYLD_LIBRARY_PATH =
(env.DYLD_LIBRARY_PATH ? env.DYLD_LIBRARY_PATH + path.delimiter : '') +
kExecPath;
// For Windows.
env.PATH = (env.PATH ? env.PATH + path.delimiter : '') + kExecPath;
}
// Get the full path of shared lib.
function getSharedLibPath() {
if (common.isWindows) {
return path.join(kExecPath, 'node.dll');
}
return path.join(kExecPath, `libnode.${kShlibSuffix}`);
}
// Get the binary path of stack frames.
function getBinaryPath() {
return kNodeShared ? getSharedLibPath() : process.execPath;
}
module.exports = {
addLibraryPath,
getBinaryPath,
getSharedLibPath,
};

65
test/common/snapshot.js Normal file
View File

@ -0,0 +1,65 @@
'use strict';
const tmpdir = require('../common/tmpdir');
const { spawnSync } = require('child_process');
const fs = require('fs');
const assert = require('assert');
function buildSnapshot(entry, env) {
const child = spawnSync(process.execPath, [
'--snapshot-blob',
tmpdir.resolve('snapshot.blob'),
'--build-snapshot',
entry,
], {
cwd: tmpdir.path,
env: {
...process.env,
...env,
},
});
const stderr = child.stderr.toString();
const stdout = child.stdout.toString();
console.log('[stderr]');
console.log(stderr);
console.log('[stdout]');
console.log(stdout);
assert.strictEqual(child.status, 0);
const stats = fs.statSync(tmpdir.resolve('snapshot.blob'));
assert(stats.isFile());
return { child, stderr, stdout };
}
function runWithSnapshot(entry, env) {
const args = ['--snapshot-blob', tmpdir.resolve('snapshot.blob')];
if (entry !== undefined) {
args.push(entry);
}
const child = spawnSync(process.execPath, args, {
cwd: tmpdir.path,
env: {
...process.env,
...env,
},
});
const stderr = child.stderr.toString();
const stdout = child.stdout.toString();
console.log('[stderr]');
console.log(stderr);
console.log('[stdout]');
console.log(stdout);
assert.strictEqual(child.status, 0);
return { child, stderr, stdout };
}
module.exports = {
buildSnapshot,
runWithSnapshot,
};

View File

@ -0,0 +1,41 @@
'use strict';
const { relative } = require('node:path');
const { inspect } = require('node:util');
const cwd = process.cwd();
module.exports = async function* errorReporter(source) {
for await (const event of source) {
if (event.type === 'test:fail') {
const { name, details, line, column, file } = event.data;
let { error } = details;
if (error?.failureType === 'subtestsFailed') {
// In the interest of keeping things concise, skip failures that are
// only due to nested failures.
continue;
}
if (error?.code === 'ERR_TEST_FAILURE') {
error = error.cause;
}
const output = [
`Test failure: '${name}'`,
];
if (file) {
output.push(`Location: ${relative(cwd, file)}:${line}:${column}`);
}
output.push(inspect(error));
output.push('\n');
yield output.join('\n');
if (process.env.FAIL_FAST) {
yield `\nBailing on failed test: ${event.data.name}\n`;
process.exitCode = 1;
process.emit('SIGINT');
}
}
}
};

12
test/common/tick.js Normal file
View File

@ -0,0 +1,12 @@
'use strict';
module.exports = function tick(x, cb) {
function ontick() {
if (--x === 0) {
if (typeof cb === 'function') cb();
} else {
setImmediate(ontick);
}
}
setImmediate(ontick);
};

189
test/common/tls.js Normal file
View File

@ -0,0 +1,189 @@
/* eslint-disable node-core/crypto-check */
'use strict';
const crypto = require('crypto');
const net = require('net');
const assert = require('assert');
exports.ccs = Buffer.from('140303000101', 'hex');
class TestTLSSocket extends net.Socket {
constructor(server_cert) {
super();
this.server_cert = server_cert;
this.version = Buffer.from('0303', 'hex');
this.handshake_list = [];
// AES128-GCM-SHA256
this.ciphers = Buffer.from('000002009c0', 'hex');
this.pre_primary_secret =
Buffer.concat([this.version, crypto.randomBytes(46)]);
this.primary_secret = null;
this.write_seq = 0;
this.client_random = crypto.randomBytes(32);
this.on('handshake', (msg) => {
this.handshake_list.push(msg);
});
this.on('server_random', (server_random) => {
this.primary_secret = PRF12('sha256', this.pre_primary_secret,
'primary secret',
Buffer.concat([this.client_random,
server_random]),
48);
const key_block = PRF12('sha256', this.primary_secret,
'key expansion',
Buffer.concat([server_random,
this.client_random]),
40);
this.client_writeKey = key_block.slice(0, 16);
this.client_writeIV = key_block.slice(32, 36);
});
}
createClientHello() {
const compressions = Buffer.from('0100', 'hex'); // null
const msg = addHandshakeHeader(0x01, Buffer.concat([
this.version, this.client_random, this.ciphers, compressions,
]));
this.emit('handshake', msg);
return addRecordHeader(0x16, msg);
}
createClientKeyExchange() {
const encrypted_pre_primary_secret = crypto.publicEncrypt({
key: this.server_cert,
padding: crypto.constants.RSA_PKCS1_PADDING,
}, this.pre_primary_secret);
const length = Buffer.alloc(2);
length.writeUIntBE(encrypted_pre_primary_secret.length, 0, 2);
const msg = addHandshakeHeader(0x10, Buffer.concat([
length, encrypted_pre_primary_secret]));
this.emit('handshake', msg);
return addRecordHeader(0x16, msg);
}
createFinished() {
const shasum = crypto.createHash('sha256');
shasum.update(Buffer.concat(this.handshake_list));
const message_hash = shasum.digest();
const r = PRF12('sha256', this.primary_secret,
'client finished', message_hash, 12);
const msg = addHandshakeHeader(0x14, r);
this.emit('handshake', msg);
return addRecordHeader(0x16, msg);
}
createIllegalHandshake() {
const illegal_handshake = Buffer.alloc(5);
return addRecordHeader(0x16, illegal_handshake);
}
parseTLSFrame(buf) {
let offset = 0;
const record = buf.slice(offset, 5);
const type = record[0];
const length = record.slice(3, 5).readUInt16BE(0);
offset += 5;
let remaining = buf.slice(offset, offset + length);
if (type === 0x16) {
do {
remaining = this.parseTLSHandshake(remaining);
} while (remaining.length > 0);
}
offset += length;
return buf.slice(offset);
}
parseTLSHandshake(buf) {
let offset = 0;
const handshake_type = buf[offset];
if (handshake_type === 0x02) {
const server_random = buf.slice(6, 6 + 32);
this.emit('server_random', server_random);
}
offset += 1;
const length = buf.readUIntBE(offset, 3);
offset += 3;
const handshake = buf.slice(0, offset + length);
this.emit('handshake', handshake);
offset += length;
const remaining = buf.slice(offset);
return remaining;
}
encrypt(plain) {
const type = plain.slice(0, 1);
const version = plain.slice(1, 3);
const nonce = crypto.randomBytes(8);
const iv = Buffer.concat([this.client_writeIV.slice(0, 4), nonce]);
const bob = crypto.createCipheriv('aes-128-gcm', this.client_writeKey, iv);
const write_seq = Buffer.alloc(8);
write_seq.writeUInt32BE(this.write_seq++, 4);
const aad = Buffer.concat([write_seq, plain.slice(0, 5)]);
bob.setAAD(aad);
const encrypted1 = bob.update(plain.slice(5));
const encrypted = Buffer.concat([encrypted1, bob.final()]);
const tag = bob.getAuthTag();
const length = Buffer.alloc(2);
length.writeUInt16BE(nonce.length + encrypted.length + tag.length, 0);
return Buffer.concat([type, version, length, nonce, encrypted, tag]);
}
}
function addRecordHeader(type, frame) {
const record_layer = Buffer.from('0003030000', 'hex');
record_layer[0] = type;
record_layer.writeUInt16BE(frame.length, 3);
return Buffer.concat([record_layer, frame]);
}
function addHandshakeHeader(type, msg) {
const handshake_header = Buffer.alloc(4);
handshake_header[0] = type;
handshake_header.writeUIntBE(msg.length, 1, 3);
return Buffer.concat([handshake_header, msg]);
}
function PRF12(algo, secret, label, seed, size) {
const newSeed = Buffer.concat([Buffer.from(label, 'utf8'), seed]);
return P_hash(algo, secret, newSeed, size);
}
function P_hash(algo, secret, seed, size) {
const result = Buffer.alloc(size);
let hmac = crypto.createHmac(algo, secret);
hmac.update(seed);
let a = hmac.digest();
let j = 0;
while (j < size) {
hmac = crypto.createHmac(algo, secret);
hmac.update(a);
hmac.update(seed);
const b = hmac.digest();
let todo = b.length;
if (j + todo > size) {
todo = size - j;
}
b.copy(result, j, 0, todo);
j += todo;
hmac = crypto.createHmac(algo, secret);
hmac.update(a);
a = hmac.digest();
}
return result;
}
exports.assertIsCAArray = function assertIsCAArray(certs) {
assert(Array.isArray(certs));
assert(certs.length > 0);
// The certificates looks PEM-encoded.
for (const cert of certs) {
const trimmed = cert.trim();
assert.match(trimmed, /^-----BEGIN CERTIFICATE-----/);
assert.match(trimmed, /-----END CERTIFICATE-----$/);
}
};
exports.TestTLSSocket = TestTLSSocket;

112
test/common/tmpdir.js Normal file
View File

@ -0,0 +1,112 @@
'use strict';
const { spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const { isMainThread } = require('worker_threads');
const isUnixLike = process.platform !== 'win32';
let escapePOSIXShell;
function rmSync(pathname, useSpawn) {
if (useSpawn) {
if (isUnixLike) {
escapePOSIXShell ??= require('./index.js').escapePOSIXShell;
for (let i = 0; i < 3; i++) {
const { status } = spawnSync(...escapePOSIXShell`rm -rf "${pathname}"`);
if (status === 0) {
break;
}
}
} else {
spawnSync(
process.execPath,
[
'-e',
`fs.rmSync(${JSON.stringify(pathname)}, { maxRetries: 3, recursive: true, force: true });`,
],
);
}
} else {
fs.rmSync(pathname, { maxRetries: 3, recursive: true, force: true });
}
}
const testRoot = process.env.NODE_TEST_DIR ?
fs.realpathSync(process.env.NODE_TEST_DIR) : path.resolve(__dirname, '..');
// Using a `.` prefixed name, which is the convention for "hidden" on POSIX,
// gets tools to ignore it by default or by simple rules, especially eslint.
const tmpdirName = '.tmp.' +
(process.env.TEST_SERIAL_ID || process.env.TEST_THREAD_ID || '0');
let tmpPath = path.join(testRoot, tmpdirName);
let firstRefresh = true;
function refresh(useSpawn = false) {
rmSync(tmpPath, useSpawn);
fs.mkdirSync(tmpPath);
if (firstRefresh) {
firstRefresh = false;
// Clean only when a test uses refresh. This allows for child processes to
// use the tmpdir and only the parent will clean on exit.
process.on('exit', () => {
return onexit(useSpawn);
});
}
}
function onexit(useSpawn) {
// Change directory to avoid possible EBUSY
if (isMainThread)
process.chdir(testRoot);
try {
rmSync(tmpPath, useSpawn);
} catch (e) {
console.error('Can\'t clean tmpdir:', tmpPath);
const files = fs.readdirSync(tmpPath);
console.error('Files blocking:', files);
if (files.some((f) => f.startsWith('.nfs'))) {
// Warn about NFS "silly rename"
console.error('Note: ".nfs*" might be files that were open and ' +
'unlinked but not closed.');
console.error('See http://nfs.sourceforge.net/#faq_d2 for details.');
}
console.error();
throw e;
}
}
function resolve(...paths) {
return path.resolve(tmpPath, ...paths);
}
function hasEnoughSpace(size) {
const { bavail, bsize } = fs.statfsSync(tmpPath);
return bavail >= Math.ceil(size / bsize);
}
function fileURL(...paths) {
// When called without arguments, add explicit trailing slash
const fullPath = path.resolve(tmpPath + path.sep, ...paths);
return pathToFileURL(fullPath);
}
module.exports = {
fileURL,
hasEnoughSpace,
refresh,
resolve,
get path() {
return tmpPath;
},
set path(newPath) {
tmpPath = path.resolve(newPath);
},
};

24
test/common/udp.js Normal file
View File

@ -0,0 +1,24 @@
'use strict';
const dgram = require('dgram');
const options = { type: 'udp4', reusePort: true };
function checkSupportReusePort() {
return new Promise((resolve, reject) => {
const socket = dgram.createSocket(options);
socket.bind(0);
socket.on('listening', () => {
socket.close(resolve);
});
socket.on('error', (err) => {
console.log('The `reusePort` option is not supported:', err.message);
socket.close();
reject(err);
});
});
}
module.exports = {
checkSupportReusePort,
options,
};

70
test/common/v8.js Normal file
View File

@ -0,0 +1,70 @@
'use strict';
const assert = require('assert');
const { GCProfiler } = require('v8');
function collectGCProfile({ duration }) {
return new Promise((resolve) => {
const profiler = new GCProfiler();
profiler.start();
setTimeout(() => {
resolve(profiler.stop());
}, duration);
});
}
function checkGCProfile(data) {
assert.ok(data.version > 0);
assert.ok(data.startTime >= 0);
assert.ok(data.endTime >= 0);
assert.ok(Array.isArray(data.statistics));
// If the array is not empty, check it
if (data.statistics.length) {
// Just check the first one
const item = data.statistics[0];
assert.ok(typeof item.gcType === 'string');
assert.ok(item.cost >= 0);
assert.ok(typeof item.beforeGC === 'object');
assert.ok(typeof item.afterGC === 'object');
// The content of beforeGC and afterGC is same, so we just check afterGC
assert.ok(typeof item.afterGC.heapStatistics === 'object');
const heapStatisticsKeys = [
'externalMemory',
'heapSizeLimit',
'mallocedMemory',
'peakMallocedMemory',
'totalAvailableSize',
'totalGlobalHandlesSize',
'totalHeapSize',
'totalHeapSizeExecutable',
'totalPhysicalSize',
'usedGlobalHandlesSize',
'usedHeapSize',
];
heapStatisticsKeys.forEach((key) => {
assert.ok(item.afterGC.heapStatistics[key] >= 0);
});
assert.ok(typeof item.afterGC.heapSpaceStatistics === 'object');
const heapSpaceStatisticsKeys = [
'physicalSpaceSize',
'spaceAvailableSize',
'spaceName',
'spaceSize',
'spaceUsedSize',
];
heapSpaceStatisticsKeys.forEach((key) => {
const value = item.afterGC.heapSpaceStatistics[0][key];
assert.ok(key === 'spaceName' ? typeof value === 'string' : value >= 0);
});
}
}
async function testGCProfiler() {
const data = await collectGCProfile({ duration: 5000 });
checkGCProfile(data);
}
module.exports = {
collectGCProfile,
checkGCProfile,
testGCProfiler,
};

43
test/common/wasi.js Normal file
View File

@ -0,0 +1,43 @@
// Test version set to preview1
'use strict';
const { spawnSyncAndAssert } = require('./child_process');
const fixtures = require('./fixtures');
const childPath = fixtures.path('wasi-preview-1.js');
function testWasiPreview1(args, spawnArgs = {}, expectations = {}) {
const newEnv = {
...process.env,
NODE_DEBUG_NATIVE: 'wasi',
NODE_PLATFORM: process.platform,
...spawnArgs.env,
};
spawnArgs.env = newEnv;
console.log('Testing with --turbo-fast-api-calls:', ...args);
spawnSyncAndAssert(
process.execPath, [
'--turbo-fast-api-calls',
childPath,
...args,
],
spawnArgs,
expectations,
);
console.log('Testing with --no-turbo-fast-api-calls:', ...args);
spawnSyncAndAssert(
process.execPath,
[
'--no-turbo-fast-api-calls',
childPath,
...args,
],
spawnArgs,
expectations,
);
}
module.exports = {
testWasiPreview1,
};

955
test/common/wpt.js Normal file
View File

@ -0,0 +1,955 @@
'use strict';
const assert = require('assert');
const fixtures = require('../common/fixtures');
const fs = require('fs');
const fsPromises = fs.promises;
const path = require('path');
const events = require('events');
const os = require('os');
const { inspect } = require('util');
const { Worker } = require('worker_threads');
const workerPath = path.join(__dirname, 'wpt/worker.js');
function getBrowserProperties() {
const { node: version } = process.versions; // e.g. 18.13.0, 20.0.0-nightly202302078e6e215481
const release = /^\d+\.\d+\.\d+$/.test(version);
const browser = {
browser_channel: release ? 'stable' : 'experimental',
browser_version: version,
};
return browser;
}
/**
* Return one of three expected values
* https://github.com/web-platform-tests/wpt/blob/1c6ff12/tools/wptrunner/wptrunner/tests/test_update.py#L953-L958
*/
function getOs() {
switch (os.type()) {
case 'Linux':
return 'linux';
case 'Darwin':
return 'mac';
case 'Windows_NT':
return 'win';
default:
throw new Error('Unsupported os.type()');
}
}
// https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3705
function sanitizeUnpairedSurrogates(str) {
return str.replace(
/([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g,
function(_, low, prefix, high) {
let output = prefix || ''; // Prefix may be undefined
const string = low || high; // Only one of these alternates can match
for (let i = 0; i < string.length; i++) {
output += codeUnitStr(string[i]);
}
return output;
});
}
function codeUnitStr(char) {
return 'U+' + char.charCodeAt(0).toString(16);
}
class ReportResult {
#startTime;
constructor(name) {
this.test = name;
this.status = 'OK';
this.subtests = [];
this.#startTime = Date.now();
}
addSubtest(name, status, message) {
const subtest = {
status,
// https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3722
name: sanitizeUnpairedSurrogates(name),
};
if (message) {
// https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L4506
subtest.message = sanitizeUnpairedSurrogates(message);
}
this.subtests.push(subtest);
return subtest;
}
finish(status) {
this.status = status ?? 'OK';
this.duration = Date.now() - this.#startTime;
}
}
// Generates a report that can be uploaded to wpt.fyi.
// Checkout https://github.com/web-platform-tests/wpt.fyi/tree/main/api#results-creation
// for more details.
class WPTReport {
constructor(path) {
this.filename = `report-${path.replaceAll('/', '-')}.json`;
/** @type {Map<string, ReportResult>} */
this.results = new Map();
this.time_start = Date.now();
}
/**
* Get or create a ReportResult for a test spec.
* @param {WPTTestSpec} spec
*/
getResult(spec) {
const name = `/${spec.getRelativePath()}${spec.variant}`;
if (this.results.has(name)) {
return this.results.get(name);
}
const result = new ReportResult(name);
this.results.set(name, result);
return result;
}
write() {
this.time_end = Date.now();
const results = Array.from(this.results.values())
.map((result) => {
const url = new URL(result.test, 'http://wpt');
url.pathname = url.pathname.replace(/\.js$/, '.html');
result.test = url.href.slice(url.origin.length);
return result;
});
/**
* Return required and some optional properties
* https://github.com/web-platform-tests/wpt.fyi/blob/60da175/api/README.md?plain=1#L331-L335
*/
this.run_info = {
product: 'node.js',
...getBrowserProperties(),
revision: process.env.WPT_REVISION || 'unknown',
os: getOs(),
};
fs.writeFileSync(`out/wpt/${this.filename}`, JSON.stringify({
time_start: this.time_start,
time_end: this.time_end,
run_info: this.run_info,
results: results,
}));
}
}
// https://github.com/web-platform-tests/wpt/blob/HEAD/resources/testharness.js
// TODO: get rid of this half-baked harness in favor of the one
// pulled from WPT
const harnessMock = {
test: (fn, desc) => {
try {
fn();
} catch (err) {
console.error(`In ${desc}:`);
throw err;
}
},
assert_equals: assert.strictEqual,
assert_true: (value, message) => assert.strictEqual(value, true, message),
assert_false: (value, message) => assert.strictEqual(value, false, message),
assert_throws: (code, func, desc) => {
assert.throws(func, function(err) {
return typeof err === 'object' &&
'name' in err &&
err.name.startsWith(code.name);
}, desc);
},
assert_array_equals: assert.deepStrictEqual,
assert_unreached(desc) {
assert.fail(`Reached unreachable code: ${desc}`);
},
};
class ResourceLoader {
constructor(path) {
this.path = path;
}
toRealFilePath(from, url) {
// We need to patch this to load the WebIDL parser
url = url.replace(
'/resources/WebIDLParser.js',
'/resources/webidl2/lib/webidl2.js',
);
const base = path.dirname(from);
return url.startsWith('/') ?
fixtures.path('wpt', url) :
fixtures.path('wpt', base, url);
}
/**
* Load a resource in test/fixtures/wpt specified with a URL
* @param {string} from the path of the file loading this resource,
* relative to the WPT folder.
* @param {string} url the url of the resource being loaded.
*/
read(from, url) {
const file = this.toRealFilePath(from, url);
return fs.readFileSync(file, 'utf8');
}
/**
* Load a resource in test/fixtures/wpt specified with a URL
* @param {string} from the path of the file loading this resource,
* relative to the WPT folder.
* @param {string} url the url of the resource being loaded.
*/
async readAsFetch(from, url) {
const file = this.toRealFilePath(from, url);
const data = await fsPromises.readFile(file);
return {
ok: true,
arrayBuffer() { return data.buffer; },
json() { return JSON.parse(data.toString()); },
text() { return data.toString(); },
};
}
}
class StatusRule {
constructor(key, value, pattern) {
this.key = key;
this.requires = value.requires || [];
this.fail = value.fail;
this.skip = value.skip;
if (pattern) {
this.pattern = this.transformPattern(pattern);
}
// TODO(joyeecheung): implement this
this.scope = value.scope;
this.comment = value.comment;
}
/**
* Transform a filename pattern into a RegExp
* @param {string} pattern
* @returns {RegExp}
*/
transformPattern(pattern) {
const result = path.normalize(pattern).replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&');
return new RegExp(result.replace('*', '.*'));
}
}
class StatusRuleSet {
constructor() {
// We use two sets of rules to speed up matching
this.exactMatch = {};
this.patternMatch = [];
}
/**
* @param {object} rules
*/
addRules(rules) {
for (const key of Object.keys(rules)) {
if (key.includes('*')) {
this.patternMatch.push(new StatusRule(key, rules[key], key));
} else {
const normalizedPath = path.normalize(key);
this.exactMatch[normalizedPath] = new StatusRule(key, rules[key]);
}
}
}
match(file) {
const result = [];
const exact = this.exactMatch[file];
if (exact) {
result.push(exact);
}
for (const item of this.patternMatch) {
if (item.pattern.test(file)) {
result.push(item);
}
}
return result;
}
}
// A specification of WPT test
class WPTTestSpec {
#content;
/**
* @param {string} mod name of the WPT module, e.g.
* 'html/webappapis/microtask-queuing'
* @param {string} filename path of the test, relative to mod, e.g.
* 'test.any.js'
* @param {StatusRule[]} rules
* @param {string} variant test file variant
*/
constructor(mod, filename, rules, variant = '') {
this.module = mod;
this.filename = filename;
this.variant = variant;
this.requires = new Set();
this.failedTests = [];
this.flakyTests = [];
this.skipReasons = [];
for (const item of rules) {
if (item.requires.length) {
for (const req of item.requires) {
this.requires.add(req);
}
}
if (Array.isArray(item.fail?.expected)) {
this.failedTests.push(...item.fail.expected);
}
if (Array.isArray(item.fail?.flaky)) {
this.failedTests.push(...item.fail.flaky);
this.flakyTests.push(...item.fail.flaky);
}
if (item.skip) {
this.skipReasons.push(item.skip);
}
}
this.failedTests = [...new Set(this.failedTests)];
this.flakyTests = [...new Set(this.flakyTests)];
this.skipReasons = [...new Set(this.skipReasons)];
}
/**
* @param {string} mod
* @param {string} filename
* @param {StatusRule[]} rules
*/
static from(mod, filename, rules) {
const spec = new WPTTestSpec(mod, filename, rules);
const meta = spec.getMeta();
return meta.variant?.map((variant) => new WPTTestSpec(mod, filename, rules, variant)) || [spec];
}
getRelativePath() {
return path.join(this.module, this.filename);
}
getAbsolutePath() {
return fixtures.path('wpt', this.getRelativePath());
}
/**
* @returns {string}
*/
getContent() {
this.#content ||= fs.readFileSync(this.getAbsolutePath(), 'utf8');
return this.#content;
}
/**
* @returns {{ script?: string[]; variant?: string[]; [key: string]: string }} parsed META tags of a spec file
*/
getMeta() {
const matches = this.getContent().match(/\/\/ META: .+/g);
if (!matches) {
return {};
}
const result = {};
for (const match of matches) {
const parts = match.match(/\/\/ META: ([^=]+?)=(.+)/);
const key = parts[1];
const value = parts[2];
if (key === 'script' || key === 'variant') {
if (result[key]) {
result[key].push(value);
} else {
result[key] = [value];
}
} else {
result[key] = value;
}
}
return result;
}
}
const kIntlRequirement = {
none: 0,
small: 1,
full: 2,
// TODO(joyeecheung): we may need to deal with --with-intl=system-icu
};
class BuildRequirement {
constructor() {
this.currentIntl = kIntlRequirement.none;
if (process.config.variables.v8_enable_i18n_support === 0) {
this.currentIntl = kIntlRequirement.none;
return;
}
// i18n enabled
if (process.config.variables.icu_small) {
this.currentIntl = kIntlRequirement.small;
} else {
this.currentIntl = kIntlRequirement.full;
}
// Not using common.hasCrypto because of the global leak checks
this.hasCrypto = Boolean(process.versions.openssl) &&
!process.env.NODE_SKIP_CRYPTO;
}
/**
* @param {Set} requires
* @returns {string|false} The config that the build is lacking, or false
*/
isLacking(requires) {
const current = this.currentIntl;
if (requires.has('full-icu') && current !== kIntlRequirement.full) {
return 'full-icu';
}
if (requires.has('small-icu') && current < kIntlRequirement.small) {
return 'small-icu';
}
if (requires.has('crypto') && !this.hasCrypto) {
return 'crypto';
}
return false;
}
}
const buildRequirements = new BuildRequirement();
class StatusLoader {
/**
* @param {string} path relative path of the WPT subset
*/
constructor(path) {
this.path = path;
this.rules = new StatusRuleSet();
/** @type {WPTTestSpec[]} */
this.specs = [];
}
/**
* Grep for all .*.js file recursively in a directory.
* @param {string} dir
*/
grep(dir) {
let result = [];
const list = fs.readdirSync(dir);
for (const file of list) {
const filepath = path.join(dir, file);
const stat = fs.statSync(filepath);
if (stat.isDirectory()) {
const list = this.grep(filepath);
result = result.concat(list);
} else {
if (!(/\.\w+\.js$/.test(filepath))) {
continue;
}
result.push(filepath);
}
}
return result;
}
load() {
const dir = path.join(__dirname, '..', 'wpt');
let statusFile = path.join(dir, 'status', `${this.path}.json`);
let result;
if (fs.existsSync(statusFile)) {
result = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
} else {
statusFile = path.join(dir, 'status', `${this.path}.cjs`);
result = require(statusFile);
}
this.rules.addRules(result);
const subDir = fixtures.path('wpt', this.path);
const list = this.grep(subDir);
for (const file of list) {
const relativePath = path.relative(subDir, file);
const match = this.rules.match(relativePath);
this.specs.push(...WPTTestSpec.from(this.path, relativePath, match));
}
}
}
const kPass = 'pass';
const kFail = 'fail';
const kSkip = 'skip';
const kTimeout = 'timeout';
const kIncomplete = 'incomplete';
const kUncaught = 'uncaught';
const NODE_UNCAUGHT = 100;
const limit = (concurrency) => {
let running = 0;
const queue = [];
const execute = async (fn) => {
if (running < concurrency) {
running++;
try {
await fn();
} finally {
running--;
if (queue.length > 0) {
execute(queue.shift());
}
}
} else {
queue.push(fn);
}
};
return execute;
};
class WPTRunner {
constructor(path, { concurrency = os.availableParallelism() - 1 || 1 } = {}) {
this.path = path;
this.resource = new ResourceLoader(path);
this.concurrency = concurrency;
this.flags = [];
this.globalThisInitScripts = [];
this.initScript = null;
this.status = new StatusLoader(path);
this.status.load();
this.specs = new Set(this.status.specs);
this.results = {};
this.inProgress = new Set();
this.workers = new Map();
this.unexpectedFailures = [];
if (process.env.WPT_REPORT != null) {
this.report = new WPTReport(path);
}
}
/**
* Sets the Node.js flags passed to the worker.
* @param {string[]} flags
*/
setFlags(flags) {
this.flags = flags;
}
/**
* Sets a script to be run in the worker before executing the tests.
* @param {string} script
*/
setInitScript(script) {
this.initScript = script;
}
/**
* Set the scripts modifier for each script.
* @param {(meta: { code: string, filename: string }) => void} modifier
*/
setScriptModifier(modifier) {
this.scriptsModifier = modifier;
}
/**
* @param {WPTTestSpec} spec
*/
fullInitScript(spec) {
const url = new URL(`/${spec.getRelativePath().replace(/\.js$/, '.html')}${spec.variant}`, 'http://wpt');
const title = spec.getMeta().title;
let { initScript } = this;
initScript = `${initScript}\n\n//===\nglobalThis.location = new URL("${url.href}");`;
if (title) {
initScript = `${initScript}\n\n//===\nglobalThis.META_TITLE = "${title}";`;
}
if (this.globalThisInitScripts.length === null) {
return initScript;
}
const globalThisInitScript = this.globalThisInitScripts.join('\n\n//===\n');
if (initScript === null) {
return globalThisInitScript;
}
return `${globalThisInitScript}\n\n//===\n${initScript}`;
}
/**
* Pretend the runner is run in `name`'s environment (globalThis).
* @param {'Window'} name
* @see {@link https://github.com/nodejs/node/blob/24673ace8ae196bd1c6d4676507d6e8c94cf0b90/test/fixtures/wpt/resources/idlharness.js#L654-L671}
*/
pretendGlobalThisAs(name) {
switch (name) {
case 'Window': {
this.globalThisInitScripts.push('globalThis.Window = Object.getPrototypeOf(globalThis).constructor;');
break;
}
// TODO(XadillaX): implement `ServiceWorkerGlobalScope`,
// `DedicateWorkerGlobalScope`, etc.
//
// e.g. `ServiceWorkerGlobalScope` should implement dummy
// `addEventListener` and so on.
default: throw new Error(`Invalid globalThis type ${name}.`);
}
}
// TODO(joyeecheung): work with the upstream to port more tests in .html
// to .js.
async runJsTests() {
const queue = this.buildQueue();
const run = limit(this.concurrency);
for (const spec of queue) {
const content = spec.getContent();
const meta = spec.getMeta(content);
const absolutePath = spec.getAbsolutePath();
const relativePath = spec.getRelativePath();
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
// Scripts specified with the `// META: script=` header
const scriptsToRun = meta.script?.map((script) => {
const obj = {
filename: this.resource.toRealFilePath(relativePath, script),
code: this.resource.read(relativePath, script),
};
this.scriptsModifier?.(obj);
return obj;
}) ?? [];
// The actual test
const obj = {
code: content,
filename: absolutePath,
};
this.scriptsModifier?.(obj);
scriptsToRun.push(obj);
run(async () => {
const worker = new Worker(workerPath, {
execArgv: this.flags,
workerData: {
testRelativePath: relativePath,
wptRunner: __filename,
wptPath: this.path,
initScript: this.fullInitScript(spec),
harness: {
code: fs.readFileSync(harnessPath, 'utf8'),
filename: harnessPath,
},
scriptsToRun,
needsGc: !!meta.script?.find((script) => script === '/common/gc.js'),
},
});
this.inProgress.add(spec);
this.workers.set(spec, worker);
const reportResult = this.report?.getResult(spec);
worker.on('message', (message) => {
switch (message.type) {
case 'result':
return this.resultCallback(spec, message.result, reportResult);
case 'completion':
return this.completionCallback(spec, message.status, reportResult);
default:
throw new Error(`Unexpected message from worker: ${message.type}`);
}
});
worker.on('error', (err) => {
if (!this.inProgress.has(spec)) {
// The test is already finished. Ignore errors that occur after it.
// This can happen normally, for example in timers tests.
return;
}
// Generate a subtest failure for visibility.
// No need to record this synthetic failure with wpt.fyi.
this.fail(
spec,
{
status: NODE_UNCAUGHT,
name: 'evaluation in WPTRunner.runJsTests()',
message: err.message,
stack: inspect(err),
},
kUncaught,
);
// Mark the whole test as failed in wpt.fyi report.
reportResult?.finish('ERROR');
this.inProgress.delete(spec);
});
await events.once(worker, 'exit').catch(() => {});
});
}
process.on('exit', () => {
for (const spec of this.inProgress) {
// No need to record this synthetic failure with wpt.fyi.
this.fail(spec, { name: 'Incomplete' }, kIncomplete);
// Mark the whole test as failed in wpt.fyi report.
const reportResult = this.report?.getResult(spec);
reportResult?.finish('ERROR');
}
inspect.defaultOptions.depth = Infinity;
// Sorts the rules to have consistent output
console.log('');
console.log(JSON.stringify(Object.keys(this.results).sort().reduce(
(obj, key) => {
obj[key] = this.results[key];
return obj;
},
{},
), null, 2));
const failures = [];
let expectedFailures = 0;
let skipped = 0;
for (const [key, item] of Object.entries(this.results)) {
if (item.fail?.unexpected) {
failures.push(key);
}
if (item.fail?.expected) {
expectedFailures++;
}
if (item.skip) {
skipped++;
}
}
const unexpectedPasses = [];
for (const specs of queue) {
const key = specs.filename;
// File has no expected failures
if (!specs.failedTests.length) {
continue;
}
// File was (maybe even conditionally) skipped
if (this.results[key]?.skip) {
continue;
}
// Full check: every expected to fail test is present
if (specs.failedTests.some((expectedToFail) => {
if (specs.flakyTests.includes(expectedToFail)) {
return false;
}
return this.results[key]?.fail?.expected?.includes(expectedToFail) !== true;
})) {
unexpectedPasses.push(key);
continue;
}
}
this.report?.write();
const ran = queue.length;
const total = ran + skipped;
const passed = ran - expectedFailures - failures.length;
console.log('');
console.log(`Ran ${ran}/${total} tests, ${skipped} skipped,`,
`${passed} passed, ${expectedFailures} expected failures,`,
`${failures.length} unexpected failures,`,
`${unexpectedPasses.length} unexpected passes`);
if (failures.length > 0) {
const file = path.join('test', 'wpt', 'status', `${this.path}.json`);
throw new Error(
`Found ${failures.length} unexpected failures. ` +
`Consider updating ${file} for these files:\n${failures.join('\n')}`);
}
if (unexpectedPasses.length > 0) {
const file = path.join('test', 'wpt', 'status', `${this.path}.json`);
throw new Error(
`Found ${unexpectedPasses.length} unexpected passes. ` +
`Consider updating ${file} for these files:\n${unexpectedPasses.join('\n')}`);
}
});
}
// Map WPT test status to strings
getTestStatus(status) {
switch (status) {
case 1:
return kFail;
case 2:
return kTimeout;
case 3:
return kIncomplete;
case NODE_UNCAUGHT:
return kUncaught;
default:
return kPass;
}
}
/**
* Report the status of each specific test case (there could be multiple
* in one test file).
* @param {WPTTestSpec} spec
* @param {Test} test The Test object returned by WPT harness
* @param {ReportResult} reportResult The report result object
*/
resultCallback(spec, test, reportResult) {
const status = this.getTestStatus(test.status);
if (status !== kPass) {
this.fail(spec, test, status, reportResult);
} else {
this.succeed(test, status, reportResult);
}
}
/**
* Report the status of each WPT test (one per file)
* @param {WPTTestSpec} spec
* @param {object} harnessStatus - The status object returned by WPT harness.
* @param {ReportResult} reportResult The report result object
*/
completionCallback(spec, harnessStatus, reportResult) {
const status = this.getTestStatus(harnessStatus.status);
// Treat it like a test case failure
if (status === kTimeout) {
// No need to record this synthetic failure with wpt.fyi.
this.fail(spec, { name: 'WPT testharness timeout' }, kTimeout);
// Mark the whole test as TIMEOUT in wpt.fyi report.
reportResult?.finish('TIMEOUT');
} else if (status !== kPass) {
// No need to record this synthetic failure with wpt.fyi.
this.fail(spec, {
status: status,
name: 'WPT test harness error',
message: harnessStatus.message,
stack: harnessStatus.stack,
}, status);
// Mark the whole test as ERROR in wpt.fyi report.
reportResult?.finish('ERROR');
} else {
reportResult?.finish();
}
this.inProgress.delete(spec);
// Always force termination of the worker. Some tests allocate resources
// that would otherwise keep it alive.
this.workers.get(spec).terminate();
}
addTestResult(spec, item) {
let result = this.results[spec.filename];
result ||= this.results[spec.filename] = {};
if (item.status === kSkip) {
// { filename: { skip: 'reason' } }
result[kSkip] = item.reason;
} else {
// { filename: { fail: { expected: [ ... ],
// unexpected: [ ... ] } }}
result[item.status] ||= {};
const key = item.expected ? 'expected' : 'unexpected';
result[item.status][key] ||= [];
const hasName = result[item.status][key].includes(item.name);
if (!hasName) {
result[item.status][key].push(item.name);
}
}
}
succeed(test, status, reportResult) {
console.log(`[${status.toUpperCase()}] ${test.name}`);
reportResult?.addSubtest(test.name, 'PASS');
}
fail(spec, test, status, reportResult) {
const expected = spec.failedTests.includes(test.name);
if (expected) {
console.log(`[EXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`);
} else {
console.log(`[UNEXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`);
}
if (status === kFail || status === kUncaught) {
console.log(test.message);
console.log(test.stack);
}
const command = `${process.execPath} ${process.execArgv}` +
` ${require.main.filename} '${spec.filename}${spec.variant}'`;
console.log(`Command: ${command}\n`);
reportResult?.addSubtest(test.name, 'FAIL', test.message);
this.addTestResult(spec, {
name: test.name,
expected,
status: kFail,
reason: test.message || status,
});
}
skip(spec, reasons) {
const joinedReasons = reasons.join('; ');
console.log(`[SKIPPED] ${spec.filename}${spec.variant}: ${joinedReasons}`);
this.addTestResult(spec, {
status: kSkip,
reason: joinedReasons,
});
}
buildQueue() {
const queue = [];
let argFilename;
let argVariant;
if (process.argv[2]) {
([argFilename, argVariant = ''] = process.argv[2].split('?'));
}
for (const spec of this.specs) {
if (argFilename) {
if (spec.filename === argFilename && (!argVariant || spec.variant.substring(1) === argVariant)) {
queue.push(spec);
}
continue;
}
if (spec.skipReasons.length > 0) {
this.skip(spec, spec.skipReasons);
continue;
}
const lackingSupport = buildRequirements.isLacking(spec.requires);
if (lackingSupport) {
this.skip(spec, [ `requires ${lackingSupport}` ]);
continue;
}
queue.push(spec);
}
// If the tests are run as `node test/wpt/test-something.js subset.any.js`,
// only `subset.any.js` (all variants) will be run by the runner.
// If the tests are run as `node test/wpt/test-something.js 'subset.any.js?1-10'`,
// only the `?1-10` variant of `subset.any.js` will be run by the runner.
if (argFilename && queue.length === 0) {
throw new Error(`${process.argv[2]} not found!`);
}
return queue;
}
}
module.exports = {
harness: harnessMock,
ResourceLoader,
WPTRunner,
};

70
test/common/wpt/worker.js Normal file
View File

@ -0,0 +1,70 @@
'use strict';
const { runInNewContext, runInThisContext } = require('vm');
const { setFlagsFromString } = require('v8');
const { parentPort, workerData } = require('worker_threads');
const { ResourceLoader } = require(workerData.wptRunner);
const resource = new ResourceLoader(workerData.wptPath);
if (workerData.needsGc) {
// See https://github.com/nodejs/node/issues/16595#issuecomment-340288680
setFlagsFromString('--expose-gc');
globalThis.gc = runInNewContext('gc');
}
globalThis.self = global;
globalThis.GLOBAL = {
isWindow() { return false; },
isShadowRealm() { return false; },
};
globalThis.require = require;
// This is a mock for non-fetch tests that use fetch to resolve
// a relative fixture file.
// Actual Fetch API WPTs are executed in nodejs/undici.
globalThis.fetch = function fetch(file) {
return resource.readAsFetch(workerData.testRelativePath, file);
};
if (workerData.initScript) {
runInThisContext(workerData.initScript);
}
runInThisContext(workerData.harness.code, {
filename: workerData.harness.filename,
});
// eslint-disable-next-line no-undef
add_result_callback((result) => {
parentPort.postMessage({
type: 'result',
result: {
status: result.status,
name: result.name,
message: result.message,
stack: result.stack,
},
});
});
// Keep the event loop alive
const timeout = setTimeout(() => {
parentPort.postMessage({
type: 'completion',
status: { status: 2 },
});
}, 2 ** 31 - 1); // Max timeout is 2^31-1, when overflown the timeout is set to 1.
// eslint-disable-next-line no-undef
add_completion_callback((_, status) => {
clearTimeout(timeout);
parentPort.postMessage({
type: 'completion',
status,
});
});
for (const scriptToRun of workerData.scriptsToRun) {
runInThisContext(scriptToRun.code, { filename: scriptToRun.filename });
}