1#!/usr/bin/env python 2# Copyright (C) 2010 Google Inc. All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without 5# modification, are permitted provided that the following conditions are 6# met: 7# 8# * Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# * Redistributions in binary form must reproduce the above 11# copyright notice, this list of conditions and the following disclaimer 12# in the documentation and/or other materials provided with the 13# distribution. 14# * Neither the name of Google Inc. nor the names of its 15# contributors may be used to endorse or promote products derived from 16# this software without specific prior written permission. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30"""A Thread object for running the test shell and processing URLs from a 31shared queue. 32 33Each thread runs a separate instance of the test_shell binary and validates 34the output. When there are no more URLs to process in the shared queue, the 35thread exits. 36""" 37 38import copy 39import logging 40import os 41import Queue 42import signal 43import sys 44import thread 45import threading 46import time 47 48import test_failures 49 50 51def process_output(port, test_info, test_types, test_args, target, output_dir, 52 crash, timeout, test_run_time, actual_checksum, 53 output, error): 54 """Receives the output from a test_shell process, subjects it to a number 55 of tests, and returns a list of failure types the test produced. 56 57 Args: 58 port: port-specific hooks 59 proc: an active test_shell process 60 test_info: Object containing the test filename, uri and timeout 61 test_types: list of test types to subject the output to 62 test_args: arguments to be passed to each test 63 target: Debug or Release 64 output_dir: directory to put crash stack traces into 65 66 Returns: a list of failure objects and times for the test being processed 67 """ 68 failures = [] 69 70 # Some test args, such as the image hash, may be added or changed on a 71 # test-by-test basis. 72 local_test_args = copy.copy(test_args) 73 74 local_test_args.hash = actual_checksum 75 76 if crash: 77 failures.append(test_failures.FailureCrash()) 78 if timeout: 79 failures.append(test_failures.FailureTimeout()) 80 81 if crash: 82 logging.debug("Stacktrace for %s:\n%s" % (test_info.filename, error)) 83 # Strip off "file://" since RelativeTestFilename expects 84 # filesystem paths. 85 filename = os.path.join(output_dir, test_info.filename) 86 filename = os.path.splitext(filename)[0] + "-stack.txt" 87 port.maybe_make_directory(os.path.split(filename)[0]) 88 open(filename, "wb").write(error) 89 elif error: 90 logging.debug("Previous test output extra lines after dump:\n%s" % 91 error) 92 93 # Check the output and save the results. 94 start_time = time.time() 95 time_for_diffs = {} 96 for test_type in test_types: 97 start_diff_time = time.time() 98 new_failures = test_type.compare_output(port, test_info.filename, 99 output, local_test_args, 100 target) 101 # Don't add any more failures if we already have a crash, so we don't 102 # double-report those tests. We do double-report for timeouts since 103 # we still want to see the text and image output. 104 if not crash: 105 failures.extend(new_failures) 106 time_for_diffs[test_type.__class__.__name__] = ( 107 time.time() - start_diff_time) 108 109 total_time_for_all_diffs = time.time() - start_diff_time 110 return TestStats(test_info.filename, failures, test_run_time, 111 total_time_for_all_diffs, time_for_diffs) 112 113 114class TestStats: 115 116 def __init__(self, filename, failures, test_run_time, 117 total_time_for_all_diffs, time_for_diffs): 118 self.filename = filename 119 self.failures = failures 120 self.test_run_time = test_run_time 121 self.total_time_for_all_diffs = total_time_for_all_diffs 122 self.time_for_diffs = time_for_diffs 123 124 125class SingleTestThread(threading.Thread): 126 """Thread wrapper for running a single test file.""" 127 128 def __init__(self, port, image_path, shell_args, test_info, 129 test_types, test_args, target, output_dir): 130 """ 131 Args: 132 port: object implementing port-specific hooks 133 test_info: Object containing the test filename, uri and timeout 134 output_dir: Directory to put crash stacks into. 135 See TestShellThread for documentation of the remaining arguments. 136 """ 137 138 threading.Thread.__init__(self) 139 self._port = port 140 self._image_path = image_path 141 self._shell_args = shell_args 142 self._test_info = test_info 143 self._test_types = test_types 144 self._test_args = test_args 145 self._target = target 146 self._output_dir = output_dir 147 148 def run(self): 149 driver = self._port.start_test_driver(self._image_path, 150 self._shell_args) 151 start = time.time() 152 crash, timeout, actual_checksum, output, error = \ 153 driver.run_test(test_info.uri.strip(), test_info.timeout, 154 test_info.image_hash) 155 end = time.time() 156 self._test_stats = process_output(self._port, 157 self._test_info, self._test_types, self._test_args, 158 self._target, self._output_dir, crash, timeout, end - start, 159 actual_checksum, output, error) 160 driver.stop() 161 162 def get_test_stats(self): 163 return self._test_stats 164 165 166class TestShellThread(threading.Thread): 167 168 def __init__(self, port, filename_list_queue, result_queue, 169 test_types, test_args, image_path, shell_args, options): 170 """Initialize all the local state for this test shell thread. 171 172 Args: 173 port: interface to port-specific hooks 174 filename_list_queue: A thread safe Queue class that contains lists 175 of tuples of (filename, uri) pairs. 176 result_queue: A thread safe Queue class that will contain tuples of 177 (test, failure lists) for the test results. 178 test_types: A list of TestType objects to run the test output 179 against. 180 test_args: A TestArguments object to pass to each TestType. 181 shell_args: Any extra arguments to be passed to test_shell.exe. 182 options: A property dictionary as produced by optparse. The 183 command-line options should match those expected by 184 run_webkit_tests; they are typically passed via the 185 run_webkit_tests.TestRunner class.""" 186 threading.Thread.__init__(self) 187 self._port = port 188 self._filename_list_queue = filename_list_queue 189 self._result_queue = result_queue 190 self._filename_list = [] 191 self._test_types = test_types 192 self._test_args = test_args 193 self._driver = None 194 self._image_path = image_path 195 self._shell_args = shell_args 196 self._options = options 197 self._canceled = False 198 self._exception_info = None 199 self._directory_timing_stats = {} 200 self._test_stats = [] 201 self._num_tests = 0 202 self._start_time = 0 203 self._stop_time = 0 204 205 # Current directory of tests we're running. 206 self._current_dir = None 207 # Number of tests in self._current_dir. 208 self._num_tests_in_current_dir = None 209 # Time at which we started running tests from self._current_dir. 210 self._current_dir_start_time = None 211 212 def get_directory_timing_stats(self): 213 """Returns a dictionary mapping test directory to a tuple of 214 (number of tests in that directory, time to run the tests)""" 215 return self._directory_timing_stats 216 217 def get_individual_test_stats(self): 218 """Returns a list of (test_filename, time_to_run_test, 219 total_time_for_all_diffs, time_for_diffs) tuples.""" 220 return self._test_stats 221 222 def cancel(self): 223 """Set a flag telling this thread to quit.""" 224 self._canceled = True 225 226 def get_exception_info(self): 227 """If run() terminated on an uncaught exception, return it here 228 ((type, value, traceback) tuple). 229 Returns None if run() terminated normally. Meant to be called after 230 joining this thread.""" 231 return self._exception_info 232 233 def get_total_time(self): 234 return max(self._stop_time - self._start_time, 0.0) 235 236 def get_num_tests(self): 237 return self._num_tests 238 239 def run(self): 240 """Delegate main work to a helper method and watch for uncaught 241 exceptions.""" 242 self._start_time = time.time() 243 self._num_tests = 0 244 try: 245 logging.debug('%s starting' % (self.getName())) 246 self._run(test_runner=None, result_summary=None) 247 logging.debug('%s done (%d tests)' % (self.getName(), 248 self.get_num_tests())) 249 except: 250 # Save the exception for our caller to see. 251 self._exception_info = sys.exc_info() 252 self._stop_time = time.time() 253 # Re-raise it and die. 254 logging.error('%s dying: %s' % (self.getName(), 255 self._exception_info)) 256 raise 257 self._stop_time = time.time() 258 259 def run_in_main_thread(self, test_runner, result_summary): 260 """This hook allows us to run the tests from the main thread if 261 --num-test-shells==1, instead of having to always run two or more 262 threads. This allows us to debug the test harness without having to 263 do multi-threaded debugging.""" 264 self._run(test_runner, result_summary) 265 266 def _run(self, test_runner, result_summary): 267 """Main work entry point of the thread. Basically we pull urls from the 268 filename queue and run the tests until we run out of urls. 269 270 If test_runner is not None, then we call test_runner.UpdateSummary() 271 with the results of each test.""" 272 batch_size = 0 273 batch_count = 0 274 if self._options.batch_size: 275 try: 276 batch_size = int(self._options.batch_size) 277 except: 278 logging.info("Ignoring invalid batch size '%s'" % 279 self._options.batch_size) 280 281 # Append tests we're running to the existing tests_run.txt file. 282 # This is created in run_webkit_tests.py:_PrepareListsAndPrintOutput. 283 tests_run_filename = os.path.join(self._options.results_directory, 284 "tests_run.txt") 285 tests_run_file = open(tests_run_filename, "a") 286 287 while True: 288 if self._canceled: 289 logging.info('Testing canceled') 290 tests_run_file.close() 291 return 292 293 if len(self._filename_list) is 0: 294 if self._current_dir is not None: 295 self._directory_timing_stats[self._current_dir] = \ 296 (self._num_tests_in_current_dir, 297 time.time() - self._current_dir_start_time) 298 299 try: 300 self._current_dir, self._filename_list = \ 301 self._filename_list_queue.get_nowait() 302 except Queue.Empty: 303 self._kill_test_shell() 304 tests_run_file.close() 305 return 306 307 self._num_tests_in_current_dir = len(self._filename_list) 308 self._current_dir_start_time = time.time() 309 310 test_info = self._filename_list.pop() 311 312 # We have a url, run tests. 313 batch_count += 1 314 self._num_tests += 1 315 if self._options.run_singly: 316 failures = self._run_test_singly(test_info) 317 else: 318 failures = self._run_test(test_info) 319 320 filename = test_info.filename 321 tests_run_file.write(filename + "\n") 322 if failures: 323 # Check and kill test shell if we need too. 324 if len([1 for f in failures if f.should_kill_test_shell()]): 325 self._kill_test_shell() 326 # Reset the batch count since the shell just bounced. 327 batch_count = 0 328 # Print the error message(s). 329 error_str = '\n'.join([' ' + f.message() for f in failures]) 330 logging.debug("%s %s failed:\n%s" % (self.getName(), 331 self._port.relative_test_filename(filename), 332 error_str)) 333 else: 334 logging.debug("%s %s passed" % (self.getName(), 335 self._port.relative_test_filename(filename))) 336 self._result_queue.put((filename, failures)) 337 338 if batch_size > 0 and batch_count > batch_size: 339 # Bounce the shell and reset count. 340 self._kill_test_shell() 341 batch_count = 0 342 343 if test_runner: 344 test_runner.update_summary(result_summary) 345 346 def _run_test_singly(self, test_info): 347 """Run a test in a separate thread, enforcing a hard time limit. 348 349 Since we can only detect the termination of a thread, not any internal 350 state or progress, we can only run per-test timeouts when running test 351 files singly. 352 353 Args: 354 test_info: Object containing the test filename, uri and timeout 355 356 Return: 357 A list of TestFailure objects describing the error. 358 """ 359 worker = SingleTestThread(self._port, self._image_path, 360 self._shell_args, 361 test_info, 362 self._test_types, 363 self._test_args, 364 self._options.target, 365 self._options.results_directory) 366 367 worker.start() 368 369 # When we're running one test per test_shell process, we can enforce 370 # a hard timeout. the test_shell watchdog uses 2.5x the timeout 371 # We want to be larger than that. 372 worker.join(int(test_info.timeout) * 3.0 / 1000.0) 373 if worker.isAlive(): 374 # If join() returned with the thread still running, the 375 # test_shell.exe is completely hung and there's nothing 376 # more we can do with it. We have to kill all the 377 # test_shells to free it up. If we're running more than 378 # one test_shell thread, we'll end up killing the other 379 # test_shells too, introducing spurious crashes. We accept that 380 # tradeoff in order to avoid losing the rest of this thread's 381 # results. 382 logging.error('Test thread hung: killing all test_shells') 383 worker._driver.stop() 384 385 try: 386 stats = worker.get_test_stats() 387 self._test_stats.append(stats) 388 failures = stats.failures 389 except AttributeError, e: 390 failures = [] 391 logging.error('Cannot get results of test: %s' % 392 test_info.filename) 393 394 return failures 395 396 def _run_test(self, test_info): 397 """Run a single test file using a shared test_shell process. 398 399 Args: 400 test_info: Object containing the test filename, uri and timeout 401 402 Return: 403 A list of TestFailure objects describing the error. 404 """ 405 self._ensure_test_shell_is_running() 406 # The pixel_hash is used to avoid doing an image dump if the 407 # checksums match, so it should be set to a blank value if we 408 # are generating a new baseline. (Otherwise, an image from a 409 # previous run will be copied into the baseline.) 410 image_hash = test_info.image_hash 411 if image_hash and self._test_args.new_baseline: 412 image_hash = "" 413 start = time.time() 414 crash, timeout, actual_checksum, output, error = \ 415 self._driver.run_test(test_info.uri, test_info.timeout, image_hash) 416 end = time.time() 417 418 stats = process_output(self._port, test_info, self._test_types, 419 self._test_args, self._options.target, 420 self._options.results_directory, crash, 421 timeout, end - start, actual_checksum, 422 output, error) 423 424 self._test_stats.append(stats) 425 return stats.failures 426 427 def _ensure_test_shell_is_running(self): 428 """Start the shared test shell, if it's not running. Not for use when 429 running tests singly, since those each start a separate test shell in 430 their own thread. 431 """ 432 if (not self._driver or self._driver.poll() is not None): 433 self._driver = self._port.start_driver( 434 self._image_path, self._shell_args) 435 436 def _kill_test_shell(self): 437 """Kill the test shell process if it's running.""" 438 if self._driver: 439 self._driver.stop() 440 self._driver = None 441