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