|
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/. |
|
5 |
|
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 |
|
14 |
|
15 sys.path.insert(0, os.path.abspath(os.path.realpath( |
|
16 os.path.dirname(sys.argv[0])))) |
|
17 |
|
18 from automation import Automation |
|
19 from runtests import Mochitest, MochitestOptions |
|
20 |
|
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) |
|
26 |
|
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) |
|
33 |
|
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 |
|
39 |
|
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 |
|
45 |
|
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 |
|
50 |
|
51 self.add_option("--repeat-until-failure", |
|
52 action = "store_true", dest = "repeatUntilFailure", |
|
53 help = "Runs tests continuously until failure") |
|
54 defaults["repeatUntilFailure"] = False |
|
55 |
|
56 self.set_defaults(**defaults) |
|
57 |
|
58 class VMwareMochitest(Mochitest): |
|
59 _pathFixRegEx = re.compile(r'^[cC](\:[\\\/]+)') |
|
60 |
|
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) |
|
65 |
|
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 |
|
74 |
|
75 value = options.__dict__[key] |
|
76 valueType = type(value) |
|
77 |
|
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 |
|
86 |
|
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 |
|
97 |
|
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))) |
|
103 |
|
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)) |
|
110 |
|
111 return tuple(args) |
|
112 |
|
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 |
|
120 |
|
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 |
|
128 |
|
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 |
|
136 |
|
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) |
|
145 |
|
146 commonErrors = ["Error: Invalid user name or password for the guest OS", |
|
147 "Unable to connect to host."] |
|
148 expectedErrors.extend(commonErrors) |
|
149 |
|
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 |
|
158 |
|
159 for line in process.stdout.readlines(): |
|
160 line = line.strip() |
|
161 if not line: |
|
162 continue |
|
163 errorString = line |
|
164 break |
|
165 |
|
166 expected = False |
|
167 for error in expectedErrors: |
|
168 if errorString.startswith(error): |
|
169 expected = True |
|
170 |
|
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 |
|
176 |
|
177 if not silent: |
|
178 self.automation.log.info("INFO | runtests.py | Running command again.") |
|
179 |
|
180 return (result, process.stdout.readlines()) |
|
181 |
|
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 |
|
188 |
|
189 self.automation.log.info("INFO | runtests.py | Waiting for test process to " |
|
190 "start.") |
|
191 |
|
192 listProcessesCommand = self.vmrunargs + ("listProcessesInGuest", self.vmx) |
|
193 expectedErrors = [ "Error: The virtual machine is not powered on" ] |
|
194 |
|
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) |
|
211 |
|
212 self.automation.log.info("INFO | runtests.py | Found test process, " |
|
213 "monitoring log.") |
|
214 |
|
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() |
|
233 |
|
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 |
|
240 |
|
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 |
|
255 |
|
256 return success |
|
257 |
|
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 |
|
272 |
|
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 |
|
280 |
|
281 result = self.launchVM(options) |
|
282 if result: |
|
283 return result |
|
284 |
|
285 if options.vmwareRecording: |
|
286 snapshots = self.getCurentSnapshotList() |
|
287 |
|
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) |
|
340 |
|
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() |
|
353 |
|
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]) |
|
359 |
|
360 if result: |
|
361 return result |
|
362 |
|
363 if options.shutdownVM: |
|
364 result = self.shutdownVM() |
|
365 if result: |
|
366 return result |
|
367 |
|
368 return 0 |
|
369 |
|
370 def main(): |
|
371 automation = Automation() |
|
372 mochitest = VMwareMochitest(automation) |
|
373 |
|
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) |
|
379 |
|
380 if options.vmx is None: |
|
381 parser.error("A virtual machine must be specified with " + |
|
382 "--with-vmware-vm") |
|
383 |
|
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") |
|
393 |
|
394 sys.exit(mochitest.runTests(parser, options)) |
|
395 |
|
396 if __name__ == "__main__": |
|
397 main() |