# Any copyright is dedicated to the Public Domain. # http://creativecommons.org/publicdomain/zero/1.0/ # This test is not written in a very modular way because update tests are generally not done with # Marionette, so this is sort of a one-off. We can't successfully just load all the actual setup # code that the normal update tests use, so this test basically just copies the bits that it needs. # The reason that this is a Marionette test at all is that, even if we stub out the quit/restart # call, we need no windows to be open to test the relevant functionality, but xpcshell doesn't do # windows at all and mochitest has a test runner window that Firefox recognizes, but mustn't close # during testing. from marionette_driver import Wait, errors from marionette_harness import MarionetteTestCase class TestNoWindowUpdateRestart(MarionetteTestCase): def setUp(self): super(TestNoWindowUpdateRestart, self).setUp() self.marionette.delete_session() self.marionette.start_session({"moz:windowless": True}) # See Bug 1777956 window = self.marionette.window_handles[0] self.marionette.switch_to_window(window) # Every part of this test ought to run in the chrome context. self.marionette.set_context(self.marionette.CONTEXT_CHROME) self.setUpBrowser() self.origDisabledForTesting = self.marionette.get_pref( "app.update.disabledForTesting" ) self.resetUpdate() def setUpBrowser(self): self.origAppUpdateAuto = self.marionette.execute_async_script( """ let [resolve] = arguments; (async () => { Services.prefs.setIntPref("app.update.download.attempts", 0); Services.prefs.setIntPref("app.update.download.maxAttempts", 0); Services.prefs.setBoolPref("app.update.staging.enabled", false); Services.prefs.setBoolPref("app.update.noWindowAutoRestart.enabled", true); Services.prefs.setIntPref("app.update.noWindowAutoRestart.delayMs", 1000); Services.prefs.clearUserPref("testing.no_window_update_restart.silent_restart_env"); let { UpdateUtils } = ChromeUtils.importESModule( "resource://gre/modules/UpdateUtils.sys.mjs" ); let origAppUpdateAuto = await UpdateUtils.getAppUpdateAutoEnabled(); await UpdateUtils.setAppUpdateAutoEnabled(true); // Prevent the update sync manager from thinking there are two instances running let exePath = Services.dirsvc.get("XREExeF", Ci.nsIFile); let dirProvider = { getFile: function AGP_DP_getFile(aProp, aPersistent) { aPersistent.value = false; switch (aProp) { case "XREExeF": exePath.append("browser-test"); return exePath; } return null; }, QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), }; let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService); ds.QueryInterface(Ci.nsIProperties).undefine("XREExeF"); ds.registerProvider(dirProvider); let gSyncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( Ci.nsIUpdateSyncManager ); gSyncManager.resetLock(); ds.unregisterProvider(dirProvider); return origAppUpdateAuto; })().then(resolve); """ ) def tearDown(self): self.tearDownBrowser() self.resetUpdate() # Reset context to the default. self.marionette.set_context(self.marionette.CONTEXT_CONTENT) super(TestNoWindowUpdateRestart, self).tearDown() def tearDownBrowser(self): self.marionette.execute_async_script( """ let [origAppUpdateAuto, origDisabledForTesting, resolve] = arguments; (async () => { Services.prefs.setBoolPref("app.update.disabledForTesting", origDisabledForTesting); Services.prefs.clearUserPref("app.update.download.attempts"); Services.prefs.clearUserPref("app.update.download.maxAttempts"); Services.prefs.clearUserPref("app.update.staging.enabled"); Services.prefs.clearUserPref("app.update.noWindowAutoRestart.enabled"); Services.prefs.clearUserPref("app.update.noWindowAutoRestart.delayMs"); Services.prefs.clearUserPref("testing.no_window_update_restart.silent_restart_env"); let { UpdateUtils } = ChromeUtils.importESModule( "resource://gre/modules/UpdateUtils.sys.mjs" ); await UpdateUtils.setAppUpdateAutoEnabled(origAppUpdateAuto); })().then(resolve); """, script_args=(self.origAppUpdateAuto, self.origDisabledForTesting), ) def test_update_on_last_window_close(self): # By preparing an update and closing all windows, we activate the No # Window Update Restart feature (see Bug 1720742) which causes Firefox # to restart to install updates. self.marionette.restart( callback=self.prepare_update_and_close_all_windows, in_app=True ) # Firefox should come back without any windows (i.e. silently). with self.assertRaises(errors.TimeoutException): wait = Wait( self.marionette, ignored_exceptions=errors.NoSuchWindowException, timeout=5, ) wait.until(lambda _: self.marionette.window_handles) # Reset the browser and active WebDriver session self.marionette.restart(in_app=True) self.marionette.delete_session() self.marionette.start_session() self.marionette.set_context(self.marionette.CONTEXT_CHROME) quit_flags_correct = self.marionette.get_pref( "testing.no_window_update_restart.silent_restart_env" ) self.assertTrue(quit_flags_correct) # Normally, the update status file would have been removed at this point by Post Update # Processing. But restarting resets app.update.disabledForTesting, which causes that to be # skipped, allowing us to look at the update status file directly. update_status_path = self.marionette.execute_script( """ let statusFile = FileUtils.getDir("UpdRootD", ["updates", "0"]); statusFile.append("update.status"); return statusFile.path; """ ) with open(update_status_path, "r") as f: # If Firefox was built with "--enable-unverified-updates" (or presumably if we tested # with an actual, signed update), the update should succeed. Otherwise, it will fail # with CERT_VERIFY_ERROR (error code 19). Unfortunately, there is no good way to tell # which of those situations we are in. Luckily, it doesn't matter, because we aren't # trying to test whether the update applied successfully, just whether the # "No Window Update Restart" feature works. self.assertIn(f.read().strip(), ["succeeded", "failed: 19"]) def resetUpdate(self): self.marionette.execute_script( """ let UM = Cc["@mozilla.org/updates/update-manager;1"].getService(Ci.nsIUpdateManager); UM.QueryInterface(Ci.nsIObserver).observe(null, "um-reload-update-data", "skip-files"); let { UpdateListener } = ChromeUtils.import("resource://gre/modules/UpdateListener.jsm"); UpdateListener.reset(); let { AppMenuNotifications } = ChromeUtils.importESModule( "resource://gre/modules/AppMenuNotifications.sys.mjs" ); AppMenuNotifications.removeNotification(/.*/); // Remove old update files so that they don't interfere with tests. let rootUpdateDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile); let updateDir = rootUpdateDir.clone(); updateDir.append("updates"); let patchDir = updateDir.clone(); patchDir.append("0"); let filesToRemove = []; let addFileToRemove = (dir, filename) => { let file = dir.clone(); file.append(filename); filesToRemove.push(file); }; addFileToRemove(rootUpdateDir, "active-update.xml"); addFileToRemove(rootUpdateDir, "updates.xml"); addFileToRemove(patchDir, "bt.result"); addFileToRemove(patchDir, "update.status"); addFileToRemove(patchDir, "update.version"); addFileToRemove(patchDir, "update.mar"); addFileToRemove(patchDir, "updater.ini"); addFileToRemove(updateDir, "backup-update.log"); addFileToRemove(updateDir, "last-update.log"); addFileToRemove(patchDir, "update.log"); for (const file of filesToRemove) { try { if (file.exists()) { file.remove(false); } } catch (e) { console.warn("Unable to remove file. Path: '" + file.path + "', Exception: " + e); } } """ ) def prepare_update_and_close_all_windows(self): self.marionette.execute_async_script( """ let [updateURLString, resolve] = arguments; (async () => { let updateDownloadedPromise = new Promise(innerResolve => { Services.obs.addObserver(function callback() { Services.obs.removeObserver(callback, "update-downloaded"); innerResolve(); }, "update-downloaded"); }); // Set the update URL to the one that was passed in. let mockAppInfo = Object.create(Services.appinfo, { updateURL: { configurable: true, enumerable: true, writable: false, value: updateURLString, }, }); Services.appinfo = mockAppInfo; // We aren't going to flip this until after the URL is set because the test fails // if we hit the real update server. Services.prefs.setBoolPref("app.update.disabledForTesting", false); let aus = Cc["@mozilla.org/updates/update-service;1"] .getService(Ci.nsIApplicationUpdateService); aus.checkForBackgroundUpdates(); await updateDownloadedPromise; Services.obs.addObserver((aSubject, aTopic, aData) => { let silent_restart = Services.env.get("MOZ_APP_SILENT_START") == 1 && Services.env.get("MOZ_APP_RESTART") == 1; Services.prefs.setBoolPref("testing.no_window_update_restart.silent_restart_env", silent_restart); }, "quit-application-granted"); for (const win of Services.wm.getEnumerator("navigator:browser")) { win.close(); } })().then(resolve); """, script_args=(self.marionette.absolute_url("update.xml"),), )