try-runner.js (7367B)
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 } = 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 "Activity Stream bundle": { 66 path: path.join("data", "content", "activity-stream.bundle.js"), 67 }, 68 "activity-stream.html": { 69 path: path.join("prerendered", "activity-stream.html"), 70 }, 71 "activity-stream-debug.html": { 72 path: path.join("prerendered", "activity-stream-debug.html"), 73 }, 74 "activity-stream-noscripts.html": { 75 path: path.join("prerendered", "activity-stream-noscripts.html"), 76 }, 77 "activity-stream.css": { 78 path: path.join("css", "activity-stream.css"), 79 }, 80 }; 81 const errors = []; 82 83 for (const name of Object.keys(items)) { 84 const item = items[name]; 85 item.before = readFileSync(item.path, item.encoding || "utf8"); 86 } 87 88 let newtabBundleExitCode = execOut(npmCommand, ["run", "bundle"]).exitCode; 89 90 for (const name of Object.keys(items)) { 91 const item = items[name]; 92 const after = readFileSync(item.path, item.encoding || "utf8"); 93 94 if (item.before !== after) { 95 errors.push(`${name} out of date`); 96 } 97 98 if (item.extraCheck) { 99 const extraError = item.extraCheck(after); 100 if (extraError) { 101 errors.push(extraError); 102 } 103 } 104 } 105 106 if (newtabBundleExitCode !== 0) { 107 errors.push("newtab npm:bundle did not run successfully"); 108 } 109 110 logErrors("bundles", errors); 111 return errors.length === 0; 112 }, 113 114 karma() { 115 logStart(`karma ${process.cwd()}`); 116 117 const errors = []; 118 const { exitCode, out } = execOut(npmCommand, [ 119 "run", 120 "testmc:unit", 121 // , "--", "--log-level", "--verbose", 122 // to debug the karma integration, uncomment the above line 123 ]); 124 125 // karma spits everything to stdout, not stderr, so if nothing came back on 126 // stdout, give up now. 127 if (!out) { 128 return false; 129 } 130 131 // Detect mocha failures 132 let jsonContent; 133 try { 134 // Note that this will be overwritten at each run, but that shouldn't 135 // matter. 136 jsonContent = readFileSync(path.join("logs", "karma-run-results.json")); 137 } catch (ex) { 138 console.error("exception reading karma-run-results.json: ", ex); 139 return false; 140 } 141 const results = JSON.parse(jsonContent); 142 // eslint-disable-next-line guard-for-in 143 for (let testArray in results.result) { 144 let failedTests = Array.from(results.result[testArray]).filter( 145 test => !test.success && !test.skipped 146 ); 147 148 errors.push( 149 ...failedTests.map( 150 test => `${test.suite.join(":")} ${test.description}: ${test.log[0]}` 151 ) 152 ); 153 } 154 155 // Detect istanbul failures (coverage thresholds set in karma config) 156 const coverage = out.match(/ERROR.+coverage-istanbul.+/g); 157 if (coverage) { 158 errors.push(...coverage.map(line => line.match(/Coverage.+/)[0])); 159 } 160 161 logErrors(`karma ${process.cwd()}`, errors); 162 163 console.log("-----karma stdout below this line---"); 164 console.log(out); 165 console.log("-----karma stdout above this line---"); 166 167 // Pass if there's no detected errors and nothing unexpected. 168 return errors.length === 0 && !exitCode; 169 }, 170 171 zipCodeCoverage() { 172 logStart("zipCodeCoverage"); 173 174 const { exitCode, out } = execOut("zip", [ 175 "-j", 176 "logs/coverage/code-coverage-grcov", 177 "logs/coverage/lcov.info", 178 ]); 179 180 console.log("zipCodeCoverage log output: ", out); 181 182 if (!exitCode) { 183 return true; 184 } 185 186 return false; 187 }, 188 }; 189 190 async function main() { 191 const { default: meow } = await import("meow"); 192 const fileUrl = pathToFileURL(__filename); 193 const cli = meow( 194 ` 195 Usage 196 $ node bin/try-runner.js <tests> [options] 197 198 Options 199 -t NAME, --test NAME Run only the specified test. If not specified, 200 all tests will be run. Argument can be passed 201 multiple times to run multiple tests. 202 --help Show this help message. 203 204 Examples 205 $ node bin/try-runner.js bundles karma 206 $ node bin/try-runner.js -t karma -t zip 207 `, 208 { 209 description: false, 210 // `pkg` is a tiny optimization. It prevents meow from looking for a package 211 // that doesn't technically exist. meow searches for a package and changes 212 // the process name to the package name. It resolves to the newtab 213 // package.json, which would give a confusing name and be wasteful. 214 pkg: { 215 name: "try-runner", 216 version: "1.0.0", 217 }, 218 // `importMeta` is required by meow 10+. It was added to support ESM, but 219 // meow now requires it, and no longer supports CJS style imports. But it 220 // only uses import.meta.url, which can be polyfilled like this: 221 importMeta: { url: fileUrl }, 222 flags: { 223 test: { 224 type: "string", 225 isMultiple: true, 226 shortFlag: "t", 227 }, 228 }, 229 } 230 ); 231 const aliases = { 232 bundle: "bundles", 233 build: "bundles", 234 coverage: "karma", 235 cov: "karma", 236 zip: "zipCodeCoverage", 237 }; 238 239 const inputs = [...cli.input, ...cli.flags.test].map(input => 240 (aliases[input] || input).toLowerCase() 241 ); 242 243 function shouldRunTest(name) { 244 if (inputs.length) { 245 return inputs.includes(name.toLowerCase()); 246 } 247 return true; 248 } 249 250 const results = []; 251 for (const name of Object.keys(tests)) { 252 if (shouldRunTest(name)) { 253 results.push([name, tests[name]()]); 254 } else { 255 logSkip(name); 256 } 257 } 258 259 for (const [name, result] of results) { 260 // colorize output based on result 261 console.log(result ? chalk.green(`✓ ${name}`) : chalk.red(`✗ ${name}`)); 262 } 263 264 const success = results.every(([, result]) => result); 265 process.exitCode = success ? 0 : 1; 266 console.log("CODE", process.exitCode); 267 } 268 269 main();