Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/* globals Assert */
/* globals info */
/**
* This file contains utilities that can be shared between xpcshell tests and mochitests.
*/
const { ProfilerTestUtils } = ChromeUtils.importESModule(
);
/**
* This is a helper function be able to run `await wait(500)`. Unfortunately
* this is needed as the act of collecting functions relies on the periodic
* sampling of the threads. See:
*
* @param {number} time
* @returns {Promise}
*/
async function wait(time) {
return new Promise(resolve => {
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
setTimeout(resolve, time);
});
}
/**
* This escapes all characters that have a special meaning in RegExps.
* so it is licence MIT and:
* See the full license in https://raw.githubusercontent.com/sindresorhus/escape-string-regexp/main/license.
* @param {string} string The string to be escaped
* @returns {string} The result
*/
function escapeStringRegexp(string) {
if (typeof string !== "string") {
throw new TypeError("Expected a string");
}
// Escape characters with special meaning either inside or outside character
// sets. Use a simple backslash escape when it’s always valid, and a `\xnn`
// escape when the simpler form would be disallowed by Unicode patterns’
// stricter grammar.
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
}
/** ------ Utility functions and definitions for raising POSIX signals ------ */
ChromeUtils.defineESModuleGetters(this, {
Downloads: "resource://gre/modules/Downloads.sys.mjs",
});
// Find the path for a profile written to disk because of signals
async function getFullProfilePath(pid) {
// Initially look for "MOZ_UPLOAD_DIR". If this exists, firefox will write the
// profile file here, so this is where we need to look.
let path = Services.env.get("MOZ_UPLOAD_DIR");
if (!path) {
path = await Downloads.getSystemDownloadsDirectory();
}
return PathUtils.join(path, `profile_0_${pid}.json`);
}
// Hardcode the constants SIGUSR1 and SIGUSR2.
// This is an absolutely terrible idea, as they are implementation defined!
// However, it turns out that for 99% of the platforms we care about, and for
// 99.999% of the platforms we test, these constants are, well, constant.
// Additionally, these constants are only for _testing_ the signal handling
// feature - the actual feature relies on platform specific definitions. This
// may cause a mismatch if we test on on, say, a gnu hurd kernel, or on a
// linux kernel running on sparc, but the feature will not break - only
// the testing.
const SIGUSR1 = Services.appinfo.OS === "Darwin" ? 30 : 10;
const SIGUSR2 = Services.appinfo.OS === "Darwin" ? 31 : 12;
// Derived heavily from equivalent sandbox testing code. For more details see:
function raiseSignal(pid, sig) {
const { ctypes } = ChromeUtils.importESModule(
"resource://gre/modules/ctypes.sys.mjs"
);
// Derived from functionality in js/src/devtools/rootAnalysis/utility.js
function openLibrary(names) {
for (const name of names) {
try {
return ctypes.open(name);
} catch (e) {}
}
return undefined;
}
try {
const libc = openLibrary([
"libc.so.6",
"libc.so",
"libc.dylib",
"libSystem.B.dylib",
]);
if (!libc) {
info("Failed to open any libc shared object");
return { ok: false };
}
// This choice of typing for `pid` is complex, and brittle, as it's platform
// dependent. Getting it wrong can result in incoreect generation/calling of
// the `kill` function. Unfortunately, as it's defined as `pid_t` in a
// header, we can't easily get access to it. For now, we just use an
// integer, and hope that the system int size aligns with the `pid_t` size.
const kill = libc.declare(
"kill",
ctypes.default_abi,
ctypes.int, // return value
ctypes.int32_t, // pid
ctypes.int // sig
);
let kres = kill(pid, sig);
if (kres != 0) {
info(`Kill returned a non-zero result ${kres}.`);
return { ok: false };
}
libc.close();
} catch (e) {
info(`Exception ${e} thrown while trying to call kill`);
return { ok: false };
}
return { ok: true };
}
/** ------ Assertions helper ------ */
/**
* This assert helper function makes it easy to check a lot of properties in an
* object. We augment Assert.sys.mjs to make it easier to use.
*/
Object.assign(Assert, {
/*
* It checks if the properties on the right are all present in the object on
* the left. Note that the object might still have other properties (see
* objectContainsOnly below if you want the stricter form).
*
* The basic form does basic equality on each expected property:
*
* Assert.objectContains(fixture, {
* foo: "foo",
* bar: 1,
* baz: true,
* });
*
* But it also has a more powerful form with expectations. The available
* expectations are:
* - any(): this only checks for the existence of the property, not its value
* - number(), string(), boolean(), bigint(), function(), symbol(), object():
* this checks if the value is of this type
* - objectContains(expected): this applies Assert.objectContains()
* recursively on this property.
* - stringContains(needle): this checks if the expected value is included in
* the property value.
* - stringMatches(regexp): this checks if the property value matches this
* regexp. The regexp can be passed as a string, to be dynamically built.
*
* example:
*
* Assert.objectContains(fixture, {
* name: Expect.stringMatches(`Load \\d+:.*${url}`),
* data: Expect.objectContains({
* status: "STATUS_STOP",
* URI: Expect.stringContains("https://"),
* requestMethod: "GET",
* contentType: Expect.string(),
* startTime: Expect.number(),
* cached: Expect.boolean(),
* }),
* });
*
* Each expectation will translate into one or more Assert call. Therefore if
* one expectation fails, this will be clearly visible in the test output.
*
* Expectations can also be normal functions, for example:
*
* Assert.objectContains(fixture, {
* number: value => Assert.greater(value, 5)
* });
*
* Note that you'll need to use Assert inside this function.
*/
objectContains(object, expectedProperties) {
// Basic tests: we don't want to run other assertions if these tests fail.
if (typeof object !== "object") {
this.ok(
false,
`The first parameter should be an object, but found: ${object}.`
);
return;
}
if (typeof expectedProperties !== "object") {
this.ok(
false,
`The second parameter should be an object, but found: ${expectedProperties}.`
);
return;
}
for (const key of Object.keys(expectedProperties)) {
const expected = expectedProperties[key];
if (!(key in object)) {
this.report(
true,
object,
expectedProperties,
`The object should contain the property "${key}", but it's missing.`
);
continue;
}
if (typeof expected === "function") {
// This is a function, so let's call it.
expected(
object[key],
`The object should contain the property "${key}" with an expected value and type.`
);
} else {
// Otherwise, we check for equality.
this.equal(
object[key],
expectedProperties[key],
`The object should contain the property "${key}" with an expected value.`
);
}
}
},
/**
* This is very similar to the previous `objectContains`, but this also looks
* at the number of the objects' properties. Thus this will fail if the
* objects don't have the same properties exactly.
*/
objectContainsOnly(object, expectedProperties) {
// Basic tests: we don't want to run other assertions if these tests fail.
if (typeof object !== "object") {
this.ok(
false,
`The first parameter should be an object but found: ${object}.`
);
return;
}
if (typeof expectedProperties !== "object") {
this.ok(
false,
`The second parameter should be an object but found: ${expectedProperties}.`
);
return;
}
// In objectContainsOnly, we specifically want to check if all properties
// from the fixture object are expected.
// We'll be failing a test only for the specific properties that weren't
// expected, and only fail with one message, so that the test outputs aren't
// spammed.
const extraProperties = [];
for (const fixtureKey of Object.keys(object)) {
if (!(fixtureKey in expectedProperties)) {
extraProperties.push(fixtureKey);
}
}
if (extraProperties.length) {
// Some extra properties have been found.
this.report(
true,
object,
expectedProperties,
`These properties are present, but shouldn't: "${extraProperties.join(
'", "'
)}".`
);
}
// Now, let's carry on the rest of our work.
this.objectContains(object, expectedProperties);
},
});
const Expect = {
any:
() =>
() => {} /* We don't check anything more than the presence of this property. */,
};
/* These functions are part of the Assert object, and we want to reuse them. */
[
"stringContains",
"stringMatches",
"objectContains",
"objectContainsOnly",
].forEach(
assertChecker =>
(Expect[assertChecker] =
expected =>
(actual, ...moreArgs) =>
Assert[assertChecker](actual, expected, ...moreArgs))
);
/* These functions will only check for the type. */
[
"number",
"string",
"boolean",
"bigint",
"symbol",
"object",
"function",
].forEach(type => (Expect[type] = makeTypeChecker(type)));
function makeTypeChecker(type) {
return (...unexpectedArgs) => {
if (unexpectedArgs.length) {
throw new Error(
"Type checkers expectations aren't expecting any argument."
);
}
return (actual, message) => {
const isCorrect = typeof actual === type;
Assert.report(!isCorrect, actual, type, message, "has type");
};
};
}
/* ------ End of assertion helper ------ */