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;