/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ /* global __dirname, process */ "use strict"; /** * This is a test runner dedicated to run DevTools node tests continuous integration * platforms. It will parse the logs to output errors compliant with treeherder tooling. * * See taskcluster/ci/source-test/node.yml for the definition of the task running those * tests on try. */ const { execFileSync } = require("child_process"); const { writeFileSync } = require("fs"); const { chdir } = require("process"); const path = require("path"); const os = require("os"); const REPOSITORY_ROOT = __dirname.replace("devtools/client/bin", ""); // All Windows platforms report "win32", even for 64bit editions. const isWin = os.platform() === "win32"; // On Windows, the ".cmd" suffix is mandatory to invoke yarn ; or executables in // general. const YARN_PROCESS = isWin ? "yarn.cmd" : "yarn"; // Supported node test suites for DevTools const TEST_TYPES = { JEST: "jest", TYPESCRIPT: "typescript", }; const SUITES = { aboutdebugging: { path: "../aboutdebugging/test/node", type: TEST_TYPES.JEST, }, accessibility: { path: "../accessibility/test/node", type: TEST_TYPES.JEST, }, application: { path: "../application/test/node", type: TEST_TYPES.JEST, }, compatibility: { path: "../inspector/compatibility/test/node", type: TEST_TYPES.JEST, }, debugger: { path: "../debugger", type: TEST_TYPES.JEST, }, framework: { path: "../framework/test/node", type: TEST_TYPES.JEST, }, netmonitor: { path: "../netmonitor/test/node", type: TEST_TYPES.JEST, }, performance: { path: "../performance-new", type: TEST_TYPES.TYPESCRIPT, }, shared_components: { path: "../shared/components/test/node", type: TEST_TYPES.JEST, }, webconsole: { path: "../webconsole/test/node", type: TEST_TYPES.JEST, dependencies: ["../debugger"], }, }; function execOut(...args) { let out; let err; try { out = execFileSync(...args); } catch (e) { out = e.stdout; err = e.stderr; } return { out: out.toString(), err: err && err.toString() }; } function getErrors(suite, out, err, testPath) { switch (SUITES[suite].type) { case TEST_TYPES.JEST: return getJestErrors(out, err); case TEST_TYPES.TYPESCRIPT: return getTypescriptErrors(out, err, testPath); default: throw new Error("Unsupported suite type: " + SUITES[suite].type); } } const JEST_ERROR_SUMMARY_REGEX = /\sā—\s/; function getJestErrors(out, err) { // The string out has extra content before the JSON object starts. const jestJsonOut = out.substring(out.indexOf("{"), out.lastIndexOf("}") + 1); const results = JSON.parse(jestJsonOut); /** * We don't have individual information, but a multiple line string in testResult.message, * which looks like * * ā— Simple function * * expect(received).toEqual(expected) // deep equality * * Expected: false * Received: true * * 391 | url: "test.js", * 392 | }); * > 393 | expect(true).toEqual(false); * | ^ * 394 | expect(actual.code).toMatchSnapshot(); * 395 | * 396 | const smc = await new SourceMapConsumer(actual.map.toJSON()); * * at Object. (src/workers/pretty-print/tests/prettyFast.spec.js:393:18) * at asyncGeneratorStep (src/workers/pretty-print/tests/prettyFast.spec.js:7:103) * at _next (src/workers/pretty-print/tests/prettyFast.spec.js:9:194) * at src/workers/pretty-print/tests/prettyFast.spec.js:9:364 * at Object. (src/workers/pretty-print/tests/prettyFast.spec.js:9:97) * */ const errors = []; for (const testResult of results.testResults) { if (testResult.status != "failed") { continue; } let currentError; let errorLine; const lines = testResult.message.split("\n"); lines.forEach((line, i) => { if (line.match(JEST_ERROR_SUMMARY_REGEX) || i == lines.length - 1) { // This is the name of the test, if we were gathering information from a previous // error, we add it to the errors if (currentError) { errors.push({ // The file should be relative from the repository file: testResult.name.replace(REPOSITORY_ROOT, ""), line: errorLine, // we don't have information for the column column: 0, message: currentError.trim(), }); } // Handle the new error currentError = line; } else { // We put any line that is not a test name in the error message as it may be // valuable for the user. currentError += "\n" + line; // The actual line of the error is marked with " > XXX |" const res = line.match(/> (?\d+) \|/); if (res) { errorLine = parseInt(res.groups.line, 10); } } }); } return errors; } function getTypescriptErrors(out, err, testPath) { console.log(out); // Typescript error lines look like: // popup/panel.jsm.js(103,7): error TS2531: Object is possibly 'null'. // Which means: // {file_path}({line},{col}): error TS{error_code}: {message} const tsErrorRegex = /(?(\w|\/|\.)+)\((?\d+),(?\d+)\): (?error TS\d+\:.*)/; const errors = []; for (const line of out.split("\n")) { const res = line.match(tsErrorRegex); if (!res) { continue; } // TypeScript gives us the path from the directory the command is executed in, so we // need to prepend the directory path. const fileAbsPath = testPath + res.groups.file; errors.push({ // The file should be relative from the repository. file: fileAbsPath.replace(REPOSITORY_ROOT, ""), line: parseInt(res.groups.line, 10), column: parseInt(res.groups.column, 10), message: res.groups.message.trim(), }); } return errors; } function runTests() { console.log("[devtools-node-test-runner] Extract suite argument"); const suiteArg = process.argv.find(arg => arg.includes("suite=")); const suite = suiteArg.split("=")[1]; if (suite !== "all" && !SUITES[suite]) { throw new Error( "Invalid suite argument to devtools-node-test-runner: " + suite ); } console.log("[devtools-node-test-runner] Check `yarn` is available"); try { // This will throw if yarn is unavailable execFileSync(YARN_PROCESS, ["--version"]); } catch (e) { console.log( "[devtools-node-test-runner] ERROR: `yarn` is not installed. " + "See https://yarnpkg.com/docs/install/ " ); return false; } const artifactArg = process.argv.find(arg => arg.includes("artifact=")); const artifactFilePath = artifactArg && artifactArg.split("=")[1]; const artifactErrors = {}; const failedSuites = []; const suites = suite == "all" ? SUITES : { [suite]: SUITES[suite] }; for (const [suiteName, suiteData] of Object.entries(suites)) { console.log("[devtools-node-test-runner] Running suite: " + suiteName); if (suiteData.dependencies) { console.log( "[devtools-node-test-runner] Running `yarn` for dependencies" ); for (const dep of suiteData.dependencies) { const depPath = path.join(__dirname, dep); chdir(depPath); console.log("[devtools-node-test-runner] Run `yarn` in " + depPath); execOut(YARN_PROCESS); } } const testPath = path.join(__dirname, suiteData.path); chdir(testPath); console.log("[devtools-node-test-runner] Run `yarn` in test folder"); execOut(YARN_PROCESS); console.log(`TEST START | ${suiteData.type} | ${suiteName}`); console.log("[devtools-node-test-runner] Run `yarn test` in test folder"); const { out, err } = execOut(YARN_PROCESS, ["test-ci"]); if (err) { console.log("[devtools-node-test-runner] Error log"); console.log(err); } console.log("[devtools-node-test-runner] Parse errors from the test logs"); const errors = getErrors(suiteName, out, err, testPath) || []; if (errors.length) { failedSuites.push(suiteName); } for (const error of errors) { if (!artifactErrors[error.file]) { artifactErrors[error.file] = []; } artifactErrors[error.file].push({ path: error.file, line: error.line, column: error.column, level: "error", message: error.message, analyzer: suiteName, }); console.log( `TEST-UNEXPECTED-FAIL | ${suiteData.type} | ${suiteName} | ${error.file}:${error.line}: ${error.message}` ); } } if (artifactFilePath) { console.log( `[devtools-node-test-runner] Writing artifact to ${artifactFilePath}` ); writeFileSync(artifactFilePath, JSON.stringify(artifactErrors, null, 2)); } const success = failedSuites.length === 0; if (success) { console.log( `[devtools-node-test-runner] Test suites [${Object.keys(suites).join( ", " )}] succeeded` ); } else { console.log( `[devtools-node-test-runner] Test suites [${failedSuites.join( ", " )}] failed` ); console.log( "[devtools-node-test-runner] You can find documentation about the " + "devtools node tests at https://firefox-source-docs.mozilla.org/devtools/tests/node-tests.html" ); } return success; } process.exitCode = runTests() ? 0 : 1;