try-runner.js (8975B)
1 /* eslint-disable no-console */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 5 6 /* 7 * A small test runner/reporter for node-based tests, 8 * which are run via taskcluster node(debugger). 9 * 10 * Forked from 11 * https://searchfox.org/mozilla-central/rev/c3453c7a0427eb27d467e1582f821f402aed9850/devtools/client/debugger/bin/try-runner.js 12 */ 13 14 const { execFileSync } = require("child_process"); 15 const { readFileSync, writeFileSync } = require("fs"); 16 const path = require("path"); 17 const { pathToFileURL } = require("url"); 18 const chalk = require("chalk"); 19 20 function logErrors(tool, errors) { 21 for (const error of errors) { 22 console.log(`TEST-UNEXPECTED-FAIL | ${tool} | ${error}`); 23 } 24 return errors; 25 } 26 27 function execOut(...args) { 28 let exitCode = 0; 29 let out; 30 let err; 31 32 try { 33 out = execFileSync(...args, { 34 silent: false, 35 }); 36 } catch (e) { 37 // For debugging on (eg) try server... 38 // 39 // if (e) { 40 // logErrors("execOut", ["execFileSync returned exception: ", e]); 41 // } 42 43 out = e && e.stdout; 44 err = e && e.stderr; 45 exitCode = e && e.status; 46 } 47 return { exitCode, out: out && out.toString(), err: err && err.toString() }; 48 } 49 50 function logStart(name) { 51 console.log(`TEST-START | ${name}`); 52 } 53 54 function logSkip(name) { 55 console.log(`TEST-SKIP | ${name}`); 56 } 57 58 const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; 59 60 const tests = { 61 bundles() { 62 logStart("bundles"); 63 64 const items = { 65 "about:welcome bundle": { 66 path: path.join( 67 "../", 68 "aboutwelcome", 69 "content", 70 "aboutwelcome.bundle.js" 71 ), 72 }, 73 "aboutwelcome.css": { 74 path: path.join("../", "aboutwelcome", "content", "aboutwelcome.css"), 75 extraCheck: content => { 76 if (content.match(/^\s*@import/m)) { 77 return "aboutwelcome.css contains an @import statement. We should not import styles through the stylesheet, because it is loaded in multiple environments, including the browser chrome for feature callouts. To add other stylesheets to about:welcome or spotlight, add them to aboutwelcome.html or spotlight.html instead."; 78 } 79 return null; 80 }, 81 }, 82 "about:asrouter bundle": { 83 path: path.join( 84 "../", 85 "asrouter", 86 "content", 87 "asrouter-admin.bundle.js" 88 ), 89 }, 90 "ASRouterAdmin.css": { 91 path: path.join( 92 "../", 93 "asrouter", 94 "content", 95 "components", 96 "ASRouterAdmin", 97 "ASRouterAdmin.css" 98 ), 99 }, 100 }; 101 const errors = []; 102 103 for (const name of Object.keys(items)) { 104 const item = items[name]; 105 item.before = readFileSync(item.path, item.encoding || "utf8"); 106 } 107 108 // Run about:welcome bundle script. 109 let cwd = process.cwd(); 110 process.chdir("../aboutwelcome"); 111 let welcomeBundleExitCode = execOut(npmCommand, ["run", "bundle"]).exitCode; 112 process.chdir(cwd); 113 114 let asrouterBundleExitCode = execOut(npmCommand, [ 115 "run", 116 "bundle", 117 ]).exitCode; 118 119 for (const name of Object.keys(items)) { 120 const item = items[name]; 121 const after = readFileSync(item.path, item.encoding || "utf8"); 122 123 if (item.before !== after) { 124 errors.push(`${name} out of date`); 125 } 126 127 if (item.extraCheck) { 128 const extraError = item.extraCheck(after); 129 if (extraError) { 130 errors.push(extraError); 131 } 132 } 133 } 134 135 if (welcomeBundleExitCode !== 0) { 136 errors.push("about:welcome npm:bundle did not run successfully"); 137 } 138 139 if (asrouterBundleExitCode !== 0) { 140 errors.push("about:asrouter npm:bundle did not run successfully"); 141 } 142 143 logErrors("bundles", errors); 144 return errors.length === 0; 145 }, 146 147 karma() { 148 logStart(`karma ${process.cwd()}`); 149 150 const errors = []; 151 const { exitCode, out } = execOut(npmCommand, [ 152 "run", 153 "testmc:unit", 154 // , "--", "--log-level", "--verbose", 155 // to debug the karma integration, uncomment the above line 156 ]); 157 158 // karma spits everything to stdout, not stderr, so if nothing came back on 159 // stdout, give up now. 160 if (!out) { 161 return false; 162 } 163 164 // Detect mocha failures 165 let jsonContent; 166 try { 167 // Note that this will be overwritten at each run, but that shouldn't 168 // matter. 169 jsonContent = readFileSync(path.join("logs", "karma-run-results.json")); 170 } catch (ex) { 171 console.error("exception reading karma-run-results.json: ", ex); 172 return false; 173 } 174 const results = JSON.parse(jsonContent); 175 // eslint-disable-next-line guard-for-in 176 for (let testArray in results.result) { 177 let failedTests = Array.from(results.result[testArray]).filter( 178 test => !test.success && !test.skipped 179 ); 180 181 errors.push( 182 ...failedTests.map( 183 test => `${test.suite.join(":")} ${test.description}: ${test.log[0]}` 184 ) 185 ); 186 } 187 188 // Detect istanbul failures (coverage thresholds set in karma config) 189 const coverage = out.match(/ERROR.+coverage-istanbul.+/g); 190 if (coverage) { 191 errors.push(...coverage.map(line => line.match(/Coverage.+/)[0])); 192 } 193 194 logErrors(`karma ${process.cwd()}`, errors); 195 196 console.log("-----karma stdout below this line---"); 197 console.log(out); 198 console.log("-----karma stdout above this line---"); 199 200 // Pass if there's no detected errors and nothing unexpected. 201 return errors.length === 0 && !exitCode; 202 }, 203 204 welcomekarma() { 205 let cwd = process.cwd(); 206 process.chdir("../aboutwelcome"); 207 const result = this.karma(); 208 process.chdir(cwd); 209 return result; 210 }, 211 212 zipCodeCoverage() { 213 logStart("zipCodeCoverage"); 214 215 const welcomeCoveragePath = "../aboutwelcome/logs/coverage/lcov.info"; 216 const asrouterCoveragePath = "logs/coverage/lcov.info"; 217 218 const welcomeCoverage = readFileSync(welcomeCoveragePath, "utf8"); 219 let asrouterCoverage = readFileSync(asrouterCoveragePath, "utf8"); 220 221 asrouterCoverage = `${welcomeCoverage}${asrouterCoverage}`; 222 writeFileSync(asrouterCoveragePath, asrouterCoverage, "utf8"); 223 224 const { exitCode, out } = execOut("zip", [ 225 "-j", 226 "logs/coverage/code-coverage-grcov", 227 "logs/coverage/lcov.info", 228 ]); 229 230 console.log("zipCodeCoverage log output: ", out); 231 232 if (!exitCode) { 233 return true; 234 } 235 236 return false; 237 }, 238 }; 239 240 async function main() { 241 const { default: meow } = await import("meow"); 242 const fileUrl = pathToFileURL(__filename); 243 const cli = meow( 244 ` 245 Usage 246 $ node bin/try-runner.js <tests> [options] 247 248 Options 249 -t NAME, --test NAME Run only the specified test. If not specified, 250 all tests will be run. Argument can be passed 251 multiple times to run multiple tests. 252 --help Show this help message. 253 254 Examples 255 $ node bin/try-runner.js bundles karma 256 $ node bin/try-runner.js -t karma -t zip 257 `, 258 { 259 description: false, 260 // `pkg` is a tiny optimization. It prevents meow from looking for a package 261 // that doesn't technically exist. meow searches for a package and changes 262 // the process name to the package name. It resolves to the newtab 263 // package.json, which would give a confusing name and be wasteful. 264 pkg: { 265 name: "try-runner", 266 version: "1.0.0", 267 }, 268 // `importMeta` is required by meow 10+. It was added to support ESM, but 269 // meow now requires it, and no longer supports CJS style imports. But it 270 // only uses import.meta.url, which can be polyfilled like this: 271 importMeta: { url: fileUrl }, 272 flags: { 273 test: { 274 type: "string", 275 isMultiple: true, 276 shortFlag: "t", 277 }, 278 }, 279 } 280 ); 281 const aliases = { 282 bundle: "bundles", 283 build: "bundles", 284 coverage: "karma", 285 cov: "karma", 286 zip: "zipCodeCoverage", 287 welcomecoverage: "welcomekarma", 288 welcomecov: "welcomekarma", 289 }; 290 291 const inputs = [...cli.input, ...cli.flags.test].map(input => 292 (aliases[input] || input).toLowerCase() 293 ); 294 295 function shouldRunTest(name) { 296 if (inputs.length) { 297 return inputs.includes(name.toLowerCase()); 298 } 299 return true; 300 } 301 302 const results = []; 303 for (const name of Object.keys(tests)) { 304 if (shouldRunTest(name)) { 305 results.push([name, tests[name]()]); 306 } else { 307 logSkip(name); 308 } 309 } 310 311 for (const [name, result] of results) { 312 // colorize output based on result 313 console.log(result ? chalk.green(`✓ ${name}`) : chalk.red(`✗ ${name}`)); 314 } 315 316 const success = results.every(([, result]) => result); 317 process.exitCode = success ? 0 : 1; 318 console.log("CODE", process.exitCode); 319 } 320 321 main();