Bug 888624 - add test for Firefox refresh, r=MattN,AutomatedTester draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 08 Apr 2016 17:17:47 +0100
changeset 366353 330f995bd8f5f00a50107c86bf748e619d171d8c
parent 366352 092d7ffc5eeed5c0096117fff18638bbda0ebf6e
child 520754 150b06621e111ca37cbf8ffb504ab64ebfcb5803
push id17960
push usergijskruitbosch@gmail.com
push dateThu, 12 May 2016 14:07:44 +0000
reviewersMattN, AutomatedTester
bugs888624
milestone49.0a1
Bug 888624 - add test for Firefox refresh, r=MattN,AutomatedTester MozReview-Commit-ID: 4svIbYYGTKX
browser/components/migration/moz.build
browser/components/migration/tests/marionette/manifest.ini
browser/components/migration/tests/marionette/test_refresh_firefox.py
testing/marionette/components/marionettecomponent.js
testing/marionette/harness/marionette/tests/unit-tests.ini
--- a/browser/components/migration/moz.build
+++ b/browser/components/migration/moz.build
@@ -1,16 +1,18 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
 
+MARIONETTE_UNIT_MANIFESTS += ['tests/marionette/manifest.ini']
+
 JAR_MANIFESTS += ['jar.mn']
 
 XPIDL_SOURCES += [
     'nsIBrowserProfileMigrator.idl',
 ]
 
 XPIDL_MODULE = 'migration'
 
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/marionette/manifest.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+qemu = false
+b2g = false
+browser = true
+skip = false
+
+[test_refresh_firefox.py]
+
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/marionette/test_refresh_firefox.py
@@ -0,0 +1,407 @@
+import os, shutil
+from marionette import MarionetteTestCase
+
+
+class TestFirefoxRefresh(MarionetteTestCase):
+    _username = "marionette-test-login"
+    _password = "marionette-test-password"
+    _bookmarkURL = "about:mozilla"
+    _bookmarkText = "Some bookmark from Marionette"
+
+    _cookieHost = "firefox-refresh.marionette-test.mozilla.org"
+    _cookiePath = "some/cookie/path"
+    _cookieName = "somecookie"
+    _cookieValue = "some cookie value"
+
+    _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/"
+    _historyTitle = "Test visit for Firefox Reset"
+
+    _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field"
+    _formHistoryValue = "special-pumpkin-value"
+
+    _expectedURLs = ["about:robots", "about:mozilla"]
+
+    def savePassword(self):
+        self.runCode("""
+          let myLogin = new global.LoginInfo(
+            "test.marionette.mozilla.com",
+            "http://test.marionette.mozilla.com/some/form/",
+            null,
+            arguments[0],
+            arguments[1],
+            "username",
+            "password"
+          );
+          Services.logins.addLogin(myLogin)
+        """, script_args=[self._username, self._password])
+
+    def createBookmark(self):
+        self.marionette.execute_script("""
+          let url = arguments[0];
+          let title = arguments[1];
+          PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder,
+            makeURI(url), 0, title);
+        """, script_args=[self._bookmarkURL, self._bookmarkText])
+
+    def createHistory(self):
+        error = self.runAsyncCode("""
+          // Copied from PlacesTestUtils, which isn't available in Marionette tests.
+          let didReturn;
+          PlacesUtils.asyncHistory.updatePlaces(
+            [{title: arguments[1], uri: makeURI(arguments[0]), visits: [{
+                transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+                visitDate: (Date.now() - 5000) * 1000,
+                referrerURI: makeURI("about:mozilla"),
+              }]
+            }],
+            {
+              handleError(resultCode, place) {
+                didReturn = true;
+                marionetteScriptFinished("Unexpected error in adding visit: " + resultCode);
+              },
+              handleResult() {},
+              handleCompletion() {
+                if (!didReturn) {
+                  marionetteScriptFinished(false);
+                }
+              },
+            }
+          );
+        """, script_args=[self._historyURL, self._historyTitle])
+        if error:
+            print error
+
+    def createFormHistory(self):
+        error = self.runAsyncCode("""
+          let updateDefinition = {
+            op: "add",
+            fieldname: arguments[0],
+            value: arguments[1],
+            firstUsed: (Date.now() - 5000) * 1000,
+          };
+          let finished = false;
+          global.FormHistory.update(updateDefinition, {
+            handleError(error) {
+              finished = true;
+              marionetteScriptFinished(error);
+            },
+            handleCompletion() {
+              if (!finished) {
+                marionetteScriptFinished(false);
+              }
+            }
+          });
+        """, script_args=[self._formHistoryFieldName, self._formHistoryValue])
+        if error:
+          print error
+
+    def createCookie(self):
+        self.runCode("""
+          // Expire in 15 minutes:
+          let expireTime = Math.floor(Date.now() / 1000) + 15 * 60;
+          Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3],
+                               true, false, false, expireTime);
+        """, script_args=[self._cookieHost, self._cookiePath, self._cookieName, self._cookieValue])
+
+    def createSession(self):
+        self.runAsyncCode("""
+          const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP +
+                                 Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+          let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+          let expectedURLs = Array.from(arguments[0])
+          gBrowser.addTabsProgressListener({
+            onStateChange(browser, webprogress, request, flags, status) {
+              try {
+                request && request.QueryInterface(Ci.nsIChannel);
+              } catch (ex) {}
+              let uriLoaded = request.originalURI && request.originalURI.spec;
+              if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded &&
+                  expectedURLs.includes(uriLoaded)) {
+                TabStateFlusher.flush(browser).then(function() {
+                  expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1);
+                  if (!expectedURLs.length) {
+                    gBrowser.removeTabsProgressListener(this);
+                    marionetteScriptFinished();
+                  }
+                });
+              }
+            }
+          });
+          for (let url of expectedURLs) {
+            gBrowser.addTab(url);
+          }
+        """, script_args=[self._expectedURLs])
+
+    def checkPassword(self):
+        loginInfo = self.marionette.execute_script("""
+          let ary = Services.logins.findLogins({},
+            "test.marionette.mozilla.com",
+            "http://test.marionette.mozilla.com/some/form/",
+            null, {});
+          return ary.length ? ary : {username: "null", password: "null"};
+        """)
+        self.assertEqual(len(loginInfo), 1)
+        self.assertEqual(loginInfo[0]['username'], self._username)
+        self.assertEqual(loginInfo[0]['password'], self._password)
+
+        loginCount = self.marionette.execute_script("""
+          return Services.logins.getAllLogins().length;
+        """)
+        self.assertEqual(loginCount, 1, "No other logins are present")
+
+    def checkBookmark(self):
+        titleInBookmarks = self.marionette.execute_script("""
+          let url = arguments[0];
+          let bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(makeURI(url), {}, {});
+          return bookmarkIds.length == 1 ? PlacesUtils.bookmarks.getItemTitle(bookmarkIds[0]) : "";
+        """, script_args=[self._bookmarkURL])
+        self.assertEqual(titleInBookmarks, self._bookmarkText)
+
+    def checkHistory(self):
+        historyResults = self.runAsyncCode("""
+          let placeInfos = [];
+          PlacesUtils.asyncHistory.getPlacesInfo(makeURI(arguments[0]), {
+            handleError(resultCode, place) {
+              placeInfos = null;
+              marionetteScriptFinished("Unexpected error in fetching visit: " + resultCode);
+            },
+            handleResult(placeInfo) {
+              placeInfos.push(placeInfo);
+            },
+            handleCompletion() {
+              if (placeInfos) {
+                if (!placeInfos.length) {
+                  marionetteScriptFinished("No visits found");
+                } else {
+                  marionetteScriptFinished(placeInfos);
+                }
+              }
+            },
+          });
+        """, script_args=[self._historyURL])
+        if type(historyResults) == str:
+            self.fail(historyResults)
+            return
+
+        historyCount = len(historyResults)
+        self.assertEqual(historyCount, 1, "Should have exactly 1 entry for URI, got %d" % historyCount)
+        if historyCount == 1:
+            self.assertEqual(historyResults[0]['title'], self._historyTitle)
+
+    def checkFormHistory(self):
+        formFieldResults = self.runAsyncCode("""
+          let results = [];
+          global.FormHistory.search(["value"], {fieldname: arguments[0]}, {
+            handleError(error) {
+              results = error;
+            },
+            handleResult(result) {
+              results.push(result);
+            },
+            handleCompletion() {
+              marionetteScriptFinished(results);
+            },
+          });
+        """, script_args=[self._formHistoryFieldName])
+        if type(formFieldResults) == str:
+            self.fail(formFieldResults)
+            return
+
+        formFieldResultCount = len(formFieldResults)
+        self.assertEqual(formFieldResultCount, 1, "Should have exactly 1 entry for this field, got %d" % formFieldResultCount)
+        if formFieldResultCount == 1:
+            self.assertEqual(formFieldResults[0]['value'], self._formHistoryValue)
+
+        formHistoryCount = self.runAsyncCode("""
+          let count;
+          let callbacks = {
+            handleResult: rv => count = rv,
+            handleCompletion() {
+              marionetteScriptFinished(count);
+            },
+          };
+          global.FormHistory.count({}, callbacks);
+        """)
+        self.assertEqual(formHistoryCount, 1, "There should be only 1 entry in the form history")
+
+    def checkCookie(self):
+        cookieInfo = self.runCode("""
+          try {
+            let cookieEnum = Services.cookies.getCookiesFromHost(arguments[0]);
+            let cookie = null;
+            while (cookieEnum.hasMoreElements()) {
+              if (cookie != null) {
+                return "more than 1 cookie! That shouldn't happen!";
+              }
+              cookie = cookieEnum.getNext();
+              cookie.QueryInterface(Ci.nsICookie2);
+            }
+            return {path: cookie.path, name: cookie.name, value: cookie.value};
+          } catch (ex) {
+            return "got exception trying to fetch cookie: " + ex;
+          }
+        """, script_args=[self._cookieHost])
+        self.assertEqual(cookieInfo['path'], self._cookiePath)
+        self.assertEqual(cookieInfo['value'], self._cookieValue)
+        self.assertEqual(cookieInfo['name'], self._cookieName)
+
+    def checkSession(self):
+        tabURIs = self.runCode("""
+          return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)
+        """)
+        self.assertSequenceEqual(tabURIs, ["about:welcomeback"])
+
+        tabURIs = self.runAsyncCode("""
+          let mm = gBrowser.selectedBrowser.messageManager;
+          let fs = function() {
+            content.document.getElementById("errorTryAgain").click();
+          };
+          let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+          window.addEventListener("SSWindowStateReady", function testSSPostReset() {
+            window.removeEventListener("SSWindowStateReady", testSSPostReset, false);
+            Promise.all(gBrowser.browsers.map(b => TabStateFlusher.flush(b))).then(function() {
+              marionetteScriptFinished([... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec));
+            });
+          }, false);
+          mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true);
+        """)
+        self.assertSequenceEqual(tabURIs, ["about:blank"] + self._expectedURLs)
+        pass
+
+    def checkProfile(self, hasMigrated=False):
+        self.checkPassword()
+        self.checkBookmark()
+        self.checkHistory()
+        self.checkFormHistory()
+        self.checkCookie()
+        if hasMigrated:
+            self.checkSession()
+
+    def createProfileData(self):
+        self.savePassword()
+        self.createBookmark()
+        self.createHistory()
+        self.createFormHistory()
+        self.createCookie()
+        self.createSession()
+
+    def setUpScriptData(self):
+        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+        self.marionette.execute_script("""
+          global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+          global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
+          global.Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
+          global.FormHistory = Cu.import("resource://gre/modules/FormHistory.jsm", {}).FormHistory;
+        """, new_sandbox=False, sandbox='system')
+
+    def runCode(self, script, *args, **kwargs):
+        return self.marionette.execute_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)
+
+    def runAsyncCode(self, script, *args, **kwargs):
+        return self.marionette.execute_async_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)
+
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        self.setUpScriptData()
+
+        self.reset_profile_path = None
+        self.desktop_backup_path = None
+
+        self.createProfileData()
+
+    def tearDown(self):
+        # Force yet another restart with a clean profile to disconnect from the
+        # profile and environment changes we've made, to leave a more or less
+        # blank slate for the next person.
+        self.marionette.restart(clean=True, in_app=False)
+        self.setUpScriptData()
+
+        # Super
+        MarionetteTestCase.tearDown(self)
+
+        # Some helpers to deal with removing a load of files
+        import errno, stat
+        def handleRemoveReadonly(func, path, exc):
+            excvalue = exc[1]
+            if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES:
+                os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
+                func(path)
+            else:
+                raise
+
+        if self.desktop_backup_path:
+            shutil.rmtree(self.desktop_backup_path, ignore_errors=False, onerror=handleRemoveReadonly)
+
+        if self.reset_profile_path:
+            # Remove ourselves from profiles.ini
+            profileLeafName = os.path.basename(os.path.normpath(self.reset_profile_path))
+            self.runCode("""
+              let [salt, name] = arguments[0].split(".");
+              let profile = global.profSvc.getProfileByName(name);
+              profile.remove(false)
+              global.profSvc.flush();
+            """, script_args=[profileLeafName])
+            # And delete all the files.
+            shutil.rmtree(self.reset_profile_path, ignore_errors=False, onerror=handleRemoveReadonly)
+
+    def doReset(self):
+        self.runCode("""
+          // Ensure the current (temporary) profile is in profiles.ini:
+          let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+          let profileName = "marionette-test-profile-" + Date.now();
+          let myProfile = global.profSvc.createProfile(profD, profileName);
+          global.profSvc.flush()
+
+          // Now add the reset parameters:
+          let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+          let allMarionettePrefs = Services.prefs.getChildList("marionette.");
+          let prefObj = {};
+          for (let pref of allMarionettePrefs) {
+            let prefSuffix = pref.substr("marionette.".length);
+            let prefVal = global.Preferences.get(pref);
+            prefObj[prefSuffix] = prefVal;
+          }
+          let marionetteInfo = JSON.stringify(prefObj);
+          env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", marionetteInfo);
+          env.set("MOZ_RESET_PROFILE_RESTART", "1");
+          env.set("XRE_PROFILE_PATH", arguments[0]);
+          env.set("XRE_PROFILE_NAME", profileName);
+        """, script_args=[self.marionette.instance.profile.profile])
+
+        profileLeafName = os.path.basename(os.path.normpath(self.marionette.instance.profile.profile))
+
+        # Now restart the browser to get it reset:
+        self.marionette.restart(clean=False, in_app=True)
+        self.setUpScriptData()
+
+        # Determine the new profile path (we'll need to remove it when we're done)
+        self.reset_profile_path = self.runCode("""
+          let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+          return profD.path;
+        """)
+
+        # Determine the backup path
+        self.desktop_backup_path = self.runCode("""
+          let container;
+          try {
+            container = Services.dirsvc.get("Desk", Ci.nsIFile);
+          } catch (ex) {
+            container = Services.dirsvc.get("Home", Ci.nsIFile);
+          }
+          let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties");
+          let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name], 1);
+          container.append(dirName);
+          container.append(arguments[0]);
+          return container.path;
+        """, script_args = [profileLeafName])
+
+        self.assertTrue(os.path.isdir(self.reset_profile_path), "Reset profile path should be present")
+        self.assertTrue(os.path.isdir(self.desktop_backup_path), "Backup profile path should be present")
+
+    def testReset(self):
+        self.checkProfile()
+
+        self.doReset()
+
+        # Now check that we're doing OK...
+        self.checkProfile(hasMigrated=True)
--- a/testing/marionette/components/marionettecomponent.js
+++ b/testing/marionette/components/marionettecomponent.js
@@ -1,25 +1,39 @@
 /* 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/. */
 
 "use strict";
 
-const {Constructor: CC, interfaces: Ci, utils: Cu} = Components;
+const {Constructor: CC, interfaces: Ci, utils: Cu, classes: Cc} = Components;
 
 const MARIONETTE_CONTRACTID = "@mozilla.org/marionette;1";
 const MARIONETTE_CID = Components.ID("{786a1369-dca5-4adc-8486-33d23c88010a}");
 
 const DEFAULT_PORT = 2828;
 const ENABLED_PREF = "marionette.defaultPrefs.enabled";
 const PORT_PREF = "marionette.defaultPrefs.port";
 const FORCELOCAL_PREF = "marionette.force-local";
 const LOG_PREF = "marionette.logging";
 
+/**
+ * Besides starting based on existing prefs in a profile and a commandline flag,
+ * we also support inheriting prefs out of an env var, and to start marionette
+ * that way.
+ * This allows marionette prefs to persist when we do a restart into a
+ * different profile in order to test things like Firefox refresh.
+ * The env var itself, if present, is interpreted as a JSON structure, with the
+ * keys mapping to preference names in the "marionette." branch, and the values
+ * to the values of those prefs. So something like {"defaultPrefs.enabled": true}
+ * in the env var would result in the marionette.defaultPrefs.enabled pref being
+ * set to true, thus triggering marionette being enabled for that startup.
+ */
+const ENV_PREF_VAR = "MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS";
+
 const ServerSocket = CC("@mozilla.org/network/server-socket;1",
     "nsIServerSocket",
     "initSpecialConnection");
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -100,16 +114,17 @@ MarionetteComponent.prototype.handle = f
     this.logger.debug("Marionette enabled via command-line flag");
     this.init();
   }
 };
 
 MarionetteComponent.prototype.observe = function(subj, topic, data) {
   switch (topic) {
     case "profile-after-change":
+      this.maybeReadPrefsFromEnvironment();
       // Using final-ui-startup as the xpcom category doesn't seem to work,
       // so we wait for that by adding an observer here.
       this.observerService.addObserver(this, "final-ui-startup", false);
 #ifdef ENABLE_MARIONETTE
       this.enabled = Preferences.get(ENABLED_PREF, false);
       if (this.enabled) {
         this.logger.debug("Marionette enabled via build flag and pref");
 
@@ -136,16 +151,35 @@ MarionetteComponent.prototype.observe = 
 
     case "xpcom-shutdown":
       this.observerService.removeObserver(this, "xpcom-shutdown");
       this.uninit();
       break;
   }
 };
 
+MarionetteComponent.prototype.maybeReadPrefsFromEnvironment = function() {
+  let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+  if (env.exists(ENV_PREF_VAR)) {
+    let prefStr = env.get(ENV_PREF_VAR);
+    let prefs;
+    try {
+      prefs = JSON.parse(prefStr);
+    } catch (ex) {
+      Cu.reportError("Invalid marionette prefs in environment; prefs won't have been applied.");
+      Cu.reportError(ex);
+    }
+    if (prefs) {
+      for (let prefName of Object.keys(prefs)) {
+        Preferences.set("marionette." + prefName, prefs[prefName]);
+      }
+    }
+  }
+}
+
 MarionetteComponent.prototype.suppressSafeModeDialog_ = function(win) {
   // Wait for the modal dialog to finish loading.
   win.addEventListener("load", function onload() {
     win.removeEventListener("load", onload);
 
     if (win.document.getElementById("safeModeDialog")) {
       // Accept the dialog to start in safe-mode
       win.setTimeout(() => {
--- a/testing/marionette/harness/marionette/tests/unit-tests.ini
+++ b/testing/marionette/harness/marionette/tests/unit-tests.ini
@@ -18,8 +18,11 @@ test_container = true
 ; layout tests
 [include:../../../../../layout/base/tests/marionette/manifest.ini]
 
 ; loop tests
 [include:../../../../../browser/extensions/loop/manifest.ini]
 
 ; microformats tests
 [include:../../../../../toolkit/components/microformats/manifest.ini]
+
+; migration tests
+[include:../../../../../browser/components/migration/tests/marionette/manifest.ini]