339 lines
11 KiB
JavaScript
339 lines
11 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/. */
|
||
|
||
/* globals Assert */
|
||
/* globals info */
|
||
|
||
/**
|
||
* This file contains utilities that can be shared between xpcshell tests and mochitests.
|
||
*/
|
||
|
||
const { ProfilerTestUtils } = ChromeUtils.importESModule(
|
||
"resource://testing-common/ProfilerTestUtils.sys.mjs"
|
||
);
|
||
|
||
/**
|
||
* 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:
|
||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1529053
|
||
*
|
||
* @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.
|
||
* This was stolen from https://github.com/sindresorhus/escape-string-regexp and
|
||
* so it is licence MIT and:
|
||
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com).
|
||
* 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, icecat 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:
|
||
// https://searchfox.org/mozilla-central/rev/1aaacaeb4fa3aca6837ecc157e43e947229ba8ce/security/sandbox/test/browser_content_sandbox_utils.js#89
|
||
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 };
|
||
}
|
||
|
||
// c.f. https://man7.org/linux/man-pages/man2/kill.2.html
|
||
// 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 ------ */
|