StructuredLog.sys.mjs (6875B)
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 /** 6 * TestLogger: Logger class generating messages compliant with the 7 * structured logging protocol for tests exposed by mozlog 8 * 9 * @param {string} name 10 * The name of the logger to instantiate. 11 * @param {function} [dumpFun] 12 * An underlying function to be used to log raw messages. This function 13 * will receive the complete serialized json string to log. 14 * @param {object} [scope] 15 * The scope that the dumpFun is loaded in, so that messages are cloned 16 * into that scope before passing them. 17 */ 18 export class StructuredLogger { 19 name = null; 20 #dumpFun = null; 21 #dumpScope = null; 22 23 constructor(name, dumpFun = dump, scope = null) { 24 this.name = name; 25 this.#dumpFun = dumpFun; 26 this.#dumpScope = scope; 27 } 28 29 testStart(test) { 30 var data = { test: this.#testId(test) }; 31 this.logData("test_start", data); 32 } 33 34 testStatus( 35 test, 36 subtest, 37 status, 38 expected = "PASS", 39 message = null, 40 stack = null, 41 extra = null 42 ) { 43 var data = { 44 test: this.#testId(test), 45 subtest, 46 status, 47 }; 48 49 // handle case: known fail 50 if (expected === status && status != "SKIP") { 51 data.status = "PASS"; 52 } 53 if (expected != status && status != "SKIP") { 54 data.expected = expected; 55 } 56 if (message !== null) { 57 data.message = String(message); 58 } 59 if (stack !== null) { 60 data.stack = stack; 61 } 62 if (extra !== null) { 63 data.extra = extra; 64 } 65 66 this.logData("test_status", data); 67 } 68 69 testEnd( 70 test, 71 status, 72 expected = "OK", 73 message = null, 74 stack = null, 75 extra = null 76 ) { 77 var data = { test: this.#testId(test), status }; 78 79 // handle case: known fail 80 if (expected === status && status != "SKIP") { 81 data.status = "OK"; 82 } 83 if (expected != status && status != "SKIP") { 84 data.expected = expected; 85 } 86 if (message !== null) { 87 data.message = String(message); 88 } 89 if (stack !== null) { 90 data.stack = stack; 91 } 92 if (extra !== null) { 93 data.extra = extra; 94 } 95 96 this.logData("test_end", data); 97 } 98 99 assertionCount(test, count, minExpected = 0, maxExpected = 0) { 100 var data = { 101 test: this.#testId(test), 102 min_expected: minExpected, 103 max_expected: maxExpected, 104 count, 105 }; 106 107 this.logData("assertion_count", data); 108 } 109 110 suiteStart( 111 ids, 112 name = null, 113 runinfo = null, 114 versioninfo = null, 115 deviceinfo = null, 116 extra = null 117 ) { 118 Object.keys(ids).map(function (manifest) { 119 ids[manifest] = ids[manifest].map(x => this.#testId(x)); 120 }, this); 121 var data = { tests: ids }; 122 123 if (name !== null) { 124 data.name = name; 125 } 126 127 if (runinfo !== null) { 128 data.runinfo = runinfo; 129 } 130 131 if (versioninfo !== null) { 132 data.versioninfo = versioninfo; 133 } 134 135 if (deviceinfo !== null) { 136 data.deviceinfo = deviceinfo; 137 } 138 139 if (extra !== null) { 140 data.extra = extra; 141 } 142 143 this.logData("suite_start", data); 144 } 145 146 suiteEnd(extra = null) { 147 var data = {}; 148 149 if (extra !== null) { 150 data.extra = extra; 151 } 152 153 this.logData("suite_end", data); 154 } 155 156 /** 157 * Unstructured logging functions. The "extra" parameter can always by used to 158 * log suite specific data. If a "stack" field is provided it is logged at the 159 * top level of the data object for the benefit of mozlog's formatters. 160 */ 161 log(level, message, extra = null) { 162 var data = { 163 level, 164 message: String(message), 165 }; 166 167 if (extra !== null) { 168 data.extra = extra; 169 if ("stack" in extra) { 170 data.stack = extra.stack; 171 } 172 } 173 174 this.logData("log", data); 175 } 176 177 debug(message, extra = null) { 178 this.log("DEBUG", message, extra); 179 } 180 181 info(message, extra = null) { 182 this.log("INFO", message, extra); 183 } 184 185 warning(message, extra = null) { 186 this.log("WARNING", message, extra); 187 } 188 189 error(message, extra = null) { 190 this.log("ERROR", message, extra); 191 } 192 193 critical(message, extra = null) { 194 this.log("CRITICAL", message, extra); 195 } 196 197 processOutput(thread, message) { 198 this.logData("process_output", { 199 message, 200 thread, 201 }); 202 } 203 204 logData(action, data = {}) { 205 var allData = { 206 action, 207 time: Date.now(), 208 thread: null, 209 pid: null, 210 source: this.name, 211 }; 212 213 for (var field in data) { 214 allData[field] = data[field]; 215 } 216 217 if (this.#dumpScope) { 218 try { 219 allData = Cu.cloneInto(allData, this.#dumpScope); 220 } catch (e) { 221 try { 222 this.error(`Failed to cloneInto: ${e}`); 223 this.warning(`Tried to clone: ${uneval(allData)}`); 224 } catch (e2) { 225 console.error("Failed to handle clone error", e, e2); 226 } 227 } 228 } 229 this.#dumpFun(allData); 230 } 231 232 #testId(test) { 233 if (Array.isArray(test)) { 234 return test.join(" "); 235 } 236 return test; 237 } 238 } 239 240 /** 241 * StructuredFormatter: Formatter class turning structured messages 242 * into human-readable messages. 243 */ 244 export class StructuredFormatter { 245 // The time at which the whole suite of tests started. 246 #suiteStartTime = null; 247 248 #testStartTimes = new Map(); 249 250 log(message) { 251 return message.message; 252 } 253 254 suite_start(message) { 255 this.#suiteStartTime = message.time; 256 return "SUITE-START | Running " + message.tests.length + " tests"; 257 } 258 259 test_start(message) { 260 this.#testStartTimes.set(message.test, new Date().getTime()); 261 return "TEST-START | " + message.test; 262 } 263 264 test_status(message) { 265 var statusInfo = 266 message.test + 267 " | " + 268 message.subtest + 269 (message.message ? " | " + message.message : ""); 270 if (message.expected) { 271 return ( 272 "TEST-UNEXPECTED-" + 273 message.status + 274 " | " + 275 statusInfo + 276 " - expected: " + 277 message.expected 278 ); 279 } 280 return "TEST-" + message.status + " | " + statusInfo; 281 } 282 283 test_end(message) { 284 var startTime = this.#testStartTimes.get(message.test); 285 this.#testStartTimes.delete(message.test); 286 var statusInfo = 287 message.test + (message.message ? " | " + String(message.message) : ""); 288 var result; 289 if (message.expected) { 290 result = 291 "TEST-UNEXPECTED-" + 292 message.status + 293 " | " + 294 statusInfo + 295 " - expected: " + 296 message.expected; 297 } else { 298 return "TEST-" + message.status + " | " + statusInfo; 299 } 300 result = result + " | took " + message.time - startTime + "ms"; 301 return result; 302 } 303 304 suite_end(message) { 305 return "SUITE-END | took " + message.time - this.#suiteStartTime + "ms"; 306 } 307 }