Bug 1360493 write a test asserting that Firefox launches without hanging; r=rstrong
authorCarl Corcoran <carlco@gmail.com>
Wed, 17 May 2017 08:22:08 +0200
changeset 359020 ea1bbc02a7ea10b6e9d9c97343761376807218f3
parent 359019 0f5e027fe25a7d6b1effc6fd455e19d3afd00c52
child 359021 3e17a3e6de6f156ce68c4be10093f84e786aac3a
push id42936
push userrstrong@mozilla.com
push dateThu, 18 May 2017 17:55:30 +0000
treeherderautoland@ea1bbc02a7ea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrstrong
bugs1360493
milestone55.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 1360493 write a test asserting that Firefox launches without hanging; r=rstrong MozReview-Commit-ID: D0axTNp4KCt
toolkit/xre/moz.build
toolkit/xre/nsAppRunner.cpp
toolkit/xre/test/.eslintrc.js
toolkit/xre/test/test_launch_without_hang.js
toolkit/xre/test/xpcshell.ini
--- a/toolkit/xre/moz.build
+++ b/toolkit/xre/moz.build
@@ -7,16 +7,17 @@
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'Startup and Profile System')
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     TEST_DIRS += ['test/win']
 
 MOCHITEST_MANIFESTS += ['test/mochitest.ini']
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
 XPIDL_SOURCES += [
     'nsINativeAppSupport.idl',
 ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     XPIDL_SOURCES += [
         'nsIWinAppHelper.idl',
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -3977,16 +3977,22 @@ XREMain::XRE_mainStartup(bool* aExitFlag
     nsAutoCString desktopStartupEnv;
     desktopStartupEnv.AssignLiteral("DESKTOP_STARTUP_ID=");
     desktopStartupEnv.Append(mDesktopStartupID);
     // Leak it with extreme prejudice!
     PR_SetEnv(ToNewCString(desktopStartupEnv));
   }
 #endif
 
+  // Support exiting early for testing startup sequence. Bug 1360493
+  if (CheckArg("test-launch-without-hang")) {
+    *aExitFlag = true;
+    return 0;
+  }
+
 #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID) && !defined(MOZ_WIDGET_GONK)
   // Check for and process any available updates
   nsCOMPtr<nsIFile> updRoot;
   bool persistent;
   rv = mDirProvider.GetFile(XRE_UPDATE_ROOT_DIR, &persistent,
                             getter_AddRefs(updRoot));
   // XRE_UPDATE_ROOT_DIR may fail. Fallback to appDir if failed
   if (NS_FAILED(rv))
--- a/toolkit/xre/test/.eslintrc.js
+++ b/toolkit/xre/test/.eslintrc.js
@@ -1,8 +1,9 @@
 "use strict";
 
 module.exports = {
   "extends": [
     "plugin:mozilla/mochitest-test",
-    "plugin:mozilla/browser-test"
+    "plugin:mozilla/browser-test",
+    "plugin:mozilla/xpcshell-test"
   ]
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/xre/test/test_launch_without_hang.js
@@ -0,0 +1,237 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// bug 1360493
+// Launch the browser a number of times, testing startup hangs.
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+
+const APP_TIMER_TIMEOUT_MS = 1000 * 15;
+const TRY_COUNT = 50;
+
+
+// Sets a group of environment variables, returning the old values.
+// newVals AND return value is an array of { key: "", value: "" }
+function setEnvironmentVariables(newVals) {
+  let oldVals = [];
+  let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+  for (let i = 0; i < newVals.length; ++i) {
+    let key = newVals[i].key;
+    let value = newVals[i].value;
+    let oldObj = { key };
+    if (env.exists(key)) {
+      oldObj.value = env.get(key);
+    } else {
+      oldObj.value = null;
+    }
+
+    env.set(key, value);
+    oldVals.push(oldObj);
+  }
+  return oldVals;
+}
+
+
+function getFirefoxExecutableFilename() {
+  if (AppConstants.platform === "win") {
+      return AppConstants.MOZ_APP_NAME + ".exe";
+  }
+  return AppConstants.MOZ_APP_NAME;
+}
+
+
+// Returns a nsIFile to the firefox.exe executable file
+function getFirefoxExecutableFile() {
+  let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+  file = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+
+  file.append(getFirefoxExecutableFilename());
+  return file;
+}
+
+
+// Takes an executable and arguments, and wraps it in a call to the system shell.
+// Technique adapted from \toolkit\mozapps\update\tests\unit_service_updater\xpcshellUtilsAUS.js
+// to avoid child process console output polluting the xpcshell log.
+// returns { file: (nsIFile), args: [] }
+function wrapLaunchInShell(file, args) {
+  let ret = { };
+
+  if (AppConstants.platform === "win") {
+    ret.file = Services.dirsvc.get("WinD", Ci.nsILocalFile);
+    ret.file.append("System32");
+    ret.file.append("cmd.exe");
+    ret.args = ["/D", "/Q", "/C", file.path].concat(args).concat([">nul"]);
+  } else {
+    ret.file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+    ret.file.initWithPath("/usr/bin/env");
+    ret.args = [file.path].concat(args).concat(["> /dev/null"]);
+  }
+
+  Assert.ok(ret.file.exists(), "Executable file should exist: " + ret.file.path);
+
+  return ret;
+}
+
+
+// Needed because process.kill() kills the console, not its child process, firefox.
+function terminateFirefox(completion) {
+  let executableName = getFirefoxExecutableFilename();
+  let file;
+  let args;
+
+  if (AppConstants.platform === "win") {
+    file = Services.dirsvc.get("WinD", Ci.nsILocalFile);
+    file.append("System32");
+    file.append("taskkill.exe");
+    args = ["/F", "/IM", executableName];
+  } else {
+    file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+    file.initWithPath("/usr/bin/killall");
+    args = [executableName];
+  }
+
+  do_print("launching application: " + file.path);
+  do_print("            with args: " + args.join(" "));
+
+  let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+  process.init(file);
+
+  let processObserver = {
+    observe: function PO_observe(aSubject, aTopic, aData) {
+      do_print("topic: " + aTopic + ", process exitValue: " + process.exitValue);
+
+      Assert.equal(process.exitValue, 0,
+                   "Terminate firefox process exit value should be 0");
+      Assert.equal(aTopic, "process-finished",
+                   "Terminate firefox observer topic should be " +
+                   "process-finished");
+
+      if (completion) {
+        completion();
+      }
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+  };
+
+  process.runAsync(args, args.length, processObserver);
+
+  do_print("             with pid: " + process.pid);
+}
+
+
+// Launches file with args asynchronously, failing if the process did not
+// exit within timeoutMS milliseconds. If a timeout occurs, handler()
+// is called.
+function launchProcess(file, args, env, timeoutMS, handler, attemptCount) {
+  let state = { };
+
+  state.attempt = attemptCount;
+
+  state.processObserver = {
+    observe: function PO_observe(aSubject, aTopic, aData) {
+      if (!state.appTimer) {
+        // the app timer has been canceled; this process has timed out already so don't process further.
+        handler(false);
+        return;
+      }
+
+      do_print("topic: " + aTopic + ", process exitValue: " + state.process.exitValue);
+
+      do_print("Restoring environment variables");
+      setEnvironmentVariables(state.oldEnv);
+
+      state.appTimer.cancel();
+      state.appTimer = null;
+
+      Assert.equal(state.process.exitValue, 0,
+                   "the application process exit value should be 0");
+      Assert.equal(aTopic, "process-finished",
+                   "the application process observer topic should be " +
+                   "process-finished");
+
+      handler(true);
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+  };
+
+  // The timer callback to kill the process if it takes too long.
+  state.appTimerCallback = {
+    notify: function TC_notify(aTimer) {
+      state.appTimer = null;
+
+      do_print("Restoring environment variables");
+      setEnvironmentVariables(state.oldEnv);
+
+      if (state.process.isRunning) {
+        do_print("attempting to kill process");
+
+        // This will cause the shell process to exit as well, triggering our process observer.
+        terminateFirefox(function terminateFirefoxCompletion() {
+          Assert.ok(false, "Launch application timer expired");
+        });
+      }
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+  };
+
+  do_print("launching application: " + file.path);
+  do_print("            with args: " + args.join(" "));
+  do_print("     with environment: ");
+  for (let i = 0; i < env.length; ++i) {
+    do_print("             " + env[i].key + "=" + env[i].value);
+  }
+
+  state.process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+  state.process.init(file);
+
+  state.appTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  state.appTimer.initWithCallback(state.appTimerCallback, timeoutMS, Ci.nsITimer.TYPE_ONE_SHOT);
+
+  state.oldEnv = setEnvironmentVariables(env);
+
+  state.process.runAsync(args, args.length, state.processObserver);
+
+  do_print("             with pid: " + state.process.pid);
+}
+
+
+function run_test() {
+  do_test_pending();
+
+  let env = [
+    { key: "MOZ_CRASHREPORTER_DISABLE", value: null },
+    { key: "MOZ_CRASHREPORTER", value: "1" },
+    { key: "MOZ_CRASHREPORTER_NO_REPORT", value: "1" },
+    { key: "MOZ_CRASHREPORTER_SHUTDOWN", value: "1" },
+    { key: "XPCOM_DEBUG_BREAK", value: "stack-and-abort" }
+  ];
+
+  let triesStarted = 1;
+
+  let handler = function launchFirefoxHandler(okToContinue) {
+    triesStarted++;
+    if ((triesStarted <= TRY_COUNT) && okToContinue) {
+      testTry();
+    } else {
+      do_test_finished();
+    }
+  };
+
+  let testTry = function testTry() {
+    let shell = wrapLaunchInShell(getFirefoxExecutableFile(), ["-no-remote", "-test-launch-without-hang"]);
+    do_print("Try attempt #" + triesStarted);
+    launchProcess(shell.file, shell.args, env, APP_TIMER_TIMEOUT_MS, handler, triesStarted);
+  };
+
+  testTry();
+}
+
new file mode 100644
--- /dev/null
+++ b/toolkit/xre/test/xpcshell.ini
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+tags = native
+
+[test_launch_without_hang.js]
+run-sequentially = Has to launch application binary
+skip-if = toolkit == 'android'
+