@ 261106.2
current
'use strict';

const path = require('path');
const { pathToFileURL } = require('url');
const {
  runInNewContext,
  runInThisContext,
  constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER },
} = require('vm');
const { setFlagsFromString } = require('v8');
const { inspect } = require('util');
const honorAllowedRejections = require('./rejections');
const {
  isMainThread,
  parentPort,
  workerData: threadData,
} = require('worker_threads');

// The runner starts this either on a worker thread, where the spec to run
// arrives as workerData, or in a child process, where it arrives over IPC.
const send = isMainThread ?
  (message) => process.send(message) :
  (message) => parentPort.postMessage(message);

if (isMainThread) {
  // A worker thread surfaces uncaught errors through its 'error' event; a
  // process has to report them itself so that the runner names the failure
  // exactly the same way.
  process.on('uncaughtException', (err) => {
    process.send({
      type: 'uncaught',
      error: { name: `${err}`, message: err.message, stack: inspect(err) },
    }, () => process.exit(1));
  });
  process.once('message', run);
} else {
  run(threadData);
}

function run(workerData) {
  const { ResourceLoader } = require(workerData.wptRunner);
  const resource = new ResourceLoader(workerData.wptPath);

  // Tests create workers with URLs the WPT server would have served them
  // from; map them into the fixtures directory.
  const RealWorker = globalThis.Worker;
  globalThis.Worker = class Worker extends RealWorker {
    constructor(url, options) {
      super(resource.mapServerURL(workerData.testRelativePath, url), options);
    }
  };

  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, {
      importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
    });
  }

  runInThisContext(workerData.harness.code, {
    filename: workerData.harness.filename,
    importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
  });
  honorAllowedRejections();

  // If there are skip patterns, wrap test functions to prevent execution of
  // matching tests. This must happen after testharness.js is loaded but before
  // the test scripts run.
  if (workerData.skippedTests?.length) {
    const isSkipped = (name) => {
      for (const matcher of workerData.skippedTests) {
        if (typeof matcher === 'string') {
          if (name === matcher) return true;
        } else if (matcher.test(name)) {
          return true;
        }
      }
      return false;
    };
    for (const fn of ['test', 'async_test', 'promise_test']) {
      const original = globalThis[fn];
      globalThis[fn] = function(func, name, ...rest) {
        if (typeof name === 'string' && isSkipped(name)) {
          send({ type: 'skip', name });
          return;
        }
        return original.call(this, func, name, ...rest);
      };
    }
  }

  // eslint-disable-next-line no-undef
  add_result_callback((result) => {
    send({
      type: 'result',
      result: {
        status: result.status,
        name: result.name,
        message: result.message,
        stack: result.stack,
      },
    });
  });

  // Keep the event loop alive
  const timeout = setTimeout(() => {
    send({
      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);
    send({
      type: 'completion',
      status,
    });
  });

  for (const scriptToRun of workerData.scriptsToRun) {
    runInThisContext(scriptToRun.code, {
      filename: scriptToRun.filename,
      importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
    });
  }

  if (workerData.webWorker) {
    const worker = new RealWorker(
      pathToFileURL(path.join(__dirname, 'webworker.js')));
    worker.postMessage({
      wptRunner: workerData.wptRunner,
      wptPath: workerData.wptPath,
      testRelativePath: workerData.testRelativePath,
      ...workerData.webWorker,
    });

    let completed = false;
    worker.addEventListener('message', (event) => {
      if (event.data?.type === 'complete') {
        completed = true;
      }
      // Skipped subtests never register with the testharness inside the
      // worker; the runner is notified about them directly.
      if (event.data?.type === 'skip') {
        send({ type: 'skip', name: event.data.name });
      }
    });
    worker.addEventListener('error', (event) => {
      if (completed) {
        return;
      }
      clearTimeout(timeout);
      send({
        type: 'completion',
        status: {
          status: 1,
          message: event.message,
          stack: event.error?.stack,
        },
      });
    });

    // eslint-disable-next-line no-undef
    fetch_tests_from_worker(worker);
  }
}