tor-browser

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

devtools-node-test-runner.js (9622B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 /* global __dirname, process */
      6 
      7 "use strict";
      8 
      9 /**
     10 * This is a test runner dedicated to run DevTools node tests continuous integration
     11 * platforms. It will parse the logs to output errors compliant with treeherder tooling.
     12 *
     13 * See taskcluster/kinds/source-test/node.yml for the definition of the task running those
     14 * tests on try.
     15 */
     16 
     17 const { execFileSync } = require("child_process");
     18 const { writeFileSync } = require("fs");
     19 const { chdir } = require("process");
     20 const path = require("path");
     21 const os = require("os");
     22 
     23 const REPOSITORY_ROOT = __dirname.replace("devtools/client/bin", "");
     24 
     25 // All Windows platforms report "win32", even for 64bit editions.
     26 const isWin = os.platform() === "win32";
     27 
     28 // On Windows, the ".cmd" suffix is mandatory to invoke yarn ; or executables in
     29 // general.
     30 const YARN_PROCESS = isWin ? "yarn.cmd" : "yarn";
     31 
     32 // Supported node test suites for DevTools
     33 const TEST_TYPES = {
     34  JEST: "jest",
     35  TYPESCRIPT: "typescript",
     36 };
     37 
     38 const SUITES = {
     39  aboutdebugging: {
     40    path: "../aboutdebugging/test/node",
     41    type: TEST_TYPES.JEST,
     42  },
     43  accessibility: {
     44    path: "../accessibility/test/node",
     45    type: TEST_TYPES.JEST,
     46  },
     47  application: {
     48    path: "../application/test/node",
     49    type: TEST_TYPES.JEST,
     50  },
     51  compatibility: {
     52    path: "../inspector/compatibility/test/node",
     53    type: TEST_TYPES.JEST,
     54  },
     55  debugger: {
     56    path: "../debugger",
     57    type: TEST_TYPES.JEST,
     58  },
     59  framework: {
     60    path: "../framework/test/node",
     61    type: TEST_TYPES.JEST,
     62  },
     63  netmonitor: {
     64    path: "../netmonitor/test/node",
     65    type: TEST_TYPES.JEST,
     66  },
     67  performance: {
     68    path: "../performance-new",
     69    type: TEST_TYPES.TYPESCRIPT,
     70  },
     71  shared_components: {
     72    path: "../shared/components/test/node",
     73    type: TEST_TYPES.JEST,
     74  },
     75  webconsole: {
     76    path: "../webconsole/test/node",
     77    type: TEST_TYPES.JEST,
     78    dependencies: ["../debugger"],
     79  },
     80 };
     81 
     82 function execOut(...args) {
     83  let out;
     84  let err;
     85  try {
     86    out = execFileSync(...args);
     87  } catch (e) {
     88    out = e.stdout;
     89    err = e.stderr;
     90  }
     91  return { out: out.toString(), err: err && err.toString() };
     92 }
     93 
     94 function getErrors(suite, out, err, testPath) {
     95  switch (SUITES[suite].type) {
     96    case TEST_TYPES.JEST:
     97      return getJestErrors(out, err);
     98    case TEST_TYPES.TYPESCRIPT:
     99      return getTypescriptErrors(out, err, testPath);
    100    default:
    101      throw new Error("Unsupported suite type: " + SUITES[suite].type);
    102  }
    103 }
    104 
    105 const JEST_ERROR_SUMMARY_REGEX = /\sā—\s/;
    106 
    107 function getJestErrors(out) {
    108  // The string out has extra content before the JSON object starts.
    109  const jestJsonOut = out.substring(out.indexOf("{"), out.lastIndexOf("}") + 1);
    110  const results = JSON.parse(jestJsonOut);
    111 
    112  /**
    113   * We don't have individual information, but a multiple line string in testResult.message,
    114   * which looks like
    115   *
    116   *  ā— Simple function
    117   *
    118   *        expect(received).toEqual(expected) // deep equality
    119   *
    120   *        Expected: false
    121   *        Received: true
    122   *
    123   *          391 |       url: "test.js",
    124   *          392 |     });
    125   *        > 393 |     expect(true).toEqual(false);
    126   *              |                  ^
    127   *          394 |     expect(actual.code).toMatchSnapshot();
    128   *          395 |
    129   *          396 |     const smc = await new SourceMapConsumer(actual.map.toJSON());
    130   *
    131   *          at Object.<anonymous> (src/workers/pretty-print/tests/prettyFast.spec.js:393:18)
    132   *          at asyncGeneratorStep (src/workers/pretty-print/tests/prettyFast.spec.js:7:103)
    133   *          at _next (src/workers/pretty-print/tests/prettyFast.spec.js:9:194)
    134   *          at src/workers/pretty-print/tests/prettyFast.spec.js:9:364
    135   *          at Object.<anonymous> (src/workers/pretty-print/tests/prettyFast.spec.js:9:97)
    136   *
    137   */
    138 
    139  const errors = [];
    140  for (const testResult of results.testResults) {
    141    if (testResult.status != "failed") {
    142      continue;
    143    }
    144    let currentError;
    145    let errorLine;
    146 
    147    const lines = testResult.message.split("\n");
    148    lines.forEach((line, i) => {
    149      if (line.match(JEST_ERROR_SUMMARY_REGEX) || i == lines.length - 1) {
    150        // This is the name of the test, if we were gathering information from a previous
    151        // error, we add it to the errors
    152        if (currentError) {
    153          errors.push({
    154            // The file should be relative from the repository
    155            file: testResult.name.replace(REPOSITORY_ROOT, ""),
    156            line: errorLine,
    157            // we don't have information for the column
    158            column: 0,
    159            message: currentError.trim(),
    160          });
    161        }
    162 
    163        // Handle the new error
    164        currentError = line;
    165      } else {
    166        // We put any line that is not a test name in the error message as it may be
    167        // valuable for the user.
    168        currentError += "\n" + line;
    169 
    170        // The actual line of the error is marked with " > XXX |"
    171        const res = line.match(/> (?<line>\d+) \|/);
    172        if (res) {
    173          errorLine = parseInt(res.groups.line, 10);
    174        }
    175      }
    176    });
    177  }
    178 
    179  return errors;
    180 }
    181 
    182 function getTypescriptErrors(out, err, testPath) {
    183  console.log(out);
    184  // Typescript error lines look like:
    185  //   popup/panel.jsm.js(103,7): error TS2531: Object is possibly 'null'.
    186  // Which means:
    187  //   {file_path}({line},{col}): error TS{error_code}: {message}
    188  const tsErrorRegex =
    189    /(?<file>(\w|\/|\.)+)\((?<line>\d+),(?<column>\d+)\): (?<message>error TS\d+\:.*)/;
    190  const errors = [];
    191  for (const line of out.split("\n")) {
    192    const res = line.match(tsErrorRegex);
    193    if (!res) {
    194      continue;
    195    }
    196    // TypeScript gives us the path from the directory the command is executed in, so we
    197    // need to prepend the directory path.
    198    const fileAbsPath = testPath + res.groups.file;
    199    errors.push({
    200      // The file should be relative from the repository.
    201      file: fileAbsPath.replace(REPOSITORY_ROOT, ""),
    202      line: parseInt(res.groups.line, 10),
    203      column: parseInt(res.groups.column, 10),
    204      message: res.groups.message.trim(),
    205    });
    206  }
    207  return errors;
    208 }
    209 
    210 function runTests() {
    211  console.log("[devtools-node-test] Extract suite argument");
    212  const suiteArg = process.argv.find(arg => arg.includes("suite="));
    213  const suite = suiteArg.split("=")[1];
    214  if (suite !== "all" && !SUITES[suite]) {
    215    throw new Error("Invalid suite argument to devtools-node-test: " + suite);
    216  }
    217 
    218  console.log("[devtools-node-test] Check `yarn` is available");
    219  try {
    220    // This will throw if yarn is unavailable
    221    execFileSync(YARN_PROCESS, ["--version"]);
    222  } catch (e) {
    223    console.log(
    224      "[devtools-node-test] ERROR: `yarn` is not installed. " +
    225        "See https://yarnpkg.com/docs/install/ "
    226    );
    227    return false;
    228  }
    229 
    230  const artifactArg = process.argv.find(arg => arg.includes("artifact="));
    231  const artifactFilePath = artifactArg && artifactArg.split("=")[1];
    232  const artifactErrors = {};
    233 
    234  const failedSuites = [];
    235  const suites = suite == "all" ? SUITES : { [suite]: SUITES[suite] };
    236  for (const [suiteName, suiteData] of Object.entries(suites)) {
    237    console.log("[devtools-node-test] Running suite: " + suiteName);
    238 
    239    if (suiteData.dependencies) {
    240      console.log("[devtools-node-test] Running `yarn` for dependencies");
    241      for (const dep of suiteData.dependencies) {
    242        const depPath = path.join(__dirname, dep);
    243        chdir(depPath);
    244 
    245        console.log("[devtools-node-test] Run `yarn` in " + depPath);
    246        execOut(YARN_PROCESS);
    247      }
    248    }
    249 
    250    const testPath = path.join(__dirname, suiteData.path);
    251    chdir(testPath);
    252 
    253    console.log("[devtools-node-test] Run `yarn` in test folder");
    254    execOut(YARN_PROCESS);
    255 
    256    console.log(`TEST START | ${suiteData.type} | ${suiteName}`);
    257 
    258    console.log("[devtools-node-test] Run `yarn test` in test folder");
    259    const { out, err } = execOut(YARN_PROCESS, ["test-ci"]);
    260 
    261    if (err) {
    262      console.log("[devtools-node-test] Error log");
    263      console.log(err);
    264    }
    265 
    266    console.log("[devtools-node-test] Parse errors from the test logs");
    267    const errors = getErrors(suiteName, out, err, testPath) || [];
    268    if (errors.length) {
    269      failedSuites.push(suiteName);
    270    }
    271    for (const error of errors) {
    272      if (!artifactErrors[error.file]) {
    273        artifactErrors[error.file] = [];
    274      }
    275      artifactErrors[error.file].push({
    276        path: error.file,
    277        line: error.line,
    278        column: error.column,
    279        level: "error",
    280        message: error.message,
    281        analyzer: suiteName,
    282      });
    283 
    284      console.log(
    285        `TEST-UNEXPECTED-FAIL | ${suiteData.type} | ${suiteName} | ${error.file}:${error.line}: ${error.message}`
    286      );
    287    }
    288  }
    289 
    290  if (artifactFilePath) {
    291    console.log(`[devtools-node-test] Writing artifact to ${artifactFilePath}`);
    292    writeFileSync(artifactFilePath, JSON.stringify(artifactErrors, null, 2));
    293  }
    294 
    295  const success = failedSuites.length === 0;
    296  if (success) {
    297    console.log(
    298      `[devtools-node-test] Test suites [${Object.keys(suites).join(
    299        ", "
    300      )}] succeeded`
    301    );
    302  } else {
    303    console.log(
    304      `[devtools-node-test] Test suites [${failedSuites.join(", ")}] failed`
    305    );
    306    console.log(
    307      "TEST-UNEXPECTED-FAIL | mach devtools-node-test failed. Documentation " +
    308        "at https://firefox-source-docs.mozilla.org/devtools/tests/node-tests.html"
    309    );
    310  }
    311  return success;
    312 }
    313 
    314 process.exitCode = runTests() ? 0 : 1;