michael@0: # michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: import sys michael@0: import os michael@0: import re michael@0: import types michael@0: from optparse import OptionValueError michael@0: from subprocess import PIPE michael@0: from time import sleep michael@0: from tempfile import mkstemp michael@0: michael@0: sys.path.insert(0, os.path.abspath(os.path.realpath( michael@0: os.path.dirname(sys.argv[0])))) michael@0: michael@0: from automation import Automation michael@0: from runtests import Mochitest, MochitestOptions michael@0: michael@0: class VMwareOptions(MochitestOptions): michael@0: def __init__(self, automation, mochitest, **kwargs): michael@0: defaults = {} michael@0: self._automation = automation or Automation() michael@0: MochitestOptions.__init__(self, mochitest.SCRIPT_DIRECTORY) michael@0: michael@0: def checkPathCallback(option, opt_str, value, parser): michael@0: path = mochitest.getFullPath(value) michael@0: if not os.path.exists(path): michael@0: raise OptionValueError("Path %s does not exist for %s option" michael@0: % (path, opt_str)) michael@0: setattr(parser.values, option.dest, path) michael@0: michael@0: self.add_option("--with-vmware-vm", michael@0: action = "callback", type = "string", dest = "vmx", michael@0: callback = checkPathCallback, michael@0: help = "launches the given VM and runs mochitests inside") michael@0: defaults["vmx"] = None michael@0: michael@0: self.add_option("--with-vmrun-executable", michael@0: action = "callback", type = "string", dest = "vmrun", michael@0: callback = checkPathCallback, michael@0: help = "specifies the vmrun.exe to use for VMware control") michael@0: defaults["vmrun"] = None michael@0: michael@0: self.add_option("--shutdown-vm-when-done", michael@0: action = "store_true", dest = "shutdownVM", michael@0: help = "shuts down the VM when mochitests complete") michael@0: defaults["shutdownVM"] = False michael@0: michael@0: self.add_option("--repeat-until-failure", michael@0: action = "store_true", dest = "repeatUntilFailure", michael@0: help = "Runs tests continuously until failure") michael@0: defaults["repeatUntilFailure"] = False michael@0: michael@0: self.set_defaults(**defaults) michael@0: michael@0: class VMwareMochitest(Mochitest): michael@0: _pathFixRegEx = re.compile(r'^[cC](\:[\\\/]+)') michael@0: michael@0: def convertHostPathsToGuestPaths(self, string): michael@0: """ converts a path on the host machine to a path on the guest machine """ michael@0: # XXXbent Lame! michael@0: return self._pathFixRegEx.sub(r'z\1', string) michael@0: michael@0: def prepareGuestArguments(self, parser, options): michael@0: """ returns an array of command line arguments needed to replicate the michael@0: current set of options in the guest """ michael@0: args = [] michael@0: for key in options.__dict__.keys(): michael@0: # Don't send these args to the vm test runner! michael@0: if key == "vmrun" or key == "vmx" or key == "repeatUntilFailure": michael@0: continue michael@0: michael@0: value = options.__dict__[key] michael@0: valueType = type(value) michael@0: michael@0: # Find the option in the parser's list. michael@0: option = None michael@0: for index in range(len(parser.option_list)): michael@0: if str(parser.option_list[index].dest) == key: michael@0: option = parser.option_list[index] michael@0: break michael@0: if not option: michael@0: continue michael@0: michael@0: # No need to pass args on the command line if they're just going to set michael@0: # default values. The exception is list values... For some reason the michael@0: # option parser modifies the defaults as well as the values when using the michael@0: # "append" action. michael@0: if value == parser.defaults[option.dest]: michael@0: if valueType == types.StringType and \ michael@0: value == self.convertHostPathsToGuestPaths(value): michael@0: continue michael@0: if valueType != types.ListType: michael@0: continue michael@0: michael@0: def getArgString(arg, option): michael@0: if option.action == "store_true" or option.action == "store_false": michael@0: return str(option) michael@0: return "%s=%s" % (str(option), michael@0: self.convertHostPathsToGuestPaths(str(arg))) michael@0: michael@0: if valueType == types.ListType: michael@0: # Expand lists into separate args. michael@0: for item in value: michael@0: args.append(getArgString(item, option)) michael@0: else: michael@0: args.append(getArgString(value, option)) michael@0: michael@0: return tuple(args) michael@0: michael@0: def launchVM(self, options): michael@0: """ launches the VM and enables shared folders """ michael@0: # Launch VM first. michael@0: self.automation.log.info("INFO | runtests.py | Launching the VM.") michael@0: (result, stdout) = self.runVMCommand(self.vmrunargs + ("start", self.vmx)) michael@0: if result: michael@0: return result michael@0: michael@0: # Make sure that shared folders are enabled. michael@0: self.automation.log.info("INFO | runtests.py | Enabling shared folders in " michael@0: "the VM.") michael@0: (result, stdout) = self.runVMCommand(self.vmrunargs + \ michael@0: ("enableSharedFolders", self.vmx)) michael@0: if result: michael@0: return result michael@0: michael@0: def shutdownVM(self): michael@0: """ shuts down the VM """ michael@0: self.automation.log.info("INFO | runtests.py | Shutting down the VM.") michael@0: command = self.vmrunargs + ("runProgramInGuest", self.vmx, michael@0: "c:\\windows\\system32\\shutdown.exe", "/s", "/t", "1") michael@0: (result, stdout) = self.runVMCommand(command) michael@0: return result michael@0: michael@0: def runVMCommand(self, command, expectedErrors=[], silent=False): michael@0: """ runs a command in the VM using the vmrun.exe helper """ michael@0: commandString = "" michael@0: for part in command: michael@0: commandString += str(part) + " " michael@0: if not silent: michael@0: self.automation.log.info("INFO | runtests.py | Running command: %s" michael@0: % commandString) michael@0: michael@0: commonErrors = ["Error: Invalid user name or password for the guest OS", michael@0: "Unable to connect to host."] michael@0: expectedErrors.extend(commonErrors) michael@0: michael@0: # VMware can't run commands until the VM has fully loaded so keep running michael@0: # this command in a loop until it succeeds or we try 100 times. michael@0: errorString = "" michael@0: for i in range(100): michael@0: process = Automation.Process(command, stdout=PIPE) michael@0: result = process.wait() michael@0: if result == 0: michael@0: break michael@0: michael@0: for line in process.stdout.readlines(): michael@0: line = line.strip() michael@0: if not line: michael@0: continue michael@0: errorString = line michael@0: break michael@0: michael@0: expected = False michael@0: for error in expectedErrors: michael@0: if errorString.startswith(error): michael@0: expected = True michael@0: michael@0: if not expected: michael@0: self.automation.log.warning("WARNING | runtests.py | Command \"%s\" " michael@0: "failed with result %d, : %s" michael@0: % (commandString, result, errorString)) michael@0: break michael@0: michael@0: if not silent: michael@0: self.automation.log.info("INFO | runtests.py | Running command again.") michael@0: michael@0: return (result, process.stdout.readlines()) michael@0: michael@0: def monitorVMExecution(self, appname, logfilepath): michael@0: """ monitors test execution in the VM. Waits for the test process to start, michael@0: then watches the log file for test failures and checks the status of the michael@0: process to catch crashes. Returns True if mochitests ran successfully. michael@0: """ michael@0: success = True michael@0: michael@0: self.automation.log.info("INFO | runtests.py | Waiting for test process to " michael@0: "start.") michael@0: michael@0: listProcessesCommand = self.vmrunargs + ("listProcessesInGuest", self.vmx) michael@0: expectedErrors = [ "Error: The virtual machine is not powered on" ] michael@0: michael@0: running = False michael@0: for i in range(100): michael@0: (result, stdout) = self.runVMCommand(listProcessesCommand, expectedErrors, michael@0: silent=True) michael@0: if result: michael@0: self.automation.log.warning("WARNING | runtests.py | Failed to get " michael@0: "list of processes in VM!") michael@0: return False michael@0: for line in stdout: michael@0: line = line.strip() michael@0: if line.find(appname) != -1: michael@0: running = True michael@0: break michael@0: if running: michael@0: break michael@0: sleep(1) michael@0: michael@0: self.automation.log.info("INFO | runtests.py | Found test process, " michael@0: "monitoring log.") michael@0: michael@0: completed = False michael@0: nextLine = 0 michael@0: while running: michael@0: log = open(logfilepath, "rb") michael@0: lines = log.readlines() michael@0: if len(lines) > nextLine: michael@0: linesToPrint = lines[nextLine:] michael@0: for line in linesToPrint: michael@0: line = line.strip() michael@0: if line.find("INFO SimpleTest FINISHED") != -1: michael@0: completed = True michael@0: continue michael@0: if line.find("ERROR TEST-UNEXPECTED-FAIL") != -1: michael@0: self.automation.log.info("INFO | runtests.py | Detected test " michael@0: "failure: \"%s\"" % line) michael@0: success = False michael@0: nextLine = len(lines) michael@0: log.close() michael@0: michael@0: (result, stdout) = self.runVMCommand(listProcessesCommand, expectedErrors, michael@0: silent=True) michael@0: if result: michael@0: self.automation.log.warning("WARNING | runtests.py | Failed to get " michael@0: "list of processes in VM!") michael@0: return False michael@0: michael@0: stillRunning = False michael@0: for line in stdout: michael@0: line = line.strip() michael@0: if line.find(appname) != -1: michael@0: stillRunning = True michael@0: break michael@0: if stillRunning: michael@0: sleep(5) michael@0: else: michael@0: if not completed: michael@0: self.automation.log.info("INFO | runtests.py | Test process exited " michael@0: "without finishing tests, maybe crashed.") michael@0: success = False michael@0: running = stillRunning michael@0: michael@0: return success michael@0: michael@0: def getCurentSnapshotList(self): michael@0: """ gets a list of snapshots from the VM """ michael@0: (result, stdout) = self.runVMCommand(self.vmrunargs + ("listSnapshots", michael@0: self.vmx)) michael@0: snapshots = [] michael@0: if result != 0: michael@0: self.automation.log.warning("WARNING | runtests.py | Failed to get list " michael@0: "of snapshots in VM!") michael@0: return snapshots michael@0: for line in stdout: michael@0: if line.startswith("Total snapshots:"): michael@0: continue michael@0: snapshots.append(line.strip()) michael@0: return snapshots michael@0: michael@0: def runTests(self, parser, options): michael@0: """ runs mochitests in the VM """ michael@0: # Base args that must always be passed to vmrun. michael@0: self.vmrunargs = (options.vmrun, "-T", "ws", "-gu", "Replay", "-gp", michael@0: "mozilla") michael@0: self.vmrun = options.vmrun michael@0: self.vmx = options.vmx michael@0: michael@0: result = self.launchVM(options) michael@0: if result: michael@0: return result michael@0: michael@0: if options.vmwareRecording: michael@0: snapshots = self.getCurentSnapshotList() michael@0: michael@0: def innerRun(): michael@0: """ subset of the function that must run every time if we're running until michael@0: failure """ michael@0: # Make a new shared file for the log file. michael@0: (logfile, logfilepath) = mkstemp(suffix=".log") michael@0: os.close(logfile) michael@0: # Get args to pass to VM process. Make sure we autorun and autoclose. michael@0: options.autorun = True michael@0: options.closeWhenDone = True michael@0: options.logFile = logfilepath michael@0: self.automation.log.info("INFO | runtests.py | Determining guest " michael@0: "arguments.") michael@0: runtestsArgs = self.prepareGuestArguments(parser, options) michael@0: runtestsPath = self.convertHostPathsToGuestPaths(self.SCRIPT_DIRECTORY) michael@0: runtestsPath = os.path.join(runtestsPath, "runtests.py") michael@0: runtestsCommand = self.vmrunargs + ("runProgramInGuest", self.vmx, michael@0: "-activeWindow", "-interactive", "-noWait", michael@0: "c:\\mozilla-build\\python25\\python.exe", michael@0: runtestsPath) + runtestsArgs michael@0: expectedErrors = [ "Unable to connect to host.", michael@0: "Error: The virtual machine is not powered on" ] michael@0: self.automation.log.info("INFO | runtests.py | Launching guest test " michael@0: "runner.") michael@0: (result, stdout) = self.runVMCommand(runtestsCommand, expectedErrors) michael@0: if result: michael@0: return (result, False) michael@0: self.automation.log.info("INFO | runtests.py | Waiting for guest test " michael@0: "runner to complete.") michael@0: mochitestsSucceeded = self.monitorVMExecution( michael@0: os.path.basename(options.app), logfilepath) michael@0: if mochitestsSucceeded: michael@0: self.automation.log.info("INFO | runtests.py | Guest tests passed!") michael@0: else: michael@0: self.automation.log.info("INFO | runtests.py | Guest tests failed.") michael@0: if mochitestsSucceeded and options.vmwareRecording: michael@0: newSnapshots = self.getCurentSnapshotList() michael@0: if len(newSnapshots) > len(snapshots): michael@0: self.automation.log.info("INFO | runtests.py | Removing last " michael@0: "recording.") michael@0: (result, stdout) = self.runVMCommand(self.vmrunargs + \ michael@0: ("deleteSnapshot", self.vmx, michael@0: newSnapshots[-1])) michael@0: self.automation.log.info("INFO | runtests.py | Removing guest log file.") michael@0: for i in range(30): michael@0: try: michael@0: os.remove(logfilepath) michael@0: break michael@0: except: michael@0: sleep(1) michael@0: self.automation.log.warning("WARNING | runtests.py | Couldn't remove " michael@0: "guest log file, trying again.") michael@0: return (result, mochitestsSucceeded) michael@0: michael@0: if options.repeatUntilFailure: michael@0: succeeded = True michael@0: result = 0 michael@0: count = 1 michael@0: while result == 0 and succeeded: michael@0: self.automation.log.info("INFO | runtests.py | Beginning mochitest run " michael@0: "(%d)." % count) michael@0: count += 1 michael@0: (result, succeeded) = innerRun() michael@0: else: michael@0: self.automation.log.info("INFO | runtests.py | Beginning mochitest run.") michael@0: (result, succeeded) = innerRun() michael@0: michael@0: if not succeeded and options.vmwareRecording: michael@0: newSnapshots = self.getCurentSnapshotList() michael@0: if len(newSnapshots) > len(snapshots): michael@0: self.automation.log.info("INFO | runtests.py | Failed recording saved " michael@0: "as '%s'." % newSnapshots[-1]) michael@0: michael@0: if result: michael@0: return result michael@0: michael@0: if options.shutdownVM: michael@0: result = self.shutdownVM() michael@0: if result: michael@0: return result michael@0: michael@0: return 0 michael@0: michael@0: def main(): michael@0: automation = Automation() michael@0: mochitest = VMwareMochitest(automation) michael@0: michael@0: parser = VMwareOptions(automation, mochitest) michael@0: options, args = parser.parse_args() michael@0: options = parser.verifyOptions(options, mochitest) michael@0: if (options == None): michael@0: sys.exit(1) michael@0: michael@0: if options.vmx is None: michael@0: parser.error("A virtual machine must be specified with " + michael@0: "--with-vmware-vm") michael@0: michael@0: if options.vmrun is None: michael@0: options.vmrun = os.path.join("c:\\", "Program Files", "VMware", michael@0: "VMware VIX", "vmrun.exe") michael@0: if not os.path.exists(options.vmrun): michael@0: options.vmrun = os.path.join("c:\\", "Program Files (x86)", "VMware", michael@0: "VMware VIX", "vmrun.exe") michael@0: if not os.path.exists(options.vmrun): michael@0: parser.error("Could not locate vmrun.exe, use --with-vmrun-executable" + michael@0: " to identify its location") michael@0: michael@0: sys.exit(mochitest.runTests(parser, options)) michael@0: michael@0: if __name__ == "__main__": michael@0: main()