Bug 1391594 - Add a test that checks that hangs are dealt with on shutdown. r?billm draft
authorMike Conley <mconley@mozilla.com>
Mon, 02 Oct 2017 14:37:42 -0400
changeset 682773 5e5733884c26d3e854bf1e1b36cf38aa54267731
parent 682772 3ea3490286365321e793ae2a2dc53f0f3bad5bef
child 682774 659d3438ec8a887e683b03f0a4f65c910a2f1d3b
child 682843 7d138e2aef65a9ce97220893951fdc9116eaf2b5
push id85140
push usermconley@mozilla.com
push dateWed, 18 Oct 2017 18:30:30 +0000
reviewersbillm
bugs1391594
milestone58.0a1
Bug 1391594 - Add a test that checks that hangs are dealt with on shutdown. r?billm MozReview-Commit-ID: FvHrufdCw6d
browser/modules/test/browser/browser_ProcessHangNotifications.js
--- 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");
+});