Bug 1360493 write a test asserting that Firefox launches without hanging; r?rstrong draft
authorCarl Corcoran <carlco@gmail.com>
Wed, 17 May 2017 08:22:08 +0200
changeset 580225 fba6cc9e60f706226ccb87f5d9c69eaf923b6f6b
parent 577554 e66dedabe582ba7b394aee4f89ed70fe389b3c46
child 629207 40d7e4341ca74f1c134b24e6854f6e4e3ce70d90
push id59470
push userbmo:ccorcoran@mozilla.com
push dateThu, 18 May 2017 08:09:16 +0000
reviewersrstrong
bugs1360493
milestone55.0a1
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
@@ -3980,16 +3980,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'
+