current commit
260828.2 · test: use common spawnSync helpers in more tests
'use strict';

// The WPT runner can run each spec on a worker thread or in a child process.
// Suites opt into either, and share the status files, so both backends have to
// report the same things in the same way.

const common = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
const { backends, WPTRunner } = require('../common/wpt');

const queueProbe = process.env.NODE_TEST_WPT_QUEUE_PROBE === '1';
const backendProbe = process.env.NODE_TEST_WPT_BACKEND_PROBE;

const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
const specPath = fixtures.path('wpt-backends-spec.js');
const execArgv = ['--experimental-web-worker'];

function payload(throws) {
  return {
    testRelativePath: 'backends.any.js',
    wptRunner: path.join(__dirname, '../common/wpt.js'),
    wptPath: 'compression',
    initScript: throws ? 'globalThis.WPT_BACKENDS_THROW = true;' : null,
    harness: {
      code: fs.readFileSync(harnessPath, 'utf8'),
      filename: harnessPath,
    },
    scriptsToRun: [{
      code: fs.readFileSync(specPath, 'utf8'),
      filename: specPath,
    }],
    needsGc: false,
    // A regular expression only survives the trip to a child process because
    // the runner forks with advanced serialization.
    skippedTests: ['skipped by name', /^skipped by pattern$/],
  };
}

// Collect what a backend reports for one spec, driving it the way the runner
// does so that the real spawn options are exercised.
async function collect(backend, throws) {
  const events = [];
  let done = false;

  const handle = backends[backend](execArgv, payload(throws), {
    message: (message) => {
      switch (message.type) {
        case 'result':
          events.push(`result ${message.result.status} ${message.result.name}`);
          break;
        case 'skip':
          events.push(`skip ${message.name}`);
          break;
        case 'completion':
          events.push(`completion ${message.status.status}`);
          done = true;
          handle.kill();
          break;
        default:
          assert.fail(`Unexpected message type: ${message.type}`);
      }
    },
    failure: (failure) => {
      if (done) {
        return false;
      }
      done = true;
      events.push(`uncaught ${failure.name} | ${failure.message}`);
      return true;
    },
  });

  const watchdog = setTimeout(
    common.mustNotCall(`The ${backend} WPT backend did not finish`),
    common.platformTimeout(10_000),
  );
  try {
    await handle.finished;
  } finally {
    clearTimeout(watchdog);
  }
  return events;
}

async function compare(throws) {
  const comparable = async (backend) => {
    const events = await collect(backend, throws);
    // A worker thread keeps delivering messages after it throws, so `events`
    // is copied here and only the error is compared; what the spec reports
    // before throwing is covered by the run that completes normally.
    const wanted = throws ? (event) => event.startsWith('uncaught ') : () => true;
    return events.filter(wanted).sort();
  };

  const thread = await comparable('thread');
  const child = await comparable('process');
  assert.notStrictEqual(thread.length, 0);
  assert.deepStrictEqual(thread, child);
  return thread;
}

// End to end: the runner's own reporting has to match too. The webidl spec
// reports an uncaught error whose name the status file matches on.
function runDriver(driver, spec, backend) {
  const { child: { stdout } } = spawnSyncAndExitWithoutError(
    process.execPath,
    [path.join(__dirname, '../wpt', driver), spec],
    { env: { ...process.env, WPT_BACKEND: backend }, encoding: 'utf8' },
  );
  // Specs run concurrently, so the lines arrive in an arbitrary order.
  return stdout.split('\n').filter((line) => line.startsWith('[')).sort();
}

function runQueueProbe() {
  backends.process = common.mustCall((execArgv, workerData, handlers) => {
    queueMicrotask(() => handlers.message({
      type: 'completion',
      status: { status: 0 },
    }));
    return {
      kill() {},
      // Model the gap between a spec completing and its process closing. The
      // close notification alone must not be responsible for keeping the
      // parent alive long enough to start the next queued spec.
      finished: new Promise((resolve) => {
        setTimeout(common.mustCall(resolve), 1).unref();
      }),
    };
  }, 2);

  process.argv[2] = 'compression-bad-chunks.any.js';
  const runner = new WPTRunner('compression', {
    backend: 'process',
    concurrency: 1,
  });
  // WPT drivers intentionally do not await this method.
  runner.runJsTests();
}

function checkQueuedSpecsKeepRunnerAlive() {
  const result = spawnSync(process.execPath, [__filename], {
    env: {
      ...process.env,
      NODE_TEST_WPT_QUEUE_PROBE: '1',
      WPT_BACKEND: 'process',
    },
    encoding: 'utf8',
    timeout: common.platformTimeout(10_000),
  });
  const { error, status, stdout, stderr } = result;
  assert.ifError(error);
  assert.strictEqual(status, 0, `Queued WPT probe failed:\n${stdout}${stderr}`);
}

function runDefaultBackendProbe() {
  const runner = new WPTRunner('compression');
  assert.strictEqual(runner.runSpec, backends[backendProbe]);
}

function checkBackendSelection() {
  const env = { ...process.env };
  delete env.WPT_BACKEND;
  delete env.WPT_INSPECT;

  for (const [name, expected, args, overrides] of [
    ['default', 'thread', [__filename]],
    ['environment override', 'process', [__filename], { WPT_BACKEND: 'process' }],
    ['inspect override', 'process', [__filename], {
      WPT_BACKEND: 'thread',
      WPT_INSPECT: '1',
    }],
  ]) {
    const result = spawnSync(process.execPath, args, {
      env: {
        ...env,
        ...overrides,
        NODE_TEST_WPT_BACKEND_PROBE: expected,
      },
      encoding: 'utf8',
      timeout: common.platformTimeout(10_000),
    });
    const { error, status, stdout, stderr } = result;
    assert.ifError(error);
    assert.strictEqual(
      status,
      0,
      `${name} WPT backend probe failed:\n${stdout}${stderr}`,
    );
  }
}

function checkInspectSelection() {
  const driver = path.join(__dirname, '../wpt/test-compression.js');
  const env = { ...process.env, WPT_INSPECT: '1' };
  delete env.WPT_BACKEND;

  const runFailure = common.mustCall((args, expected) => {
    const result = spawnSync(process.execPath, [driver, ...args], {
      env,
      encoding: 'utf8',
      timeout: common.platformTimeout(10_000),
    });
    const { error, status, stdout, stderr } = result;
    assert.ifError(error);
    assert.strictEqual(status, 1, `WPT inspect probe passed:\n${stdout}${stderr}`);
    assert.match(stderr, expected);
  }, 3);

  runFailure([], /WPT_INSPECT requires a WPT test path/);
  runFailure(
    ['compression-bad-chunks.any.js'],
    /WPT_INSPECT requires exactly one generated WPT test path; .* matched 2:\r?\ncompression\/compression-bad-chunks\.any\.html\r?\ncompression\/compression-bad-chunks\.any\.worker\.html/,
  );
  runFailure(
    ['compression/compression-bad-chunks.any.worker.html'],
    /WPT_INSPECT does not support worker tests: compression\/compression-bad-chunks\.any\.worker\.html/,
  );
}

async function main() {
  checkBackendSelection();
  checkInspectSelection();
  checkQueuedSpecsKeepRunnerAlive();

  const completed = await compare(false);
  assert.deepStrictEqual(completed, [
    'completion 0',
    'result 0 passes',
    'result 0 passes asynchronously',
    'result 1 fails',
    'skip skipped by name',
    'skip skipped by pattern',
  ]);

  const uncaught = await compare(true);
  assert.deepStrictEqual(uncaught, [
    'uncaught Error: deliberate uncaught error | deliberate uncaught error',
  ]);

  for (const [driver, spec] of [
    ['test-compression.js', 'compression-bad-chunks.any.js'],
    ['test-webidl.js', 'ecmascript-binding/global-object-implicit-this-value.any.js'],
  ]) {
    assert.deepStrictEqual(
      runDriver(driver, spec, 'thread'),
      runDriver(driver, spec, 'process'),
    );
  }

  const windowResults = runDriver(
    'test-events.js',
    'dom/events/Event-constructors.any.html',
    'thread',
  );
  const workerResults = runDriver(
    'test-events.js',
    'dom/events/Event-constructors.any.worker.html',
    'thread',
  ).map((line) => line.replace('.any.worker.html', '.any.html'));
  assert.deepStrictEqual(workerResults, windowResults);
}

if (backendProbe) {
  runDefaultBackendProbe();
} else if (queueProbe) {
  runQueueProbe();
} else {
  main().then(common.mustCall());
}