| Index: tests/selenium/test_runner.py
|
| ===================================================================
|
| --- tests/selenium/test_runner.py (revision 0)
|
| +++ tests/selenium/test_runner.py (revision 0)
|
| @@ -0,0 +1,398 @@
|
| +#!/usr/bin/python2.4
|
| +# Copyright 2009, Google Inc.
|
| +# All rights reserved.
|
| +#
|
| +# Redistribution and use in source and binary forms, with or without
|
| +# modification, are permitted provided that the following conditions are
|
| +# met:
|
| +#
|
| +# * Redistributions of source code must retain the above copyright
|
| +# notice, this list of conditions and the following disclaimer.
|
| +# * Redistributions in binary form must reproduce the above
|
| +# copyright notice, this list of conditions and the following disclaimer
|
| +# in the documentation and/or other materials provided with the
|
| +# distribution.
|
| +# * Neither the name of Google Inc. nor the names of its
|
| +# contributors may be used to endorse or promote products derived from
|
| +# this software without specific prior written permission.
|
| +#
|
| +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
| +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
| +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
| +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
| +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
| +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| +
|
| +
|
| +"""Test runners and associated classes.
|
| +
|
| +Each test runner has its own thread, which attempts to perform a given test.
|
| +If a test hangs, the test runner can be aborted or exited.
|
| +
|
| +"""
|
| +
|
| +import os
|
| +import sys
|
| +
|
| +import socket
|
| +import subprocess
|
| +import threading
|
| +import time
|
| +import unittest
|
| +import gflags
|
| +import selenium
|
| +import selenium_constants
|
| +import Queue
|
| +import thread
|
| +import copy
|
| +
|
| +class StringBuffer:
|
| + """Primitive string buffer.
|
| +
|
| + Members:
|
| + data: the contents of the buffer
|
| + """
|
| + def __init__(self):
|
| + self.data = ""
|
| + def write(self, data):
|
| + self.data += str(data)
|
| + def writeln(self, data=None):
|
| + if data is not None:
|
| + self.write(data)
|
| + self.write("\n")
|
| + def get(self):
|
| + get_data = self.data
|
| + self.data = ""
|
| + return get_data
|
| +
|
| +class TestResult(unittest.TestResult):
|
| + """A specialized class that prints formatted text results to a stream.
|
| +
|
| + """
|
| + separator1 = "=" * 70
|
| + separator2 = "-" * 70
|
| +
|
| + def __init__(self, stream, browser):
|
| + unittest.TestResult.__init__(self)
|
| + self.stream = stream
|
| + # Dictionary of start times
|
| + self.start_times = {}
|
| + # Dictionary of results
|
| + self.results = {}
|
| + self.browser = browser
|
| +
|
| + def getDescription(self, test):
|
| + """Gets description of test."""
|
| + return test.shortDescription() or str(test)
|
| +
|
| + def startTest(self, test):
|
| + """Starts test."""
|
| + # Records the start time
|
| + self.start_times[test] = time.time()
|
| + # Default testresult if success not called
|
| + self.results[test] = "FAIL"
|
| + unittest.TestResult.startTest(self, test)
|
| + self.stream.writeln()
|
| + self.stream.writeln(self.separator2)
|
| + self.stream.write(self.getDescription(test))
|
| + self.stream.writeln(" ... ")
|
| +
|
| + def stopTest(self, test):
|
| + """Called when test is ended."""
|
| + time_taken = time.time() - self.start_times[test]
|
| + result = self.results[test]
|
| + self.stream.writeln("SELENIUMRESULT %s <%s> [%.3fs]: %s"
|
| + % (test, self.browser, time_taken, result))
|
| +
|
| + def addSuccess(self, test):
|
| + """Adds success result to TestResult."""
|
| + unittest.TestResult.addSuccess(self, test)
|
| + self.results[test] = "PASS"
|
| +
|
| + def addError(self, test, err):
|
| + """Adds error result to TestResult."""
|
| + unittest.TestResult.addError(self, test, err)
|
| + self.results[test] = "FAIL"
|
| +
|
| + def addFailure(self, test, err):
|
| + """Adds failure result to TestResult."""
|
| + unittest.TestResult.addFailure(self, test, err)
|
| + self.results[test] = "FAIL"
|
| +
|
| + def noResponse(self, test):
|
| + """Configures the result for a test that did not respond."""
|
| + self.results[test] = "FAIL"
|
| + self.testsRun += 1
|
| + self.errors.append("No response from test")
|
| +
|
| + self.stream.writeln()
|
| + self.stream.writeln(self.separator2)
|
| + self.stream.write(self.getDescription(test))
|
| + self.stream.writeln(" ... ")
|
| + self.stream.writeln("SELENIUMRESULT %s <%s> [?s]: FAIL (HUNG?)"
|
| + % (test, self.browser))
|
| + self.stream.writeln()
|
| +
|
| + def printErrors(self):
|
| + """Prints all errors and failures."""
|
| + self.stream.writeln()
|
| + self.printErrorList("ERROR", self.errors)
|
| + self.printErrorList("FAIL", self.failures)
|
| +
|
| + def printErrorList(self, flavour, errors):
|
| + """Prints a given list of errors."""
|
| + for test, err in errors:
|
| + self.stream.writeln("%s:" % flavour)
|
| + self.stream.writeln("%s" % err)
|
| +
|
| + def printAll(self, stream):
|
| + """Prints the entire stream to the given stream."""
|
| + stream.write(self.stream.data)
|
| +
|
| + def merge(self, result):
|
| + """Merges the given result into this resultl."""
|
| + self.testsRun += result.testsRun
|
| + for key, entry in result.results.iteritems():
|
| + self.results[key] = entry
|
| + for error in result.errors:
|
| + self.errors.append(error)
|
| + for failure in result.failures:
|
| + self.failures.append(failure)
|
| + self.stream.write(result.stream)
|
| +
|
| +
|
| +class TestRunnerThread(threading.Thread):
|
| + """Abstract test runner class. Launches its own thread for running tests.
|
| + Formats test results.
|
| +
|
| + Members:
|
| + completely_done_event: event that occurs just before thread exits.
|
| + test: the currently running test.
|
| + browser: selenium_name of browser that will be tested.
|
| + """
|
| + def __init__(self):
|
| + threading.Thread.__init__(self)
|
| + # This thread is a daemon so that the program can exit even if the
|
| + # thread has not finished.
|
| + self.setDaemon(True)
|
| + self.completely_done_event = threading.Event()
|
| + self.test_copy = None
|
| + self.browser = "default_browser"
|
| +
|
| + def IsCompletelyDone(self):
|
| + """Returns true if this test runner is completely done."""
|
| + return self.completely_done_event.isSet()
|
| +
|
| + def run(self):
|
| + pass
|
| +
|
| + def SetBrowser(self, browser):
|
| + """Sets the browser name."""
|
| + self.browser = browser
|
| +
|
| + def GetNoResponseResult(self):
|
| + """Returns a generic no response result for last test."""
|
| + result = TestResult(StringBuffer(), self.browser)
|
| + result.noResponse(self.test)
|
| + return result
|
| +
|
| + def RunTest(self, test):
|
| + "Run the given test case or test suite."
|
| + self.test = test
|
| +
|
| + stream = StringBuffer()
|
| + result = TestResult(stream, self.browser)
|
| + startTime = time.time()
|
| + test(result)
|
| + stopTime = time.time()
|
| + timeTaken = stopTime - startTime
|
| + result.printErrors()
|
| + run = result.testsRun
|
| + stream.writeln("Took %.2fs" % timeTaken)
|
| + stream.writeln()
|
| + return result
|
| +
|
| +
|
| +class PDiffTestRunner(TestRunnerThread):
|
| + """Test runner for Perceptual Diff tests. Polls a test queue and launches
|
| + given tests. Adds result to given queue.
|
| +
|
| + Members:
|
| + pdiff_queue: list of tests to run, when they arrive.
|
| + result_queue: queue of our tests results.
|
| + browser: selenium name of browser to be tested.
|
| + end_testing_event: event that occurs when we are guaranteed no more tests
|
| + will be added to the queue.
|
| + """
|
| + def __init__(self, pdiff_queue, result_queue, browser):
|
| + TestRunnerThread.__init__(self)
|
| + self.pdiff_queue = pdiff_queue
|
| + self.result_queue = result_queue
|
| + self.browser = browser
|
| +
|
| + self.end_testing_event = threading.Event()
|
| +
|
| + def EndTesting(self):
|
| + """Called to notify thread that no more tests will be added to the test
|
| + queue."""
|
| + self.end_testing_event.set()
|
| +
|
| + def run(self):
|
| + while True:
|
| + try:
|
| + test = self.pdiff_queue.get_nowait()
|
| +
|
| + result = self.RunTest(test)
|
| +
|
| + self.result_queue.put(result)
|
| +
|
| + except Queue.Empty:
|
| + if self.end_testing_event.isSet() and self.pdiff_queue.empty():
|
| + break
|
| + else:
|
| + time.sleep(1)
|
| +
|
| + self.completely_done_event.set()
|
| +
|
| +
|
| +class SeleniumTestRunner(TestRunnerThread):
|
| + """Test runner for Selenium tests. Takes a test from a test queue and launches
|
| + it. Tries to handle hung/crashed tests gracefully.
|
| +
|
| + Members:
|
| + testing_event: event that occurs when the runner is testing.
|
| + finished_event: event that occurs when thread has finished testing and
|
| + before it starts its next test.
|
| + can_continue_lock: lock for |can_continue|.
|
| + can_continue: is True when main thread permits the test runner to continue.
|
| + sel_builder: builder that constructs new selenium sessions, as needed.
|
| + browser: selenium name of browser to be tested.
|
| + session: current selenium session being used in tests, can be None.
|
| + test_queue: queue of tests to run.
|
| + pdiff_queue: queue of perceptual diff tests to run. We add a perceptual
|
| + diff test to the queue when the related selenium test passes.
|
| + deadline: absolute time of when the test should be done.
|
| + """
|
| + def __init__(self, sel_builder, browser, test_queue, pdiff_queue):
|
| + TestRunnerThread.__init__(self)
|
| +
|
| + # Synchronization.
|
| + self.testing_event = threading.Event()
|
| + self.finished_event = threading.Event()
|
| + self.can_continue_lock = threading.Lock()
|
| + self.can_continue = False
|
| +
|
| + # Selenium variables.
|
| + self.sel_builder = sel_builder
|
| + self.browser = browser
|
| +
|
| + # Test variables.
|
| + self.test_queue = test_queue
|
| + self.pdiff_queue = pdiff_queue
|
| +
|
| + self.deadline = 0
|
| +
|
| + def IsPastDeadline(self):
|
| + if time.time() > self.deadline:
|
| + return True
|
| + return False
|
| +
|
| + def IsTesting(self):
|
| + return self.testing_event.isSet()
|
| +
|
| + def DidFinishTest(self):
|
| + return self.finished_event.isSet()
|
| +
|
| + def Continue(self):
|
| + """Signals to thread to continue testing.
|
| +
|
| + Returns:
|
| + result: the result for the recently finished test.
|
| + """
|
| +
|
| + self.finished_event.clear()
|
| +
|
| + self.can_continue_lock.acquire()
|
| + self.can_continue = True
|
| + result = self.result
|
| + self.can_continue_lock.release()
|
| +
|
| + return result
|
| +
|
| + def AbortTest(self):
|
| + self._StopSession()
|
| + self._StartSession()
|
| +
|
| + def _StartSession(self):
|
| + self.session = self.sel_builder.NewSeleniumSession(self.browser)
|
| + # Copy the session so we can shut down safely on a different thread.
|
| + self.shutdown_session = copy.deepcopy(self.session)
|
| +
|
| + def _StopSession(self):
|
| + if self.session is not None:
|
| + self.session = None
|
| + try:
|
| + # This can cause an exception on some browsers.
|
| + # Silenly disregard the exception.
|
| + self.shutdown_session.stop()
|
| + except:
|
| + pass
|
| +
|
| + def run(self):
|
| + self._StartSession()
|
| +
|
| + while not self.test_queue.empty():
|
| + try:
|
| + # Grab test from queue.
|
| + test_obj = self.test_queue.get_nowait()
|
| + if type(test_obj) == tuple:
|
| + test = test_obj[0]
|
| + pdiff_test = test_obj[1]
|
| + else:
|
| + test = test_obj
|
| + pdiff_test = None
|
| +
|
| + self.can_continue = False
|
| +
|
| + # Deadline is the time to load page timeout plus a constant.
|
| + self.deadline = (time.time() + (test.GetTestTimeout() / 1000.0) +
|
| + selenium_constants.MAX_SELENIUM_TEST_TIME)
|
| + # Supply test with necessary selenium session.
|
| + test.SetSession(self.session)
|
| +
|
| + # Run test.
|
| + self.testing_event.set()
|
| + self.result = self.RunTest(test)
|
| +
|
| + if time.time() > self.deadline:
|
| + self.result = self.GetNoResponseResult()
|
| +
|
| + self.testing_event.clear()
|
| + self.finished_event.set()
|
| +
|
| + # Wait for instruction from the main thread.
|
| + while True:
|
| + self.can_continue_lock.acquire()
|
| + can_continue = self.can_continue
|
| + self.can_continue_lock.release()
|
| + if can_continue:
|
| + break
|
| + time.sleep(.5)
|
| +
|
| + if self.pdiff_queue is not None and pdiff_test is not None:
|
| + if self.result.wasSuccessful():
|
| + # Add the dependent perceptual diff test.
|
| + self.pdiff_queue.put(pdiff_test)
|
| +
|
| + except Queue.Empty:
|
| + break
|
| +
|
| + self._StopSession()
|
| + self.completely_done_event.set()
|
| +
|
| +
|
|
|