| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
| 2 # for details. All rights reserved. Use of this source code is governed by a | |
| 3 # BSD-style license that can be found in the LICENSE file. | |
| 4 # | |
| 5 """Classes and methods for executing tasks for the test.py framework. | |
| 6 | |
| 7 This module includes: | |
| 8 - Managing parallel execution of tests using threads | |
| 9 - Windows and Unix specific code for spawning tasks and retrieving results | |
| 10 - Evaluating the output of each test as pass/fail/crash/timeout | |
| 11 """ | |
| 12 | |
| 13 import ctypes | |
| 14 import os | |
| 15 import Queue | |
| 16 import signal | |
| 17 import subprocess | |
| 18 import sys | |
| 19 import tempfile | |
| 20 import threading | |
| 21 import time | |
| 22 import traceback | |
| 23 | |
| 24 import testing | |
| 25 import utils | |
| 26 | |
| 27 | |
| 28 class Error(Exception): | |
| 29 pass | |
| 30 | |
| 31 | |
| 32 class CommandOutput(object): | |
| 33 """Represents the output of running a command.""" | |
| 34 | |
| 35 def __init__(self, pid, exit_code, timed_out, stdout, stderr): | |
| 36 self.pid = pid | |
| 37 self.exit_code = exit_code | |
| 38 self.timed_out = timed_out | |
| 39 self.stdout = stdout | |
| 40 self.stderr = stderr | |
| 41 self.failed = None | |
| 42 | |
| 43 | |
| 44 class TestOutput(object): | |
| 45 """Represents the output of running a TestCase.""" | |
| 46 | |
| 47 def __init__(self, test, command, output): | |
| 48 """Represents the output of running a TestCase. | |
| 49 | |
| 50 Args: | |
| 51 test: A TestCase instance. | |
| 52 command: the command line that was run | |
| 53 output: A CommandOutput instance. | |
| 54 """ | |
| 55 self.test = test | |
| 56 self.command = command | |
| 57 self.output = output | |
| 58 | |
| 59 def UnexpectedOutput(self): | |
| 60 """Compare the result of running the expected from the TestConfiguration. | |
| 61 | |
| 62 Returns: | |
| 63 True if the test had an unexpected output. | |
| 64 """ | |
| 65 return not self.GetOutcome() in self.test.outcomes | |
| 66 | |
| 67 def GetOutcome(self): | |
| 68 """Returns one of testing.CRASH, testing.TIMEOUT, testing.FAIL, or | |
| 69 testing.PASS.""" | |
| 70 if self.HasCrashed(): | |
| 71 return testing.CRASH | |
| 72 if self.HasTimedOut(): | |
| 73 return testing.TIMEOUT | |
| 74 if self.HasFailed(): | |
| 75 return testing.FAIL | |
| 76 return testing.PASS | |
| 77 | |
| 78 def HasCrashed(self): | |
| 79 """Returns True if the test should be considered testing.CRASH.""" | |
| 80 if utils.IsWindows(): | |
| 81 if self.output.exit_code == 3: | |
| 82 # The VM uses std::abort to terminate on asserts. | |
| 83 # std::abort terminates with exit code 3 on Windows. | |
| 84 return True | |
| 85 return (0x80000000 & self.output.exit_code | |
| 86 and not 0x3FFFFF00 & self.output.exit_code) | |
| 87 else: | |
| 88 # Timed out tests will have exit_code -signal.SIGTERM. | |
| 89 if self.output.timed_out: | |
| 90 return False | |
| 91 if self.output.exit_code == 253: | |
| 92 # The Java dartc runners exit 253 in case of unhandled exceptions. | |
| 93 return True | |
| 94 return self.output.exit_code < 0 | |
| 95 | |
| 96 def HasTimedOut(self): | |
| 97 """Returns True if the test should be considered as testing.TIMEOUT.""" | |
| 98 return self.output.timed_out | |
| 99 | |
| 100 def HasFailed(self): | |
| 101 """Returns True if the test should be considered as testing.FAIL.""" | |
| 102 execution_failed = self.test.DidFail(self.output) | |
| 103 if self.test.IsNegative(): | |
| 104 return not execution_failed | |
| 105 else: | |
| 106 return execution_failed | |
| 107 | |
| 108 | |
| 109 def Execute(args, context, timeout=None, cwd=None): | |
| 110 """Executes the specified command. | |
| 111 | |
| 112 Args: | |
| 113 args: sequence of the executable name + arguments. | |
| 114 context: An instance of Context object with global settings for test.py. | |
| 115 timeout: optional timeout to wait for results in seconds. | |
| 116 cwd: optionally change to this working directory. | |
| 117 | |
| 118 Returns: | |
| 119 An instance of CommandOutput with the collected results. | |
| 120 """ | |
| 121 (fd_out, outname) = tempfile.mkstemp() | |
| 122 (fd_err, errname) = tempfile.mkstemp() | |
| 123 (process, exit_code, timed_out) = RunProcess(context, timeout, args=args, | |
| 124 stdout=fd_out, stderr=fd_err, | |
| 125 cwd=cwd) | |
| 126 os.close(fd_out) | |
| 127 os.close(fd_err) | |
| 128 output = file(outname).read() | |
| 129 errors = file(errname).read() | |
| 130 utils.CheckedUnlink(outname) | |
| 131 utils.CheckedUnlink(errname) | |
| 132 result = CommandOutput(process.pid, exit_code, timed_out, | |
| 133 output, errors) | |
| 134 return result | |
| 135 | |
| 136 | |
| 137 def KillProcessWithID(pid): | |
| 138 """Stop a process (with SIGTERM on Unix).""" | |
| 139 if utils.IsWindows(): | |
| 140 os.popen('taskkill /T /F /PID %d' % pid) | |
| 141 else: | |
| 142 os.kill(pid, signal.SIGTERM) | |
| 143 | |
| 144 | |
| 145 MAX_SLEEP_TIME = 0.1 | |
| 146 INITIAL_SLEEP_TIME = 0.0001 | |
| 147 SLEEP_TIME_FACTOR = 1.25 | |
| 148 SEM_INVALID_VALUE = -1 | |
| 149 SEM_NOGPFAULTERRORBOX = 0x0002 # Microsoft Platform SDK WinBase.h | |
| 150 | |
| 151 | |
| 152 def Win32SetErrorMode(mode): | |
| 153 """Some weird Windows stuff you just have to do.""" | |
| 154 prev_error_mode = SEM_INVALID_VALUE | |
| 155 try: | |
| 156 prev_error_mode = ctypes.windll.kernel32.SetErrorMode(mode) | |
| 157 except ImportError: | |
| 158 pass | |
| 159 return prev_error_mode | |
| 160 | |
| 161 | |
| 162 def RunProcess(context, timeout, args, **rest): | |
| 163 """Handles the OS specific details of running a task and saving results.""" | |
| 164 if context.verbose: print '#', ' '.join(args) | |
| 165 popen_args = args | |
| 166 prev_error_mode = SEM_INVALID_VALUE | |
| 167 if utils.IsWindows(): | |
| 168 popen_args = '"' + subprocess.list2cmdline(args) + '"' | |
| 169 if context.suppress_dialogs: | |
| 170 # Try to change the error mode to avoid dialogs on fatal errors. Don't | |
| 171 # touch any existing error mode flags by merging the existing error mode. | |
| 172 # See http://blogs.msdn.com/oldnewthing/archive/2004/07/27/198410.aspx. | |
| 173 error_mode = SEM_NOGPFAULTERRORBOX | |
| 174 prev_error_mode = Win32SetErrorMode(error_mode) | |
| 175 Win32SetErrorMode(error_mode | prev_error_mode) | |
| 176 process = subprocess.Popen(shell=utils.IsWindows(), | |
| 177 args=popen_args, | |
| 178 **rest) | |
| 179 if (utils.IsWindows() and context.suppress_dialogs | |
| 180 and prev_error_mode != SEM_INVALID_VALUE): | |
| 181 Win32SetErrorMode(prev_error_mode) | |
| 182 # Compute the end time - if the process crosses this limit we | |
| 183 # consider it timed out. | |
| 184 if timeout is None: end_time = None | |
| 185 else: end_time = time.time() + timeout | |
| 186 timed_out = False | |
| 187 # Repeatedly check the exit code from the process in a | |
| 188 # loop and keep track of whether or not it times out. | |
| 189 exit_code = None | |
| 190 sleep_time = INITIAL_SLEEP_TIME | |
| 191 while exit_code is None: | |
| 192 if (not end_time is None) and (time.time() >= end_time): | |
| 193 # Kill the process and wait for it to exit. | |
| 194 KillProcessWithID(process.pid) | |
| 195 # Drain the output pipe from the process to avoid deadlock | |
| 196 process.communicate() | |
| 197 exit_code = process.wait() | |
| 198 timed_out = True | |
| 199 else: | |
| 200 exit_code = process.poll() | |
| 201 time.sleep(sleep_time) | |
| 202 sleep_time *= SLEEP_TIME_FACTOR | |
| 203 if sleep_time > MAX_SLEEP_TIME: | |
| 204 sleep_time = MAX_SLEEP_TIME | |
| 205 return (process, exit_code, timed_out) | |
| 206 | |
| 207 | |
| 208 class TestRunner(object): | |
| 209 """Base class for runners.""" | |
| 210 | |
| 211 def __init__(self, work_queue, tasks, progress): | |
| 212 self.work_queue = work_queue | |
| 213 self.tasks = tasks | |
| 214 self.terminate = False | |
| 215 self.progress = progress | |
| 216 self.threads = [] | |
| 217 self.shutdown_lock = threading.Lock() | |
| 218 | |
| 219 | |
| 220 class BatchRunner(TestRunner): | |
| 221 """Implements communication with a set of subprocesses using threads.""" | |
| 222 | |
| 223 def __init__(self, work_queue, tasks, progress, batch_cmd): | |
| 224 super(BatchRunner, self).__init__(work_queue, tasks, progress) | |
| 225 self.runners = {} | |
| 226 self.last_activity = {} | |
| 227 self.context = progress.context | |
| 228 | |
| 229 # Scale the number of tasks to the nubmer of CPUs on the machine | |
| 230 # 1:1 is too much of an overload on many machines in batch mode, | |
| 231 # so scale the ratio of threads to CPUs back. On Windows running | |
| 232 # more than one task is not safe. | |
| 233 if tasks == testing.USE_DEFAULT_CPUS: | |
| 234 if utils.IsWindows(): | |
| 235 tasks = 1 | |
| 236 else: | |
| 237 tasks = .75 * testing.HOST_CPUS | |
| 238 | |
| 239 # Start threads | |
| 240 for i in xrange(tasks): | |
| 241 thread = threading.Thread(target=self.RunThread, args=[batch_cmd, i]) | |
| 242 self.threads.append(thread) | |
| 243 thread.daemon = True | |
| 244 thread.start() | |
| 245 | |
| 246 def RunThread(self, batch_cmd, thread_number): | |
| 247 """A thread started to feed a single TestRunner.""" | |
| 248 try: | |
| 249 runner = None | |
| 250 while not self.terminate and not self.work_queue.empty(): | |
| 251 runner = subprocess.Popen(batch_cmd, | |
| 252 stdin=subprocess.PIPE, | |
| 253 stderr=subprocess.STDOUT, | |
| 254 stdout=subprocess.PIPE) | |
| 255 self.runners[thread_number] = runner | |
| 256 self.FeedTestRunner(runner, thread_number) | |
| 257 if thread_number in self.last_activity: | |
| 258 del self.last_activity[thread_number] | |
| 259 | |
| 260 # Cleanup | |
| 261 self.EndRunner(runner) | |
| 262 | |
| 263 except: | |
| 264 self.Shutdown() | |
| 265 raise | |
| 266 finally: | |
| 267 if thread_number in self.last_activity: | |
| 268 del self.last_activity[thread_number] | |
| 269 if runner: self.EndRunner(runner) | |
| 270 | |
| 271 def EndRunner(self, runner): | |
| 272 """Cleans up a single runner, killing the child if necessary.""" | |
| 273 with self.shutdown_lock: | |
| 274 if runner: | |
| 275 returncode = runner.poll() | |
| 276 if returncode is None: | |
| 277 runner.kill() | |
| 278 for (found_runner, thread_number) in self.runners.items(): | |
| 279 if runner == found_runner: | |
| 280 del self.runners[thread_number] | |
| 281 break | |
| 282 try: | |
| 283 runner.communicate() | |
| 284 except ValueError: | |
| 285 pass | |
| 286 | |
| 287 def CheckForTimeouts(self): | |
| 288 now = time.time() | |
| 289 for (thread_number, start_time) in self.last_activity.items(): | |
| 290 if now - start_time > self.context.timeout: | |
| 291 self.runners[thread_number].kill() | |
| 292 | |
| 293 def WaitForCompletion(self): | |
| 294 """Wait for threads to finish, and monitor test runners for timeouts.""" | |
| 295 for t in self.threads: | |
| 296 while True: | |
| 297 self.CheckForTimeouts() | |
| 298 t.join(timeout=5) | |
| 299 if not t.isAlive(): | |
| 300 break | |
| 301 | |
| 302 def FeedTestRunner(self, runner, thread_number): | |
| 303 """Feed commands to the fork'ed TestRunner through a Popen object.""" | |
| 304 | |
| 305 last_case = {} | |
| 306 last_buf = '' | |
| 307 | |
| 308 while not self.terminate: | |
| 309 # Is the runner still alive? | |
| 310 returninfo = runner.poll() | |
| 311 if returninfo is not None: | |
| 312 buf = last_buf + '\n' + runner.stdout.read() | |
| 313 if last_case: | |
| 314 self.RecordPassFail(last_case, buf, testing.CRASH) | |
| 315 else: | |
| 316 with self.progress.lock: | |
| 317 print >>sys. stderr, ('%s: runner unexpectedly exited: %d' | |
| 318 % (threading.currentThread().name, | |
| 319 returninfo)) | |
| 320 print 'Crash Output: ' | |
| 321 print | |
| 322 print buf | |
| 323 return | |
| 324 | |
| 325 try: | |
| 326 case = self.work_queue.get_nowait() | |
| 327 with self.progress.lock: | |
| 328 self.progress.AboutToRun(case.case) | |
| 329 | |
| 330 except Queue.Empty: | |
| 331 return | |
| 332 test_case = case.case | |
| 333 cmd = ' '.join(test_case.GetCommand()[1:]) | |
| 334 | |
| 335 try: | |
| 336 print >>runner.stdin, cmd | |
| 337 except IOError: | |
| 338 with self.progress.lock: | |
| 339 traceback.print_exc() | |
| 340 | |
| 341 # Child exited before starting the next command. | |
| 342 buf = last_buf + '\n' + runner.stdout.read() | |
| 343 self.RecordPassFail(last_case, buf, testing.CRASH) | |
| 344 | |
| 345 # We never got a chance to run this command - queue it back up. | |
| 346 self.work_queue.put(case) | |
| 347 return | |
| 348 | |
| 349 buf = '' | |
| 350 self.last_activity[thread_number] = time.time() | |
| 351 while not self.terminate: | |
| 352 line = runner.stdout.readline() | |
| 353 if self.terminate: | |
| 354 break | |
| 355 case.case.duration = time.time() - self.last_activity[thread_number] | |
| 356 if not line: | |
| 357 # EOF. Child has exited. | |
| 358 if case.case.duration > self.context.timeout: | |
| 359 with self.progress.lock: | |
| 360 print 'Child timed out after %d seconds' % self.context.timeout | |
| 361 self.RecordPassFail(case, buf, testing.TIMEOUT) | |
| 362 elif buf: | |
| 363 self.RecordPassFail(case, buf, testing.CRASH) | |
| 364 return | |
| 365 | |
| 366 # Look for TestRunner batch status escape sequence. e.g. | |
| 367 # >>> TEST PASS | |
| 368 if line.startswith('>>> '): | |
| 369 result = line.split() | |
| 370 if result[1] == 'TEST': | |
| 371 outcome = result[2].lower() | |
| 372 | |
| 373 # Read the rest of the output buffer (possible crash output) | |
| 374 if outcome == testing.CRASH: | |
| 375 buf += runner.stdout.read() | |
| 376 | |
| 377 self.RecordPassFail(case, buf, outcome) | |
| 378 | |
| 379 # Always handle crashes by restarting the runner. | |
| 380 if outcome == testing.CRASH: | |
| 381 return | |
| 382 break | |
| 383 elif result[1] == 'BATCH': | |
| 384 pass | |
| 385 else: | |
| 386 print 'Unknown cmd from batch runner: %s' % line | |
| 387 else: | |
| 388 buf += line | |
| 389 | |
| 390 # If the process crashes before the next command is executed, | |
| 391 # save info to report diagnostics. | |
| 392 last_buf = buf | |
| 393 last_case = case | |
| 394 | |
| 395 def RecordPassFail(self, case, stdout_buf, outcome): | |
| 396 """An unexpected failure occurred.""" | |
| 397 if outcome == testing.PASS or outcome == testing.OKAY: | |
| 398 exit_code = 0 | |
| 399 elif outcome == testing.CRASH: | |
| 400 exit_code = -1 | |
| 401 elif outcome == testing.FAIL or outcome == testing.TIMEOUT: | |
| 402 exit_code = 1 | |
| 403 else: | |
| 404 assert False, 'Unexpected outcome: %s' % outcome | |
| 405 | |
| 406 cmd_output = CommandOutput(0, exit_code, | |
| 407 outcome == testing.TIMEOUT, stdout_buf, '') | |
| 408 test_output = TestOutput(case.case, | |
| 409 case.case.GetCommand(), | |
| 410 cmd_output) | |
| 411 with self.progress.lock: | |
| 412 if test_output.UnexpectedOutput(): | |
| 413 self.progress.failed.append(test_output) | |
| 414 else: | |
| 415 self.progress.succeeded += 1 | |
| 416 if outcome == testing.CRASH: | |
| 417 self.progress.crashed += 1 | |
| 418 self.progress.remaining -= 1 | |
| 419 self.progress.HasRun(test_output) | |
| 420 | |
| 421 def Shutdown(self): | |
| 422 """Kill all active runners.""" | |
| 423 print 'Shutting down remaining runners.' | |
| 424 self.terminate = True | |
| 425 for runner in self.runners.values(): | |
| 426 runner.kill() | |
| 427 # Give threads a chance to exit gracefully | |
| 428 time.sleep(2) | |
| 429 for runner in self.runners.values(): | |
| 430 self.EndRunner(runner) | |
| OLD | NEW |