Bug 642175 - Part 2: Allow mochitests to clean up plugin and IPC process crash dumps. r=ted
authorCameron McCormack <cam@mcc.id.au>
Tue, 21 Jun 2011 12:11:50 +1200
changeset 71694 276ff11f697f2128b99a946d6a2d27723f0645d1
parent 71693 9fa1dd22e55f4ac04208ab5882aee9807e027720
child 71695 7360bb236cc10f568255dd9a4344686bc40b1c4e
push id209
push userbzbarsky@mozilla.com
push dateTue, 05 Jul 2011 17:42:16 +0000
treeherdermozilla-aurora@cc6e30cce8af [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted
bugs642175
milestone7.0a1
Bug 642175 - Part 2: Allow mochitests to clean up plugin and IPC process crash dumps. r=ted
build/automationutils.py
testing/mochitest/specialpowers/components/SpecialPowersObserver.js
testing/mochitest/specialpowers/content/specialpowers.js
testing/mochitest/tests/SimpleTest/SimpleTest.js
testing/mochitest/tests/SimpleTest/TestRunner.js
--- a/build/automationutils.py
+++ b/build/automationutils.py
@@ -109,16 +109,17 @@ def checkForCrashes(dumpDir, symbolsPath
       testName = os.path.basename(sys._getframe(1).f_code.co_filename)
     except:
       testName = "unknown"
 
   foundCrash = False
   dumps = glob.glob(os.path.join(dumpDir, '*.dmp'))
   for d in dumps:
     log.info("PROCESS-CRASH | %s | application crashed (minidump found)", testName)
+    print "Crash dump filename: " + d
     if symbolsPath and stackwalkPath and os.path.exists(stackwalkPath):
       p = subprocess.Popen([stackwalkPath, d, symbolsPath],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE)
       (out, err) = p.communicate()
       if len(out) > 3:
         # minidump_stackwalk is chatty, so ignore stderr when it succeeds.
         print out
--- a/testing/mochitest/specialpowers/components/SpecialPowersObserver.js
+++ b/testing/mochitest/specialpowers/components/SpecialPowersObserver.js
@@ -59,58 +59,155 @@ function SpecialPowersException(aMsg) {
   this.name = "SpecialPowersException";
 }
 
 SpecialPowersException.prototype.toString = function() {
   return this.name + ': "' + this.message + '"';
 };
 
 /* XPCOM gunk */
-function SpecialPowersObserver() {}
+function SpecialPowersObserver() {
+  this._isFrameScriptLoaded = false;
+  this._messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
+                         getService(Ci.nsIChromeFrameMessageManager);
+}
 
 SpecialPowersObserver.prototype = {
   classDescription: "Special powers Observer for use in testing.",
   classID:          Components.ID("{59a52458-13e0-4d93-9d85-a637344f29a1}"),
   contractID:       "@mozilla.org/special-powers-observer;1",
   QueryInterface:   XPCOMUtils.generateQI([Components.interfaces.nsIObserver]),
   _xpcom_categories: [{category: "profile-after-change", service: true }],
-  isFrameScriptLoaded: false,
 
   observe: function(aSubject, aTopic, aData)
   {
-    if (aTopic == "profile-after-change") {
-      this.init();
-    } else if (!this.isFrameScriptLoaded && 
-               aTopic == "chrome-document-global-created") {
-     
-      var messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
-                           getService(Ci.nsIChromeFrameMessageManager);
-      // Register for any messages our API needs us to handle
-      messageManager.addMessageListener("SPPrefService", this);
+    switch (aTopic) {
+      case "profile-after-change":
+        this.init();
+        break;
+
+      case "chrome-document-global-created":
+        if (!this._isFrameScriptLoaded) {
+          // Register for any messages our API needs us to handle
+          this._messageManager.addMessageListener("SPPrefService", this);
+          this._messageManager.addMessageListener("SPProcessCrashService", this);
+          this._messageManager.addMessageListener("SPPingService", this);
+
+          this._messageManager.loadFrameScript(CHILD_SCRIPT, true);
+          this._isFrameScriptLoaded = true;
+        }
+        break;
 
-      messageManager.loadFrameScript(CHILD_SCRIPT, true);
-      this.isFrameScriptLoaded = true;
-    } else if (aTopic == "xpcom-shutdown") {
-      this.uninit();
+      case "xpcom-shutdown":
+        this.uninit();
+        break;
+
+      case "plugin-crashed":
+      case "ipc:content-shutdown":
+        function addDumpIDToMessage(propertyName) {
+          var id = aSubject.getPropertyAsAString(propertyName);
+          if (id) {
+            message.dumpIDs.push(id);
+          }
+        }
+
+        var message = { type: "crash-observed", dumpIDs: [] };
+        aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2);
+        if (aTopic == "plugin-crashed") {
+          addDumpIDToMessage("pluginDumpID");
+          addDumpIDToMessage("browserDumpID");
+        } else { // ipc:content-shutdown
+          addDumpIDToMessage("dumpID");
+        }
+        this._messageManager.sendAsyncMessage("SPProcessCrashService", message);
+        break;
     }
   },
 
   init: function()
   {
     var obs = Services.obs;
     obs.addObserver(this, "xpcom-shutdown", false);
     obs.addObserver(this, "chrome-document-global-created", false);
   },
 
   uninit: function()
   {
     var obs = Services.obs;
     obs.removeObserver(this, "chrome-document-global-created", false);
+    this.removeProcessCrashObservers();
   },
   
+  addProcessCrashObservers: function() {
+    if (this._processCrashObserversRegistered) {
+      return;
+    }
+
+    Services.obs.addObserver(this, "plugin-crashed", false);
+    Services.obs.addObserver(this, "ipc:content-shutdown", false);
+    this._processCrashObserversRegistered = true;
+  },
+
+  removeProcessCrashObservers: function() {
+    if (!this._processCrashObserversRegistered) {
+      return;
+    }
+
+    Services.obs.removeObserver(this, "plugin-crashed");
+    Services.obs.removeObserver(this, "ipc:content-shutdown");
+    this._processCrashObserversRegistered = false;
+  },
+
+  getCrashDumpDir: function() {
+    if (!this._crashDumpDir) {
+      var directoryService = Cc["@mozilla.org/file/directory_service;1"]
+                             .getService(Ci.nsIProperties);
+      this._crashDumpDir = directoryService.get("ProfD", Ci.nsIFile);
+      this._crashDumpDir.append("minidumps");
+    }
+    return this._crashDumpDir;
+  },
+
+  deleteCrashDumpFiles: function(aFilenames) {
+    var crashDumpDir = this.getCrashDumpDir();
+    if (!crashDumpDir.exists()) {
+      return false;
+    }
+
+    var success = aFilenames.length != 0;
+    aFilenames.forEach(function(crashFilename) {
+      var file = crashDumpDir.clone();
+      file.append(crashFilename);
+      if (file.exists()) {
+        file.remove(false);
+      } else {
+        success = false;
+      }
+    });
+    return success;
+  },
+
+  findCrashDumpFiles: function(aToIgnore) {
+    var crashDumpDir = this.getCrashDumpDir();
+    var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries;
+    if (!entries) {
+      return [];
+    }
+
+    var crashDumpFiles = [];
+    while (entries.hasMoreElements()) {
+      var file = entries.getNext().QueryInterface(Ci.nsIFile);
+      var path = String(file.path);
+      if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) {
+        crashDumpFiles.push(path);
+      }
+    }
+    return crashDumpFiles.concat();
+  },
+
   /**
    * messageManager callback function
    * This will get requests from our API in the window and process them in chrome for it
    **/
   receiveMessage: function(aMessage) {
     switch(aMessage.name) {
       case "SPPrefService":
         var prefs = Services.prefs;
@@ -154,15 +251,43 @@ SpecialPowersObserver.prototype = {
               return(prefs.setComplexValue(prefName, prefValue[0], prefValue[1]));
           case "":
             if (aMessage.json.op == "clear") {
               prefs.clearUserPref(prefName);
               return;
             }
         }
         break;
+
+      case "SPProcessCrashService":
+        switch (aMessage.json.op) {
+          case "register-observer":
+            this.addProcessCrashObservers();
+            break;
+          case "unregister-observer":
+            this.removeProcessCrashObservers();
+            break;
+          case "delete-crash-dump-files":
+            return this.deleteCrashDumpFiles(aMessage.json.filenames);
+          case "find-crash-dump-files":
+            return this.findCrashDumpFiles(aMessage.json.crashDumpFilesToIgnore);
+          default:
+            throw new SpecialPowersException("Invalid operation for SPProcessCrashService");
+        }
+        break;
+
+      case "SPPingService":
+        if (aMessage.json.op == "ping") {
+          aMessage.target
+                  .QueryInterface(Ci.nsIFrameLoaderOwner)
+                  .frameLoader
+                  .messageManager
+                  .sendAsyncMessage("SPPingService", { op: "pong" });
+        }
+        break;
+
       default:
         throw new SpecialPowersException("Unrecognized Special Powers API");
     }
   }
 };
 
 const NSGetFactory = XPCOMUtils.generateNSGetFactory([SpecialPowersObserver]);
--- a/testing/mochitest/specialpowers/content/specialpowers.js
+++ b/testing/mochitest/specialpowers/content/specialpowers.js
@@ -39,16 +39,22 @@
  */
 
 var Ci = Components.interfaces;
 var Cc = Components.classes;
 
 function SpecialPowers(window) {
   this.window = window;
   bindDOMWindowUtils(this, window);
+  this._encounteredCrashDumpFiles = [];
+  this._unexpectedCrashDumpFiles = { };
+  this._crashDumpDir = null;
+  this._pongHandlers = [];
+  this._messageListener = this._messageReceived.bind(this);
+  addMessageListener("SPPingService", this._messageListener);
 }
 
 function bindDOMWindowUtils(sp, window) {
   var util = window.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindowUtils);
   // This bit of magic brought to you by the letters
   // B Z, and E, S and the number 5.
   //
@@ -209,16 +215,87 @@ SpecialPowers.prototype = {
   hasContentProcesses: function() {
     try {
       var rt = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
       return rt.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
     } catch (e) {
       return true;
     }
   },
+
+  registerProcessCrashObservers: function() {
+    addMessageListener("SPProcessCrashService", this._messageListener);
+    sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
+  },
+
+  _messageReceived: function(aMessage) {
+    switch (aMessage.name) {
+      case "SPProcessCrashService":
+        if (aMessage.json.type == "crash-observed") {
+          var self = this;
+          aMessage.json.dumpIDs.forEach(function(id) {
+            self._encounteredCrashDumpFiles.push(id + ".dmp");
+            self._encounteredCrashDumpFiles.push(id + ".extra");
+          });
+        }
+        break;
+
+      case "SPPingService":
+        if (aMessage.json.op == "pong") {
+          var handler = this._pongHandlers.shift();
+          if (handler) {
+            handler();
+          }
+        }
+        break;
+    }
+    return true;
+  },
+
+  removeExpectedCrashDumpFiles: function(aExpectingProcessCrash) {
+    var success = true;
+    if (aExpectingProcessCrash) {
+      var message = {
+        op: "delete-crash-dump-files",
+        filenames: this._encounteredCrashDumpFiles 
+      };
+      if (!sendSyncMessage("SPProcessCrashService", message)[0]) {
+        success = false;
+      }
+    }
+    this._encounteredCrashDumpFiles.length = 0;
+    return success;
+  },
+
+  findUnexpectedCrashDumpFiles: function() {
+    var self = this;
+    var message = {
+      op: "find-crash-dump-files",
+      crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles
+    };
+    var crashDumpFiles = sendSyncMessage("SPProcessCrashService", message)[0];
+    crashDumpFiles.forEach(function(aFilename) {
+      self._unexpectedCrashDumpFiles[aFilename] = true;
+    });
+    return crashDumpFiles;
+  },
+
+  executeAfterFlushingMessageQueue: function(aCallback) {
+    this._pongHandlers.push(aCallback);
+    sendAsyncMessage("SPPingService", { op: "ping" });
+  },
+
+  executeSoon: function(aFunc) {
+    var tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+    tm.mainThread.dispatch({
+      run: function() {
+        aFunc();
+      }
+    }, Ci.nsIThread.DISPATCH_NORMAL);
+  }
 };
 
 // Expose everything but internal APIs (starting with underscores) to
 // web content.
 SpecialPowers.prototype.__exposedProps__ = {};
 for each (i in Object.keys(SpecialPowers.prototype).filter(function(v) {return v.charAt(0) != "_";})) {
   SpecialPowers.prototype.__exposedProps__[i] = "r";
 }
--- a/testing/mochitest/tests/SimpleTest/SimpleTest.js
+++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js
@@ -124,16 +124,20 @@ SimpleTest._logResult = function(test, p
         } else {
             parentRunner.log(msg);
         }
     } else {
         dump(msg + "\n");
     }
 };
 
+SimpleTest._logInfo = function(name, message) {
+    this._logResult({result:true, name:name, diag:message}, "TEST-INFO");
+};
+
 /**
  * Copies of is and isnot with the call to ok replaced by a call to todo.
 **/
 
 SimpleTest.todo_is = function (a, b, name) {
     var repr = MochiKit.Base.repr;
     var pass = (a == b);
     var diag = pass ? repr(a) + " should equal " + repr(b)
@@ -325,17 +329,17 @@ SimpleTest.waitForFocus = function (call
     var fm = Components.classes["@mozilla.org/focus-manager;1"].
                         getService(Components.interfaces.nsIFocusManager);
 
     var childTargetWindow = { };
     fm.getFocusedElementForWindow(targetWindow, true, childTargetWindow);
     childTargetWindow = childTargetWindow.value;
 
     function info(msg) {
-        SimpleTest._logResult({result: true, name: msg}, "TEST-INFO");
+        SimpleTest._logInfo("", msg);
     }
 
     function debugFocusLog(prefix) {
         netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
 
         var baseWindow = targetWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                                      .getInterface(Components.interfaces.nsIWebNavigation)
                                      .QueryInterface(Components.interfaces.nsIBaseWindow);
@@ -517,16 +521,18 @@ SimpleTest.waitForClipboard = function(a
          }, aFailureFn, "text/unicode");
 }
 
 /**
  * Executes a function shortly after the call, but lets the caller continue
  * working (or finish).
  */
 SimpleTest.executeSoon = function(aFunc) {
+    // Once SpecialPowers is available in chrome mochitests, we can replace the
+    // body of this function with a call to SpecialPowers.executeSoon().
     if ("Components" in window && "classes" in window.Components) {
         try {
             netscape.security.PrivilegeManager
               .enablePrivilege("UniversalXPConnect");
             var tm = Components.classes["@mozilla.org/thread-manager;1"]
                        .getService(Components.interfaces.nsIThreadManager);
 
             tm.mainThread.dispatch({
@@ -551,16 +557,27 @@ SimpleTest.finish = function () {
     if (parentRunner) {
         /* We're running in an iframe, and the parent has a TestRunner */
         parentRunner.testFinished(SimpleTest._tests);
     } else {
         SimpleTest.showReport();
     }
 };
 
+/**
+ * Indicates to the test framework that the current test expects one or
+ * more crashes (from plugins or IPC documents), and that the minidumps from
+ * those crashes should be removed.
+ */
+SimpleTest.expectChildProcessCrash = function () {
+    if (parentRunner) {
+        parentRunner.expectChildProcessCrash();
+    }
+};
+
 
 addLoadEvent(function() {
     if (SimpleTest._stopOnLoad) {
         SimpleTest.finish();
     }
 });
 
 //  --------------- Test.Builder/Test.More isDeeply() -----------------
--- a/testing/mochitest/tests/SimpleTest/TestRunner.js
+++ b/testing/mochitest/tests/SimpleTest/TestRunner.js
@@ -44,16 +44,18 @@ TestRunner.maxTimeouts = 4; // halt test
 
 // running in e10s build and need to use IPC?
 if (typeof SpecialPowers != 'undefined') {
     TestRunner.ipcMode = SpecialPowers.hasContentProcesses();
 } else {
     TestRunner.ipcMode = false;
 }
 
+TestRunner._expectingProcessCrash = false;
+
 /**
  * Make sure the tests don't hang indefinitely.
 **/
 TestRunner._numTimeouts = 0;
 TestRunner._currentTestStartTime = new Date().valueOf();
 TestRunner._timeoutFactor = 1;
 
 TestRunner._checkForHangs = function() {
@@ -163,16 +165,20 @@ TestRunner._makeIframe = function (url, 
  * TestRunner entry point.
  *
  * The arguments are the URLs of the test to be ran.
  *
 **/
 TestRunner.runTests = function (/*url...*/) {
     TestRunner.log("SimpleTest START");
 
+    if (typeof SpecialPowers != "undefined") {
+        SpecialPowers.registerProcessCrashObservers();
+    }
+
     TestRunner._urls = flattenArguments(arguments);
     $('testframe').src="";
     TestRunner._checkForHangs();
     window.focus();
     $('testframe').focus();
     TestRunner.runNextTest();
 };
 
@@ -221,28 +227,71 @@ TestRunner.runNextTest = function() {
         TestRunner.log("SimpleTest FINISHED");
 
         if (TestRunner.onComplete) {
             TestRunner.onComplete();
         }
     }
 };
 
+TestRunner.expectChildProcessCrash = function() {
+    if (typeof SpecialPowers == "undefined") {
+        throw "TestRunner.expectChildProcessCrash must only be called from plain mochitests.";
+    }
+
+    TestRunner._expectingProcessCrash = true;
+};
+
 /**
  * This stub is called by SimpleTest when a test is finished.
 **/
 TestRunner.testFinished = function(tests) {
-    var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;
-    TestRunner.log("TEST-END | " +
-                   TestRunner._urls[TestRunner._currentTest] +
-                   " | finished in " + runtime + "ms");
+    function cleanUpCrashDumpFiles() {
+        if (!SpecialPowers.removeExpectedCrashDumpFiles(TestRunner._expectingProcessCrash)) {
+            TestRunner.error("TEST-UNEXPECTED-FAIL | " +
+                             TestRunner.currentTestURL +
+                             " | This test did not leave any crash dumps behind, but we were expecting some!");
+            tests.push({ result: false });
+        }
+        var unexpectedCrashDumpFiles =
+            SpecialPowers.findUnexpectedCrashDumpFiles();
+        TestRunner._expectingProcessCrash = false;
+        if (unexpectedCrashDumpFiles.length) {
+            TestRunner.error("TEST-UNEXPECTED-FAIL | " +
+                             TestRunner.currentTestURL +
+                             " | This test left crash dumps behind, but we " +
+                             "weren't expecting it to!");
+            tests.push({ result: false });
+            unexpectedCrashDumpFiles.sort().forEach(function(aFilename) {
+                TestRunner.log("TEST-INFO | Found unexpected crash dump file " +
+                               aFilename + ".");
+            });
+        }
+    }
 
-    TestRunner.updateUI(tests);
-    TestRunner._currentTest++;
-    TestRunner.runNextTest();
+    function runNextTest() {
+        var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;
+        TestRunner.log("TEST-END | " +
+                       TestRunner._urls[TestRunner._currentTest] +
+                       " | finished in " + runtime + "ms");
+        TestRunner.log("TEST-INFO | Time is " + Math.floor(Date.now() / 1000));
+
+        TestRunner.updateUI(tests);
+        TestRunner._currentTest++;
+        TestRunner.runNextTest();
+    }
+
+    if (typeof SpecialPowers != 'undefined') {
+        SpecialPowers.executeAfterFlushingMessageQueue(function() {
+            cleanUpCrashDumpFiles();
+            runNextTest();
+        });
+    } else {
+        runNextTest();
+    }
 };
 
 /**
  * Get the results.
  */
 TestRunner.countResults = function(tests) {
   var nOK = 0;
   var nNotOK = 0;