Bug 688300 - 'mochitest runtests.py --install-extension is totally broken'. r=ted.
authorBen Turner <bent.mozilla@gmail.com>
Fri, 14 Oct 2011 08:45:58 -0700
changeset 80123 870bd2683c5e7e432e379e0e89ff8e10615830b6
parent 80122 e82ecd01882523247a428164bf2d1de8ee4095b6
child 80124 e39e7e990e3a5495c5afba45c14bdcb8f02ec7f1
push id434
push userclegnitto@mozilla.com
push dateWed, 21 Dec 2011 12:10:54 +0000
treeherdermozilla-beta@bddb6ed8dd47 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted
bugs688300
milestone10.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 688300 - 'mochitest runtests.py --install-extension is totally broken'. r=ted.
build/automation.py.in
testing/mochitest/runtests.py
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -32,31 +32,32 @@
 # 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 *****
 
+from __future__ import with_statement
 import codecs
 from datetime import datetime, timedelta
 import itertools
 import logging
 import os
 import re
 import select
 import shutil
 import signal
 import subprocess
 import sys
 import threading
 import tempfile
+import sqlite3
 import zipfile
-import sqlite3
 
 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
 sys.path.insert(0, SCRIPT_DIR)
 import automationutils
 
 _DEFAULT_WEB_SERVER = "127.0.0.1"
 _DEFAULT_HTTP_PORT = 8888
 _DEFAULT_SSL_PORT = 4443
@@ -92,16 +93,79 @@ else:
 # threads, which is needed to process the output of the server and application
 # processes simultaneously.
 _log = logging.getLogger()
 handler = logging.StreamHandler(sys.stdout)
 _log.setLevel(logging.INFO)
 _log.addHandler(handler)
 
 
+class ZipFileReader(object):
+  """
+  Class to read zip files in Python 2.5 and later. Limited to only what we
+  actually use.
+  """
+
+  def __init__(self, filename):
+    self._zipfile = zipfile.ZipFile(filename, "r")
+
+  def __del__(self):
+    self._zipfile.close()
+
+  def _getnormalizedpath(self, path):
+    """
+    Gets a normalized path from 'path' (or the current working directory if
+    'path' is None). Also asserts that the path exists.
+    """
+    if path is None:
+      path = os.curdir
+    path = os.path.normpath(os.path.expanduser(path))
+    assert os.path.isdir(path)
+    return path
+
+  def _extractname(self, name, path):
+    """
+    Extracts a file with the given name from the zip file to the given path.
+    Also creates any directories needed along the way.
+    """
+    filename = os.path.normpath(os.path.join(path, name))
+    if name.endswith("/"):
+      os.makedirs(filename)
+    else:
+      path = os.path.split(filename)[0]
+      if not os.path.isdir(path):
+        os.makedirs(path)
+      with open(filename, "wb") as dest:
+        dest.write(self._zipfile.read(name))
+
+  def namelist(self):
+    return self._zipfile.namelist()
+
+  def read(self, name):
+    return self._zipfile.read(name)
+
+  def extract(self, name, path = None):
+    if hasattr(self._zipfile, "extract"):
+      return self._zipfile.extract(name, path)
+
+    # This will throw if name is not part of the zip file.
+    self._zipfile.getinfo(name)
+
+    self._extractname(name, self._getnormalizedpath(path))
+
+  def extractall(self, path = None):
+    if hasattr(self._zipfile, "extractall"):
+      return self._zipfile.extractall(path)
+
+    path = self._getnormalizedpath(path)
+
+    for name in self._zipfile.namelist():
+      self._extractname(name, path)
+
+
 #################
 # PROFILE SETUP #
 #################
 
 class SyntaxError(Exception):
   "Signifies a syntax error on a particular line in server-locations.txt."
 
   def __init__(self, lineno, msg = None):
@@ -918,40 +982,92 @@ user_pref("camino.use_system_proxy_setti
     if os.path.exists(processLog):
       os.unlink(processLog)
 
     if self.IS_TEST_BUILD and runSSLTunnel:
       ssltunnelProcess.kill()
 
     return status
 
-  """
-   Copies an extension into the extensions directory of the given profile.
-   extensionSource - the source location of the extension files.  This can be either
-                     a directory or a path to an xpi file.
-   profileDir      - the profile directory we are copying into.  We will create the
-                     "extensions" directory there if it doesn't exist.
-   extensionID     - the id of the extension to be used as the containing directory for the
-                     extension, if extensionSource is a directory, i.e.
-                 this is the name of the folder in the <profileDir>/extensions/<extensionID>
-  """
+  def getExtensionIDFromRDF(self, rdfSource):
+    """
+    Retrieves the extension id from an install.rdf file (or string).
+    """
+    from xml.dom.minidom import parse, parseString, Node
+
+    if isinstance(rdfSource, file):
+      document = parse(rdfSource)
+    else:
+      document = parseString(rdfSource)
+
+    # Find the <em:id> element. There can be multiple <em:id> tags
+    # within <em:targetApplication> tags, so we have to check this way.
+    for rdfChild in document.documentElement.childNodes:
+      if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description":
+        for descChild in rdfChild.childNodes:
+          if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id":
+            return descChild.childNodes[0].data
+
+    return None
+
   def installExtension(self, extensionSource, profileDir, extensionID = None):
+    """
+    Copies an extension into the extensions directory of the given profile.
+    extensionSource - the source location of the extension files.  This can be either
+                      a directory or a path to an xpi file.
+    profileDir      - the profile directory we are copying into.  We will create the
+                      "extensions" directory there if it doesn't exist.
+    extensionID     - the id of the extension to be used as the containing directory for the
+                      extension, if extensionSource is a directory, i.e.
+                  this is the name of the folder in the <profileDir>/extensions/<extensionID>
+    """
     if not os.path.isdir(profileDir):
       self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir)
       return
 
-    extnsdir = os.path.join(profileDir, "extensions")
+    installRDFFilename = "install.rdf"
+
+    extensionsRootDir = os.path.join(profileDir, "extensions")
+    if not os.path.isdir(extensionsRootDir):
+      os.mkdir(extensionsRootDir)
 
     if os.path.isfile(extensionSource):
-      # Copy extension xpi directly.
-      # "destination file is created or overwritten".
-      shutil.copy2(extensionSource, extnsdir)
-    elif os.path.isdir(extensionSource):
-      if extensionID == None:
+      reader = ZipFileReader(extensionSource)
+
+      for filename in reader.namelist():
+        # Sanity check the zip file.
+        if os.path.isabs(filename):
+          self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi")
+          return
+
+        # We may need to dig the extensionID out of the zip file...
+        if extensionID is None and filename == installRDFFilename:
+          extensionID = self.getExtensionIDFromRDF(reader.read(filename))
+
+      # We must know the extensionID now.
+      if extensionID is None:
         self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
         return
 
+      # Make the extension directory.
+      extensionDir = os.path.join(extensionsRootDir, extensionID)
+      os.mkdir(extensionDir)
+
+      # Extract all files.
+      reader.extractall(extensionDir)
+
+    elif os.path.isdir(extensionSource):
+      if extensionID is None:
+        filename = os.path.join(extensionSource, installRDFFilename)
+        if os.path.isfile(filename):
+          with open(filename, "r") as installRDF:
+            extensionID = self.getExtensionIDFromRDF(installRDF)
+
+        if extensionID is None:
+          self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
+          return
+
       # Copy extension tree into its own directory.
       # "destination directory must not already exist".
-      shutil.copytree(extensionSource, os.path.join(extnsdir, extensionID))
+      shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID))
+
     else:
       self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource)
-      return
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -855,17 +855,24 @@ overlay chrome://navigator/content/navig
     # Install distributed extensions, if application has any.
     distExtDir = os.path.join(options.app[ : options.app.rfind(os.sep)], "distribution", "extensions")
     if os.path.isdir(distExtDir):
       for f in os.listdir(distExtDir):
         self.automation.installExtension(os.path.join(distExtDir, f), options.profilePath)
 
     # Install custom extensions.
     for f in options.extensionsToInstall:
-      self.automation.installExtension(self.getFullPath(f), options.profilePath)
+      if f.endswith(os.sep):
+        f = f[:-1]
+
+      extensionPath = self.getFullPath(f)
+
+      self.automation.log.info("INFO | runtests.py | Installing extension at %s to %s." %
+                               (extensionPath, options.profilePath))
+      self.automation.installExtension(extensionPath, options.profilePath)
 
 def main():
   automation = Automation()
   mochitest = Mochitest(automation)
   parser = MochitestOptions(automation, mochitest.SCRIPT_DIRECTORY)
   options, args = parser.parse_args()
 
   options = parser.verifyOptions(options, mochitest)