From 2e80270b6edc3b65a627d603df475e9faedb9dda Mon Sep 17 00:00:00 2001 From: Carl Corcoran Date: Wed, 17 May 2017 08:22:08 +0200 Subject: [PATCH] Bug 1360493 write a test asserting that Firefox launches without hanging; r=rstrong MozReview-Commit-ID: D0axTNp4KCt --HG-- extra : rebase_source : b56359ba3797a62f51fbc421d404409f994df11f --- toolkit/xre/moz.build | 1 + toolkit/xre/nsAppRunner.cpp | 6 + toolkit/xre/test/.eslintrc.js | 3 +- toolkit/xre/test/test_launch_without_hang.js | 237 +++++++++++++++++++ toolkit/xre/test/xpcshell.ini | 11 + 5 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 toolkit/xre/test/test_launch_without_hang.js create mode 100644 toolkit/xre/test/xpcshell.ini diff --git a/toolkit/xre/moz.build b/toolkit/xre/moz.build index 143e42cbaa19..7a65cd5d296b 100644 --- a/toolkit/xre/moz.build +++ b/toolkit/xre/moz.build @@ -12,6 +12,7 @@ if CONFIG['OS_ARCH'] == 'WINNT': MOCHITEST_MANIFESTS += ['test/mochitest.ini'] BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] +XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini'] XPIDL_SOURCES += [ 'nsINativeAppSupport.idl', diff --git a/toolkit/xre/nsAppRunner.cpp b/toolkit/xre/nsAppRunner.cpp index 00487f3b385e..012e19b238ad 100644 --- a/toolkit/xre/nsAppRunner.cpp +++ b/toolkit/xre/nsAppRunner.cpp @@ -3982,6 +3982,12 @@ XREMain::XRE_mainStartup(bool* aExitFlag) } #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 updRoot; diff --git a/toolkit/xre/test/.eslintrc.js b/toolkit/xre/test/.eslintrc.js index ff1b5b846239..feac7911d425 100644 --- a/toolkit/xre/test/.eslintrc.js +++ b/toolkit/xre/test/.eslintrc.js @@ -3,6 +3,7 @@ module.exports = { "extends": [ "plugin:mozilla/mochitest-test", - "plugin:mozilla/browser-test" + "plugin:mozilla/browser-test", + "plugin:mozilla/xpcshell-test" ] }; diff --git a/toolkit/xre/test/test_launch_without_hang.js b/toolkit/xre/test/test_launch_without_hang.js new file mode 100644 index 000000000000..179f6d2021ce --- /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(); +} + diff --git a/toolkit/xre/test/xpcshell.ini b/toolkit/xre/test/xpcshell.ini new file mode 100644 index 000000000000..fc6f7befc756 --- /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' +