Bug 504653 - Create test for update testing, r=adesai, r=mrogers
authorHenrik Skupin <hskupin@mozilla.com>
Tue, 17 Nov 2009 00:05:29 +0100
changeset 217 4ad8c3954f13122da72701cffc37fdf1d9bfad93
parent 215 64bd32e5b4b5568ec0c032d30bf72a208e93475a
child 219 907927cc853e0185c08ad0987eef5befd3839fc2
push id124
push userhskupin@mozilla.com
push dateMon, 16 Nov 2009 23:11:19 +0000
reviewersadesai, mrogers
bugs504653
Bug 504653 - Create test for update testing, r=adesai, r=mrogers
.hgignore
firefox/softwareUpdate/testDirectUpdate/test1.js
firefox/softwareUpdate/testDirectUpdate/test2.js
firefox/softwareUpdate/testFallbackUpdate/test1.js
firefox/softwareUpdate/testFallbackUpdate/test2.js
firefox/softwareUpdate/testFallbackUpdate/test3.js
scripts/libs/install.py
scripts/libs/prefs.py
scripts/update.py
shared-modules/testSoftwareUpdateAPI.js
--- a/.hgignore
+++ b/.hgignore
@@ -1,3 +1,4 @@
 # Filenames that should be ignored wherever they appear
 ~$
 \.DS_Store$
+.pyc$
new file mode 100644
--- /dev/null
+++ b/firefox/softwareUpdate/testDirectUpdate/test1.js
@@ -0,0 +1,87 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is MozMill Test code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Henrik Skupin <hskupin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Litmus test #8696: Apply software update should display Restart Confirmation
+ */
+
+// Include necessary modules
+var RELATIVE_ROOT = '../../../shared-modules';
+var MODULE_REQUIRES = ['SoftwareUpdateAPI', 'UtilsAPI'];
+
+var setupModule = function(module)
+{
+  module.controller = mozmill.getBrowserController();
+  module.update = new SoftwareUpdateAPI.softwareUpdate();
+
+  // Collect some data of the current build
+  module.persisted.preBuildId = UtilsAPI.appInfo.buildID;
+  module.persisted.preLocale = UtilsAPI.appInfo.locale;
+  module.persisted.preUserAgent = UtilsAPI.appInfo.userAgent;
+  module.persisted.preVersion = UtilsAPI.appInfo.version;
+}
+
+var teardownModule = function(module)
+{
+  // Save the update properties for later usage
+  module.persisted.updateBuildId = update.activeUpdate.buildID;
+  module.persisted.updateType = update.isCompleteUpdate ? "complete" : "partial";
+  module.persisted.updateVersion = update.activeUpdate.version;
+}
+
+/**
+ * Download a minor update via the given update channel
+ */
+var testDirectUpdate_Download = function()
+{
+  // Check if the user has permissions to run the update
+  if (!update.allowed) {
+    throw new Error("User does not have permissions to run the software update.");
+  }
+
+  // Open the software update dialog and wait until the check has been finished
+  update.openDialog(controller);
+  update.waitForCheckFinished();
+
+  // Check that an update is available
+  update.assertUpdateStep('updatesfound');
+
+  // Download the given type of update from the specified channel
+  update.download(persisted.type, persisted.channel);
+
+  // We should be ready for restart
+  update.assertUpdateStep('finished');
+}
new file mode 100644
--- /dev/null
+++ b/firefox/softwareUpdate/testDirectUpdate/test2.js
@@ -0,0 +1,87 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is MozMill Test code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Henrik Skupin <hskupin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Litmus test #8187: No Updates Found
+ */
+
+// Include necessary modules
+var RELATIVE_ROOT = '../../../shared-modules';
+var MODULE_REQUIRES = ['SoftwareUpdateAPI', 'UtilsAPI'];
+
+var setupModule = function(module)
+{
+  module.controller = mozmill.getBrowserController();
+  module.update = new SoftwareUpdateAPI.softwareUpdate();
+
+  // Collect some data of the current build
+  module.persisted.postBuildId = UtilsAPI.appInfo.buildID;
+  module.persisted.postLocale = UtilsAPI.appInfo.locale;
+  module.persisted.postUserAgent = UtilsAPI.appInfo.userAgent;
+  module.persisted.postVersion = UtilsAPI.appInfo.version;
+
+  // The current version should be identical with the listed version by
+  // the updater, we shouldn't have downgraded the version, and the
+  // locale should be the same as before the update
+  if ((module.persisted.postVersion == module.persisted.updateVersion) &
+     (module.persisted.preLocale == module.persisted.postLocale))
+    persisted.success = true;
+  else
+    persisted.success = false;
+}
+
+/**
+ * Test that the update has been correctly applied and no further updates
+ * can be found.
+ */
+var testDirectUpdate_AppliedAndNoUpdatesFound = function()
+{
+  // Open the software update dialog and wait until the check has been finished
+  update.openDialog(controller);
+  update.waitForCheckFinished();
+
+  // No updates should be offered now - filter out major updates
+  try {
+    update.assertUpdateStep('noupdatesfound');
+  } catch (ex) {
+    // If a major update is offered we shouldn't fail
+    if (update.updateType == persisted.type)
+      throw new Error("Another " + persisted.type + " update has been offered.");
+  }
+
+  if (!persisted.success)
+    throw new Error("Software update failed.");
+}
new file mode 100644
--- /dev/null
+++ b/firefox/softwareUpdate/testFallbackUpdate/test1.js
@@ -0,0 +1,88 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is MozMill Test code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Henrik Skupin <hskupin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Litmus test #8696: Apply software update should display Restart Confirmation
+ */
+
+// Include necessary modules
+var RELATIVE_ROOT = '../../../shared-modules';
+var MODULE_REQUIRES = ['SoftwareUpdateAPI', 'UtilsAPI'];
+
+var setupModule = function(module)
+{
+  module.controller = mozmill.getBrowserController();
+  module.update = new SoftwareUpdateAPI.softwareUpdate();
+
+  // Collect some data of the current build
+  module.persisted.preBuildId = UtilsAPI.appInfo.buildID;
+  module.persisted.preLocale = UtilsAPI.appInfo.locale;
+  module.persisted.preUserAgent = UtilsAPI.appInfo.userAgent;
+  module.persisted.preVersion = UtilsAPI.appInfo.version;
+}
+
+var teardownModule = function(module)
+{
+  // Save the update properties for later usage
+  module.persisted.updateBuildId = update.activeUpdate.buildID;
+  module.persisted.updateType = update.isCompleteUpdate ? "complete" : "partial";
+  module.persisted.updateType += "+fallback";
+  module.persisted.updateVersion = update.activeUpdate.version;
+}
+
+var testFallbackUpdate_Download = function()
+{
+  // Check if the user has permissions to run the update
+  if (!update.allowed) {
+    throw new Error("User does not have permissions to run the software update.");
+  }
+
+  // Open the software update dialog and wait until the check has been finished
+  update.openDialog(controller);
+  update.waitForCheckFinished();
+
+  // An update should have been found
+  update.assertUpdateStep('updatesfound');
+
+  // Download the given update from the specified channel
+  update.download(persisted.type, persisted.channel);
+
+  // We should be ready for restart
+  update.assertUpdateStep('finished');
+
+  // Put the downloaded update into failed state
+  update.forceFallback();
+}
new file mode 100644
--- /dev/null
+++ b/firefox/softwareUpdate/testFallbackUpdate/test2.js
@@ -0,0 +1,62 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is MozMill Test code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Henrik Skupin <hskupin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Include necessary modules
+var RELATIVE_ROOT = '../../../shared-modules';
+var MODULE_REQUIRES = ['SoftwareUpdateAPI', 'UtilsAPI'];
+
+var setupModule = function(module)
+{
+  module.controller = mozmill.getBrowserController();
+  module.update = new SoftwareUpdateAPI.softwareUpdate();
+}
+
+/**
+ * Test that the patch hasn't been applied and the complete patch gets downloaded
+ **/
+var testFallbackUpdate_ErrorPatching = function()
+{
+  // The dialog should be open in the background and shows a failure
+  update.waitForDialogOpen(controller);
+  update.assertUpdateStep('errorpatching');
+  controller.window.focus();
+
+  // Start downloading the fallback patch
+  update.download(persisted.type, persisted.channel);
+
+  // We should be ready for restart
+  update.assertUpdateStep('finished');
+}
new file mode 100644
--- /dev/null
+++ b/firefox/softwareUpdate/testFallbackUpdate/test3.js
@@ -0,0 +1,87 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is MozMill Test code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Henrik Skupin <hskupin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Litmus test #8187: No Updates Found
+ */
+
+// Include necessary modules
+var RELATIVE_ROOT = '../../../shared-modules';
+var MODULE_REQUIRES = ['SoftwareUpdateAPI', 'UtilsAPI'];
+
+var setupModule = function(module)
+{
+  module.controller = mozmill.getBrowserController();
+  module.update = new SoftwareUpdateAPI.softwareUpdate();
+
+  // Collect some data of the current build
+  module.persisted.postBuildId = UtilsAPI.appInfo.buildID;
+  module.persisted.postLocale = UtilsAPI.appInfo.locale;
+  module.persisted.postUserAgent = UtilsAPI.appInfo.userAgent;
+  module.persisted.postVersion = UtilsAPI.appInfo.version;
+
+  // The current version should be identical with the listed version by
+  // the updater, we shouldn't have downgraded the version, and the
+  // locale should be the same as before the update
+  if ((module.persisted.postVersion == module.persisted.updateVersion) &
+     (module.persisted.preLocale == module.persisted.postLocale))
+    persisted.success = true;
+  else
+    persisted.success = false;
+}
+
+/**
+ * Test that the update has been correctly applied and no further updates
+ * can be found.
+ */
+var testFallbackUpdate_AppliedAndNoUpdatesFound = function()
+{
+  // Open the software update dialog and wait until the check has been finished
+  update.openDialog(controller);
+  update.waitForCheckFinished();
+
+  // No updates should be offered now - filter out major updates
+  try {
+    update.assertUpdateStep('noupdatesfound');
+  } catch (ex) {
+    // If a major update is offered we shouldn't fail
+    if (update.updateType == persisted.type)
+      throw new Error("Another " + persisted.type + " update has been offered.");
+  }
+
+  if (!persisted.success)
+    throw new Error("Software update failed.");
+}
new file mode 100644
--- /dev/null
+++ b/scripts/libs/install.py
@@ -0,0 +1,156 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is MozMill Test code.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Henrik Skupin <hskupin@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+import glob
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+
+class Installer(object):
+
+    def install(self, package=None, destination="./"):
+        # Check for a valid install package
+        if not os.path.isfile(package):
+            str = "%s is not valid install package." % (package)
+            raise Exception(str)
+
+        destination = os.path.join(destination, "")
+        fileName = os.path.basename(package)
+        fileExt = os.path.splitext(package)[1]
+
+        # Windows
+        if sys.platform == "win32":
+            # If there is an existing installation remove it now
+            self.uninstall(destination)
+
+            if fileExt == ".exe":
+                print "Installing %s => %s" % (fileName, destination)
+                cmdArgs = [package, "-ms", "/D=%s" % destination]
+                result = subprocess.call(cmdArgs)
+            else:
+                raise Exception("File type %s not supported." % fileExt)
+
+        # Linux
+        elif sys.platform == "linux2":
+            # If there is an existing installation remove it now
+            self.uninstall(destination)
+
+            if fileExt == ".bz2":
+                print "Installing %s => %s" % (fileName, destination)
+                try:
+                    os.mkdir(destination);
+                except:
+                    pass
+
+                cmdArgs = ["tar", "xjf", package, "-C", destination,
+                           "--strip-components=1"]
+                result = subprocess.call(cmdArgs)
+            else:
+                raise Exception("File type %s not supported." % fileExt)
+
+        elif sys.platform == "darwin":
+            # Mount disk image to a temporary mount point
+            mountpoint = tempfile.mkdtemp()
+            print "Mounting %s => %s" % (fileName, mountpoint)
+            cmdArgs = ["hdiutil", "attach", package, "-mountpoint", mountpoint]
+            result = subprocess.call(cmdArgs)
+
+            if not result:
+                # Copy application to destination folder
+                try:
+                    bundle = glob.glob(mountpoint + '/*.app')[0]
+                    targetFolder = destination + os.path.basename(bundle)
+
+                    # If there is an existing installation remove it now
+                    self.uninstall(targetFolder)
+                    print "Copying %s to %s" % (bundle, targetFolder)
+                    shutil.copytree(bundle, targetFolder)
+
+                    destination = targetFolder
+                except Exception, e:
+                    if not os.path.exists(targetFolder):
+                        print "Failure in copying the application files"
+                    raise e
+                else:
+                    # Unmount disk image
+                    print "Unmounting %s..." % fileName
+                    cmdArgs = ["hdiutil", "detach", mountpoint]
+                    result = subprocess.call(cmdArgs)
+
+        # Return the real installation folder
+        return destination
+
+    def uninstall(self, folder):
+        ''' Uninstall the build in the given folder '''
+
+        if sys.platform == "win32":
+            # On Windows we try to use the uninstaller first
+            log_file = "%suninstall\uninstall.log" % folder
+            if os.path.exists(log_file):
+                try:
+                    print "Uninstalling %s" % folder
+                    cmdArgs = ["%suninstall\helper.exe" % folder, "/S"]
+                    result = subprocess.call(cmdArgs)
+            
+                    # The uninstaller spawns another process so the system call returns
+                    # immediately. We have to wait until the uninstall folder has been
+                    # removed or until we run into a timeout.
+                    timeout = 20
+                    while (os.path.exists(log_file) & (timeout > 0)):
+                        time.sleep(1)
+                        timeout -= 1
+                except Exception, e:
+                    pass
+
+        # Check for an existent application.ini file so we don't recursively delete
+        # the wrong folder tree.
+        contents = "Contents/MacOS/" if sys.platform == "darwin" else ""
+        if os.path.exists("%s/%sapplication.ini" % (folder, contents)):
+            try:
+                print "Removing old installation at %s" % (folder)
+                shutil.rmtree(folder)
+
+                # Wait at maximum 20s for the removal
+                timeout = 20
+                while (os.path.exists(folder) & (timeout > 0)):
+                    time.sleep(1)
+                    timeout -= 1
+            except:
+                print "Folder '%s' could not be removed" % (folder)
+                pass
new file mode 100644
--- /dev/null
+++ b/scripts/libs/prefs.py
@@ -0,0 +1,113 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is MozMill Test code.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Henrik Skupin <hskupin@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+import re
+import sys
+
+class UpdateChannel(object):
+
+    # List of available update channels
+    channels = ["betatest", "beta", "nightly", "releasetest", "release"]
+
+    # Check if it's a valid update channel
+    def isValidChannel(self, channel):
+        try:
+            self.channels.index(channel);
+            return True
+        except:
+            return False
+
+    # Get the current update channel
+    def getChannel(self):
+        try:
+            file = open(self.getPrefFolder(), "r")
+        except IOError, e:
+            raise e
+        else:
+            content = file.read()
+            file.close()
+
+            result = re.search(r"(" + '|'.join(self.channels) + ")", content)
+            return result.group(0)
+
+    # Set the update channel to the specified one
+    def setChannel(self, channel):
+        print "Setting update channel to '%s'..." % channel
+
+        if not self.isValidChannel(channel):
+            raise Exception("%s is not a valid update channel" % channel)
+
+        try:
+            file = open(self.getPrefFolder(), "r")
+        except IOError, e:
+            raise e
+        else:
+            # Replace the current update channel with the specified one
+            content = file.read()
+            file.close()
+
+            # Replace the current channel with the specified one
+            result = re.sub(r"(" + '|'.join(self.channels) + ")",
+                            channel, content)
+
+            try:
+                file = open(self.getPrefFolder(), "w")
+            except IOError, e:
+                raise e
+            else:
+                file.write(result)
+                file.close()
+
+                # Check that the correct channel has been set
+                if channel != self.getChannel():
+                    raise Exception("Update channel wasn't set correctly.")
+
+    # Get the application's folder
+    def getFolder(self):
+        return self.folder
+
+    # Set the application's folder
+    def setFolder(self, folder):
+        self.folder = folder
+
+    # Get the default preferences folder
+    def getPrefFolder(self):
+        if sys.platform == "darwin":
+            return self.folder + "/Contents/MacOS/defaults/pref/channel-prefs.js"
+        elif sys.platform == "linux2":
+            return self.folder + "/defaults/pref/channel-prefs.js"
+        elif sys.platform == "win32":
+            return self.folder  + "\\defaults\\pref\\channel-prefs.js"
new file mode 100755
--- /dev/null
+++ b/scripts/update.py
@@ -0,0 +1,213 @@
+#!/usr/bin/python
+
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is MozMill Test code.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Henrik Skupin <hskupin@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+import copy
+import datetime
+import mozmill
+import optparse
+import os
+import sys
+
+try:
+    import json
+except:
+    import simplejson as json
+
+abs_path = os.path.dirname(os.path.abspath(__file__))
+
+# Import local libraries
+sys.path.append(os.path.join(abs_path, "libs"))
+from install import Installer
+from prefs import UpdateChannel
+
+class SoftwareUpdateCLI(mozmill.RestartCLI):
+    app_binary = {'darwin' : '', 'linux2' : '/firefox', 'win32' : '/firefox.exe'}
+    test_path = abs_path + "/../firefox/softwareUpdate/"
+
+    # Parser options and arguments
+    parser_options = copy.copy(mozmill.RestartCLI.parser_options)
+    parser_options.pop(("-s", "--shell"))
+    parser_options.pop(("-t", "--test",))
+    parser_options.pop(("-u", "--usecode"))
+    parser_options.pop(("-w", "--plugins"))
+
+    parser_options[("-c", "--channel",)] = dict(dest="channel", default=None, 
+                                           help="Update channel (betatest, beta, nightly, releasetest, release)")
+    parser_options[("--no-fallback",)] = dict(dest="no_fallback", default=None,
+                                              action = "store_true",
+                                              help="No fallback update should be performed")
+    parser_options[("-t", "--type",)] = dict(dest="type", default="minor",
+                                             nargs = 1,
+                                             help="Type of the update (minor, major)")
+    parser_options[("-i", "--install",)] = dict(dest="install", default=None,
+                                                nargs = 1,
+                                                help="Installation folder for the build")
+
+    def __init__(self):
+        super(SoftwareUpdateCLI, self).__init__()
+        self.options.shell = None
+        self.options.usecode = None
+        self.options.plugins = None
+
+        # We need at least one argument
+        if len(self.args) < 1:
+            print "No arguments specified. Please run with --help to see all options"
+            sys.exit(1)
+
+        # If a folder is given as argument it will be expanded to the contained
+        # files. So we only have to specify the folder instead of all builds
+        if self.options.install and os.path.isdir(self.args[0]):
+            folder = self.args.pop(0)
+            files = os.listdir(folder)
+
+            if files is None: files = []
+            for file in files:
+                full_path = os.path.join(folder, file)
+                if os.path.isfile(full_path):
+                    self.args.append(full_path)
+
+        # Check the type of the update and default to minor
+        if self.options.type != "minor" and self.options.type != "major":
+            self.options.type = "minor"
+
+    def prepare_channel(self):
+        channel = UpdateChannel()
+        channel.setFolder(self.options.folder)
+
+        if self.options.channel is None:
+            self.channel = channel.getChannel()
+        else:
+            channel.setChannel(self.options.channel)
+            self.channel = self.options.channel
+
+    def prepare_build(self, binary):
+        ''' Prepare the build for the test run '''
+        if self.options.install is not None:
+            self.options.folder = Installer().install(binary, self.options.install)
+            self.options.binary = self.options.folder + self.app_binary[sys.platform]
+        else:
+            folder = os.path.dirname(binary)
+            self.options.folder = folder if not os.path.isdir(binary) else binary
+            self.options.binary = binary
+
+    def cleanup_build(self):
+        # Always remove the build when it has been installed
+        if self.options.install:
+            Installer().uninstall(self.options.folder)
+
+    def build_wiki_entry(self, results):
+        entry = "* %s => %s, %s, %s, %s, %s, %s, '''%s'''\n" \
+                "** %s ID:%s\n** %s ID:%s\n" \
+                "** Passed %d :: Failed %d :: Skipped %d\n" % \
+                (results.get("preVersion", ""),
+                 results.get("postVersion", ""),
+                 results.get("type"),
+                 results.get("preLocale", ""),
+                 results.get("updateType", "unknown type"),
+                 results.get("channel", ""),
+                 datetime.date.today(),
+                 "PASS" if results.get("success", False) else "FAIL",
+                 results.get("preUserAgent", ""), results.get("preBuildId", ""),
+                 results.get("postUserAgent", ""), results.get("postBuildId", ""),
+                 len(results.get("passes")),
+                 len(results.get("fails")),
+                 len(results.get("skipped")))
+        return entry
+
+    def run_test(self, binary, is_fallback = False, *args, **kwargs):
+        try:
+            self.prepare_build(binary)
+            self.prepare_channel()
+
+            self.mozmill.passes = []
+            self.mozmill.fails = []
+            self.mozmill.skipped = []
+            self.mozmill.alltests = []
+
+            self.mozmill.persisted = {}
+            self.mozmill.persisted["channel"] = self.channel
+            self.mozmill.persisted["type"] = self.options.type
+
+            if is_fallback:
+                self.options.test = self.test_path + "testFallbackUpdate/"
+            else:
+                self.options.test = self.test_path + "testDirectUpdate/"
+
+            super(SoftwareUpdateCLI, self)._run(*args, **kwargs)
+        except Exception, e:
+            print e
+
+        self.cleanup_build()
+
+        # If a Mozmill test fails the update will also fail
+        if self.mozmill.fails:
+            self.mozmill.persisted["success"] = False
+
+        self.mozmill.persisted["passes"] = self.mozmill.passes
+        self.mozmill.persisted["fails"] = self.mozmill.fails
+        self.mozmill.persisted["skipped"] = self.mozmill.skipped
+
+        return self.mozmill.persisted
+
+    def run(self, *args, **kwargs):
+        ''' Run software update tests for all specified builds '''
+
+        # Run direct and fallback update tests for each build
+        self.wiki = []
+        for binary in self.args:
+            direct = self.run_test(binary, False)
+            result_direct = direct.get("success", False);
+
+            if not self.options.no_fallback:
+                fallback = self.run_test(binary, True)
+                result_fallback = fallback.get("success", False)
+            else:
+                result_fallback = False
+
+            if not (result_direct and result_fallback):
+                self.wiki.append(self.build_wiki_entry(direct))
+            if not self.options.no_fallback:
+                self.wiki.append(self.build_wiki_entry(fallback))
+
+        # Print results to the console
+        print "\nResults:\n========"
+        for result in self.wiki:
+            print result
+
+if __name__ == "__main__":
+    SoftwareUpdateCLI().run()
new file mode 100644
--- /dev/null
+++ b/shared-modules/testSoftwareUpdateAPI.js
@@ -0,0 +1,278 @@
+/* * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is MozMill Test code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Henrik Skupin <hskupin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * **** END LICENSE BLOCK ***** */
+
+/**
+ * @fileoverview
+ * The SoftwareUpdateAPI adds support for an easy access to the update process.
+ *
+ * @version 1.0.0
+ */
+
+const MODULE_NAME = 'SoftwareUpdateAPI';
+
+const RELATIVE_ROOT = '.';
+const MODULE_REQUIRES = ['PrefsAPI'];
+
+const gTimeout = 5000;
+const gTimeoutUpdateCheck = 10000;
+const gTimeoutUpdateDownload = 360000;
+
+/**
+ * Constructor for software update class
+ */
+function softwareUpdate()
+{
+  this._controller = null;
+  this._wizard = null;
+
+  this._aus = Cc["@mozilla.org/updates/update-service;1"]
+                 .getService(Ci.nsIApplicationUpdateService);
+  this._ums = Cc["@mozilla.org/updates/update-manager;1"]
+                 .getService(Ci.nsIUpdateManager);
+
+  // Get all available buttons for later clicks
+  // http://mxr.mozilla.org/mozilla-central/source/toolkit/content/widgets/wizard.xml#467
+  if (mozmill.isMac) {
+    var template = '/id("updates")/anon({"anonid":"Buttons"})/anon({"flex":"1"})' +
+                   '/{"class":"wizard-buttons-btm"}/';
+    this._buttons = {
+                      back: template + '{"dlgtype":"back"}',
+                      next: template + '{"dlgtype":"next"}',
+                      cancel: template + '{"dlgtype":"cancel"}',
+                      finish: template + '{"dlgtype":"finish"}',
+                      extra1: template + '{"dlgtype":"extra1"}',
+                      extra2: template + '{"dlgtype":"extra2"}'
+                    };
+  } else if (mozmill.isLinux || mozmill.isWindows) {
+    var template = '/id("updates")/anon({"anonid":"Buttons"})/anon({"flex":"1"})' +
+                   '/{"class":"wizard-buttons-box-2"}/';
+    this._buttons = {
+                      back: template + '{"dlgtype":"back"}',
+                      next: template + 'anon({"anonid":"WizardButtonDeck"})/[1]' +
+                                       '/{"dlgtype":"next"}',
+                      cancel: template + '{"dlgtype":"cancel"}',
+                      finish: template + 'anon({"anonid":"WizardButtonDeck"})/[0]' +
+                                         '/{"dlgtype":"finish"}',
+                      extra1: template + '{"dlgtype":"extra1"}',
+                      extra2: template + '{"dlgtype":"extra2"}'
+                    };
+  }
+}
+
+/**
+ * Class for software updates
+ */
+softwareUpdate.prototype = {
+
+  /**
+   * Returns the active update
+   */
+  get activeUpdate() {
+    return this._ums.activeUpdate;
+  },
+
+  /**
+   * Check if the user has permissions to run the software update
+   */
+  get allowed() {
+    return this._aus.canUpdate;
+  },
+
+  /**
+   * Returns the current step of the software update dialog wizard
+   */
+  get currentStep() {
+    return this._wizard.getAttribute('currentpageid');
+  },
+
+  /**
+   * Returns if the offered update is a complete update
+   */
+  get isCompleteUpdate() {
+    // XXX: Bug 514040: _ums.isCompleteUpdate doesn't work at the moment
+    if (this.activeUpdate.patchCount > 1) {
+      var patch1 = this.activeUpdate.getPatchAt(0);
+      var patch2 = this.activeUpdate.getPatchAt(1);
+
+      return (patch1.URL == patch2.URL);
+    } else {
+      return (this.activeUpdate.getPatchAt(0).type == "complete");
+    }
+  },
+
+  /**
+   * Returns the update type (minor or major)
+   */
+  get updateType() {
+    updateType = new elementslib.ID(this._controller.window.document, "updateType");
+    return updateType.getNode().getAttribute("severity");
+  },
+
+  /**
+   * Asserts the given step of the update dialog wizard
+   * Available steps: dummy, checking, noupdatesfound, incompatibleCheck,
+   *                  updatesfound, license, incompatibleList, downloading,
+   *                  errors, errorpatching, finished, finishedBackground,
+   *                  installed
+   */
+  assertUpdateStep : function softwareUpdate_assertUpdateStep(step) {
+    this._controller.waitForEval("subject.currentStep == '" + step + "'",
+                                 gTimeout, 100, this);
+  },
+
+  /**
+   * Close the software update dialog
+   */
+  closeDialog: function softwareUpdate_closeDialog() {
+    if (this._controller) {
+      this._controller.keypress(null, "VK_ESCAPE", {});
+      this._controller.sleep(500);
+      this._controller = null;
+    }
+  },
+
+  /**
+   * Download the update of the given channel and type
+   * @param {string} updateType
+   *        Update type of the update (minor or major)
+   * @param {string} channel
+   *        Update channel to use
+   */
+  download : function softwareUpdate_download(updateType, channel, timeout) {
+    timeout = timeout ? timeout : gTimeoutUpdateDownload;
+
+    if (this.currentStep == "updatesfound") {
+      // Check if the correct channel has been set
+      var prefs = collector.getModule('PrefsAPI').preferences;
+      if (channel != prefs.getPref("app.update.channel", ""))
+        throw new Error("Expected channel " + channel + "but got " + channelPref + "");
+
+      // Only allow updates of the given type
+      if (updateType != this.updateType)
+        throw new Error("Expected update " + updateType + "but got " + this.updateType + "");
+    }
+
+    // Click the next button
+    var next = new elementslib.Lookup(this._controller.window.document,
+                                      this._buttons.next);
+    this._controller.click(next);
+
+    // Wait until update has been downloaded
+    var progress = this._controller.window.document.getElementById("downloadProgress");
+    this._controller.waitForEval("subject.value == 100", timeout, 100, progress);
+  },
+
+  /**
+   * Update the update.status file and set the status to 'failed:6'
+   */
+  forceFallback : function softwareUpdate_forceFallback() {
+    var dirService = Cc["@mozilla.org/file/directory_service;1"]
+                        .getService(Ci.nsIProperties);
+
+    var updateDir;
+    var updateStatus;
+
+    // Check the global update folder first
+    try {
+      updateDir = dirService.get("UpdRootD", Ci.nsIFile);
+      updateDir.append("updates");
+      updateDir.append("0");
+
+      updateStatus = updateDir.clone();
+      updateStatus.append("update.status");
+    } catch (ex) {
+    }
+
+    if (updateStatus == undefined || !updateStatus.exists()) {
+      updateDir = dirService.get("XCurProcD", Ci.nsIFile);
+      updateDir.append("updates");
+      updateDir.append("0");
+
+      updateStatus = updateDir.clone();
+      updateStatus.append("update.status");
+    }
+
+    var foStream = Cc["@mozilla.org/network/file-output-stream;1"]
+                      .createInstance(Ci.nsIFileOutputStream);
+
+    foStream.init(updateStatus, 0x02 | 0x08 | 0x20, -1, 0);
+    foStream.write("failed:6\n", 9);
+    foStream.close();
+  },
+
+  /**
+   * Open software update dialog and get window controller
+   * @param {MozMillController} browserController
+   *        Mozmill controller of the browser window
+   */
+  openDialog: function softwareUpdate_openDialog(browserController) {
+    // Allow only one instance of the controller
+    if (this._controller)
+      return;
+
+    var updateMenu = new elementslib.Elem(browserController.menus.helpMenu.checkForUpdates);
+    browserController.click(updateMenu);
+
+    this.waitForDialogOpen(browserController);
+  },
+
+  /**
+   * Wait that check for updates has been finished
+   * @param {number} timeout
+   */
+  waitForCheckFinished : function softwareUpdate_waitForCheckFinished(timeout) {
+    timeout = timeout ? timeout : gTimeoutUpdateCheck;
+
+    this._controller.waitForEval("subject.currentStep != 'checking'", timeout, 100, this);
+  },
+
+  /**
+   * Wait for the software update dialog
+   * @param {MozMillController} browserController
+   *        Mozmill controller of the browser window
+   */
+  waitForDialogOpen : function softwareUpdate_waitForDialogOpen(browserController) {
+    browserController.sleep(500);
+
+    var window = mozmill.wm.getMostRecentWindow('Update:Wizard');
+    this._controller = new mozmill.controller.MozMillController(window);
+    this._wizard = this._controller.window.document.getElementById('updates');
+
+    // Wait until the dummy wizard page isn't visible anymore
+    this._controller.waitForEval("subject.currentStep != 'dummy'", gTimeout, 100, this);
+    this._controller.window.focus();
+  }
+}