OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python2.4 |
| 2 # Copyright 2009, Google Inc. |
| 3 # All rights reserved. |
| 4 # |
| 5 # Redistribution and use in source and binary forms, with or without |
| 6 # modification, are permitted provided that the following conditions are |
| 7 # met: |
| 8 # |
| 9 # * Redistributions of source code must retain the above copyright |
| 10 # notice, this list of conditions and the following disclaimer. |
| 11 # * Redistributions in binary form must reproduce the above |
| 12 # copyright notice, this list of conditions and the following disclaimer |
| 13 # in the documentation and/or other materials provided with the |
| 14 # distribution. |
| 15 # * Neither the name of Google Inc. nor the names of its |
| 16 # contributors may be used to endorse or promote products derived from |
| 17 # this software without specific prior written permission. |
| 18 # |
| 19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 30 |
| 31 |
| 32 """Test runners and associated classes. |
| 33 |
| 34 Each test runner has its own thread, which attempts to perform a given test. |
| 35 If a test hangs, the test runner can be aborted or exited. |
| 36 |
| 37 """ |
| 38 |
| 39 import os |
| 40 import sys |
| 41 |
| 42 import socket |
| 43 import subprocess |
| 44 import threading |
| 45 import time |
| 46 import unittest |
| 47 import gflags |
| 48 import selenium |
| 49 import selenium_constants |
| 50 import Queue |
| 51 import thread |
| 52 import copy |
| 53 |
| 54 class StringBuffer: |
| 55 """Primitive string buffer. |
| 56 |
| 57 Members: |
| 58 data: the contents of the buffer |
| 59 """ |
| 60 def __init__(self): |
| 61 self.data = "" |
| 62 def write(self, data): |
| 63 self.data += str(data) |
| 64 def writeln(self, data=None): |
| 65 if data is not None: |
| 66 self.write(data) |
| 67 self.write("\n") |
| 68 def get(self): |
| 69 get_data = self.data |
| 70 self.data = "" |
| 71 return get_data |
| 72 |
| 73 class TestResult(unittest.TestResult): |
| 74 """A specialized class that prints formatted text results to a stream. |
| 75 |
| 76 """ |
| 77 separator1 = "=" * 70 |
| 78 separator2 = "-" * 70 |
| 79 |
| 80 def __init__(self, stream, browser): |
| 81 unittest.TestResult.__init__(self) |
| 82 self.stream = stream |
| 83 # Dictionary of start times |
| 84 self.start_times = {} |
| 85 # Dictionary of results |
| 86 self.results = {} |
| 87 self.browser = browser |
| 88 |
| 89 def getDescription(self, test): |
| 90 """Gets description of test.""" |
| 91 return test.shortDescription() or str(test) |
| 92 |
| 93 def startTest(self, test): |
| 94 """Starts test.""" |
| 95 # Records the start time |
| 96 self.start_times[test] = time.time() |
| 97 # Default testresult if success not called |
| 98 self.results[test] = "FAIL" |
| 99 unittest.TestResult.startTest(self, test) |
| 100 self.stream.writeln() |
| 101 self.stream.writeln(self.separator2) |
| 102 self.stream.write(self.getDescription(test)) |
| 103 self.stream.writeln(" ... ") |
| 104 |
| 105 def stopTest(self, test): |
| 106 """Called when test is ended.""" |
| 107 time_taken = time.time() - self.start_times[test] |
| 108 result = self.results[test] |
| 109 self.stream.writeln("SELENIUMRESULT %s <%s> [%.3fs]: %s" |
| 110 % (test, self.browser, time_taken, result)) |
| 111 |
| 112 def addSuccess(self, test): |
| 113 """Adds success result to TestResult.""" |
| 114 unittest.TestResult.addSuccess(self, test) |
| 115 self.results[test] = "PASS" |
| 116 |
| 117 def addError(self, test, err): |
| 118 """Adds error result to TestResult.""" |
| 119 unittest.TestResult.addError(self, test, err) |
| 120 self.results[test] = "FAIL" |
| 121 |
| 122 def addFailure(self, test, err): |
| 123 """Adds failure result to TestResult.""" |
| 124 unittest.TestResult.addFailure(self, test, err) |
| 125 self.results[test] = "FAIL" |
| 126 |
| 127 def noResponse(self, test): |
| 128 """Configures the result for a test that did not respond.""" |
| 129 self.results[test] = "FAIL" |
| 130 self.testsRun += 1 |
| 131 self.errors.append("No response from test") |
| 132 |
| 133 self.stream.writeln() |
| 134 self.stream.writeln(self.separator2) |
| 135 self.stream.write(self.getDescription(test)) |
| 136 self.stream.writeln(" ... ") |
| 137 self.stream.writeln("SELENIUMRESULT %s <%s> [?s]: FAIL (HUNG?)" |
| 138 % (test, self.browser)) |
| 139 self.stream.writeln() |
| 140 |
| 141 def printErrors(self): |
| 142 """Prints all errors and failures.""" |
| 143 self.stream.writeln() |
| 144 self.printErrorList("ERROR", self.errors) |
| 145 self.printErrorList("FAIL", self.failures) |
| 146 |
| 147 def printErrorList(self, flavour, errors): |
| 148 """Prints a given list of errors.""" |
| 149 for test, err in errors: |
| 150 self.stream.writeln("%s:" % flavour) |
| 151 self.stream.writeln("%s" % err) |
| 152 |
| 153 def printAll(self, stream): |
| 154 """Prints the entire stream to the given stream.""" |
| 155 stream.write(self.stream.data) |
| 156 |
| 157 def merge(self, result): |
| 158 """Merges the given result into this resultl.""" |
| 159 self.testsRun += result.testsRun |
| 160 for key, entry in result.results.iteritems(): |
| 161 self.results[key] = entry |
| 162 for error in result.errors: |
| 163 self.errors.append(error) |
| 164 for failure in result.failures: |
| 165 self.failures.append(failure) |
| 166 self.stream.write(result.stream) |
| 167 |
| 168 |
| 169 class TestRunnerThread(threading.Thread): |
| 170 """Abstract test runner class. Launches its own thread for running tests. |
| 171 Formats test results. |
| 172 |
| 173 Members: |
| 174 completely_done_event: event that occurs just before thread exits. |
| 175 test: the currently running test. |
| 176 browser: selenium_name of browser that will be tested. |
| 177 """ |
| 178 def __init__(self): |
| 179 threading.Thread.__init__(self) |
| 180 # This thread is a daemon so that the program can exit even if the |
| 181 # thread has not finished. |
| 182 self.setDaemon(True) |
| 183 self.completely_done_event = threading.Event() |
| 184 self.test_copy = None |
| 185 self.browser = "default_browser" |
| 186 |
| 187 def IsCompletelyDone(self): |
| 188 """Returns true if this test runner is completely done.""" |
| 189 return self.completely_done_event.isSet() |
| 190 |
| 191 def run(self): |
| 192 pass |
| 193 |
| 194 def SetBrowser(self, browser): |
| 195 """Sets the browser name.""" |
| 196 self.browser = browser |
| 197 |
| 198 def GetNoResponseResult(self): |
| 199 """Returns a generic no response result for last test.""" |
| 200 result = TestResult(StringBuffer(), self.browser) |
| 201 result.noResponse(self.test) |
| 202 return result |
| 203 |
| 204 def RunTest(self, test): |
| 205 "Run the given test case or test suite." |
| 206 self.test = test |
| 207 |
| 208 stream = StringBuffer() |
| 209 result = TestResult(stream, self.browser) |
| 210 startTime = time.time() |
| 211 test(result) |
| 212 stopTime = time.time() |
| 213 timeTaken = stopTime - startTime |
| 214 result.printErrors() |
| 215 run = result.testsRun |
| 216 stream.writeln("Took %.2fs" % timeTaken) |
| 217 stream.writeln() |
| 218 return result |
| 219 |
| 220 |
| 221 class PDiffTestRunner(TestRunnerThread): |
| 222 """Test runner for Perceptual Diff tests. Polls a test queue and launches |
| 223 given tests. Adds result to given queue. |
| 224 |
| 225 Members: |
| 226 pdiff_queue: list of tests to run, when they arrive. |
| 227 result_queue: queue of our tests results. |
| 228 browser: selenium name of browser to be tested. |
| 229 end_testing_event: event that occurs when we are guaranteed no more tests |
| 230 will be added to the queue. |
| 231 """ |
| 232 def __init__(self, pdiff_queue, result_queue, browser): |
| 233 TestRunnerThread.__init__(self) |
| 234 self.pdiff_queue = pdiff_queue |
| 235 self.result_queue = result_queue |
| 236 self.browser = browser |
| 237 |
| 238 self.end_testing_event = threading.Event() |
| 239 |
| 240 def EndTesting(self): |
| 241 """Called to notify thread that no more tests will be added to the test |
| 242 queue.""" |
| 243 self.end_testing_event.set() |
| 244 |
| 245 def run(self): |
| 246 while True: |
| 247 try: |
| 248 test = self.pdiff_queue.get_nowait() |
| 249 |
| 250 result = self.RunTest(test) |
| 251 |
| 252 self.result_queue.put(result) |
| 253 |
| 254 except Queue.Empty: |
| 255 if self.end_testing_event.isSet() and self.pdiff_queue.empty(): |
| 256 break |
| 257 else: |
| 258 time.sleep(1) |
| 259 |
| 260 self.completely_done_event.set() |
| 261 |
| 262 |
| 263 class SeleniumTestRunner(TestRunnerThread): |
| 264 """Test runner for Selenium tests. Takes a test from a test queue and launches |
| 265 it. Tries to handle hung/crashed tests gracefully. |
| 266 |
| 267 Members: |
| 268 testing_event: event that occurs when the runner is testing. |
| 269 finished_event: event that occurs when thread has finished testing and |
| 270 before it starts its next test. |
| 271 can_continue_lock: lock for |can_continue|. |
| 272 can_continue: is True when main thread permits the test runner to continue. |
| 273 sel_builder: builder that constructs new selenium sessions, as needed. |
| 274 browser: selenium name of browser to be tested. |
| 275 session: current selenium session being used in tests, can be None. |
| 276 test_queue: queue of tests to run. |
| 277 pdiff_queue: queue of perceptual diff tests to run. We add a perceptual |
| 278 diff test to the queue when the related selenium test passes. |
| 279 deadline: absolute time of when the test should be done. |
| 280 """ |
| 281 def __init__(self, sel_builder, browser, test_queue, pdiff_queue): |
| 282 TestRunnerThread.__init__(self) |
| 283 |
| 284 # Synchronization. |
| 285 self.testing_event = threading.Event() |
| 286 self.finished_event = threading.Event() |
| 287 self.can_continue_lock = threading.Lock() |
| 288 self.can_continue = False |
| 289 |
| 290 # Selenium variables. |
| 291 self.sel_builder = sel_builder |
| 292 self.browser = browser |
| 293 |
| 294 # Test variables. |
| 295 self.test_queue = test_queue |
| 296 self.pdiff_queue = pdiff_queue |
| 297 |
| 298 self.deadline = 0 |
| 299 |
| 300 def IsPastDeadline(self): |
| 301 if time.time() > self.deadline: |
| 302 return True |
| 303 return False |
| 304 |
| 305 def IsTesting(self): |
| 306 return self.testing_event.isSet() |
| 307 |
| 308 def DidFinishTest(self): |
| 309 return self.finished_event.isSet() |
| 310 |
| 311 def Continue(self): |
| 312 """Signals to thread to continue testing. |
| 313 |
| 314 Returns: |
| 315 result: the result for the recently finished test. |
| 316 """ |
| 317 |
| 318 self.finished_event.clear() |
| 319 |
| 320 self.can_continue_lock.acquire() |
| 321 self.can_continue = True |
| 322 result = self.result |
| 323 self.can_continue_lock.release() |
| 324 |
| 325 return result |
| 326 |
| 327 def AbortTest(self): |
| 328 self._StopSession() |
| 329 self._StartSession() |
| 330 |
| 331 def _StartSession(self): |
| 332 self.session = self.sel_builder.NewSeleniumSession(self.browser) |
| 333 # Copy the session so we can shut down safely on a different thread. |
| 334 self.shutdown_session = copy.deepcopy(self.session) |
| 335 |
| 336 def _StopSession(self): |
| 337 if self.session is not None: |
| 338 self.session = None |
| 339 try: |
| 340 # This can cause an exception on some browsers. |
| 341 # Silenly disregard the exception. |
| 342 self.shutdown_session.stop() |
| 343 except: |
| 344 pass |
| 345 |
| 346 def run(self): |
| 347 self._StartSession() |
| 348 |
| 349 while not self.test_queue.empty(): |
| 350 try: |
| 351 # Grab test from queue. |
| 352 test_obj = self.test_queue.get_nowait() |
| 353 if type(test_obj) == tuple: |
| 354 test = test_obj[0] |
| 355 pdiff_test = test_obj[1] |
| 356 else: |
| 357 test = test_obj |
| 358 pdiff_test = None |
| 359 |
| 360 self.can_continue = False |
| 361 |
| 362 # Deadline is the time to load page timeout plus a constant. |
| 363 self.deadline = (time.time() + (test.GetTestTimeout() / 1000.0) + |
| 364 selenium_constants.MAX_SELENIUM_TEST_TIME) |
| 365 # Supply test with necessary selenium session. |
| 366 test.SetSession(self.session) |
| 367 |
| 368 # Run test. |
| 369 self.testing_event.set() |
| 370 self.result = self.RunTest(test) |
| 371 |
| 372 if time.time() > self.deadline: |
| 373 self.result = self.GetNoResponseResult() |
| 374 |
| 375 self.testing_event.clear() |
| 376 self.finished_event.set() |
| 377 |
| 378 # Wait for instruction from the main thread. |
| 379 while True: |
| 380 self.can_continue_lock.acquire() |
| 381 can_continue = self.can_continue |
| 382 self.can_continue_lock.release() |
| 383 if can_continue: |
| 384 break |
| 385 time.sleep(.5) |
| 386 |
| 387 if self.pdiff_queue is not None and pdiff_test is not None: |
| 388 if self.result.wasSuccessful(): |
| 389 # Add the dependent perceptual diff test. |
| 390 self.pdiff_queue.put(pdiff_test) |
| 391 |
| 392 except Queue.Empty: |
| 393 break |
| 394 |
| 395 self._StopSession() |
| 396 self.completely_done_event.set() |
| 397 |
| 398 |
OLD | NEW |