#
# ***** 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 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(
  os.path.dirname(sys.argv[0]))))

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)

    self.add_option("--with-vmware-vm",
                    action = "callback", type = "string", dest = "vmx",
                    callback = checkPathCallback,
                    help = "launches the given VM and runs mochitests inside")
    defaults["vmx"] = None

    self.add_option("--with-vmrun-executable",
                    action = "callback", type = "string", dest = "vmrun",
                    callback = checkPathCallback,
                    help = "specifies the vmrun.exe to use for VMware control")
    defaults["vmrun"] = None
 
    self.add_option("--shutdown-vm-when-done",
                    action = "store_true", dest = "shutdownVM",
                    help = "shuts down the VM when mochitests complete")
    defaults["shutdownVM"] = False

    self.add_option("--repeat-until-failure",
                    action = "store_true", dest = "repeatUntilFailure",
                    help = "Runs tests continuously until failure")
    defaults["repeatUntilFailure"] = False

    self.set_defaults(**defaults)

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":
        continue

      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]
          break
      if not option:
        continue

      # 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):
          continue
        if valueType != types.ListType:
          continue

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

      if valueType == types.ListType:
        # Expand lists into separate args.
        for item in value:
          args.append(getArgString(item, option))
      else:
        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."]
    expectedErrors.extend(commonErrors)

    # 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:
        break

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

      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))
        break

      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 "
                             "start.")

    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,
                                           silent=True)
      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
          break
      if running:
        break
      sleep(1)

    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
            continue
          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)
      log.close()

      (result, stdout) = self.runVMCommand(listProcessesCommand, expectedErrors,
                                           silent=True)
      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
          break
      if stillRunning:
        sleep(5)
      else:
        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",
                                                           self.vmx))
    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:"):
        continue
      snapshots.append(line.strip())
    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",
                      "mozilla")
    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")
      os.close(logfile)
      # 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 "
                               "arguments.")
      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",
                        "c:\\mozilla-build\\python25\\python.exe",
                        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 "
                               "runner.")
      (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!")
      else:
        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 "
                                   "recording.")
          (result, stdout) = self.runVMCommand(self.vmrunargs + \
                                               ("deleteSnapshot", self.vmx,
                                                newSnapshots[-1]))
      self.automation.log.info("INFO | runtests.py | Removing guest log file.")
      for i in range(30):
        try:
          os.remove(logfilepath)
          break
        except:
          sleep(1)
          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()
    else:
      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):
    sys.exit(1)

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

  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__":
  main()
