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 71420 276ff11f697f2128b99a946d6a2d27723f0645d1
parent 71419 9fa1dd22e55f4ac04208ab5882aee9807e027720
child 71421 7360bb236cc10f568255dd9a4344686bc40b1c4e
push id20539
push usermlamouri@mozilla.com
push dateTue, 21 Jun 2011 07:35:09 +0000
treeherdermozilla-central@bf714fffdfdf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted
bugs642175
milestone7.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;