async function run(filepath, serializedOptions = '{}') {
  if (!filepath) {
    throw createInvalidArgumentTypeError(
      'Expected a non-empty "filepath" argument',
      'file',
      'string'
    );
  }

  debug('run(): running test file %s', filepath);

  if (typeof serializedOptions !== 'string') {
    throw createInvalidArgumentTypeError(
      'run() expects second parameter to be a string which was serialized by the `serialize-javascript` module',
      'serializedOptions',
      'string'
    );
  }
  let argv;
  try {
    // eslint-disable-next-line no-eval
    argv = eval('(' + serializedOptions + ')');
  } catch (err) {
    throw createInvalidArgumentValueError(
      'run() was unable to deserialize the options',
      'serializedOptions',
      serializedOptions
    );
  }

  const opts = Object.assign({ui: 'bdd'}, argv, {
    // if this was true, it would cause infinite recursion.
    parallel: false,
    // this doesn't work in parallel mode
    forbidOnly: true,
    // it's useful for a Mocha instance to know if it's running in a worker process.
    isWorker: true
  });

  await bootstrap(opts);

  opts.rootHooks = rootHooks;

  const mocha = new Mocha(opts).addFile(filepath);

  try {
    await mocha.loadFilesAsync();
  } catch (err) {
    debug('run(): could not load file %s: %s', filepath, err);
    throw err;
  }

  return new Promise((resolve, reject) => {
    let debugInterval;
    /* istanbul ignore next */
    if (isDebugEnabled) {
      debugInterval = setInterval(() => {
        debug('run(): still running %s...', filepath);
      }, 5000).unref();
    }
    mocha.run(result => {
      // Runner adds these; if we don't remove them, we'll get a leak.
      process.removeAllListeners('uncaughtException');
      process.removeAllListeners('unhandledRejection');

      try {
        const serialized = serialize(result);
        debug(
          'run(): completed run with %d test failures; returning to main process',
          typeof result.failures === 'number' ? result.failures : 0
        );
        resolve(serialized);
      } catch (err) {
        // TODO: figure out exactly what the sad path looks like here.
        // rejection should only happen if an error is "unrecoverable"
        debug('run(): serialization failed; rejecting: %O', err);
        reject(err);
      } finally {
        clearInterval(debugInterval);
      }
    });
  });
}
Runs a single test file in a worker thread.
@param {string} filepath - Filepath of test file
@param {string} [serializedOptions] - **Serialized** options. This string will be eval'd!
@see https://npm.im/serialize-javascript
@returns {Promise<{failures: number, events: BufferedEvent[]}>} - Test
failure count and list of events.