async function runMocha(tests, mochaArgs, invocationNumber) {
  const failedTestsFile = `${failedTestsDir}/output-${invocationNumber}.json`;
  const notRunnableTestsFile = `${failedTestsDir}/output-${invocationNumber}-not-runnable.json`;
  process.env.LH_FAILED_TESTS_FILE = failedTestsFile;

  const rootHooksPath = mochaArgs.require || '../test-env/mocha-setup.js';
  const {rootHooks} = await import(rootHooksPath);

  const mocha = new Mocha({
    rootHooks,
    timeout: 20_000,
    bail: mochaArgs.bail,
    grep: mochaArgs.grep,
    forbidOnly: mochaArgs.forbidOnly,
    // TODO: not working
    // parallel: parsableTests.length > 1 && mochaArgs.parallel,
    parallel: false,
    retries: mochaArgs.retries,
  });

  // Load a single test module at a time, so we know which ones fail to even import.
  const notRunnableTests = [];
  const parsableTests = [];
  for (const test of tests) {
    try {
      mocha.files = [test];
      await mocha.loadFilesAsync();
      parsableTests.push(test);
    } catch (e) {
      notRunnableTests.push({
        file: path.relative(LH_ROOT, test),
        title: '',
        error: `Failed to parse module: ${e}`,
      });
    }
  }
  mocha.files = [];

  let failingTestCount = 0;
  if (parsableTests.length) {
    try {
      failingTestCount = await new Promise(resolve => mocha.run(resolve));
    } catch (err) {
      // Something awful happened, and maybe no tests ran at all.
      const errorMessage = `Mocha failed to run: ${err}`;
      notRunnableTests.push(...parsableTests.map((test, i) => {
        return {
          file: path.relative(LH_ROOT, test),
          title: '',
          error: i === 0 ? errorMessage : '(see above failure)',
        };
      }));
    }
  }

  if (notRunnableTests.length) {
    fs.writeFileSync(notRunnableTestsFile, JSON.stringify(notRunnableTests, null, 2));
  }

  return failingTestCount + notRunnableTests.length;
}
@param {string[]} tests
@param {OurMochaArgs} mochaArgs
@param {number} invocationNumber