/* 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 http://mozilla.org/MPL/2.0/. */ "use strict"; this.EXPORTED_SYMBOLS = [ "StructuredLogger", "StructuredFormatter" ]; /** * TestLogger: Logger class generating messages compliant with the * structured logging protocol for tests exposed by mozlog * * @param name * The name of the logger to instantiate. * @param dumpFun * An underlying function to be used to log raw messages. This function * will receive the complete serialized json string to log. * @param mutators * An array of functions used to add global context to log messages. * These will each be called with the complete object to log as an * argument. */ this.StructuredLogger = function(name, dumpFun=dump, mutators=[]) { this.name = name; this._dumpFun = dumpFun; this._mutatorFuns = mutators; } /** * Log functions producing messages in the format specified by mozlog */ StructuredLogger.prototype = { testStart: function (test) { var data = {test: this._testId(test)}; this._logData("test_start", data); }, testStatus: function (test, subtest, status, expected="PASS", message=null, stack=null, extra=null) { if (subtest === null || subtest === undefined) { // Fix for assertions that don't pass in a name subtest = "undefined assertion name"; } var data = { test: this._testId(test), subtest: subtest, status: status, }; if (expected != status && status != "SKIP") { data.expected = expected; } if (message !== null) { data.message = String(message); } if (stack !== null) { data.stack = stack; } if (extra !== null) { data.extra = extra; } this._logData("test_status", data); }, testEnd: function (test, status, expected="OK", message=null, stack=null, extra=null) { var data = {test: this._testId(test), status: status}; if (expected != status && status != "SKIP") { data.expected = expected; } if (message !== null) { data.message = String(message); } if (stack !== null) { data.stack = stack; } if (extra !== null) { data.extra = extra; } this._logData("test_end", data); }, assertionCount: function (test, count, minExpected=0, maxExpected=0) { var data = {test: test, min_expected: minExpected, max_expected: maxExpected, count: count}; this._logData("assertion_count", data); }, suiteStart: function (tests, runinfo=null, versioninfo=null, deviceinfo=null, extra=null) { var data = {tests: tests.map(x => this._testId(x))}; if (runinfo !== null) { data.runinfo = runinfo; } if (versioninfo !== null) { data.versioninfo = versioninfo; } if (deviceinfo !== null) { data.deviceinfo = deviceinfo; } if (extra !== null) { data.extra = extra; } this._logData("suite_start", data); }, suiteEnd: function (extra=null) { var data = {}; if (extra !== null) { data.extra = extra; } this._logData("suite_end", data); }, /** * Unstructured logging functions. The "extra" parameter can always by used to * log suite specific data. If a "stack" field is provided it is logged at the * top level of the data object for the benefit of mozlog's formatters. */ log: function (level, message, extra=null) { var data = { level: level, message: String(message), }; if (extra !== null) { data.extra = extra; if ("stack" in extra) { data.stack = extra.stack; } } this._logData("log", data); }, debug: function (message, extra=null) { this.log("DEBUG", message, extra); }, info: function (message, extra=null) { this.log("INFO", message, extra); }, warning: function (message, extra=null) { this.log("WARNING", message, extra); }, error: function (message, extra=null) { this.log("ERROR", message, extra); }, critical: function (message, extra=null) { this.log("CRITICAL", message, extra); }, processOutput: function(thread, message) { this._logData('process_output', { message: message, thread: thread, }); }, _logData: function (action, data={}) { var allData = { action: action, time: Date.now(), thread: null, pid: null, source: this.name }; for (var field in data) { allData[field] = data[field]; } for (var fun of this._mutatorFuns) { fun(allData); } this._dumpFun(allData); }, _testId: function(test) { if (Array.isArray(test)) { return test.join(" "); } return test; }, }; /** * StructuredFormatter: Formatter class turning structured messages * into human-readable messages. */ this.StructuredFormatter = function() { this.testStartTimes = {}; }; StructuredFormatter.prototype = { log: function(message) { return message.message; }, suite_start: function(message) { this.suiteStartTime = message.time; return "SUITE-START | Running " + message.tests.length + " tests"; }, test_start: function(message) { this.testStartTimes[message.test] = new Date().getTime(); return "TEST-START | " + message.test; }, test_status: function(message) { var statusInfo = message.test + " | " + message.subtest + (message.message ? " | " + message.message : ""); if (message.expected) { return "TEST-UNEXPECTED-" + message.status + " | " + statusInfo + " - expected: " + message.expected; } else { return "TEST-" + message.status + " | " + statusInfo; } }, test_end: function(message) { var startTime = this.testStartTimes[message.test]; delete this.testStartTimes[message.test]; var statusInfo = message.test + (message.message ? " | " + String(message.message) : ""); var result; if (message.expected) { result = "TEST-UNEXPECTED-" + message.status + " | " + statusInfo + " - expected: " + message.expected; } else { return "TEST-" + message.status + " | " + statusInfo; } result = " | took " + message.time - startTime + "ms"; }, suite_end: function(message) { return "SUITE-END | took " + message.time - this.suiteStartTime + "ms"; } };