current commit
260912.8 · lib: put node:bench behind an --experimental-bench flag
// Flags: --experimental-bench --no-warnings
'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const { spawnSync } = require('child_process');
const { once } = require('events');
const path = require('path');
const { runFile } = require('node:bench');

const fixture = fixtures.path('bench-runner/run-file.cjs');
const relativeFixture = path.relative(process.cwd(), fixture);

assert.throws(() => runFile(null), { code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => runFile(fixture, null), { code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => runFile(fixture, { execArgv: null }), {
  code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => runFile(fixture, { execArgv: [1] }), {
  code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => runFile(fixture, { execArgv: ['--bench'] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { execArgv: [fixture] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { execArgv: ['-e', '0'] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { execArgv: ['-e0'] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { execArgv: ['--require'] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { execArgv: ['--require', ''] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { execArgv: ['--require='] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { execArgv: [`-r=${fixture}`] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { execArgv: ['--prof-process'] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { execArgv: ['--prof_process'] }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, {
  execArgv: ['--bench_name_pattern=run'],
}), { code: 'ERR_INVALID_ARG_VALUE' });
assert.throws(() => runFile(`${fixture}\0`), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(Buffer.from(`${fixture}\0`)), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { env: null }), {
  code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => runFile(fixture, { env: { INVALID: 1 } }), {
  code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => runFile(fixture, { env: { INVALID: 'x\0' } }), {
  code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => runFile(fixture, { env: { NODE_CHANNEL_FD: 1 } }), {
  code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => runFile(fixture, { signal: {} }), {
  code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => runFile(fixture, { signal: { aborted: false } }), {
  code: 'ERR_INVALID_ARG_TYPE',
});

async function testRunFile() {
  const execArgv = ['--expose-gc', '--no-warnings', '-r', 'fs'];
  const env = {
    __proto__: null,
    ...process.env,
    NODE_BENCH_CONTEXT: 'not-a-child',
    NODE_BENCH_OMITTED: undefined,
    NODE_BENCH_RUN_FILE: 'original',
    NODE_CHANNEL_FD: '999',
    NODE_CHANNEL_SERIALIZATION_MODE: 'json',
  };
  const stream = runFile(relativeFixture, { env, execArgv });
  execArgv.length = 0;
  env.NODE_BENCH_RUN_FILE = 'mutated';
  const records = await stream.toArray();
  const plan = records.find(({ type }) => type === 'bench:plan').data;
  const result = records.find(({ type }) => type === 'bench:complete').data;
  const summary = records.at(-1).data;

  assert.strictEqual(records.at(-1).type, 'bench:summary');
  assert.strictEqual(plan.params.context, 'child');
  assert.strictEqual(plan.params.exposed, true);
  assert.strictEqual(plan.params.omitted, true);
  assert.strictEqual(plan.params.value, 'original');
  assert.strictEqual(result.error, undefined);
  assert.strictEqual(result.samples.length, 1);
  assert.strictEqual(typeof result.samples[0].duration_ns, 'bigint');
  assert.notStrictEqual(result.samples[0].detail.pid, process.pid);
  assert(result.samples[0].detail.execArgv.includes('--expose-gc'));
  assert(result.samples[0].detail.execArgv.includes('--no-warnings'));
  assert(result.samples[0].detail.execArgv.includes('-r'));
  assert.strictEqual(summary.success, true);
  assert.strictEqual(summary.file, fixture);
  assert.strictEqual(summary.entryFile, fixture);
  assert.strictEqual(summary.fileRunId, result.fileRunId);
  assert.strictEqual(summary.runId, result.runId);
  assert.strictEqual(records.filter(
    ({ type }) => type === 'bench:summary').length, 1);
}

async function testConcurrentCalls() {
  const [cjsRecords, esmRecords] = await Promise.all([
    runFile(fixtures.fileURL('bench-runner/a.cjs')).toArray(),
    runFile(Buffer.from(path.relative(
      process.cwd(),
      fixtures.path('bench-runner/b.mjs'),
    ))).toArray(),
  ]);
  const cjsResult = cjsRecords.find(
    ({ type }) => type === 'bench:complete').data;
  const esmResult = esmRecords.find(
    ({ type }) => type === 'bench:complete').data;
  assert.strictEqual(cjsResult.name, 'alpha');
  assert.strictEqual(esmResult.name, 'beta');
  assert.notStrictEqual(cjsResult.params.pid, esmResult.params.pid);
  assert.notStrictEqual(cjsResult.runId, esmResult.runId);
  assert.strictEqual(
    cjsRecords.at(-1).data.file,
    fixtures.path('bench-runner/a.cjs'),
  );
  assert.strictEqual(
    esmRecords.at(-1).data.file,
    fixtures.path('bench-runner/b.mjs'),
  );
}

async function testLoadFailure() {
  const missing = path.resolve(fixtures.fixturesDir, 'does-not-exist.cjs');
  const records = await runFile(missing).toArray();
  const diagnostics = records.filter(
    ({ type }) => type === 'bench:diagnostic');
  assert(diagnostics.some(
    ({ data }) => data.level === 'error' && /failed with exit code/.test(
      data.message)));
  assert.strictEqual(records.some(
    ({ type }) => type === 'bench:complete'), false);
  assert.strictEqual(records.at(-1).type, 'bench:summary');
  assert.strictEqual(records.at(-1).data.success, false);
}

async function testEvaluationFailure() {
  const records = await runFile(fixtures.path(
    'bench-runner/load-error-after-declaration.cjs')).toArray();
  assert(records.some(({ type, data }) =>
    type === 'bench:diagnostic' &&
    data.message === 'load failed after declaration'));
  const result = records.find(
    ({ type }) => type === 'bench:complete').data;
  assert.strictEqual(result.name, 'declared before load error');
  assert.strictEqual(result.error, undefined);
  assert.strictEqual(records.at(-1).data.counts.completed, 1);
  assert.strictEqual(records.at(-1).data.success, false);
}

async function testStructuredError() {
  const records = await runFile(
    fixtures.path('bench-runner/error.cjs')).toArray();
  const result = records.find(
    ({ type }) => type === 'bench:complete').data;
  assert.strictEqual(result.error.code, 'ERR_BENCHMARK_FIXTURE');
  assert.deepStrictEqual(result.error.cause, { value: 42n });
  assert.match(result.error.stack, /error\.cjs/);
  assert.strictEqual(records.at(-1).data.success, false);
}

async function testAbort() {
  const controller = new AbortController();
  const reason = new Error('cancel run file');
  const stream = runFile(
    fixtures.path('bench-runner/run-file-blocked.cjs'),
    { signal: controller.signal });
  stream.once('bench:start', common.mustCall(() => controller.abort(reason)));
  const records = await stream.toArray();
  const diagnostic = records.find(({ type, data }) =>
    type === 'bench:diagnostic' && data.error?.code === 'ABORT_ERR').data;
  assert.strictEqual(diagnostic.error.cause.message, reason.message);
  assert.strictEqual(records.some(
    ({ type }) => type === 'bench:complete'), false);
  assert.strictEqual(records.at(-1).type, 'bench:summary');
  assert.strictEqual(records.at(-1).data.success, false);
  assert.deepStrictEqual(records.at(-1).data.counts, {
    __proto__: null,
    completed: 0,
    failed: 0,
    skipped: 0,
    total: 1,
  });
}

async function testPartialAbort() {
  const controller = new AbortController();
  const stream = runFile(
    fixtures.path('bench-runner/run-file-partial.cjs'),
    { signal: controller.signal });
  stream.on('bench:start', ({ name }) => {
    if (name === 'aborted run file') controller.abort();
  });
  const records = await stream.toArray();
  const summary = records.at(-1).data;
  assert.strictEqual(records.filter(
    ({ type }) => type === 'bench:complete').length, 1);
  assert.deepStrictEqual(summary.counts, {
    __proto__: null,
    completed: 1,
    failed: 0,
    skipped: 0,
    total: 2,
  });
  assert.strictEqual(summary.success, false);
}

async function testPostSummaryAbort() {
  const controller = new AbortController();
  const stream = runFile(
    fixtures.path('bench-runner/run-file-lingering.cjs'),
    { signal: controller.signal });
  stream.on('bench:diagnostic', common.mustCall(({ message, stream }) => {
    if (stream === 'stdout' && /child disconnected/.test(message)) {
      controller.abort(new Error('stop lingering child'));
    }
  }, 2));
  const records = await stream.toArray();
  assert(records.some(({ type, data }) =>
    type === 'bench:diagnostic' && data.error?.code === 'ABORT_ERR'));
  assert.strictEqual(records.at(-1).type, 'bench:summary');
  assert.strictEqual(records.at(-1).data.counts.completed, 1);
  assert.strictEqual(records.at(-1).data.success, false);
}

async function testExitCode() {
  const records = await runFile(
    fixtures.path('bench-runner/exit-code.cjs')).toArray();
  assert(records.some(({ type, data }) =>
    type === 'bench:diagnostic' &&
    /set exit code/.test(data.message)));
  assert.strictEqual(records.at(-1).data.success, false);
}

async function testParentExitCode() {
  process.exitCode = 42;
  try {
    const records = await runFile(fixture).toArray();
    assert.strictEqual(records.at(-1).data.success, true);
  } finally {
    process.exitCode = undefined;
  }
}

async function testDefaultExecArgvSnapshot() {
  const original = Array.from(process.execArgv);
  try {
    process.execArgv.push('--bench', '--eval', 'throw new Error()');
    const stream = runFile(fixture);
    process.execArgv.push('--require=/does/not/exist.cjs');
    const records = await stream.toArray();
    assert.strictEqual(records.at(-1).data.success, true);
  } finally {
    process.execArgv.length = 0;
    process.execArgv.push(...original);
  }
}

async function testExecPathSnapshot() {
  const stream = runFile(fixture);
  const execPath = process.execPath;
  process.execPath = '/does/not/exist';
  try {
    const records = await stream.toArray();
    assert.strictEqual(records.at(-1).data.success, true);
  } finally {
    process.execPath = execPath;
  }
}

async function testSpawnFailure() {
  const execPath = process.execPath;
  let stream;
  try {
    process.execPath = fixtures.path('does-not-exist-node');
    stream = runFile(fixture);
  } finally {
    process.execPath = execPath;
  }
  const records = await stream.toArray();
  const diagnostic = records.find(
    ({ type }) => type === 'bench:diagnostic').data;
  assert.strictEqual(diagnostic.error.code, 'ENOENT');
  assert.strictEqual(records.at(-1).data.success, false);
}

function testEvalParent() {
  const script = `
    require('node:bench').runFile(${JSON.stringify(fixture)})
      .on('bench:summary', (summary) => console.log(summary.success));
  `;
  const result = spawnSync(process.execPath, [
    '--no-warnings',
    '--experimental-bench',
    '-e',
    script,
  ], { encoding: 'utf8' });
  assert.strictEqual(result.status, 0, result.stderr);
  assert.strictEqual(result.stdout, 'true\n');
}

function testPermissions() {
  const fsReadScript = `
    const assert = require('assert');
    const { runFile } = require('node:bench');
    const { pathToFileURL } = require('url');
    const target = ${JSON.stringify(fixture)};
    assert.strictEqual(process.permission.has('child'), true);
    assert.strictEqual(process.permission.has('fs.read', target), false);
    Promise.all([
      target,
      Buffer.from(target),
      pathToFileURL(target),
    ].map(async (input) => {
      const records = await runFile(input).toArray();
      const diagnostic = records.find(({ type, data }) =>
        type === 'bench:diagnostic' &&
        data.error?.code === 'ERR_ACCESS_DENIED');
      assert.strictEqual(diagnostic.data.error.permission, 'FileSystemRead');
      assert.strictEqual(diagnostic.data.error.resource, target);
      assert.strictEqual(records.at(-1).type, 'bench:summary');
      assert.strictEqual(records.at(-1).data.success, false);
    })).catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
  `;
  const result = spawnSync(process.execPath, [
    '--no-warnings',
    '--experimental-bench',
    '--permission',
    '--allow-child-process',
    '-e',
    fsReadScript,
  ], { encoding: 'utf8' });
  assert.strictEqual(result.status, 0, result.stderr);

  const childProcessScript = `
    const assert = require('assert');
    const { runFile } = require('node:bench');
    const target = ${JSON.stringify(fixture)};
    assert.strictEqual(process.permission.has('fs.read', target), true);
    assert.strictEqual(process.permission.has('child'), false);
    runFile(target).toArray().then((records) => {
      const diagnostic = records.find(({ type, data }) =>
        type === 'bench:diagnostic' &&
        data.error?.code === 'ERR_ACCESS_DENIED');
      assert.strictEqual(diagnostic.data.error.permission, 'ChildProcess');
      assert.strictEqual(diagnostic.data.error.resource, process.execPath);
      assert.strictEqual(records.at(-1).type, 'bench:summary');
      assert.strictEqual(records.at(-1).data.success, false);
    }).catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
  `;
  const childProcessResult = spawnSync(process.execPath, [
    '--no-warnings',
    '--experimental-bench',
    '--permission',
    `--allow-fs-read=${fixture}`,
    '-e',
    childProcessScript,
  ], { encoding: 'utf8' });
  assert.strictEqual(
    childProcessResult.status,
    0,
    childProcessResult.stderr,
  );
}

async function testPreAborted() {
  const records = await runFile(fixture, {
    signal: AbortSignal.abort(new Error('already cancelled')),
  }).toArray();
  assert.strictEqual(records.some(({ type }) => type === 'bench:start'), false);
  assert.strictEqual(records.find(({ type }) =>
    type === 'bench:diagnostic').data.error.code, 'ABORT_ERR');
  assert.strictEqual(records.at(-1).type, 'bench:summary');
  assert.strictEqual(records.at(-1).data.success, false);
}

async function testDestroy() {
  const stream = runFile(
    fixtures.path('bench-runner/run-file-blocked.cjs'));
  stream.once('bench:start', common.mustCall(() => stream.destroy()));
  await once(stream, 'close');
  assert.strictEqual(stream.destroyed, true);
}

(async () => {
  await testRunFile();
  await testConcurrentCalls();
  await testLoadFailure();
  await testEvaluationFailure();
  await testStructuredError();
  await testAbort();
  await testPartialAbort();
  await testPostSummaryAbort();
  await testExitCode();
  await testParentExitCode();
  await testDefaultExecArgvSnapshot();
  await testExecPathSnapshot();
  await testSpawnFailure();
  await testPreAborted();
  await testDestroy();
  testEvalParent();
  testPermissions();
})().then(common.mustCall());