Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
michael@0 | 1 | # |
michael@0 | 2 | # This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
michael@0 | 5 | |
michael@0 | 6 | import sys |
michael@0 | 7 | import os |
michael@0 | 8 | import re |
michael@0 | 9 | import types |
michael@0 | 10 | from optparse import OptionValueError |
michael@0 | 11 | from subprocess import PIPE |
michael@0 | 12 | from time import sleep |
michael@0 | 13 | from tempfile import mkstemp |
michael@0 | 14 | |
michael@0 | 15 | sys.path.insert(0, os.path.abspath(os.path.realpath( |
michael@0 | 16 | os.path.dirname(sys.argv[0])))) |
michael@0 | 17 | |
michael@0 | 18 | from automation import Automation |
michael@0 | 19 | from runtests import Mochitest, MochitestOptions |
michael@0 | 20 | |
michael@0 | 21 | class VMwareOptions(MochitestOptions): |
michael@0 | 22 | def __init__(self, automation, mochitest, **kwargs): |
michael@0 | 23 | defaults = {} |
michael@0 | 24 | self._automation = automation or Automation() |
michael@0 | 25 | MochitestOptions.__init__(self, mochitest.SCRIPT_DIRECTORY) |
michael@0 | 26 | |
michael@0 | 27 | def checkPathCallback(option, opt_str, value, parser): |
michael@0 | 28 | path = mochitest.getFullPath(value) |
michael@0 | 29 | if not os.path.exists(path): |
michael@0 | 30 | raise OptionValueError("Path %s does not exist for %s option" |
michael@0 | 31 | % (path, opt_str)) |
michael@0 | 32 | setattr(parser.values, option.dest, path) |
michael@0 | 33 | |
michael@0 | 34 | self.add_option("--with-vmware-vm", |
michael@0 | 35 | action = "callback", type = "string", dest = "vmx", |
michael@0 | 36 | callback = checkPathCallback, |
michael@0 | 37 | help = "launches the given VM and runs mochitests inside") |
michael@0 | 38 | defaults["vmx"] = None |
michael@0 | 39 | |
michael@0 | 40 | self.add_option("--with-vmrun-executable", |
michael@0 | 41 | action = "callback", type = "string", dest = "vmrun", |
michael@0 | 42 | callback = checkPathCallback, |
michael@0 | 43 | help = "specifies the vmrun.exe to use for VMware control") |
michael@0 | 44 | defaults["vmrun"] = None |
michael@0 | 45 | |
michael@0 | 46 | self.add_option("--shutdown-vm-when-done", |
michael@0 | 47 | action = "store_true", dest = "shutdownVM", |
michael@0 | 48 | help = "shuts down the VM when mochitests complete") |
michael@0 | 49 | defaults["shutdownVM"] = False |
michael@0 | 50 | |
michael@0 | 51 | self.add_option("--repeat-until-failure", |
michael@0 | 52 | action = "store_true", dest = "repeatUntilFailure", |
michael@0 | 53 | help = "Runs tests continuously until failure") |
michael@0 | 54 | defaults["repeatUntilFailure"] = False |
michael@0 | 55 | |
michael@0 | 56 | self.set_defaults(**defaults) |
michael@0 | 57 | |
michael@0 | 58 | class VMwareMochitest(Mochitest): |
michael@0 | 59 | _pathFixRegEx = re.compile(r'^[cC](\:[\\\/]+)') |
michael@0 | 60 | |
michael@0 | 61 | def convertHostPathsToGuestPaths(self, string): |
michael@0 | 62 | """ converts a path on the host machine to a path on the guest machine """ |
michael@0 | 63 | # XXXbent Lame! |
michael@0 | 64 | return self._pathFixRegEx.sub(r'z\1', string) |
michael@0 | 65 | |
michael@0 | 66 | def prepareGuestArguments(self, parser, options): |
michael@0 | 67 | """ returns an array of command line arguments needed to replicate the |
michael@0 | 68 | current set of options in the guest """ |
michael@0 | 69 | args = [] |
michael@0 | 70 | for key in options.__dict__.keys(): |
michael@0 | 71 | # Don't send these args to the vm test runner! |
michael@0 | 72 | if key == "vmrun" or key == "vmx" or key == "repeatUntilFailure": |
michael@0 | 73 | continue |
michael@0 | 74 | |
michael@0 | 75 | value = options.__dict__[key] |
michael@0 | 76 | valueType = type(value) |
michael@0 | 77 | |
michael@0 | 78 | # Find the option in the parser's list. |
michael@0 | 79 | option = None |
michael@0 | 80 | for index in range(len(parser.option_list)): |
michael@0 | 81 | if str(parser.option_list[index].dest) == key: |
michael@0 | 82 | option = parser.option_list[index] |
michael@0 | 83 | break |
michael@0 | 84 | if not option: |
michael@0 | 85 | continue |
michael@0 | 86 | |
michael@0 | 87 | # No need to pass args on the command line if they're just going to set |
michael@0 | 88 | # default values. The exception is list values... For some reason the |
michael@0 | 89 | # option parser modifies the defaults as well as the values when using the |
michael@0 | 90 | # "append" action. |
michael@0 | 91 | if value == parser.defaults[option.dest]: |
michael@0 | 92 | if valueType == types.StringType and \ |
michael@0 | 93 | value == self.convertHostPathsToGuestPaths(value): |
michael@0 | 94 | continue |
michael@0 | 95 | if valueType != types.ListType: |
michael@0 | 96 | continue |
michael@0 | 97 | |
michael@0 | 98 | def getArgString(arg, option): |
michael@0 | 99 | if option.action == "store_true" or option.action == "store_false": |
michael@0 | 100 | return str(option) |
michael@0 | 101 | return "%s=%s" % (str(option), |
michael@0 | 102 | self.convertHostPathsToGuestPaths(str(arg))) |
michael@0 | 103 | |
michael@0 | 104 | if valueType == types.ListType: |
michael@0 | 105 | # Expand lists into separate args. |
michael@0 | 106 | for item in value: |
michael@0 | 107 | args.append(getArgString(item, option)) |
michael@0 | 108 | else: |
michael@0 | 109 | args.append(getArgString(value, option)) |
michael@0 | 110 | |
michael@0 | 111 | return tuple(args) |
michael@0 | 112 | |
michael@0 | 113 | def launchVM(self, options): |
michael@0 | 114 | """ launches the VM and enables shared folders """ |
michael@0 | 115 | # Launch VM first. |
michael@0 | 116 | self.automation.log.info("INFO | runtests.py | Launching the VM.") |
michael@0 | 117 | (result, stdout) = self.runVMCommand(self.vmrunargs + ("start", self.vmx)) |
michael@0 | 118 | if result: |
michael@0 | 119 | return result |
michael@0 | 120 | |
michael@0 | 121 | # Make sure that shared folders are enabled. |
michael@0 | 122 | self.automation.log.info("INFO | runtests.py | Enabling shared folders in " |
michael@0 | 123 | "the VM.") |
michael@0 | 124 | (result, stdout) = self.runVMCommand(self.vmrunargs + \ |
michael@0 | 125 | ("enableSharedFolders", self.vmx)) |
michael@0 | 126 | if result: |
michael@0 | 127 | return result |
michael@0 | 128 | |
michael@0 | 129 | def shutdownVM(self): |
michael@0 | 130 | """ shuts down the VM """ |
michael@0 | 131 | self.automation.log.info("INFO | runtests.py | Shutting down the VM.") |
michael@0 | 132 | command = self.vmrunargs + ("runProgramInGuest", self.vmx, |
michael@0 | 133 | "c:\\windows\\system32\\shutdown.exe", "/s", "/t", "1") |
michael@0 | 134 | (result, stdout) = self.runVMCommand(command) |
michael@0 | 135 | return result |
michael@0 | 136 | |
michael@0 | 137 | def runVMCommand(self, command, expectedErrors=[], silent=False): |
michael@0 | 138 | """ runs a command in the VM using the vmrun.exe helper """ |
michael@0 | 139 | commandString = "" |
michael@0 | 140 | for part in command: |
michael@0 | 141 | commandString += str(part) + " " |
michael@0 | 142 | if not silent: |
michael@0 | 143 | self.automation.log.info("INFO | runtests.py | Running command: %s" |
michael@0 | 144 | % commandString) |
michael@0 | 145 | |
michael@0 | 146 | commonErrors = ["Error: Invalid user name or password for the guest OS", |
michael@0 | 147 | "Unable to connect to host."] |
michael@0 | 148 | expectedErrors.extend(commonErrors) |
michael@0 | 149 | |
michael@0 | 150 | # VMware can't run commands until the VM has fully loaded so keep running |
michael@0 | 151 | # this command in a loop until it succeeds or we try 100 times. |
michael@0 | 152 | errorString = "" |
michael@0 | 153 | for i in range(100): |
michael@0 | 154 | process = Automation.Process(command, stdout=PIPE) |
michael@0 | 155 | result = process.wait() |
michael@0 | 156 | if result == 0: |
michael@0 | 157 | break |
michael@0 | 158 | |
michael@0 | 159 | for line in process.stdout.readlines(): |
michael@0 | 160 | line = line.strip() |
michael@0 | 161 | if not line: |
michael@0 | 162 | continue |
michael@0 | 163 | errorString = line |
michael@0 | 164 | break |
michael@0 | 165 | |
michael@0 | 166 | expected = False |
michael@0 | 167 | for error in expectedErrors: |
michael@0 | 168 | if errorString.startswith(error): |
michael@0 | 169 | expected = True |
michael@0 | 170 | |
michael@0 | 171 | if not expected: |
michael@0 | 172 | self.automation.log.warning("WARNING | runtests.py | Command \"%s\" " |
michael@0 | 173 | "failed with result %d, : %s" |
michael@0 | 174 | % (commandString, result, errorString)) |
michael@0 | 175 | break |
michael@0 | 176 | |
michael@0 | 177 | if not silent: |
michael@0 | 178 | self.automation.log.info("INFO | runtests.py | Running command again.") |
michael@0 | 179 | |
michael@0 | 180 | return (result, process.stdout.readlines()) |
michael@0 | 181 | |
michael@0 | 182 | def monitorVMExecution(self, appname, logfilepath): |
michael@0 | 183 | """ monitors test execution in the VM. Waits for the test process to start, |
michael@0 | 184 | then watches the log file for test failures and checks the status of the |
michael@0 | 185 | process to catch crashes. Returns True if mochitests ran successfully. |
michael@0 | 186 | """ |
michael@0 | 187 | success = True |
michael@0 | 188 | |
michael@0 | 189 | self.automation.log.info("INFO | runtests.py | Waiting for test process to " |
michael@0 | 190 | "start.") |
michael@0 | 191 | |
michael@0 | 192 | listProcessesCommand = self.vmrunargs + ("listProcessesInGuest", self.vmx) |
michael@0 | 193 | expectedErrors = [ "Error: The virtual machine is not powered on" ] |
michael@0 | 194 | |
michael@0 | 195 | running = False |
michael@0 | 196 | for i in range(100): |
michael@0 | 197 | (result, stdout) = self.runVMCommand(listProcessesCommand, expectedErrors, |
michael@0 | 198 | silent=True) |
michael@0 | 199 | if result: |
michael@0 | 200 | self.automation.log.warning("WARNING | runtests.py | Failed to get " |
michael@0 | 201 | "list of processes in VM!") |
michael@0 | 202 | return False |
michael@0 | 203 | for line in stdout: |
michael@0 | 204 | line = line.strip() |
michael@0 | 205 | if line.find(appname) != -1: |
michael@0 | 206 | running = True |
michael@0 | 207 | break |
michael@0 | 208 | if running: |
michael@0 | 209 | break |
michael@0 | 210 | sleep(1) |
michael@0 | 211 | |
michael@0 | 212 | self.automation.log.info("INFO | runtests.py | Found test process, " |
michael@0 | 213 | "monitoring log.") |
michael@0 | 214 | |
michael@0 | 215 | completed = False |
michael@0 | 216 | nextLine = 0 |
michael@0 | 217 | while running: |
michael@0 | 218 | log = open(logfilepath, "rb") |
michael@0 | 219 | lines = log.readlines() |
michael@0 | 220 | if len(lines) > nextLine: |
michael@0 | 221 | linesToPrint = lines[nextLine:] |
michael@0 | 222 | for line in linesToPrint: |
michael@0 | 223 | line = line.strip() |
michael@0 | 224 | if line.find("INFO SimpleTest FINISHED") != -1: |
michael@0 | 225 | completed = True |
michael@0 | 226 | continue |
michael@0 | 227 | if line.find("ERROR TEST-UNEXPECTED-FAIL") != -1: |
michael@0 | 228 | self.automation.log.info("INFO | runtests.py | Detected test " |
michael@0 | 229 | "failure: \"%s\"" % line) |
michael@0 | 230 | success = False |
michael@0 | 231 | nextLine = len(lines) |
michael@0 | 232 | log.close() |
michael@0 | 233 | |
michael@0 | 234 | (result, stdout) = self.runVMCommand(listProcessesCommand, expectedErrors, |
michael@0 | 235 | silent=True) |
michael@0 | 236 | if result: |
michael@0 | 237 | self.automation.log.warning("WARNING | runtests.py | Failed to get " |
michael@0 | 238 | "list of processes in VM!") |
michael@0 | 239 | return False |
michael@0 | 240 | |
michael@0 | 241 | stillRunning = False |
michael@0 | 242 | for line in stdout: |
michael@0 | 243 | line = line.strip() |
michael@0 | 244 | if line.find(appname) != -1: |
michael@0 | 245 | stillRunning = True |
michael@0 | 246 | break |
michael@0 | 247 | if stillRunning: |
michael@0 | 248 | sleep(5) |
michael@0 | 249 | else: |
michael@0 | 250 | if not completed: |
michael@0 | 251 | self.automation.log.info("INFO | runtests.py | Test process exited " |
michael@0 | 252 | "without finishing tests, maybe crashed.") |
michael@0 | 253 | success = False |
michael@0 | 254 | running = stillRunning |
michael@0 | 255 | |
michael@0 | 256 | return success |
michael@0 | 257 | |
michael@0 | 258 | def getCurentSnapshotList(self): |
michael@0 | 259 | """ gets a list of snapshots from the VM """ |
michael@0 | 260 | (result, stdout) = self.runVMCommand(self.vmrunargs + ("listSnapshots", |
michael@0 | 261 | self.vmx)) |
michael@0 | 262 | snapshots = [] |
michael@0 | 263 | if result != 0: |
michael@0 | 264 | self.automation.log.warning("WARNING | runtests.py | Failed to get list " |
michael@0 | 265 | "of snapshots in VM!") |
michael@0 | 266 | return snapshots |
michael@0 | 267 | for line in stdout: |
michael@0 | 268 | if line.startswith("Total snapshots:"): |
michael@0 | 269 | continue |
michael@0 | 270 | snapshots.append(line.strip()) |
michael@0 | 271 | return snapshots |
michael@0 | 272 | |
michael@0 | 273 | def runTests(self, parser, options): |
michael@0 | 274 | """ runs mochitests in the VM """ |
michael@0 | 275 | # Base args that must always be passed to vmrun. |
michael@0 | 276 | self.vmrunargs = (options.vmrun, "-T", "ws", "-gu", "Replay", "-gp", |
michael@0 | 277 | "mozilla") |
michael@0 | 278 | self.vmrun = options.vmrun |
michael@0 | 279 | self.vmx = options.vmx |
michael@0 | 280 | |
michael@0 | 281 | result = self.launchVM(options) |
michael@0 | 282 | if result: |
michael@0 | 283 | return result |
michael@0 | 284 | |
michael@0 | 285 | if options.vmwareRecording: |
michael@0 | 286 | snapshots = self.getCurentSnapshotList() |
michael@0 | 287 | |
michael@0 | 288 | def innerRun(): |
michael@0 | 289 | """ subset of the function that must run every time if we're running until |
michael@0 | 290 | failure """ |
michael@0 | 291 | # Make a new shared file for the log file. |
michael@0 | 292 | (logfile, logfilepath) = mkstemp(suffix=".log") |
michael@0 | 293 | os.close(logfile) |
michael@0 | 294 | # Get args to pass to VM process. Make sure we autorun and autoclose. |
michael@0 | 295 | options.autorun = True |
michael@0 | 296 | options.closeWhenDone = True |
michael@0 | 297 | options.logFile = logfilepath |
michael@0 | 298 | self.automation.log.info("INFO | runtests.py | Determining guest " |
michael@0 | 299 | "arguments.") |
michael@0 | 300 | runtestsArgs = self.prepareGuestArguments(parser, options) |
michael@0 | 301 | runtestsPath = self.convertHostPathsToGuestPaths(self.SCRIPT_DIRECTORY) |
michael@0 | 302 | runtestsPath = os.path.join(runtestsPath, "runtests.py") |
michael@0 | 303 | runtestsCommand = self.vmrunargs + ("runProgramInGuest", self.vmx, |
michael@0 | 304 | "-activeWindow", "-interactive", "-noWait", |
michael@0 | 305 | "c:\\mozilla-build\\python25\\python.exe", |
michael@0 | 306 | runtestsPath) + runtestsArgs |
michael@0 | 307 | expectedErrors = [ "Unable to connect to host.", |
michael@0 | 308 | "Error: The virtual machine is not powered on" ] |
michael@0 | 309 | self.automation.log.info("INFO | runtests.py | Launching guest test " |
michael@0 | 310 | "runner.") |
michael@0 | 311 | (result, stdout) = self.runVMCommand(runtestsCommand, expectedErrors) |
michael@0 | 312 | if result: |
michael@0 | 313 | return (result, False) |
michael@0 | 314 | self.automation.log.info("INFO | runtests.py | Waiting for guest test " |
michael@0 | 315 | "runner to complete.") |
michael@0 | 316 | mochitestsSucceeded = self.monitorVMExecution( |
michael@0 | 317 | os.path.basename(options.app), logfilepath) |
michael@0 | 318 | if mochitestsSucceeded: |
michael@0 | 319 | self.automation.log.info("INFO | runtests.py | Guest tests passed!") |
michael@0 | 320 | else: |
michael@0 | 321 | self.automation.log.info("INFO | runtests.py | Guest tests failed.") |
michael@0 | 322 | if mochitestsSucceeded and options.vmwareRecording: |
michael@0 | 323 | newSnapshots = self.getCurentSnapshotList() |
michael@0 | 324 | if len(newSnapshots) > len(snapshots): |
michael@0 | 325 | self.automation.log.info("INFO | runtests.py | Removing last " |
michael@0 | 326 | "recording.") |
michael@0 | 327 | (result, stdout) = self.runVMCommand(self.vmrunargs + \ |
michael@0 | 328 | ("deleteSnapshot", self.vmx, |
michael@0 | 329 | newSnapshots[-1])) |
michael@0 | 330 | self.automation.log.info("INFO | runtests.py | Removing guest log file.") |
michael@0 | 331 | for i in range(30): |
michael@0 | 332 | try: |
michael@0 | 333 | os.remove(logfilepath) |
michael@0 | 334 | break |
michael@0 | 335 | except: |
michael@0 | 336 | sleep(1) |
michael@0 | 337 | self.automation.log.warning("WARNING | runtests.py | Couldn't remove " |
michael@0 | 338 | "guest log file, trying again.") |
michael@0 | 339 | return (result, mochitestsSucceeded) |
michael@0 | 340 | |
michael@0 | 341 | if options.repeatUntilFailure: |
michael@0 | 342 | succeeded = True |
michael@0 | 343 | result = 0 |
michael@0 | 344 | count = 1 |
michael@0 | 345 | while result == 0 and succeeded: |
michael@0 | 346 | self.automation.log.info("INFO | runtests.py | Beginning mochitest run " |
michael@0 | 347 | "(%d)." % count) |
michael@0 | 348 | count += 1 |
michael@0 | 349 | (result, succeeded) = innerRun() |
michael@0 | 350 | else: |
michael@0 | 351 | self.automation.log.info("INFO | runtests.py | Beginning mochitest run.") |
michael@0 | 352 | (result, succeeded) = innerRun() |
michael@0 | 353 | |
michael@0 | 354 | if not succeeded and options.vmwareRecording: |
michael@0 | 355 | newSnapshots = self.getCurentSnapshotList() |
michael@0 | 356 | if len(newSnapshots) > len(snapshots): |
michael@0 | 357 | self.automation.log.info("INFO | runtests.py | Failed recording saved " |
michael@0 | 358 | "as '%s'." % newSnapshots[-1]) |
michael@0 | 359 | |
michael@0 | 360 | if result: |
michael@0 | 361 | return result |
michael@0 | 362 | |
michael@0 | 363 | if options.shutdownVM: |
michael@0 | 364 | result = self.shutdownVM() |
michael@0 | 365 | if result: |
michael@0 | 366 | return result |
michael@0 | 367 | |
michael@0 | 368 | return 0 |
michael@0 | 369 | |
michael@0 | 370 | def main(): |
michael@0 | 371 | automation = Automation() |
michael@0 | 372 | mochitest = VMwareMochitest(automation) |
michael@0 | 373 | |
michael@0 | 374 | parser = VMwareOptions(automation, mochitest) |
michael@0 | 375 | options, args = parser.parse_args() |
michael@0 | 376 | options = parser.verifyOptions(options, mochitest) |
michael@0 | 377 | if (options == None): |
michael@0 | 378 | sys.exit(1) |
michael@0 | 379 | |
michael@0 | 380 | if options.vmx is None: |
michael@0 | 381 | parser.error("A virtual machine must be specified with " + |
michael@0 | 382 | "--with-vmware-vm") |
michael@0 | 383 | |
michael@0 | 384 | if options.vmrun is None: |
michael@0 | 385 | options.vmrun = os.path.join("c:\\", "Program Files", "VMware", |
michael@0 | 386 | "VMware VIX", "vmrun.exe") |
michael@0 | 387 | if not os.path.exists(options.vmrun): |
michael@0 | 388 | options.vmrun = os.path.join("c:\\", "Program Files (x86)", "VMware", |
michael@0 | 389 | "VMware VIX", "vmrun.exe") |
michael@0 | 390 | if not os.path.exists(options.vmrun): |
michael@0 | 391 | parser.error("Could not locate vmrun.exe, use --with-vmrun-executable" + |
michael@0 | 392 | " to identify its location") |
michael@0 | 393 | |
michael@0 | 394 | sys.exit(mochitest.runTests(parser, options)) |
michael@0 | 395 | |
michael@0 | 396 | if __name__ == "__main__": |
michael@0 | 397 | main() |