Source code

Revision control

Copy as Markdown

Other Tools

/* Any copyright is dedicated to the Public Domain.
*/
import { TEST_PAGE_HTML, CONTEXTS, AllObjects } from "resource://testing-common/AllJavascriptTypes.mjs";
import { ObjectUtils } from "resource://gre/modules/ObjectUtils.sys.mjs";
// Name of the environment variable to set while running the test to update the expected values
const UPDATE_SNAPSHOT_ENV = "UPDATE_SNAPSHOT";
const { AddonTestUtils } = ChromeUtils.importESModule(
);
// To avoid totally unrelated exceptions about missing appinfo when running from xpcshell tests
const isXpcshell = Services.env.exists("XPCSHELL_TEST_PROFILE_DIR");
if (isXpcshell) {
AddonTestUtils.createAppInfo(
"xpcshell@tests.mozilla.org",
"XPCShell",
"42",
"42"
);
}
let gTestScope;
/**
* Initialize the test helper.
*
* @param {Object} testScope
* XPCShell or mochitest test scope (i.e. the global object of the test currently executed)
*/
function init(testScope) {
if (!testScope?.gTestPath && !testScope?.Assert) {
throw new Error("`JSObjectsTestUtils.init()` should be called with the (xpcshell or mochitest) test global object");
}
gTestScope = testScope;
if ("gTestPath" in testScope) {
AddonTestUtils.initMochitest(testScope);
} else {
AddonTestUtils.init(testScope);
}
const server = AddonTestUtils.createHttpServer({
hosts: ["example.com"],
});
server.registerPathHandler("/", (request, response) => {
response.setHeader("Content-Type", "text/html");
response.write(TEST_PAGE_HTML);
});
// Lookup for all preferences to toggle in order to have all the expected objects type functional
let prefValues = new Map();
for (const { prefs } of AllObjects) {
if (!prefs) {
continue;
}
for (const elt of prefs) {
if (elt.length != 2) {
throw new Error("Each pref should be an array of two element [prefName, prefValue]. Got: "+elt);
}
const [ name, value ] = elt;
const otherValue = prefValues.get(name);
if (otherValue && otherValue != value) {
throw new Error(`Two javascript values in AllJavascriptTypes.mjs are expecting different values for '${name}' preference. (${otherValue} vs ${value})`);
}
prefValues.set(name, value);
if (typeof(value) == "boolean") {
Services.prefs.setBoolPref(name, value);
gTestScope.registerCleanupFunction(() => {
Services.prefs.clearUserPref(name);
});
} else {
throw new Error("Unsupported pref type: "+name+" = "+value);
}
}
}
}
let gExpectedValuesFilePath;
let gCurrentTestFolderUrl;
const chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
Ci.nsIChromeRegistry
);
function loadExpectedValues(expectedValuesFileName) {
const isUpdate = Services.env.get(UPDATE_SNAPSHOT_ENV) == "true";
dump(`JS Objects test: ${isUpdate ? "Update" : "Check"} ${expectedValuesFileName}\n`);
// Depending on the test suite, mochitest will expose `gTextPath` which is a chrome://
// for the current test file.
// Otherwise xpcshell will expose `resource://test/` for the current test folder.
gCurrentTestFolderUrl = "gTestPath" in gTestScope
? gTestScope.gTestPath.substr(0, gTestScope.gTestPath.lastIndexOf("/")) + "/"
// Build the URL for the test data file
const url = gCurrentTestFolderUrl + expectedValuesFileName;
// Resolve the test data file URL into a file absolute path
if (url.startsWith("chrome")) {
const chromeURL = Services.io.newURI(url);
gExpectedValuesFilePath = chromeRegistry
.convertChromeURL(chromeURL)
.QueryInterface(Ci.nsIFileURL).file.path;
} else if (url.startsWith("resource")) {
const resURL = Services.io.newURI(url);
const resHandler = Services.io.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler);
gExpectedValuesFilePath = Services.io.newURI(resHandler.resolveURI(resURL)).QueryInterface(Ci.nsIFileURL).file.path;
}
let expectedValues;
if (!isUpdate) {
dump(`Loading test data file: ${url}\n`);
return ChromeUtils.importESModule(url).default;
}
return null;
}
async function mayBeSaveExpectedValues(evaledStrings, newExpectedValues) {
if (!newExpectedValues?.length) {
return;
}
if (evaledStrings.length != newExpectedValues.length) {
throw new Error("Unexpected discrepencies between the reported evaled strings and expected values");
}
const filePath = gExpectedValuesFilePath;
const assertionValues = [];
let i = 0;
for (const value of newExpectedValues) {
let evaled = evaledStrings[i];
// Remove any first empty line
evaled = evaled.replace(/^\s*\n/, "");
// remove the unnecessary indentation
const m = evaled.match(/^( +)/);
if (m && m[1]) {
const regexp = new RegExp("^"+m[1], "gm");
evaled = evaled.replace(regexp, "");
}
// Ensure prefixing all new lines in the evaled string with " //"
// to keep it being in a code comment.
evaled = evaled.replace(/\r?\n/g, "\n // ");
assertionValues.push(
" // " + evaled +
"\n" +
" " +
JSON.stringify(value, null, 2) +
","
);
i++;
}
const fileContent = `/* Any copyright is dedicated to the Public Domain.
/*
* THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND.
*
*/
export default [
${assertionValues.join("\n\n")}
];`;
dump("Writing: " + fileContent + " in " + filePath + "\n");
await IOUtils.write(filePath, new TextEncoder().encode(fileContent));
}
async function runTest(expectedValuesFileName, testFunction) {
if (!gTestScope) {
throw new Error("`JSObjectsTestUtils.init()` should be called before `runTest()`");
}
if (typeof (expectedValuesFileName) != "string") {
throw new Error("`JSObjectsTestUtils.runTest()` first argument should be a data file name");
}
if (typeof (testFunction) != "function") {
throw new Error("`JSObjectsTestUtils.runTest()` second argument should be a test function");
}
let expectedValues = loadExpectedValues(expectedValuesFileName);
if (expectedValues) {
// Clone the Array as we are going to mutate it via Array.shift().
expectedValues = [...expectedValues];
}
const evaledStrings = [];
const newExpectedValues = [];
let failed = false;
for (const objectDescription of AllObjects) {
if (objectDescription.disabled) {
continue;
}
const { context, expression } = objectDescription;
if (!Object.values(CONTEXTS).includes(context)) {
throw new Error("Missing, or invalid context in: " + JSON.stringify(objectDescription));
}
if (!expression) {
throw new Error("Missing a value in: " + JSON.stringify(objectDescription));
}
const actual = await testFunction({ context, expression });
// Ignore this JS object as the test function did not return any actual value.
// We assume none of the tests would store "undefined" as a target value.
if (actual == undefined) {
continue;
}
const testPath = "gtestPath" in gTestScope ? gTestScope.gTestPath.replace("chrome://mochitest/content/browser/", "") : "path/to/your/xpcshell/test";
const failureMessage = `This is a JavaScript value processing test, which includes an automatically generated snapshot file (${expectedValuesFileName}).\n` +
"You may update this file by running:`\n" +
` $ mach test ${testPath} --headless --setenv ${UPDATE_SNAPSHOT_ENV}=true\n` +
"And then carefuly review if the result is valid regarding your ongoing changes.\n" +
const isMochitest = "gTestPath" in gTestScope;
const isXpcshell = !isMochitest;
// If we aren't in "update" mode, we are reading assertion values from $EXPECTED_VALUES_FILE
// and will assert the current returned values against these values
if (expectedValues) {
const expected = expectedValues.shift();
try {
gTestScope.Assert.deepEqual(actual, expected, `Got expected output for "${expression}"`);
} catch(e) {
// deepEqual only throws in case of differences when running in XPCShell tests. Mochitest won't throw and keep running.
// XPCShell will stop at the first failing assertion, so ensure showing our failure message and ok() will throw and stop the test.
if (isXpcshell) {
gTestScope.Assert.ok(false, failureMessage);
}
throw e;
}
// As mochitest won't throw when calling deepEqual with differences in the objects,
// we have to recompute the difference in order to know if any of the tests failed.
if (isMochitest && !failed && !ObjectUtils.deepEqual(actual, expected)) {
failed = true;
}
} else {
// Otherwise, if we are in update mode, we will collected all current values
// in order to store them in $EXPECTED_VALUES_FILE
//
// Force casting to string, in case this is a function.
evaledStrings.push(String(expression));
newExpectedValues.push(actual);
}
}
if (failed) {
const failureMessage = "This is a JavaScript value processing test, which includes an automatically generated snapshot file.\n" +
"If the change made to that snapshot file makes sense, you may simply update them by running:`\n" +
" $ mach test ${testPath} --headless --setenv UPDATE_EXPECTED_VALUES=true\n" +
"`More info in devtools/shared/tests/objects/README.md\n";
gTestScope.Assert.ok(false, failureMessage);
}
mayBeSaveExpectedValues(evaledStrings, newExpectedValues);
}
export const JSObjectsTestUtils = { init, runTest };