mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-19 08:15:31 +00:00
Bug 1561150: Support test assertions in SpecialPowers.spawn sandboxes. r=nika
Differential Revision: https://phabricator.services.mozilla.com/D35747 --HG-- extra : rebase_source : 62aa469b15c2ebfb0de299c163b26ed68361ee1e extra : source : 0b3e2164f1283b639782b41b7fd640986d3feca5
This commit is contained in:
parent
b459f53a11
commit
3c304b4239
@ -22,6 +22,7 @@ support-files = file_SpecialPowersFrame1.html
|
||||
support-files =
|
||||
specialPowers_framescript.js
|
||||
[test_SpecialPowersPushPrefEnv.html]
|
||||
[test_SpecialPowersSandbox.html]
|
||||
[test_SpecialPowersSpawn.html]
|
||||
support-files = file_spawn.html
|
||||
[test_SimpletestGetTestFileURL.html]
|
||||
|
@ -0,0 +1,96 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test for SpecialPowers sandboxes</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<iframe id="iframe"></iframe>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Tests that the shared sandbox functionality for cross-process script
|
||||
* execution works as expected. In particular, ensures that Assert methods
|
||||
* report the correct diagnostics in the caller scope.
|
||||
*/
|
||||
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* globals SpecialPowers, Assert */
|
||||
|
||||
async function interceptDiagnostics(func) {
|
||||
let originalRecord = SimpleTest.record;
|
||||
try {
|
||||
let diags = [];
|
||||
|
||||
SimpleTest.record = (condition, name, diag, stack) => {
|
||||
diags.push({condition, name, diag, stack});
|
||||
};
|
||||
|
||||
await func();
|
||||
|
||||
return diags;
|
||||
} finally {
|
||||
SimpleTest.record = originalRecord;
|
||||
}
|
||||
}
|
||||
|
||||
add_task(async function() {
|
||||
let frame = document.getElementById("iframe");
|
||||
frame.src = "https://example.com/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html";
|
||||
|
||||
await new Promise(resolve => {
|
||||
frame.addEventListener("load", resolve, {once: true});
|
||||
});
|
||||
|
||||
let expected = [
|
||||
[false, "Thing - 1 == 2", "got 1, expected 2 (operator ==)"],
|
||||
[true, "Hmm - 1 == 1", undefined],
|
||||
[true, "Yay. - true == true", undefined],
|
||||
[false, "Boo!. - false == true", "got false, expected true (operator ==)"],
|
||||
];
|
||||
|
||||
// Test that a representative variety of assertions work as expected, and
|
||||
// trigger the expected calls to the harness's reporting function.
|
||||
//
|
||||
// Note: Assert.jsm has its own tests, and defers all of its reporting to a
|
||||
// single reporting function, so we don't need to test it comprehensively. We
|
||||
// just need to make sure that the general functionality works as expected.
|
||||
let tests = {
|
||||
"SpecialPowers.spawn": () => {
|
||||
return SpecialPowers.spawn(frame, [], () => {
|
||||
Assert.equal(1, 2, "Thing");
|
||||
Assert.equal(1, 1, "Hmm");
|
||||
Assert.ok(true, "Yay.");
|
||||
Assert.ok(false, "Boo!.");
|
||||
});
|
||||
},
|
||||
"SpecialPowers.loadChromeScript": async () => {
|
||||
let script = SpecialPowers.loadChromeScript(() => {
|
||||
this.addMessageListener("ping", () => "pong");
|
||||
|
||||
Assert.equal(1, 2, "Thing");
|
||||
Assert.equal(1, 1, "Hmm");
|
||||
Assert.ok(true, "Yay.");
|
||||
Assert.ok(false, "Boo!.");
|
||||
});
|
||||
|
||||
await script.sendQuery("ping");
|
||||
script.destroy();
|
||||
},
|
||||
};
|
||||
|
||||
for (let [name, func] of Object.entries(tests)) {
|
||||
info(`Starting task: ${name}`);
|
||||
|
||||
let diags = await interceptDiagnostics(func);
|
||||
|
||||
let results = diags.map(diag => [diag.condition, diag.name, diag.diag]);
|
||||
|
||||
isDeeply(results, expected, "Got expected assertions");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -17,6 +17,8 @@ ChromeUtils.defineModuleGetter(this, "MockColorPicker",
|
||||
"resource://specialpowers/MockColorPicker.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "MockPermissionPrompt",
|
||||
"resource://specialpowers/MockPermissionPrompt.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "SpecialPowersSandbox",
|
||||
"resource://specialpowers/SpecialPowersSandbox.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "WrapPrivileged",
|
||||
"resource://specialpowers/WrapPrivileged.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
|
||||
@ -130,6 +132,28 @@ class SpecialPowersAPI extends JSWindowActorChild {
|
||||
this._extensionListeners = null;
|
||||
}
|
||||
|
||||
receiveMessage(message) {
|
||||
switch (message.name) {
|
||||
case "Assert": {
|
||||
// An assertion has been done in a mochitest chrome script
|
||||
let {name, passed, stack, diag} = message.data;
|
||||
|
||||
let SimpleTest = (
|
||||
this.contentWindow &&
|
||||
this.contentWindow.wrappedJSObject.SimpleTest);
|
||||
|
||||
if (SimpleTest) {
|
||||
SimpleTest.record(passed, name, diag, stack && stack.formattedStack);
|
||||
} else {
|
||||
// Well, this is unexpected.
|
||||
dump(name + "\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*
|
||||
* Privileged object wrapping API
|
||||
*
|
||||
@ -337,7 +361,6 @@ class SpecialPowersAPI extends JSWindowActorChild {
|
||||
destroy: () => {
|
||||
listeners = [];
|
||||
this._removeMessageListener("SPChromeScriptMessage", chromeScript);
|
||||
this._removeMessageListener("SPChromeScriptAssert", chromeScript);
|
||||
},
|
||||
|
||||
receiveMessage: (aMessage) => {
|
||||
@ -356,56 +379,11 @@ class SpecialPowersAPI extends JSWindowActorChild {
|
||||
for (let listener of listeners.filter(o => o.name == name)) {
|
||||
result = listener.listener(message);
|
||||
}
|
||||
} else if (aMessage.name == "SPChromeScriptAssert") {
|
||||
assert(aMessage.json);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
this._addMessageListener("SPChromeScriptMessage", chromeScript);
|
||||
this._addMessageListener("SPChromeScriptAssert", chromeScript);
|
||||
|
||||
let assert = json => {
|
||||
// An assertion has been done in a mochitest chrome script
|
||||
let {name, err, message, stack} = json;
|
||||
|
||||
// Try to fetch a test runner from the mochitest
|
||||
// in order to properly log these assertions and notify
|
||||
// all usefull log observers
|
||||
let window = this.contentWindow;
|
||||
let parentRunner, repr = o => o;
|
||||
if (window) {
|
||||
window = window.wrappedJSObject;
|
||||
parentRunner = window.TestRunner;
|
||||
if (window.repr) {
|
||||
repr = window.repr;
|
||||
}
|
||||
}
|
||||
|
||||
// Craft a mochitest-like report string
|
||||
var resultString = err ? "TEST-UNEXPECTED-FAIL" : "TEST-PASS";
|
||||
var diagnostic =
|
||||
message ? message :
|
||||
("assertion @ " + stack.filename + ":" + stack.lineNumber);
|
||||
if (err) {
|
||||
diagnostic +=
|
||||
" - got " + repr(err.actual) +
|
||||
", expected " + repr(err.expected) +
|
||||
" (operator " + err.operator + ")";
|
||||
}
|
||||
var msg = [resultString, name, diagnostic].join(" | ");
|
||||
if (parentRunner) {
|
||||
if (err) {
|
||||
parentRunner.addFailedTest(name);
|
||||
parentRunner.error(msg);
|
||||
} else {
|
||||
parentRunner.log(msg);
|
||||
}
|
||||
} else {
|
||||
// When we are running only a single mochitest, there is no test runner
|
||||
dump(msg + "\n");
|
||||
}
|
||||
};
|
||||
|
||||
return this.wrap(chromeScript);
|
||||
}
|
||||
@ -1236,6 +1214,10 @@ class SpecialPowersAPI extends JSWindowActorChild {
|
||||
* passed will be copied via structured clone, as will its return
|
||||
* value.
|
||||
*
|
||||
* The sandbox also has access to an Assert object, as provided by
|
||||
* Assert.jsm. Any assertion methods called before the task resolves
|
||||
* will be relayed back to the test environment of the caller.
|
||||
*
|
||||
* @param {BrowsingContext or FrameLoaderOwner or WindowProxy} target
|
||||
* The target in which to run the task. This may be any element
|
||||
* which implements the FrameLoaderOwner interface (including
|
||||
@ -1269,32 +1251,26 @@ class SpecialPowersAPI extends JSWindowActorChild {
|
||||
browsingContext = BrowsingContext.getFromWindow(target);
|
||||
}
|
||||
|
||||
let {caller} = Components.stack;
|
||||
return this.sendQuery("Spawn", {
|
||||
browsingContext,
|
||||
args,
|
||||
task: String(task),
|
||||
caller: {
|
||||
filename: caller.filename,
|
||||
lineNumber: caller.lineNumber,
|
||||
},
|
||||
caller: SpecialPowersSandbox.getCallerInfo(Components.stack.caller),
|
||||
});
|
||||
}
|
||||
|
||||
_spawnTask(task, args, caller) {
|
||||
let sb = Cu.Sandbox(Cu.getGlobalForObject({}),
|
||||
{wantGlobalProperties: ["ChromeUtils"]});
|
||||
_spawnTask(task, args, caller, taskId) {
|
||||
let sb = new SpecialPowersSandbox(null, data => {
|
||||
this.sendAsyncMessage("ProxiedAssert", {taskId, data});
|
||||
});
|
||||
|
||||
sb.SpecialPowers = this;
|
||||
Object.defineProperty(sb, "content", {
|
||||
sb.sandbox.SpecialPowers = this;
|
||||
Object.defineProperty(sb.sandbox, "content", {
|
||||
get: () => { return this.contentWindow; },
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
let func = Cu.evalInSandbox(`(${task})`, sb, undefined,
|
||||
caller.filename, caller.lineNumber);
|
||||
|
||||
return func(...args);
|
||||
return sb.execute(task, args, caller);
|
||||
}
|
||||
|
||||
getFocusedElementForWindow(targetWindow, aDeep) {
|
||||
|
@ -14,6 +14,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
|
||||
PerTestCoverageUtils: "resource://testing-common/PerTestCoverageUtils.jsm",
|
||||
ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
|
||||
SpecialPowersSandbox: "resource://specialpowers/SpecialPowersSandbox.jsm",
|
||||
});
|
||||
|
||||
class SpecialPowersError extends Error {
|
||||
@ -99,6 +100,10 @@ function doPrefEnvOp(fn) {
|
||||
}
|
||||
}
|
||||
|
||||
// Supplies the unique IDs for tasks created by SpecialPowers.spawn(),
|
||||
// used to bounce assertion messages back down to the correct child.
|
||||
let nextTaskID = 1;
|
||||
|
||||
class SpecialPowersAPIParent extends JSWindowActorParent {
|
||||
constructor() {
|
||||
super();
|
||||
@ -106,6 +111,7 @@ class SpecialPowersAPIParent extends JSWindowActorParent {
|
||||
this._processCrashObserversRegistered = false;
|
||||
this._chromeScriptListeners = [];
|
||||
this._extensions = new Map();
|
||||
this._taskActors = new Map();
|
||||
}
|
||||
|
||||
_observe(aSubject, aTopic, aData) {
|
||||
@ -564,50 +570,35 @@ class SpecialPowersAPIParent extends JSWindowActorParent {
|
||||
// Setup a chrome sandbox that has access to sendAsyncMessage
|
||||
// and {add,remove}MessageListener in order to communicate with
|
||||
// the mochitest.
|
||||
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
||||
let sandboxOptions = Object.assign({wantGlobalProperties: ["ChromeUtils"]},
|
||||
aMessage.json.sandboxOptions);
|
||||
let sb = Cu.Sandbox(systemPrincipal, sandboxOptions);
|
||||
sb.sendAsyncMessage = (name, message) => {
|
||||
this.sendAsyncMessage("SPChromeScriptMessage",
|
||||
{ id, name, message });
|
||||
};
|
||||
sb.addMessageListener = (name, listener) => {
|
||||
this._chromeScriptListeners.push({ id, name, listener });
|
||||
};
|
||||
sb.removeMessageListener = (name, listener) => {
|
||||
let index = this._chromeScriptListeners.findIndex(function(obj) {
|
||||
return obj.id == id && obj.name == name && obj.listener == listener;
|
||||
});
|
||||
if (index >= 0) {
|
||||
this._chromeScriptListeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
sb.actorParent = this.manager;
|
||||
|
||||
// Also expose assertion functions
|
||||
let reporter = (err, message, stack) => {
|
||||
// Pipe assertions back to parent process
|
||||
this.sendAsyncMessage("SPChromeScriptAssert",
|
||||
{ id, name: scriptName, err, message,
|
||||
stack });
|
||||
};
|
||||
Object.defineProperty(sb, "assert", {
|
||||
get() {
|
||||
let scope = Cu.createObjectIn(sb);
|
||||
Services.scriptloader.loadSubScript("resource://specialpowers/Assert.jsm",
|
||||
scope);
|
||||
|
||||
let assert = new scope.Assert(reporter);
|
||||
delete sb.assert;
|
||||
return sb.assert = assert;
|
||||
let sb = new SpecialPowersSandbox(
|
||||
scriptName,
|
||||
data => {
|
||||
this.sendAsyncMessage("Assert", data);
|
||||
},
|
||||
configurable: true,
|
||||
aMessage.data);
|
||||
|
||||
Object.assign(sb.sandbox, {
|
||||
sendAsyncMessage: (name, message) => {
|
||||
this.sendAsyncMessage("SPChromeScriptMessage",
|
||||
{ id, name, message });
|
||||
},
|
||||
addMessageListener: (name, listener) => {
|
||||
this._chromeScriptListeners.push({ id, name, listener });
|
||||
},
|
||||
removeMessageListener: (name, listener) => {
|
||||
let index = this._chromeScriptListeners.findIndex(function(obj) {
|
||||
return obj.id == id && obj.name == name && obj.listener == listener;
|
||||
});
|
||||
if (index >= 0) {
|
||||
this._chromeScriptListeners.splice(index, 1);
|
||||
}
|
||||
},
|
||||
actorParent: this.manager,
|
||||
});
|
||||
|
||||
// Evaluate the chrome script
|
||||
try {
|
||||
Cu.evalInSandbox(jsScript, sb, "1.8", scriptName, 1);
|
||||
Cu.evalInSandbox(jsScript, sb.sandbox, "1.8", scriptName, 1);
|
||||
} catch (e) {
|
||||
throw new SpecialPowersError(
|
||||
"Error while executing chrome script '" + scriptName + "':\n" +
|
||||
@ -772,7 +763,21 @@ class SpecialPowersAPIParent extends JSWindowActorParent {
|
||||
let {browsingContext, task, args, caller} = aMessage.data;
|
||||
|
||||
let spParent = browsingContext.currentWindowGlobal.getActor("SpecialPowers");
|
||||
return spParent.sendQuery("Spawn", {task, args, caller});
|
||||
|
||||
let taskId = nextTaskID++;
|
||||
spParent._taskActors.set(taskId, this);
|
||||
|
||||
return spParent.sendQuery("Spawn", {task, args, caller, taskId}).finally(() => {
|
||||
spParent._taskActors.delete(taskId);
|
||||
});
|
||||
}
|
||||
|
||||
case "ProxiedAssert": {
|
||||
let {taskId, data} = aMessage.data;
|
||||
let actor = this._taskActors.get(taskId);
|
||||
|
||||
actor.sendAsyncMessage("Assert", data);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
case "SPRemoveAllServiceWorkers": {
|
||||
|
@ -122,8 +122,11 @@ class SpecialPowersChild extends SpecialPowersAPI {
|
||||
break;
|
||||
|
||||
case "Spawn":
|
||||
let {task, args, caller} = aMessage.data;
|
||||
return this._spawnTask(task, args, caller);
|
||||
let {task, args, caller, taskId} = aMessage.data;
|
||||
return this._spawnTask(task, args, caller, taskId);
|
||||
|
||||
default:
|
||||
return super.receiveMessage(aMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
71
testing/specialpowers/content/SpecialPowersSandbox.jsm
Normal file
71
testing/specialpowers/content/SpecialPowersSandbox.jsm
Normal file
@ -0,0 +1,71 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This modules handles creating and provisioning Sandboxes for
|
||||
* executing cross-process code from SpecialPowers. This allows all such
|
||||
* sandboxes to have a similar environment, and in particular allows
|
||||
* them to run test assertions in the target process and propagate
|
||||
* results back to the caller.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["SpecialPowersSandbox"];
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "Assert",
|
||||
"resource://testing-common/Assert.jsm");
|
||||
|
||||
class SpecialPowersSandbox {
|
||||
constructor(name, reportCallback, opts = {}) {
|
||||
this.name = name;
|
||||
this.reportCallback = reportCallback;
|
||||
|
||||
this._Assert = null;
|
||||
|
||||
this.sandbox = Cu.Sandbox(Cu.getGlobalForObject({}),
|
||||
Object.assign({wantGlobalProperties: ["ChromeUtils"]},
|
||||
opts.sandboxOptions));
|
||||
|
||||
for (let prop of ["assert", "Assert"]) {
|
||||
Object.defineProperty(this.sandbox, prop, {
|
||||
get: () => {
|
||||
return this.Assert;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static getCallerInfo(frame) {
|
||||
return {
|
||||
filename: frame.filename,
|
||||
lineNumber: frame.lineNumber,
|
||||
};
|
||||
}
|
||||
|
||||
get Assert() {
|
||||
if (!this._Assert) {
|
||||
this._Assert = new Assert((err, message, stack) => {
|
||||
this.report(err, message, stack);
|
||||
});
|
||||
}
|
||||
return this._Assert;
|
||||
}
|
||||
|
||||
report(err, name, stack) {
|
||||
let diag;
|
||||
if (err) {
|
||||
diag = `got ${uneval(err.actual)}, expected ${uneval(err.expected)} ` +
|
||||
`(operator ${err.operator})`;
|
||||
}
|
||||
|
||||
this.reportCallback({name, diag, passed: !err, stack});
|
||||
}
|
||||
|
||||
execute(task, args, caller) {
|
||||
let func = Cu.evalInSandbox(`(${task})`, this.sandbox, undefined,
|
||||
caller.filename, caller.lineNumber);
|
||||
return func(...args);
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ FINAL_TARGET_FILES.content += [
|
||||
'content/SpecialPowersAPIParent.jsm',
|
||||
'content/SpecialPowersChild.jsm',
|
||||
'content/SpecialPowersParent.jsm',
|
||||
'content/SpecialPowersSandbox.jsm',
|
||||
'content/WrapPrivileged.jsm',
|
||||
]
|
||||
|
||||
|
@ -163,6 +163,10 @@ add_task(async function test2() {
|
||||
is(result.username, "xhruser2", "Checking for username");
|
||||
is(result.password, "xhrpass2", "Checking for password");
|
||||
|
||||
// Wait for the assert from the parent script to run and send back its reply,
|
||||
// so it's processed before the test ends.
|
||||
await SpecialPowers.executeAfterFlushingMessageQueue();
|
||||
|
||||
newWin.close();
|
||||
});
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user