--- a/browser/modules/test/browser/browser_ProcessHangNotifications.js
+++ b/browser/modules/test/browser/browser_ProcessHangNotifications.js
@@ -1,8 +1,12 @@
+/* globals ProcessHangMonitor */
+
+const { WebExtensionPolicy } =
+ Cu.getGlobalForObject(Cu.import("resource://gre/modules/Services.jsm", {}));
Cu.import("resource://gre/modules/UpdateUtils.jsm");
function getNotificationBox(aWindow) {
return aWindow.document.getElementById("high-priority-global-notificationbox");
}
function promiseNotificationShown(aWindow, aName) {
@@ -10,173 +14,359 @@ function promiseNotificationShown(aWindo
let notification = getNotificationBox(aWindow);
notification.addEventListener("AlertActive", function() {
is(notification.allNotifications.length, 1, "Notification Displayed.");
resolve(notification);
}, {once: true});
});
}
-function promiseReportCallMade(aValue) {
- return new Promise((resolve) => {
- let old = gTestHangReport.testCallback;
- gTestHangReport.testCallback = function(val) {
- gTestHangReport.testCallback = old;
- is(aValue, val, "was the correct method call made on the hang report object?");
- resolve();
- };
- });
-}
-
function pushPrefs(...aPrefs) {
return SpecialPowers.pushPrefEnv({"set": aPrefs});
}
function popPrefs() {
return SpecialPowers.popPrefEnv();
}
-let gTestHangReport = {
- SLOW_SCRIPT: 1,
- PLUGIN_HANG: 2,
+const TEST_ACTION_UNKNOWN = 0;
+const TEST_ACTION_CANCELLED = 1;
+const TEST_ACTION_TERMSCRIPT = 2;
+const TEST_ACTION_TERMPLUGIN = 3;
+const TEST_ACTION_TERMGLOBAL = 4;
+const SLOW_SCRIPT = 1;
+const PLUGIN_HANG = 2;
+const ADDON_HANG = 3;
+const ADDON_ID = "fake-addon";
- TEST_CALLBACK_CANCELED: 1,
- TEST_CALLBACK_TERMSCRIPT: 2,
- TEST_CALLBACK_TERMPLUGIN: 3,
+/**
+ * A mock nsIHangReport that we can pass through nsIObserverService
+ * to trigger notifications.
+ *
+ * @param hangType
+ * One of SLOW_SCRIPT, PLUGIN_HANG, ADDON_HANG.
+ */
+let TestHangReport = function(hangType = SLOW_SCRIPT) {
+ this.promise = new Promise((resolve, reject) => {
+ this._resolver = resolve;
+ });
- _hangType: 1,
- _tcb(aCallbackType) {},
+ if (hangType == ADDON_HANG) {
+ // Add-on hangs are actually script hangs, but have an associated
+ // add-on ID for us to blame.
+ this._hangType = SLOW_SCRIPT;
+ this._addonId = ADDON_ID;
+ } else {
+ this._hangType = hangType;
+ }
+}
+
+TestHangReport.prototype = {
+ SLOW_SCRIPT,
+ PLUGIN_HANG,
+
+ get addonId() {
+ return this._addonId;
+ },
get hangType() {
return this._hangType;
},
- set hangType(aValue) {
- this._hangType = aValue;
- },
-
- set testCallback(aValue) {
- this._tcb = aValue;
- },
-
QueryInterface(aIID) {
if (aIID.equals(Components.interfaces.nsIHangReport) ||
aIID.equals(Components.interfaces.nsISupports))
return this;
throw Components.results.NS_NOINTERFACE;
},
userCanceled() {
- this._tcb(this.TEST_CALLBACK_CANCELED);
+ this._resolver(TEST_ACTION_CANCELLED);
},
terminateScript() {
- this._tcb(this.TEST_CALLBACK_TERMSCRIPT);
+ this._resolver(TEST_ACTION_TERMSCRIPT);
},
terminatePlugin() {
- this._tcb(this.TEST_CALLBACK_TERMPLUGIN);
+ this._resolver(TEST_ACTION_TERMPLUGIN);
+ },
+
+ terminateGlobal() {
+ this._resolver(TEST_ACTION_TERMGLOBAL);
},
isReportForBrowser(aFrameLoader) {
return true;
}
};
// on dev edition we add a button for js debugging of hung scripts.
let buttonCount = (UpdateUtils.UpdateChannel == "aurora" ? 3 : 2);
+add_task(async function setup() {
+ // Create a fake WebExtensionPolicy that we can use for
+ // the add-on hang notification.
+ const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ const uuid = uuidGen.generateUUID().number.slice(1, -1);
+ let policy = new WebExtensionPolicy({
+ name: "Scapegoat",
+ id: ADDON_ID,
+ mozExtensionHostname: uuid,
+ baseURL: "file:///",
+ allowedOrigins: new MatchPatternSet([]),
+ localizeCallback() {},
+ });
+ policy.active = true;
+
+ registerCleanupFunction(() => {
+ policy.active = false;
+ });
+});
+
/**
* Test if hang reports receive a terminate script callback when the user selects
* stop in response to a script hang.
*/
-
add_task(async function terminateScriptTest() {
let promise = promiseNotificationShown(window, "process-hang");
- Services.obs.notifyObservers(gTestHangReport, "process-hang-report");
+ let hangReport = new TestHangReport();
+ Services.obs.notifyObservers(hangReport, "process-hang-report");
let notification = await promise;
let buttons = notification.currentNotification.getElementsByTagName("button");
// Fails on aurora on-push builds, bug 1232204
// is(buttons.length, buttonCount, "proper number of buttons");
// Click the "Stop It" button, we should get a terminate script callback
- gTestHangReport.hangType = gTestHangReport.SLOW_SCRIPT;
- promise = promiseReportCallMade(gTestHangReport.TEST_CALLBACK_TERMSCRIPT);
buttons[0].click();
- await promise;
+ let action = await hangReport.promise;
+ is(action, TEST_ACTION_TERMSCRIPT,
+ "Clicking 'Stop It' should have terminated the script.");
});
/**
* Test if hang reports receive user canceled callbacks after a user selects wait
* and the browser frees up from a script hang on its own.
*/
-
add_task(async function waitForScriptTest() {
+ let hangReport = new TestHangReport();
let promise = promiseNotificationShown(window, "process-hang");
- Services.obs.notifyObservers(gTestHangReport, "process-hang-report");
+ Services.obs.notifyObservers(hangReport, "process-hang-report");
let notification = await promise;
let buttons = notification.currentNotification.getElementsByTagName("button");
// Fails on aurora on-push builds, bug 1232204
// is(buttons.length, buttonCount, "proper number of buttons");
await pushPrefs(["browser.hangNotification.waitPeriod", 1000]);
- function nocbcheck() {
- ok(false, "received a callback?");
- }
- let oldcb = gTestHangReport.testCallback;
- gTestHangReport.testCallback = nocbcheck;
+ let ignoringReport = true;
+
+ hangReport.promise.then((action) => {
+ if (ignoringReport) {
+ ok(false,
+ "Hang report was somehow dealt with when it " +
+ "should have been ignored.");
+ } else {
+ is(action, TEST_ACTION_CANCELLED,
+ "Hang report should have been cancelled.");
+ }
+ });
+
// Click the "Wait" button this time, we shouldn't get a callback at all.
buttons[1].click();
- gTestHangReport.testCallback = oldcb;
// send another hang pulse, we should not get a notification here
- Services.obs.notifyObservers(gTestHangReport, "process-hang-report");
+ Services.obs.notifyObservers(hangReport, "process-hang-report");
is(notification.currentNotification, null, "no notification should be visible");
- gTestHangReport.testCallback = function() {};
- Services.obs.notifyObservers(gTestHangReport, "clear-hang-report");
- gTestHangReport.testCallback = oldcb;
+ // Make sure that any queued Promises have run to give our report-ignoring
+ // then() a chance to fire.
+ await Promise.resolve();
+
+ ignoringReport = false;
+ Services.obs.notifyObservers(hangReport, "clear-hang-report");
await popPrefs();
});
/**
* Test if hang reports receive user canceled callbacks after the content
* process stops sending hang notifications.
*/
-
add_task(async function hangGoesAwayTest() {
await pushPrefs(["browser.hangNotification.expiration", 1000]);
+ let hangReport = new TestHangReport();
let promise = promiseNotificationShown(window, "process-hang");
- Services.obs.notifyObservers(gTestHangReport, "process-hang-report");
+ Services.obs.notifyObservers(hangReport, "process-hang-report");
await promise;
- promise = promiseReportCallMade(gTestHangReport.TEST_CALLBACK_CANCELED);
- Services.obs.notifyObservers(gTestHangReport, "clear-hang-report");
- await promise;
+ Services.obs.notifyObservers(hangReport, "clear-hang-report");
+ let action = await hangReport.promise;
+ is(action, TEST_ACTION_CANCELLED,
+ "Hang report should have been cancelled.");
await popPrefs();
});
/**
* Tests if hang reports receive a terminate plugin callback when the user selects
* stop in response to a plugin hang.
*/
-
add_task(async function terminatePluginTest() {
+ let hangReport = new TestHangReport(PLUGIN_HANG);
let promise = promiseNotificationShown(window, "process-hang");
- Services.obs.notifyObservers(gTestHangReport, "process-hang-report");
+ Services.obs.notifyObservers(hangReport, "process-hang-report");
let notification = await promise;
let buttons = notification.currentNotification.getElementsByTagName("button");
// Fails on aurora on-push builds, bug 1232204
// is(buttons.length, buttonCount, "proper number of buttons");
// Click the "Stop It" button, we should get a terminate script callback
- gTestHangReport.hangType = gTestHangReport.PLUGIN_HANG;
- promise = promiseReportCallMade(gTestHangReport.TEST_CALLBACK_TERMPLUGIN);
buttons[0].click();
- await promise;
+ let action = await hangReport.promise;
+ is(action, TEST_ACTION_TERMPLUGIN,
+ "Expected the 'Stop it' button to terminate the plug-in");
+});
+
+/**
+ * Tests that if we're shutting down, any pre-existing hang reports will
+ * be terminated appropriately.
+ */
+add_task(async function terminateAtShutdown() {
+ let pausedHang = new TestHangReport(SLOW_SCRIPT);
+ Services.obs.notifyObservers(pausedHang, "process-hang-report");
+ ProcessHangMonitor.waitLonger(window);
+ ok(ProcessHangMonitor.findPausedReport(gBrowser.selectedBrowser),
+ "There should be a paused report for the selected browser.");
+
+ let pluginHang = new TestHangReport(PLUGIN_HANG);
+ let scriptHang = new TestHangReport(SLOW_SCRIPT);
+ let addonHang = new TestHangReport(ADDON_HANG);
+
+ [pluginHang, scriptHang, addonHang].forEach(hangReport => {
+ Services.obs.notifyObservers(hangReport, "process-hang-report");
+ });
+
+ // Simulate the browser being told to shutdown. This should cause
+ // hangs to terminate scripts / plugins.
+ ProcessHangMonitor.onQuitApplicationGranted();
+
+ // In case this test happens to throw before it can finish, make
+ // sure to reset the shutting-down state.
+ registerCleanupFunction(() => {
+ ProcessHangMonitor._shuttingDown = false;
+ });
+
+ let pausedAction = await pausedHang.promise;
+ let pluginAction = await pluginHang.promise;
+ let scriptAction = await scriptHang.promise;
+ let addonAction = await addonHang.promise;
+
+ is(pausedAction, TEST_ACTION_TERMSCRIPT,
+ "On shutdown, should have terminated script for paused script hang.");
+ is(pluginAction, TEST_ACTION_TERMPLUGIN,
+ "On shutdown, should have terminated plugin for plugin hang.");
+ is(scriptAction, TEST_ACTION_TERMSCRIPT,
+ "On shutdown, should have terminated script for script hang.");
+ is(addonAction, TEST_ACTION_TERMGLOBAL,
+ "On shutdown, should have terminated global for add-on hang.");
+
+ // ProcessHangMonitor should now be in the "shutting down" state,
+ // meaning that any further hangs should be handled immediately
+ // without user interaction.
+ let pluginHang2 = new TestHangReport(PLUGIN_HANG);
+ let scriptHang2 = new TestHangReport(SLOW_SCRIPT);
+ let addonHang2 = new TestHangReport(ADDON_HANG);
+
+ [pluginHang2, scriptHang2, addonHang2].forEach(hangReport => {
+ Services.obs.notifyObservers(hangReport, "process-hang-report");
+ });
+
+ let pluginAction2 = await pluginHang.promise;
+ let scriptAction2 = await scriptHang.promise;
+ let addonAction2 = await addonHang.promise;
+
+ is(pluginAction2, TEST_ACTION_TERMPLUGIN,
+ "On shutdown, should have terminated plugin for plugin hang.");
+ is(scriptAction2, TEST_ACTION_TERMSCRIPT,
+ "On shutdown, should have terminated script for script hang.");
+ is(addonAction2, TEST_ACTION_TERMGLOBAL,
+ "On shutdown, should have terminated global for add-on hang.");
+
+ ProcessHangMonitor._shuttingDown = false;
});
+
+/**
+ * Test that if there happens to be no open browser windows, that any
+ * hang reports that exist or appear while in this state will be handled
+ * automatically.
+ */
+add_task(async function terminateNoWindows() {
+ let testWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ let pausedHang = new TestHangReport(SLOW_SCRIPT);
+ Services.obs.notifyObservers(pausedHang, "process-hang-report");
+ ProcessHangMonitor.waitLonger(testWin);
+ ok(ProcessHangMonitor.findPausedReport(testWin.gBrowser.selectedBrowser),
+ "There should be a paused report for the selected browser.");
+
+ let pluginHang = new TestHangReport(PLUGIN_HANG);
+ let scriptHang = new TestHangReport(SLOW_SCRIPT);
+ let addonHang = new TestHangReport(ADDON_HANG);
+
+ [pluginHang, scriptHang, addonHang].forEach(hangReport => {
+ Services.obs.notifyObservers(hangReport, "process-hang-report");
+ });
+
+ // Quick and dirty hack to trick the window mediator into thinking there
+ // are no browser windows without actually closing all browser windows.
+ document.documentElement.setAttribute("windowtype", "navigator:browsertestdummy");
+
+ // In case this test happens to throw before it can finish, make
+ // sure to reset this.
+ registerCleanupFunction(() => {
+ document.documentElement.setAttribute("windowtype", "navigator:browser");
+ });
+
+ await BrowserTestUtils.closeWindow(testWin);
+
+ let pausedAction = await pausedHang.promise;
+ let pluginAction = await pluginHang.promise;
+ let scriptAction = await scriptHang.promise;
+ let addonAction = await addonHang.promise;
+
+ is(pausedAction, TEST_ACTION_TERMSCRIPT,
+ "With no open windows, should have terminated script for paused script hang.");
+ is(pluginAction, TEST_ACTION_TERMPLUGIN,
+ "With no open windows, should have terminated plugin for plugin hang.");
+ is(scriptAction, TEST_ACTION_TERMSCRIPT,
+ "With no open windows, should have terminated script for script hang.");
+ is(addonAction, TEST_ACTION_TERMGLOBAL,
+ "With no open windows, should have terminated global for add-on hang.");
+
+ // ProcessHangMonitor should notice we're in the "no windows" state,
+ // so any further hangs should be handled immediately without user
+ // interaction.
+ let pluginHang2 = new TestHangReport(PLUGIN_HANG);
+ let scriptHang2 = new TestHangReport(SLOW_SCRIPT);
+ let addonHang2 = new TestHangReport(ADDON_HANG);
+
+ [pluginHang2, scriptHang2, addonHang2].forEach(hangReport => {
+ Services.obs.notifyObservers(hangReport, "process-hang-report");
+ });
+
+ let pluginAction2 = await pluginHang.promise;
+ let scriptAction2 = await scriptHang.promise;
+ let addonAction2 = await addonHang.promise;
+
+ is(pluginAction2, TEST_ACTION_TERMPLUGIN,
+ "With no open windows, should have terminated plugin for plugin hang.");
+ is(scriptAction2, TEST_ACTION_TERMSCRIPT,
+ "With no open windows, should have terminated script for script hang.");
+ is(addonAction2, TEST_ACTION_TERMGLOBAL,
+ "With no open windows, should have terminated global for add-on hang.");
+
+ document.documentElement.setAttribute("windowtype", "navigator:browser");
+});