tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

mocha-runner.ts (9095B)


      1 #! /usr/bin/env -S node
      2 
      3 /**
      4 * @license
      5 * Copyright 2022 Google Inc.
      6 * SPDX-License-Identifier: Apache-2.0
      7 */
      8 
      9 import {spawn} from 'node:child_process';
     10 import {randomUUID} from 'node:crypto';
     11 import fs from 'node:fs';
     12 import os from 'node:os';
     13 import path from 'node:path';
     14 
     15 import {globSync} from 'glob';
     16 import yargs from 'yargs';
     17 import {hideBin} from 'yargs/helpers';
     18 
     19 import {
     20  zPlatform,
     21  zTestSuiteFile,
     22  type MochaResults,
     23  type TestExpectation,
     24  type TestSuite,
     25  type TestSuiteFile,
     26 } from './types.js';
     27 import {
     28  extendProcessEnv,
     29  filterByParameters,
     30  filterByPlatform,
     31  getExpectationUpdates,
     32  getSuggestionsForAction,
     33  printSuggestions,
     34  readJSON,
     35  writeJSON,
     36  type RecommendedExpectation,
     37 } from './utils.js';
     38 
     39 const {
     40  _: mochaArgs,
     41  testSuite: testSuiteId,
     42  saveStatsTo,
     43  cdpTests: includeCdpTests,
     44  suggestions: provideSuggestions,
     45  coverage: useCoverage,
     46  minTests,
     47  shard,
     48  reporter,
     49  printMemory,
     50  ignoreUnexpectedlyPassing,
     51 } = yargs(hideBin(process.argv))
     52  .parserConfiguration({'unknown-options-as-args': true})
     53  .scriptName('@puppeteer/mocha-runner')
     54  .option('coverage', {
     55    boolean: true,
     56    default: false,
     57  })
     58  .option('suggestions', {
     59    boolean: true,
     60    default: true,
     61  })
     62  .option('cdp-tests', {
     63    boolean: true,
     64    default: true,
     65  })
     66  .option('save-stats-to', {
     67    string: true,
     68    requiresArg: true,
     69  })
     70  .option('min-tests', {
     71    number: true,
     72    default: 0,
     73    requiresArg: true,
     74  })
     75  .option('test-suite', {
     76    string: true,
     77    requiresArg: true,
     78  })
     79  .option('shard', {
     80    string: true,
     81    requiresArg: true,
     82  })
     83  .option('reporter', {
     84    string: true,
     85    requiresArg: true,
     86  })
     87  .option('print-memory', {
     88    boolean: true,
     89    default: false,
     90  })
     91  .option('ignore-unexpectedly-passing', {
     92    boolean: true,
     93    default: false,
     94  })
     95  .parseSync();
     96 
     97 function getApplicableTestSuites(parsedSuitesFile: TestSuiteFile): TestSuite[] {
     98  let applicableSuites: TestSuite[] = [];
     99 
    100  if (!testSuiteId) {
    101    applicableSuites = parsedSuitesFile.testSuites;
    102  } else {
    103    const testSuite = parsedSuitesFile.testSuites.find(suite => {
    104      return suite.id === testSuiteId;
    105    });
    106 
    107    if (!testSuite) {
    108      console.error(`Test suite ${testSuiteId} is not defined`);
    109      process.exit(1);
    110    }
    111 
    112    applicableSuites = [testSuite];
    113  }
    114 
    115  return applicableSuites;
    116 }
    117 
    118 async function main() {
    119  let statsPath = saveStatsTo;
    120  if (statsPath && statsPath.includes('INSERTID')) {
    121    statsPath = statsPath.replace(/INSERTID/gi, randomUUID());
    122  }
    123 
    124  const platform = zPlatform.parse(os.platform());
    125 
    126  const expectations = readJSON(
    127    path.join(process.cwd(), 'test', 'TestExpectations.json'),
    128  ) as TestExpectation[];
    129 
    130  const parsedSuitesFile = zTestSuiteFile.parse(
    131    readJSON(path.join(process.cwd(), 'test', 'TestSuites.json')),
    132  );
    133 
    134  const applicableSuites = getApplicableTestSuites(parsedSuitesFile);
    135 
    136  console.log('Planning to run the following test suites', applicableSuites);
    137  if (statsPath) {
    138    console.log('Test stats will be saved to', statsPath);
    139  }
    140 
    141  let fail = false;
    142  const recommendations: RecommendedExpectation[] = [];
    143  try {
    144    for (const suite of applicableSuites) {
    145      const parameters = suite.parameters;
    146 
    147      const applicableExpectations = filterByParameters(
    148        filterByPlatform(expectations, platform),
    149        parameters,
    150      ).reverse();
    151 
    152      // Add more logging when the GitHub Action Debugging option is set
    153      // https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
    154      const githubActionDebugging = process.env['RUNNER_DEBUG']
    155        ? {
    156            DEBUG: 'puppeteer:*',
    157            EXTRA_LAUNCH_OPTIONS: JSON.stringify({
    158              dumpio: true,
    159              extraPrefsFirefox: {
    160                'remote.log.level': 'Trace',
    161              },
    162            }),
    163          }
    164        : {};
    165 
    166      const env = extendProcessEnv([
    167        ...parameters.map(param => {
    168          return parsedSuitesFile.parameterDefinitions[param];
    169        }),
    170        {
    171          PUPPETEER_SKIPPED_TEST_CONFIG: JSON.stringify(
    172            applicableExpectations.map(ex => {
    173              return {
    174                testIdPattern: ex.testIdPattern,
    175                skip: ex.expectations.includes('SKIP'),
    176              };
    177            }),
    178          ),
    179        },
    180        githubActionDebugging,
    181      ]);
    182 
    183      const tmpDir = fs.mkdtempSync(
    184        path.join(os.tmpdir(), 'puppeteer-test-runner-'),
    185      );
    186      const tmpFilename = statsPath
    187        ? statsPath
    188        : path.join(tmpDir, 'output.json');
    189      console.log('Running', JSON.stringify(parameters), tmpFilename);
    190      const args = [
    191        '-u',
    192        path.join(__dirname, 'interface.js'),
    193        '-R',
    194        !reporter ? path.join(__dirname, 'reporter.js') : reporter,
    195        '-O',
    196        `output=${tmpFilename}`,
    197        '-n',
    198        'trace-warnings',
    199      ];
    200 
    201      if (printMemory) {
    202        args.push('-n', 'expose-gc');
    203      }
    204 
    205      const specPattern = 'test/build/**/*.spec.js';
    206      const specs = globSync(specPattern, {
    207        ignore: !includeCdpTests ? 'test/build/cdp/**/*.spec.js' : undefined,
    208      }).sort((a, b) => {
    209        return a.localeCompare(b);
    210      });
    211      if (shard) {
    212        // Shard ID is 1-based.
    213        const [shardId, shards] = shard.split('-').map(s => {
    214          return Number(s);
    215        }) as [number, number];
    216        const argsLength = args.length;
    217        for (let i = 0; i < specs.length; i++) {
    218          if (i % shards === shardId - 1) {
    219            args.push(specs[i]!);
    220          }
    221        }
    222        if (argsLength === args.length) {
    223          throw new Error('Shard did not result in any test files');
    224        }
    225        console.log(
    226          `Running shard ${shardId}-${shards}. Picked ${
    227            args.length - argsLength
    228          } files out of ${specs.length}.`,
    229        );
    230      } else {
    231        args.push(...specs);
    232      }
    233      const handle = spawn(
    234        'npx',
    235        [
    236          ...(useCoverage
    237            ? ['c8', '--check-coverage', '--lines', '90', 'npx']
    238            : []),
    239          'mocha',
    240          ...mochaArgs.map(String),
    241          ...args,
    242        ],
    243        {
    244          shell: true,
    245          cwd: process.cwd(),
    246          stdio: 'inherit',
    247          env,
    248        },
    249      );
    250      await new Promise<void>((resolve, reject) => {
    251        handle.on('error', err => {
    252          reject(err);
    253        });
    254        handle.on('close', () => {
    255          resolve();
    256        });
    257      });
    258      try {
    259        const results = (() => {
    260          try {
    261            return readJSON(tmpFilename) as MochaResults;
    262          } catch (cause) {
    263            throw new Error('Test results are not found', {
    264              cause,
    265            });
    266          }
    267        })();
    268        console.log('Finished', JSON.stringify(parameters));
    269        const updates = getExpectationUpdates(
    270          results,
    271          applicableExpectations,
    272          {
    273            platforms: [os.platform()],
    274            parameters,
    275          },
    276          ignoreUnexpectedlyPassing,
    277        );
    278        const totalTests = results.stats.tests;
    279        results.parameters = parameters;
    280        results.platform = platform;
    281        results.date = new Date().toISOString();
    282        if (updates.length > 0) {
    283          fail = true;
    284          recommendations.push(...updates);
    285          results.updates = updates;
    286          writeJSON(tmpFilename, results);
    287        } else {
    288          if (!shard && totalTests < minTests) {
    289            fail = true;
    290            console.log(
    291              `Test run matches expectations but the number of discovered tests is too low (expected: ${minTests}, actual: ${totalTests}).`,
    292            );
    293            writeJSON(tmpFilename, results);
    294            continue;
    295          }
    296          console.log('Test run matches expectations');
    297          writeJSON(tmpFilename, results);
    298          continue;
    299        }
    300      } catch (err) {
    301        fail = true;
    302        console.error(err);
    303      }
    304    }
    305  } catch (err) {
    306    fail = true;
    307    console.error(err);
    308  } finally {
    309    const added = getSuggestionsForAction(recommendations, 'add');
    310    const removed = getSuggestionsForAction(recommendations, 'remove');
    311    const updated = getSuggestionsForAction(recommendations, 'update');
    312    if (!!provideSuggestions) {
    313      printSuggestions(
    314        added,
    315        'Add the following to TestExpectations.json to ignore the error:',
    316        true,
    317      );
    318      printSuggestions(
    319        removed,
    320        'Remove the following from the TestExpectations.json to ignore the error:',
    321      );
    322      printSuggestions(
    323        updated,
    324        'Update the following expectations in the TestExpectations.json to ignore the error:',
    325        true,
    326      );
    327    }
    328    const unexpected = added.length + removed.length + updated.length;
    329    console.log(
    330      fail && Boolean(unexpected)
    331        ? `Run failed: ${unexpected} unexpected result(s).`
    332        : `Run succeeded.`,
    333    );
    334    process.exit(fail ? 1 : 0);
    335  }
    336 }
    337 
    338 main().catch(error => {
    339  console.error(error);
    340  process.exit(1);
    341 });