# # ***** 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 # # 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()