Bug 552300 - 'Use VMware VMs to run mochitests, optionally record and repeat until they fail.' r=ted.

This commit is contained in:
Ben Turner 2010-05-17 11:00:13 -07:00
parent 1890899acf
commit cf6512babc
3 changed files with 440 additions and 10 deletions

View File

@ -62,6 +62,7 @@ _SERV_FILES = \
runtests.py \
automation.py \
runtestsremote.py \
runtestsvmware.py \
$(topsrcdir)/build/mobile/devicemanager.py \
$(topsrcdir)/build/automationutils.py \
$(topsrcdir)/build/poster.zip \

View File

@ -272,7 +272,7 @@ See <http://mochikit.com/doc/html/MochiKit/Logging.html> for details on the logg
mochitest.vmwareHelperPath = os.path.join(
options.utilityPath, VMWARE_RECORDING_HELPER_BASENAME + ".dll")
if not os.path.exists(mochitest.vmwareHelperPath):
self.error("%s not found, cannot automate VMWare recording." %
self.error("%s not found, cannot automate VMware recording." %
mochitest.vmwareHelperPath)
return options
@ -504,34 +504,34 @@ class Mochitest(object):
os.remove(manifest)
shutil.rmtree(options.profilePath)
def startVMWareRecording(self, options):
def startVMwareRecording(self, options):
""" starts recording inside VMware VM using the recording helper dll """
assert(self.automation.IS_WIN32)
from ctypes import cdll
self.vmwareHelper = cdll.LoadLibrary(self.vmwareHelperPath)
if self.vmwareHelper is None:
self.automation.log.warning("WARNING | runtests.py | Failed to load "
"VMWare recording helper")
"VMware recording helper")
return
self.automation.log.info("INFO | runtests.py | Starting VMWare recording.")
self.automation.log.info("INFO | runtests.py | Starting VMware recording.")
try:
self.vmwareHelper.StartRecording()
except Exception, e:
self.automation.log.warning("WARNING | runtests.py | Failed to start "
"VMWare recording: (%s)" % str(e))
"VMware recording: (%s)" % str(e))
self.vmwareHelper = None
def stopVMWareRecording(self):
def stopVMwareRecording(self):
""" stops recording inside VMware VM using the recording helper dll """
assert(self.automation.IS_WIN32)
if self.vmwareHelper is not None:
self.automation.log.info("INFO | runtests.py | Stopping VMWare "
self.automation.log.info("INFO | runtests.py | Stopping VMware "
"recording.")
try:
self.vmwareHelper.StopRecording()
except Exception, e:
self.automation.log.warning("WARNING | runtests.py | Failed to stop "
"VMWare recording: (%s)" % str(e))
"VMware recording: (%s)" % str(e))
self.vmwareHelper = None
def runTests(self, options):
@ -570,7 +570,7 @@ class Mochitest(object):
timeout = 330.0 # default JS harness timeout is 300 seconds
if options.vmwareRecording:
self.startVMWareRecording(options);
self.startVMwareRecording(options);
self.automation.log.info("INFO | runtests.py | Running tests: start.\n")
status = self.automation.runApp(testURL, browserEnv, options.app,
@ -584,7 +584,7 @@ class Mochitest(object):
timeout = timeout)
if options.vmwareRecording:
self.stopVMWareRecording();
self.stopVMwareRecording();
self.stopWebServer(options)
processLeakLog(self.leak_report_file, options.leakThreshold)

View File

@ -0,0 +1,429 @@
#
# ***** 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()