Pass the exception's actual stack via the harness's stack field instead of synthesizing one rooted in simpletestOnerror or assertNoUncaughtRejections. For uncaught rejections, also pass the rejection date as the assertion's timestamp rather than baking it into the failure message, so otherwise identical failures fingerprint to the same message. The rejection failure message is simplified to "uncaught rejection: <message>". Plumbing changes: - Assert.report / Assert.AssertionError accept a string stack and an optional time. - StructuredLog.testStatus accepts an optional time that overrides the default Date.now(). - The mochitest reporter and browser-harness.xhtml forward the new time field through testResult and addResult. Differential Revision: https://phabricator.services.mozilla.com/D296797
594 lines
13 KiB
JavaScript
594 lines
13 KiB
JavaScript
/* 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/. */
|
|
|
|
let gSchemaRegistered = false;
|
|
|
|
function ensureSchemaRegistered() {
|
|
if (gSchemaRegistered) {
|
|
return;
|
|
}
|
|
gSchemaRegistered = true;
|
|
|
|
/**
|
|
* @backward-compat { version 149 }
|
|
*
|
|
* Bug 2011308: Remove the following typeof / early return check
|
|
*/
|
|
if (!ChromeUtils.registerMarkerSchema) {
|
|
return;
|
|
}
|
|
|
|
ChromeUtils.registerMarkerSchema({
|
|
name: "TestStatus",
|
|
tableLabel: "{marker.data.message}",
|
|
display: ["marker-chart", "marker-table"],
|
|
colorField: "color",
|
|
data: [
|
|
{
|
|
key: "message",
|
|
label: "Message",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "test",
|
|
label: "Test Name",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "subtest",
|
|
label: "Subtest",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "Status",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "expected",
|
|
label: "Expected",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "color",
|
|
hidden: true,
|
|
},
|
|
],
|
|
});
|
|
|
|
ChromeUtils.registerMarkerSchema({
|
|
name: "Test",
|
|
tooltipLabel: "{marker.data.name}",
|
|
tableLabel: "{marker.data.status} — {marker.data.test}",
|
|
chartLabel: "{marker.data.name}",
|
|
display: ["marker-chart", "marker-table"],
|
|
colorField: "color",
|
|
data: [
|
|
{
|
|
key: "test",
|
|
label: "Test Name",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "name",
|
|
label: "Short Name",
|
|
format: "string",
|
|
hidden: true,
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "Status",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "expected",
|
|
label: "Expected",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "message",
|
|
label: "Message",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "color",
|
|
hidden: true,
|
|
},
|
|
],
|
|
});
|
|
|
|
ChromeUtils.registerMarkerSchema({
|
|
name: "Log",
|
|
tableLabel: "{marker.data.message}",
|
|
display: ["marker-chart", "marker-table"],
|
|
colorField: "color",
|
|
data: [
|
|
{
|
|
key: "message",
|
|
label: "Message",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "level",
|
|
label: "Level",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "test",
|
|
label: "Test Name",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "subtest",
|
|
label: "Subtest",
|
|
format: "string",
|
|
},
|
|
{
|
|
key: "color",
|
|
hidden: true,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* TestLogger: Logger class generating messages compliant with the
|
|
* structured logging protocol for tests exposed by mozlog
|
|
*
|
|
* @param {string} name
|
|
* The name of the logger to instantiate.
|
|
* @param {function} [dumpFun]
|
|
* An underlying function to be used to log raw messages. This function
|
|
* will receive the complete serialized json string to log.
|
|
* @param {object} [scope]
|
|
* The scope that the dumpFun is loaded in, so that messages are cloned
|
|
* into that scope before passing them.
|
|
*/
|
|
export class StructuredLogger {
|
|
name = null;
|
|
#dumpFun = null;
|
|
#dumpScope = null;
|
|
#testStartTimes = new Map();
|
|
|
|
// Regexes that normalize test paths, matching MessageLogger.TEST_PATH_PREFIXES
|
|
static #TEST_PATH_PREFIXES = [
|
|
/^\/tests\//,
|
|
/^\w+:\/\/[\w.]+(:\d+)?(\/\w+)?\/(tests?|a11y|chrome)\//,
|
|
/^\w+:\/\/[\w.]+(:\d+)?(\/\w+)?\/(tests?|browser)\//,
|
|
];
|
|
|
|
constructor(name, dumpFun = dump, scope = null) {
|
|
this.name = name;
|
|
this.#dumpFun = dumpFun;
|
|
this.#dumpScope = scope;
|
|
}
|
|
|
|
/**
|
|
* Normalize a test path to match the relative path from the sourcedir.
|
|
* Matches the behavior of MessageLogger._fix_test_name in runtests.py.
|
|
*/
|
|
#normalizeTestPath(testPath) {
|
|
for (const pattern of StructuredLogger.#TEST_PATH_PREFIXES) {
|
|
const normalized = testPath.replace(pattern, "");
|
|
if (normalized !== testPath) {
|
|
return normalized;
|
|
}
|
|
}
|
|
return testPath;
|
|
}
|
|
|
|
testStart(test) {
|
|
var data = { test: this.#testId(test) };
|
|
this.logData("test_start", data);
|
|
|
|
const testId = this.#testId(test);
|
|
this.#testStartTimes.set(testId, ChromeUtils.now());
|
|
}
|
|
|
|
testStatus(
|
|
test,
|
|
subtest,
|
|
status,
|
|
expected = "PASS",
|
|
message = null,
|
|
stack = null,
|
|
extra = null,
|
|
time = null
|
|
) {
|
|
var data = {
|
|
test: this.#testId(test),
|
|
subtest,
|
|
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;
|
|
}
|
|
if (time !== null) {
|
|
data.time = time;
|
|
}
|
|
|
|
this.logData("test_status", data);
|
|
|
|
ensureSchemaRegistered();
|
|
|
|
// Determine marker name following mochitest conventions
|
|
let markerName;
|
|
if (status === expected) {
|
|
// Expected result
|
|
if (status === "FAIL") {
|
|
markerName = "TEST-KNOWN-FAIL";
|
|
} else {
|
|
markerName = "TEST-" + status;
|
|
}
|
|
} else {
|
|
// Unexpected result
|
|
markerName = "TEST-UNEXPECTED-" + status;
|
|
}
|
|
|
|
// Prepare marker data with normalized test path
|
|
const markerData = {
|
|
type: "TestStatus",
|
|
test: this.#normalizeTestPath(data.test),
|
|
status,
|
|
expected,
|
|
};
|
|
|
|
if (subtest) {
|
|
markerData.subtest = subtest;
|
|
}
|
|
|
|
if (message !== null) {
|
|
markerData.message = String(message);
|
|
}
|
|
|
|
// Determine color
|
|
if (status === "ERROR" && status !== expected) {
|
|
markerData.color = "red";
|
|
} else if (status === expected) {
|
|
markerData.color = "green";
|
|
} else {
|
|
markerData.color = "orange";
|
|
}
|
|
|
|
const options = { category: "Test" };
|
|
|
|
// Capture stack for failures and errors
|
|
if (status === "FAIL" || status === "ERROR") {
|
|
options.captureStack = true;
|
|
}
|
|
|
|
ChromeUtils.addProfilerMarker(markerName, options, markerData);
|
|
}
|
|
|
|
testEnd(
|
|
test,
|
|
status,
|
|
expected = "PASS",
|
|
message = null,
|
|
stack = null,
|
|
extra = null
|
|
) {
|
|
const testId = this.#testId(test);
|
|
var data = { test: testId, 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);
|
|
|
|
const startTime = this.#testStartTimes.get(testId);
|
|
if (!startTime) {
|
|
return;
|
|
}
|
|
this.#testStartTimes.delete(testId);
|
|
|
|
ensureSchemaRegistered();
|
|
|
|
// Normalize test path
|
|
const testPath = this.#normalizeTestPath(testId);
|
|
|
|
const markerData = {
|
|
type: "Test",
|
|
test: testPath,
|
|
name: testPath.split("/").pop(),
|
|
status,
|
|
};
|
|
|
|
if (data.expected) {
|
|
markerData.expected = data.expected;
|
|
}
|
|
|
|
if (data.message) {
|
|
markerData.message = data.message;
|
|
}
|
|
|
|
if (status) {
|
|
// Determine color based on status and expectations
|
|
if (status === "SKIP" || status === "TIMEOUT") {
|
|
markerData.color = "yellow";
|
|
} else if (status === "CRASH" || status === "ERROR") {
|
|
markerData.color = "red";
|
|
} else if (expected === status) {
|
|
markerData.color = "green";
|
|
} else {
|
|
markerData.color = "orange";
|
|
}
|
|
}
|
|
|
|
ChromeUtils.addProfilerMarker(
|
|
"test",
|
|
{
|
|
category: "Test",
|
|
startTime,
|
|
},
|
|
markerData
|
|
);
|
|
}
|
|
|
|
assertionCount(test, count, minExpected = 0, maxExpected = 0) {
|
|
var data = {
|
|
test: this.#testId(test),
|
|
min_expected: minExpected,
|
|
max_expected: maxExpected,
|
|
count,
|
|
};
|
|
|
|
this.logData("assertion_count", data);
|
|
}
|
|
|
|
suiteStart(
|
|
ids,
|
|
name = null,
|
|
runinfo = null,
|
|
versioninfo = null,
|
|
deviceinfo = null,
|
|
extra = null
|
|
) {
|
|
Object.keys(ids).map(function (manifest) {
|
|
ids[manifest] = ids[manifest].map(x => this.#testId(x));
|
|
}, this);
|
|
var data = { tests: ids };
|
|
|
|
if (name !== null) {
|
|
data.name = name;
|
|
}
|
|
|
|
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(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(level, message, extra = null) {
|
|
var data = {
|
|
level,
|
|
message: String(message),
|
|
};
|
|
|
|
if (extra !== null) {
|
|
data.extra = extra;
|
|
if ("stack" in extra) {
|
|
data.stack = extra.stack;
|
|
}
|
|
}
|
|
|
|
this.logData("log", data);
|
|
|
|
ensureSchemaRegistered();
|
|
|
|
// Add marker type
|
|
data.type = "Log";
|
|
|
|
// Copy test/subtest from extra if present, normalizing test path
|
|
if (extra) {
|
|
if (extra.test) {
|
|
data.test = this.#normalizeTestPath(extra.test);
|
|
}
|
|
if (extra.subtest) {
|
|
data.subtest = extra.subtest;
|
|
}
|
|
}
|
|
|
|
// Determine color based on log level
|
|
if (level === "CRITICAL" || level === "ERROR") {
|
|
data.color = "red";
|
|
} else if (level === "WARNING") {
|
|
data.color = "orange";
|
|
} else if (level === "DEBUG") {
|
|
data.color = "grey";
|
|
}
|
|
|
|
// Remove fields we don't want in the marker
|
|
delete data.extra;
|
|
|
|
const options = { category: "Test" };
|
|
|
|
// Capture stack for errors and critical
|
|
if (level === "CRITICAL" || level === "ERROR") {
|
|
options.captureStack = true;
|
|
}
|
|
|
|
ChromeUtils.addProfilerMarker(level, options, data);
|
|
}
|
|
|
|
debug(message, extra = null) {
|
|
this.log("DEBUG", message, extra);
|
|
}
|
|
|
|
info(message, extra = null) {
|
|
this.log("INFO", message, extra);
|
|
}
|
|
|
|
warning(message, extra = null) {
|
|
this.log("WARNING", message, extra);
|
|
}
|
|
|
|
error(message, extra = null) {
|
|
this.log("ERROR", message, extra);
|
|
}
|
|
|
|
critical(message, extra = null) {
|
|
this.log("CRITICAL", message, extra);
|
|
}
|
|
|
|
processOutput(thread, message) {
|
|
this.logData("process_output", {
|
|
message,
|
|
thread,
|
|
});
|
|
}
|
|
|
|
logData(action, data = {}) {
|
|
var allData = {
|
|
action,
|
|
time: Date.now(),
|
|
thread: null,
|
|
pid: null,
|
|
source: this.name,
|
|
};
|
|
|
|
for (var field in data) {
|
|
allData[field] = data[field];
|
|
}
|
|
|
|
if (this.#dumpScope) {
|
|
try {
|
|
allData = Cu.cloneInto(allData, this.#dumpScope);
|
|
} catch (e) {
|
|
try {
|
|
this.error(`Failed to cloneInto: ${e}`);
|
|
this.warning(`Tried to clone: ${uneval(allData)}`);
|
|
} catch (e2) {
|
|
console.error("Failed to handle clone error", e, e2);
|
|
}
|
|
}
|
|
}
|
|
this.#dumpFun(allData);
|
|
}
|
|
|
|
#testId(test) {
|
|
if (Array.isArray(test)) {
|
|
return test.join(" ");
|
|
}
|
|
return test;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* StructuredFormatter: Formatter class turning structured messages
|
|
* into human-readable messages.
|
|
*/
|
|
export class StructuredFormatter {
|
|
// The time at which the whole suite of tests started.
|
|
#suiteStartTime = null;
|
|
|
|
#testStartTimes = new Map();
|
|
|
|
log(message) {
|
|
return message.message;
|
|
}
|
|
|
|
suite_start(message) {
|
|
this.#suiteStartTime = message.time;
|
|
return "SUITE-START | Running " + message.tests.length + " tests";
|
|
}
|
|
|
|
test_start(message) {
|
|
this.#testStartTimes.set(message.test, new Date().getTime());
|
|
return "TEST-START | " + message.test;
|
|
}
|
|
|
|
test_status(message) {
|
|
var statusInfo =
|
|
message.test +
|
|
" | " +
|
|
message.subtest +
|
|
(message.message ? " | " + message.message : "");
|
|
if (message.expected) {
|
|
return (
|
|
"TEST-UNEXPECTED-" +
|
|
message.status +
|
|
" | " +
|
|
statusInfo +
|
|
" - expected: " +
|
|
message.expected
|
|
);
|
|
}
|
|
return "TEST-" + message.status + " | " + statusInfo;
|
|
}
|
|
|
|
test_end(message) {
|
|
var startTime = this.#testStartTimes.get(message.test);
|
|
this.#testStartTimes.delete(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 = result + " | took " + message.time - startTime + "ms";
|
|
return result;
|
|
}
|
|
|
|
suite_end(message) {
|
|
return "SUITE-END | took " + message.time - this.#suiteStartTime + "ms";
|
|
}
|
|
}
|