Bug 1393800 - Have mochitests expecting crashes wait for the crashes to be recorded before clean up; r?ted.mielczarek draft
authorGabriele Svelto <gsvelto@mozilla.com>
Fri, 25 Aug 2017 12:47:09 +0200
changeset 656501 82f3509fa90c5e93c2c416f47fc94b44c36cc3a7
parent 656346 04b6be50a2526c7a26a63715f441c47e1aa1f9be
child 729176 b67b209ecd76b5e9d8fe157b880f7dad7edef067
push id77251
push usergsvelto@mozilla.com
push dateThu, 31 Aug 2017 09:46:11 +0000
reviewersted.mielczarek
bugs1393800
milestone57.0a1
Bug 1393800 - Have mochitests expecting crashes wait for the crashes to be recorded before clean up; r?ted.mielczarek This patch includes a bunch of somewhat related fixes, these are: - Ensuring that when a mochitest calls SimpleTest.expectChildProcessCrash() the harness will wait for the crashes to be recorded before deleting the dump files. This involves a message round-trip between the content and parent process so to minimize its performance impact on all the non-crashing tests it is done only when required. - As an additional optimization, the SimpleTest harness will not send a message to the content process anymore whenever it receives an ipc:content-shutdown event, instead it does it only for abnormal shutdowns. - Manually fixing remaining mochitests causing crashes to wait for crashes to be recorded before finishing and deleting the dump files. - Modifying BrowserTestUtils.crashBrowser() so that it optionally does not delete the dump files, this is useful for tests that submit their dumps and thus delete them on their own. MozReview-Commit-ID: 4SLJ8BjJ18n
browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js
browser/base/content/test/tabcrashed/browser_clearEmail.js
dom/ipc/tests/chrome.ini
dom/ipc/tests/process_error.xul
dom/ipc/tests/process_error_contentscript.js
dom/plugins/test/mochitest/hang_test.js
dom/plugins/test/mochitest/test_busy_hang.xul
dom/plugins/test/mochitest/test_crash_notify.xul
dom/plugins/test/mochitest/test_idle_hang.xul
testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
testing/mochitest/tests/SimpleTest/TestRunner.js
testing/specialpowers/content/SpecialPowersObserver.jsm
testing/specialpowers/content/SpecialPowersObserverAPI.js
testing/specialpowers/content/specialpowers.js
testing/specialpowers/content/specialpowersAPI.js
--- a/browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js
+++ b/browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+
 /**
  * With e10s, plugins must run in their own process. This means we have
  * three processes at a minimum when we're running a plugin:
  *
  * 1) The main browser, or "chrome" process
  * 2) The content process hosting the plugin instance
  * 3) The plugin process
  *
@@ -74,61 +76,66 @@ function preparePlugin(browser, pluginFa
     });
     return plugin.runID;
   }).then((runID) => {
     browser.messageManager.sendAsyncMessage("BrowserPlugins:Test:ClearCrashData");
     return runID;
   });
 }
 
-add_task(async function setup() {
-  // Bypass click-to-play
-  setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
+// Bypass click-to-play
+setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
+
+// Deferred promise object used by the test to wait for the crash handler
+let crashDeferred = null;
 
-  // Clear out any minidumps we create from plugins - we really don't care
-  // about them.
-  let crashObserver = (subject, topic, data) => {
-    if (topic != "plugin-crashed") {
-      return;
-    }
+// Clear out any minidumps we create from plugins - we really don't care
+// about them.
+let crashObserver = (subject, topic, data) => {
+  if (topic != "plugin-crashed") {
+    return;
+  }
 
-    let propBag = subject.QueryInterface(Ci.nsIPropertyBag2);
-    let minidumpID = propBag.getPropertyAsAString("pluginDumpID");
+  let propBag = subject.QueryInterface(Ci.nsIPropertyBag2);
+  let minidumpID = propBag.getPropertyAsAString("pluginDumpID");
 
-    Services.crashmanager.ensureCrashIsPresent(minidumpID).then(() => {
-      let minidumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
-      minidumpDir.append("minidumps");
+  Services.crashmanager.ensureCrashIsPresent(minidumpID).then(() => {
+    let minidumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+    minidumpDir.append("minidumps");
 
-      let pluginDumpFile = minidumpDir.clone();
-      pluginDumpFile.append(minidumpID + ".dmp");
+    let pluginDumpFile = minidumpDir.clone();
+    pluginDumpFile.append(minidumpID + ".dmp");
 
-      let extraFile = minidumpDir.clone();
-      extraFile.append(minidumpID + ".extra");
+    let extraFile = minidumpDir.clone();
+    extraFile.append(minidumpID + ".extra");
 
-      ok(pluginDumpFile.exists(), "Found minidump");
-      ok(extraFile.exists(), "Found extra file");
+    ok(pluginDumpFile.exists(), "Found minidump");
+    ok(extraFile.exists(), "Found extra file");
 
-      pluginDumpFile.remove(false);
-      extraFile.remove(false);
-    });
-  };
+    pluginDumpFile.remove(false);
+    extraFile.remove(false);
+    crashDeferred.resolve();
+  });
+};
 
-  Services.obs.addObserver(crashObserver, "plugin-crashed");
-  // plugins.testmode will make BrowserPlugins:Test:ClearCrashData work.
-  Services.prefs.setBoolPref("plugins.testmode", true);
-  registerCleanupFunction(() => {
-    Services.prefs.clearUserPref("plugins.testmode");
-    Services.obs.removeObserver(crashObserver, "plugin-crashed");
-  });
+Services.obs.addObserver(crashObserver, "plugin-crashed");
+// plugins.testmode will make BrowserPlugins:Test:ClearCrashData work.
+Services.prefs.setBoolPref("plugins.testmode", true);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("plugins.testmode");
+  Services.obs.removeObserver(crashObserver, "plugin-crashed");
 });
 
 /**
  * In this case, the chrome process hears about the crash first.
  */
 add_task(async function testChromeHearsPluginCrashFirst() {
+  // Setup the crash observer promise
+  crashDeferred = PromiseUtils.defer();
+
   // Open a remote window so that we can run this test even if e10s is not
   // enabled by default.
   let win = await BrowserTestUtils.openNewBrowserWindow({remote: true});
   let browser = win.gBrowser.selectedBrowser;
 
   browser.loadURI(CRASH_URL);
   await BrowserTestUtils.browserLoaded(browser);
 
@@ -178,22 +185,26 @@ add_task(async function testChromeHearsP
       cancelable: true,
     });
 
     plugin.dispatchEvent(event);
     Assert.equal(statusDiv.getAttribute("status"), "please",
       "Should have been showing crash report UI");
   });
   await BrowserTestUtils.closeWindow(win);
+  await crashDeferred.promise;
 });
 
 /**
  * In this case, the content process hears about the crash first.
  */
 add_task(async function testContentHearsCrashFirst() {
+  // Setup the crash observer promise
+  crashDeferred = PromiseUtils.defer();
+
   // Open a remote window so that we can run this test even if e10s is not
   // enabled by default.
   let win = await BrowserTestUtils.openNewBrowserWindow({remote: true});
   let browser = win.gBrowser.selectedBrowser;
 
   browser.loadURI(CRASH_URL);
   await BrowserTestUtils.browserLoaded(browser);
 
@@ -248,9 +259,10 @@ add_task(async function testContentHears
                           .getAnonymousElementByAttribute(plugin, "anonid",
                                                           "submitStatus");
 
     Assert.equal(statusDiv.getAttribute("status"), "please",
       "Should have been showing crash report UI");
   });
 
   await BrowserTestUtils.closeWindow(win);
+  await crashDeferred.promise;
 });
--- a/browser/base/content/test/tabcrashed/browser_clearEmail.js
+++ b/browser/base/content/test/tabcrashed/browser_clearEmail.js
@@ -29,17 +29,19 @@ add_task(async function test_clear_email
     let originalEmail = prefs.getCharPref("email");
 
     // Pretend that we stored an email address from the previous
     // crash
     prefs.setCharPref("email", EMAIL);
     prefs.setBoolPref("emailMe", true);
 
     let tab = gBrowser.getTabForBrowser(browser);
-    await BrowserTestUtils.crashBrowser(browser);
+    await BrowserTestUtils.crashBrowser(browser,
+                                        /* shouldShowTabCrashPage */ true,
+                                        /* shouldClearMinidumps */ false);
     let doc = browser.contentDocument;
 
     // Since about:tabcrashed will run in the parent process, we can safely
     // manipulate its DOM nodes directly
     let emailMe = doc.getElementById("emailMe");
     emailMe.checked = false;
 
     let crashReport = promiseCrashReport({
--- a/dom/ipc/tests/chrome.ini
+++ b/dom/ipc/tests/chrome.ini
@@ -1,8 +1,7 @@
 [DEFAULT]
 skip-if = os == 'android'
 support-files =
   process_error.xul
-  process_error_contentscript.js
 
 [test_process_error.xul]
 skip-if = !crashreporter
--- a/dom/ipc/tests/process_error.xul
+++ b/dom/ipc/tests/process_error.xul
@@ -2,40 +2,36 @@
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
 	orient="vertical">
 
   <browser id="thebrowser" type="content" remote="true" />
   <script type="application/javascript"><![CDATA[
     Components.utils.import("resource://gre/modules/Services.jsm");
+    Components.utils.import("resource://testing-common/BrowserTestUtils.jsm");
 
     const ok = window.opener.wrappedJSObject.ok;
     const is = window.opener.wrappedJSObject.is;
     const done = window.opener.wrappedJSObject.done;
     const SimpleTest = window.opener.wrappedJSObject.SimpleTest;
 
     function crashObserver(subject, topic, data) {
       is(topic, 'ipc:content-shutdown', 'Received correct observer topic.');
       ok(subject instanceof Components.interfaces.nsIPropertyBag2,
          'Subject implements nsIPropertyBag2.');
 
-      var waitCrash = Promise.resolve();
       var dumpID;
       if ('nsICrashReporter' in Components.interfaces) {
         dumpID = subject.getPropertyAsAString('dumpID');
         ok(dumpID, "dumpID is present and not an empty string");
-        waitCrash = Services.crashmanager.ensureCrashIsPresent(dumpID);
       }
 
       Services.obs.removeObserver(crashObserver, 'ipc:content-shutdown');
-      waitCrash.then(done);
+      done();
     }
 
     Services.obs.addObserver(crashObserver, 'ipc:content-shutdown');
 
-    document.getElementById('thebrowser')
-            .QueryInterface(Components.interfaces.nsIFrameLoaderOwner)
-            .frameLoader.messageManager
-            .loadFrameScript('chrome://mochitests/content/chrome/dom/ipc/tests/process_error_contentscript.js', true);
+    BrowserTestUtils.crashBrowser(document.getElementById('thebrowser'));
   ]]></script>
 
 </window>
deleted file mode 100644
--- a/dom/ipc/tests/process_error_contentscript.js
+++ /dev/null
@@ -1,7 +0,0 @@
-Components.utils.import("resource://gre/modules/ctypes.jsm");
-
-privateNoteIntentionalCrash();
-
-var zero = new ctypes.intptr_t(8);
-var badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
-var crash = badptr.contents;
--- a/dom/plugins/test/mochitest/hang_test.js
+++ b/dom/plugins/test/mochitest/hang_test.js
@@ -37,24 +37,20 @@ var testObserver = {
     let extraData = parseKeyValuePairsFromFile(pluginExtraFile);
 
     // check additional dumps
 
     ok("additional_minidumps" in extraData, "got field for additional minidumps");
     let additionalDumps = extraData.additional_minidumps.split(',');
     ok(additionalDumps.indexOf('browser') >= 0, "browser in additional_minidumps");
 
-    let additionalDumpFiles = [];
     for (let name of additionalDumps) {
       let file = profD.clone();
       file.append(pluginId + "-" + name + ".dmp");
       ok(file.exists(), "additional dump '"+name+"' exists");
-      if (file.exists()) {
-        additionalDumpFiles.push(file);
-      }
     }
 
     // check cpu usage field
 
     ok("PluginCpuUsage" in extraData, "got extra field for plugin cpu usage");
     let cpuUsage = parseFloat(extraData["PluginCpuUsage"]);
     if (this.idleHang) {
       ok(cpuUsage == 0, "plugin cpu usage is 0%");
@@ -98,12 +94,10 @@ function onPluginCrashed(aEvent) {
   // allow either true or false here.
   ok("submittedCrashReport" in aEvent, "submittedCrashReport is a property of event");
   is(typeof aEvent.submittedCrashReport, "boolean", "submittedCrashReport is correct type");
 
   var os = Cc["@mozilla.org/observer-service;1"].
            getService(Ci.nsIObserverService);
   os.removeObserver(testObserver, "plugin-crashed");
 
-  Services.crashmanager.ensureCrashIsPresent(aEvent.pluginDumpID).then(() => {
-    SimpleTest.finish();
-  });
+  SimpleTest.finish();
 }
--- a/dom/plugins/test/mochitest/test_busy_hang.xul
+++ b/dom/plugins/test/mochitest/test_busy_hang.xul
@@ -14,18 +14,16 @@
   <script type="application/javascript">
     setTestPluginEnabledState(SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED);
   </script>
   <body xmlns="http://www.w3.org/1999/xhtml" onload="runTests()">
     <embed id="plugin1" type="application/x-test" width="200" height="200"></embed>
   </body>
   <script class="testbody" type="application/javascript">
     <![CDATA[
-Components.utils.import("resource://gre/modules/Services.jsm");
-
 SimpleTest.waitForExplicitFinish();
 SimpleTest.expectChildProcessCrash();
 
 function runTests() {
   // Default plugin hang timeout is too high for mochitests
   var prefs = Cc["@mozilla.org/preferences-service;1"]
                     .getService(Ci.nsIPrefBranch);
   var timeoutPref = "dom.ipc.plugins.timeoutSecs";
--- a/dom/plugins/test/mochitest/test_crash_notify.xul
+++ b/dom/plugins/test/mochitest/test_crash_notify.xul
@@ -10,24 +10,26 @@
   <script type="application/javascript">
     setTestPluginEnabledState(SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED);
   </script>
 <body xmlns="http://www.w3.org/1999/xhtml" onload="runTests()">
 <embed id="plugin1" type="application/x-test" width="200" height="200"></embed>
 </body>
 <script class="testbody" type="application/javascript">
 <![CDATA[
-Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/PromiseUtils.jsm");
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.expectChildProcessCrash();
 
 var success = false;
 
 var observerFired = false;
+var observerDeferred = PromiseUtils.defer();
+var eventListenerDeferred = PromiseUtils.defer();
 
 var testObserver = {
   observe: function(subject, topic, data) {
     observerFired = true;
     ok(true, "Observer fired");
     is(topic, "plugin-crashed", "Checking correct topic");
     is(data,  null, "Checking null data");
     ok((subject instanceof Components.interfaces.nsIPropertyBag2), "got Propbag");
@@ -41,16 +43,18 @@ var testObserver = {
     let profD = directoryService.get("ProfD", Components.interfaces.nsIFile);
     profD.append("minidumps");
     let dumpFile = profD.clone();
     dumpFile.append(id + ".dmp");
     ok(dumpFile.exists(), "minidump exists");
     let extraFile = profD.clone();
     extraFile.append(id + ".extra");
     ok(extraFile.exists(), "extra file exists");
+
+    observerDeferred.resolve();
   },
 
   QueryInterface: function(iid) {
     if (iid.equals(Components.interfaces.nsIObserver) ||
         iid.equals(Components.interfaces.nsISupportsWeakReference) ||
         iid.equals(Components.interfaces.nsISupports))
       return this;
     throw Components.results.NS_NOINTERFACE;
@@ -79,30 +83,35 @@ function onPluginCrashed(aEvent) {
   // allow either true or false here.
   ok("submittedCrashReport" in aEvent, "submittedCrashReport is a property of event");
   is(typeof aEvent.submittedCrashReport, "boolean", "submittedCrashReport is correct type");
 
   var os = Components.classes["@mozilla.org/observer-service;1"].
            getService(Components.interfaces.nsIObserverService);
   os.removeObserver(testObserver, "plugin-crashed");
 
-  Services.crashmanager.ensureCrashIsPresent(aEvent.pluginDumpID).then(() => {
-    SimpleTest.finish();
-  });
+  eventListenerDeferred.resolve();
 }
 
 function runTests() {
   var os = Components.classes["@mozilla.org/observer-service;1"].
            getService(Components.interfaces.nsIObserverService);
   os.addObserver(testObserver, "plugin-crashed", true);
 
   document.addEventListener("PluginCrashed", onPluginCrashed, false);
 
   var pluginElement = document.getElementById("plugin1");
   try {
     pluginElement.crash();
   } catch (e) {
   }
+
+  Promise.all([
+    observerDeferred.promise,
+    eventListenerDeferred.promise
+  ]).then(() => {
+    SimpleTest.finish();
+  });
 }
 ]]>
 </script>
 </window>
 
--- a/dom/plugins/test/mochitest/test_idle_hang.xul
+++ b/dom/plugins/test/mochitest/test_idle_hang.xul
@@ -14,18 +14,16 @@
   <script type="application/javascript">
     getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED;
   </script>
 <body xmlns="http://www.w3.org/1999/xhtml" onload="runTests()">
 <embed id="plugin1" type="application/x-test" width="200" height="200"></embed>
 </body>
 <script class="testbody" type="application/javascript">
 <![CDATA[
-Components.utils.import("resource://gre/modules/Services.jsm");
-
 SimpleTest.waitForExplicitFinish();
 SimpleTest.expectChildProcessCrash();
 
 function runTests() {
   // Default plugin hang timeout is too high for mochitests
   var prefs = Cc["@mozilla.org/preferences-service;1"]
                     .getService(Ci.nsIPrefBranch);
   var timeoutPref = "dom.ipc.plugins.timeoutSecs";
--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
@@ -997,22 +997,25 @@ this.BrowserTestUtils = {
    * Resolves with the data from the .extra file (the crash annotations).
    *
    * @param (Browser) browser
    *        A remote <xul:browser> element. Must not be null.
    * @param (bool) shouldShowTabCrashPage
    *        True if it is expected that the tab crashed page will be shown
    *        for this browser. If so, the Promise will only resolve once the
    *        tab crash page has loaded.
+   * @param (bool) shouldClearMinidumps
+   *        True if the minidumps left behind by the crash should be removed.
    *
    * @returns (Promise)
    * @resolves An Object with key-value pairs representing the data from the
    *           crash report's extra file (if applicable).
    */
-  async crashBrowser(browser, shouldShowTabCrashPage=true) {
+  async crashBrowser(browser, shouldShowTabCrashPage=true,
+                     shouldClearMinidumps=true) {
     let extra = {};
     let KeyValueParser = {};
     if (AppConstants.MOZ_CRASHREPORTER) {
       Cu.import("resource://gre/modules/KeyValueParser.jsm", KeyValueParser);
     }
 
     if (!browser.isRemoteBrowser) {
       throw new Error("<xul:browser> needs to be remote in order to crash");
@@ -1103,18 +1106,20 @@ this.BrowserTestUtils = {
               dump(`\nNo .extra file for dumpID: ${dumpID}\n`);
               if (AppConstants.MOZ_CRASHREPORTER) {
                 extra = KeyValueParser.parseKeyValuePairsFromFile(extrafile);
               } else {
                 dump('\nCrashReporter not enabled - will not return any extra data\n');
               }
             }
 
-            removeFile(minidumpDirectory, dumpID + '.dmp');
-            removeFile(minidumpDirectory, dumpID + '.extra');
+            if (shouldClearMinidumps) {
+              removeFile(minidumpDirectory, dumpID + '.dmp');
+              removeFile(minidumpDirectory, dumpID + '.extra');
+            }
           });
         }
 
         removalPromise.then(() => {
           Services.obs.removeObserver(observer, 'ipc:content-shutdown');
           dump("\nCrash cleaned up\n");
           // There might be other ipc:content-shutdown handlers that need to
           // run before we want to continue, so we'll resolve on the next tick
--- a/testing/mochitest/tests/SimpleTest/TestRunner.js
+++ b/testing/mochitest/tests/SimpleTest/TestRunner.js
@@ -633,18 +633,23 @@ TestRunner.testFinished = function(tests
              }
              TestRunner.updateUI([{ result: false }]);
            }
         });
         TestRunner._makeIframe(interstitialURL, 0);
     }
 
     SpecialPowers.executeAfterFlushingMessageQueue(function() {
-        cleanUpCrashDumpFiles();
-        SpecialPowers.flushPermissions(function () { SpecialPowers.flushPrefEnv(runNextTest); });
+        SpecialPowers.waitForCrashes(TestRunner._expectingProcessCrash)
+                     .then(() => {
+            cleanUpCrashDumpFiles();
+            SpecialPowers.flushPermissions(function () {
+                SpecialPowers.flushPrefEnv(runNextTest);
+            });
+        });
     });
 };
 
 TestRunner.testUnloaded = function() {
     // If we're in a debug build, check assertion counts.  This code is
     // similar to the code in Tester_nextTest in browser-test.js used
     // for browser-chrome mochitests.
     if (SpecialPowers.isDebugBuild) {
--- a/testing/specialpowers/content/SpecialPowersObserver.jsm
+++ b/testing/specialpowers/content/SpecialPowersObserver.jsm
@@ -65,16 +65,17 @@ SpecialPowersObserver.prototype.observe 
       break;
   }
 };
 
 SpecialPowersObserver.prototype._loadFrameScript = function() {
   if (!this._isFrameScriptLoaded) {
     // Register for any messages our API needs us to handle
     this._messageManager.addMessageListener("SPPrefService", this);
+    this._messageManager.addMessageListener("SPProcessCrashManagerWait", this);
     this._messageManager.addMessageListener("SPProcessCrashService", this);
     this._messageManager.addMessageListener("SPPingService", this);
     this._messageManager.addMessageListener("SpecialPowers.Quit", this);
     this._messageManager.addMessageListener("SpecialPowers.Focus", this);
     this._messageManager.addMessageListener("SpecialPowers.CreateFiles", this);
     this._messageManager.addMessageListener("SpecialPowers.RemoveFiles", this);
     this._messageManager.addMessageListener("SPPermissionManager", this);
     this._messageManager.addMessageListener("SPObserverService", this);
@@ -135,16 +136,17 @@ SpecialPowersObserver.prototype.uninit =
   obs.removeObserver(this, "http-on-modify-request");
   this._registerObservers._topics.forEach((element) => {
     obs.removeObserver(this._registerObservers, element);
   });
   this._removeProcessCrashObservers();
 
   if (this._isFrameScriptLoaded) {
     this._messageManager.removeMessageListener("SPPrefService", this);
+    this._messageManager.removeMessageListener("SPProcessCrashManagerWait", this);
     this._messageManager.removeMessageListener("SPProcessCrashService", this);
     this._messageManager.removeMessageListener("SPPingService", this);
     this._messageManager.removeMessageListener("SpecialPowers.Quit", this);
     this._messageManager.removeMessageListener("SpecialPowers.Focus", this);
     this._messageManager.removeMessageListener("SpecialPowers.CreateFiles", this);
     this._messageManager.removeMessageListener("SpecialPowers.RemoveFiles", this);
     this._messageManager.removeMessageListener("SPPermissionManager", this);
     this._messageManager.removeMessageListener("SPObserverService", this);
--- a/testing/specialpowers/content/SpecialPowersObserverAPI.js
+++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js
@@ -112,16 +112,20 @@ SpecialPowersObserverAPI.prototype = {
           let extra = this._getExtraData(pluginID);
           if (extra && ("additional_minidumps" in extra)) {
             let dumpNames = extra.additional_minidumps.split(",");
             for (let name of dumpNames) {
               message.dumpIDs.push({id: pluginID + "-" + name, extension: "dmp"});
             }
           }
         } else { // ipc:content-shutdown
+          if (!aSubject.hasKey("abnormal")) {
+            return; // This is a normal shutdown, ignore it
+          }
+
           addDumpIDToMessage("dumpID");
         }
         this._sendAsyncMessage("SPProcessCrashService", message);
         break;
     }
   },
 
   _getCrashDumpDir() {
@@ -388,16 +392,27 @@ SpecialPowersObserverAPI.prototype = {
           case "delete-pending-crash-dump-files":
             return this._deletePendingCrashDumpFiles();
           default:
             throw new SpecialPowersError("Invalid operation for SPProcessCrashService");
         }
         return undefined; // See comment at the beginning of this function.
       }
 
+      case "SPProcessCrashManagerWait": {
+        let promises = aMessage.json.crashIds.map((crashId) => {
+          return Services.crashmanager.ensureCrashIsPresent(crashId);
+        });
+
+        Promise.all(promises).then(() => {
+          this._sendReply(aMessage, "SPProcessCrashManagerWait", {});
+        });
+        return undefined; // See comment at the beginning of this function.
+      }
+
       case "SPPermissionManager": {
         let msg = aMessage.json;
         let principal = msg.principal;
 
         switch (msg.op) {
           case "add":
             Services.perms.addFromPrincipal(principal, msg.type, msg.permission, msg.expireType, msg.expireTime);
             break;
--- a/testing/specialpowers/content/specialpowers.js
+++ b/testing/specialpowers/content/specialpowers.js
@@ -44,16 +44,17 @@ function SpecialPowers(window) {
                            "SPRequestResetCoverageCounters"];
 
   this.SP_ASYNC_MESSAGES = ["SpecialPowers.Focus",
                             "SpecialPowers.Quit",
                             "SpecialPowers.CreateFiles",
                             "SpecialPowers.RemoveFiles",
                             "SPPingService",
                             "SPLoadExtension",
+                            "SPProcessCrashManagerWait",
                             "SPStartupExtension",
                             "SPUnloadExtension",
                             "SPExtensionMessage"];
   addMessageListener("SPPingService", this._messageListener);
   addMessageListener("SpecialPowers.FilesCreated", this._messageListener);
   addMessageListener("SpecialPowers.FilesError", this._messageListener);
   let self = this;
   Services.obs.addObserver(function onInnerWindowDestroyed(subject, topic, data) {
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -665,16 +665,41 @@ SpecialPowersAPI.prototype = {
 
   getDOMWindowUtils(aWindow) {
     if (aWindow == this.window.get() && this.DOMWindowUtils != null)
       return this.DOMWindowUtils;
 
     return bindDOMWindowUtils(aWindow);
   },
 
+  waitForCrashes(aExpectingProcessCrash) {
+    return new Promise((resolve, reject) => {
+      if (!aExpectingProcessCrash) {
+        resolve();
+      }
+
+      var crashIds = this._encounteredCrashDumpFiles.filter((filename) => {
+        return ((filename.length === 40) && filename.endsWith(".dmp"));
+      }).map((id) => {
+        return id.slice(0, -4); // Strip the .dmp extension to get the ID
+      });
+
+      let self = this;
+      function messageListener(msg) {
+        self._removeMessageListener("SPProcessCrashManagerWait", messageListener);
+        resolve();
+      }
+
+      this._addMessageListener("SPProcessCrashManagerWait", messageListener);
+      this._sendAsyncMessage("SPProcessCrashManagerWait", {
+        crashIds
+      });
+    });
+  },
+
   removeExpectedCrashDumpFiles(aExpectingProcessCrash) {
     var success = true;
     if (aExpectingProcessCrash) {
       var message = {
         op: "delete-crash-dump-files",
         filenames: this._encounteredCrashDumpFiles
       };
       if (!this._sendSyncMessage("SPProcessCrashService", message)[0]) {