author Daniel Brooks <db48x@db48x.net>
Fri, 29 Oct 2010 15:10:10 -0500
changeset 58868 5bc82216672f67f86b754a5a10fa0e5667e75b7f
parent 42384 cf5dcc522934ef6e484838fa0c76bbd5b0bba154
child 96742 f4157e8c410708d76703f19e4dfb61859bfe32d8
permissions -rw-r--r--
about:startup - present correctly localized dates in the tables (localizing the dates in the graph is tricker), Also, fix the calculation for the minimum value of the graph's x axis (forgot to take into account the funkiness of javascript's numbers)

# 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 VMware Mochitest Runner.
# The Initial Developer of the Original Code is
# Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2010
# the Initial Developer. All Rights Reserved.
# Contributor(s):
#   Ben Turner <bent.mozilla@gmail.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 sys
import os
import re
import types
from optparse import OptionValueError
from subprocess import PIPE
from time import sleep
from tempfile import mkstemp

sys.path.insert(0, os.path.abspath(os.path.realpath(

from automation import Automation
from runtests import Mochitest, MochitestOptions

class VMwareOptions(MochitestOptions):
  def __init__(self, automation, mochitest, **kwargs):
    defaults = {}
    MochitestOptions.__init__(self, automation, mochitest.SCRIPT_DIRECTORY)

    def checkPathCallback(option, opt_str, value, parser):
      path = mochitest.getFullPath(value)
      if not os.path.exists(path):
        raise OptionValueError("Path %s does not exist for %s option"
                               % (path, opt_str))
      setattr(parser.values, option.dest, path)

                    action = "callback", type = "string", dest = "vmx",
                    callback = checkPathCallback,
                    help = "launches the given VM and runs mochitests inside")
    defaults["vmx"] = None

                    action = "callback", type = "string", dest = "vmrun",
                    callback = checkPathCallback,
                    help = "specifies the vmrun.exe to use for VMware control")
    defaults["vmrun"] = None
                    action = "store_true", dest = "shutdownVM",
                    help = "shuts down the VM when mochitests complete")
    defaults["shutdownVM"] = False

                    action = "store_true", dest = "repeatUntilFailure",
                    help = "Runs tests continuously until failure")
    defaults["repeatUntilFailure"] = False


class VMwareMochitest(Mochitest):
  _pathFixRegEx = re.compile(r'^[cC](\:[\\\/]+)')

  def convertHostPathsToGuestPaths(self, string):
    """ converts a path on the host machine to a path on the guest machine """
    # XXXbent Lame!
    return self._pathFixRegEx.sub(r'z\1', string)

  def prepareGuestArguments(self, parser, options):
    """ returns an array of command line arguments needed to replicate the
        current set of options in the guest """
    args = []
    for key in options.__dict__.keys():
      # Don't send these args to the vm test runner!
      if key == "vmrun" or key == "vmx" or key == "repeatUntilFailure":

      value = options.__dict__[key]
      valueType = type(value)

      # Find the option in the parser's list.
      option = None
      for index in range(len(parser.option_list)):
        if str(parser.option_list[index].dest) == key:
          option = parser.option_list[index]
      if not option:

      # No need to pass args on the command line if they're just going to set
      # default values. The exception is list values... For some reason the
      # option parser modifies the defaults as well as the values when using the
      # "append" action.
      if value == parser.defaults[option.dest]:
        if valueType == types.StringType and \
           value == self.convertHostPathsToGuestPaths(value):
        if valueType != types.ListType:

      def getArgString(arg, option):
        if option.action == "store_true" or option.action == "store_false":
          return str(option)
        return "%s=%s" % (str(option),

      if valueType == types.ListType:
        # Expand lists into separate args.
        for item in value:
          args.append(getArgString(item, option))
        args.append(getArgString(value, option))

    return tuple(args)

  def launchVM(self, options):
    """ launches the VM and enables shared folders """
    # Launch VM first.
    self.automation.log.info("INFO | runtests.py | Launching the VM.")
    (result, stdout) = self.runVMCommand(self.vmrunargs + ("start", self.vmx))
    if result:
      return result

    # Make sure that shared folders are enabled.
    self.automation.log.info("INFO | runtests.py | Enabling shared folders in "
                             "the VM.")
    (result, stdout) = self.runVMCommand(self.vmrunargs + \
                                         ("enableSharedFolders", self.vmx))
    if result:
      return result

  def shutdownVM(self):
    """ shuts down the VM """
    self.automation.log.info("INFO | runtests.py | Shutting down the VM.")
    command = self.vmrunargs + ("runProgramInGuest", self.vmx,
              "c:\\windows\\system32\\shutdown.exe", "/s", "/t", "1")
    (result, stdout) = self.runVMCommand(command)
    return result

  def runVMCommand(self, command, expectedErrors=[], silent=False):
    """ runs a command in the VM using the vmrun.exe helper """
    commandString = ""
    for part in command:
      commandString += str(part) + " "
    if not silent:
      self.automation.log.info("INFO | runtests.py | Running command: %s"
                               % commandString)

    commonErrors = ["Error: Invalid user name or password for the guest OS",
                    "Unable to connect to host."]

    # VMware can't run commands until the VM has fully loaded so keep running
    # this command in a loop until it succeeds or we try 100 times.
    errorString = ""
    for i in range(100):
      process = Automation.Process(command, stdout=PIPE)
      result = process.wait()
      if result == 0:

      for line in process.stdout.readlines():
        line = line.strip()
        if not line:
        errorString = line

      expected = False
      for error in expectedErrors:
        if errorString.startswith(error):
          expected = True

      if not expected:
        self.automation.log.warning("WARNING | runtests.py | Command \"%s\" "
                                    "failed with result %d, : %s"
                                    % (commandString, result, errorString))

      if not silent:
        self.automation.log.info("INFO | runtests.py | Running command again.")

    return (result, process.stdout.readlines())

  def monitorVMExecution(self, appname, logfilepath):
    """ monitors test execution in the VM. Waits for the test process to start,
        then watches the log file for test failures and checks the status of the
        process to catch crashes. Returns True if mochitests ran successfully.
    success = True

    self.automation.log.info("INFO | runtests.py | Waiting for test process to "

    listProcessesCommand = self.vmrunargs + ("listProcessesInGuest", self.vmx)
    expectedErrors = [ "Error: The virtual machine is not powered on" ]

    running  = False
    for i in range(100):
      (result, stdout) = self.runVMCommand(listProcessesCommand, expectedErrors,
      if result:
        self.automation.log.warning("WARNING | runtests.py | Failed to get "
                                    "list of processes in VM!")
        return False
      for line in stdout:
        line = line.strip()
        if line.find(appname) != -1:
          running = True
      if running:

    self.automation.log.info("INFO | runtests.py | Found test process, "
                             "monitoring log.")

    completed = False
    nextLine = 0
    while running:
      log = open(logfilepath, "rb")
      lines = log.readlines()
      if len(lines) > nextLine:
        linesToPrint = lines[nextLine:]
        for line in linesToPrint:
          line = line.strip()
          if line.find("INFO SimpleTest FINISHED") != -1:
            completed = True
          if line.find("ERROR TEST-UNEXPECTED-FAIL") != -1:
            self.automation.log.info("INFO | runtests.py | Detected test "
                                     "failure: \"%s\"" % line)
            success = False
        nextLine = len(lines)

      (result, stdout) = self.runVMCommand(listProcessesCommand, expectedErrors,
      if result:
        self.automation.log.warning("WARNING | runtests.py | Failed to get "
                                    "list of processes in VM!")
        return False

      stillRunning = False
      for line in stdout:
        line = line.strip()
        if line.find(appname) != -1:
          stillRunning = True
      if stillRunning:
        if not completed:
          self.automation.log.info("INFO | runtests.py | Test process exited "
                                   "without finishing tests, maybe crashed.")
          success = False
        running = stillRunning

    return success

  def getCurentSnapshotList(self):
    """ gets a list of snapshots from the VM """
    (result, stdout) = self.runVMCommand(self.vmrunargs + ("listSnapshots",
    snapshots = []
    if result != 0:
      self.automation.log.warning("WARNING | runtests.py | Failed to get list "
                                  "of snapshots in VM!")
      return snapshots
    for line in stdout:
      if line.startswith("Total snapshots:"):
    return snapshots

  def runTests(self, parser, options):
    """ runs mochitests in the VM """
    # Base args that must always be passed to vmrun.
    self.vmrunargs = (options.vmrun, "-T", "ws", "-gu", "Replay", "-gp",
    self.vmrun = options.vmrun
    self.vmx = options.vmx

    result = self.launchVM(options)
    if result:
      return result

    if options.vmwareRecording:
      snapshots = self.getCurentSnapshotList()

    def innerRun():
      """ subset of the function that must run every time if we're running until
          failure """
      # Make a new shared file for the log file.
      (logfile, logfilepath) = mkstemp(suffix=".log")
      # Get args to pass to VM process. Make sure we autorun and autoclose.
      options.autorun = True
      options.closeWhenDone = True
      options.logFile = logfilepath
      self.automation.log.info("INFO | runtests.py | Determining guest "
      runtestsArgs = self.prepareGuestArguments(parser, options)
      runtestsPath = self.convertHostPathsToGuestPaths(self.SCRIPT_DIRECTORY)
      runtestsPath = os.path.join(runtestsPath, "runtests.py")
      runtestsCommand = self.vmrunargs + ("runProgramInGuest", self.vmx,
                        "-activeWindow", "-interactive", "-noWait",
                        runtestsPath) + runtestsArgs
      expectedErrors = [ "Unable to connect to host.",
                         "Error: The virtual machine is not powered on" ]
      self.automation.log.info("INFO | runtests.py | Launching guest test "
      (result, stdout) = self.runVMCommand(runtestsCommand, expectedErrors)
      if result:
        return (result, False)
      self.automation.log.info("INFO | runtests.py | Waiting for guest test "
                               "runner to complete.")
      mochitestsSucceeded = self.monitorVMExecution(
        os.path.basename(options.app), logfilepath)
      if mochitestsSucceeded:
        self.automation.log.info("INFO | runtests.py | Guest tests passed!")
        self.automation.log.info("INFO | runtests.py | Guest tests failed.")
      if mochitestsSucceeded and options.vmwareRecording:
        newSnapshots = self.getCurentSnapshotList()
        if len(newSnapshots) > len(snapshots):
          self.automation.log.info("INFO | runtests.py | Removing last "
          (result, stdout) = self.runVMCommand(self.vmrunargs + \
                                               ("deleteSnapshot", self.vmx,
      self.automation.log.info("INFO | runtests.py | Removing guest log file.")
      for i in range(30):
          self.automation.log.warning("WARNING | runtests.py | Couldn't remove "
                                      "guest log file, trying again.")
      return (result, mochitestsSucceeded)

    if options.repeatUntilFailure:
      succeeded = True
      result = 0
      count = 1
      while result == 0 and succeeded:
        self.automation.log.info("INFO | runtests.py | Beginning mochitest run "
                                 "(%d)." % count)
        count += 1
        (result, succeeded) = innerRun()
      self.automation.log.info("INFO | runtests.py | Beginning mochitest run.")
      (result, succeeded) = innerRun()

    if not succeeded and options.vmwareRecording:
      newSnapshots = self.getCurentSnapshotList()
      if len(newSnapshots) > len(snapshots):
        self.automation.log.info("INFO | runtests.py | Failed recording saved "
                                 "as '%s'." % newSnapshots[-1])

    if result:
      return result

    if options.shutdownVM:
      result = self.shutdownVM()
      if result:
        return result

    return 0

def main():
  automation = Automation()
  mochitest = VMwareMochitest(automation)

  parser = VMwareOptions(automation, mochitest)
  options, args = parser.parse_args()
  options = parser.verifyOptions(options, mochitest)
  if (options == None):

  if options.vmx is None:
    parser.error("A virtual machine must be specified with " +

  if options.vmrun is None:
    options.vmrun = os.path.join("c:\\", "Program Files", "VMware",
                                 "VMware VIX", "vmrun.exe")
    if not os.path.exists(options.vmrun):
      options.vmrun = os.path.join("c:\\", "Program Files (x86)", "VMware",
                                   "VMware VIX", "vmrun.exe")
      if not os.path.exists(options.vmrun):
        parser.error("Could not locate vmrun.exe, use --with-vmrun-executable" +
                     " to identify its location")

  sys.exit(mochitest.runTests(parser, options))

if __name__ == "__main__":