diff options
Diffstat (limited to 'Tools/Scripts/run-gtk-tests')
-rwxr-xr-x | Tools/Scripts/run-gtk-tests | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/Tools/Scripts/run-gtk-tests b/Tools/Scripts/run-gtk-tests new file mode 100755 index 000000000..a3d3e6f28 --- /dev/null +++ b/Tools/Scripts/run-gtk-tests @@ -0,0 +1,494 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012 Igalia S.L. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Library General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Library General Public License for more details. +# +# You should have received a copy of the GNU Library General Public License +# along with this library; see the file COPYING.LIB. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +import logging +import subprocess +import os +import sys +import optparse +import re +from signal import alarm, signal, SIGALRM, SIGKILL, SIGSEGV +from gi.repository import Gio, GLib + +top_level_directory = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.append(os.path.join(top_level_directory, "Tools", "jhbuild")) +sys.path.append(os.path.join(top_level_directory, "Tools", "gtk")) +import common +import jhbuildutils +from webkitpy.common.host import Host + +class SkippedTest: + ENTIRE_SUITE = None + + def __init__(self, test, test_case, reason, bug, build_type=None): + self.test = test + self.test_case = test_case + self.reason = reason + self.bug = bug + self.build_type = build_type + + def __str__(self): + skipped_test_str = "%s" % self.test + + if not(self.skip_entire_suite()): + skipped_test_str += " [%s]" % self.test_case + + skipped_test_str += ": %s (https://bugs.webkit.org/show_bug.cgi?id=%d)" % (self.reason, self.bug) + return skipped_test_str + + def skip_entire_suite(self): + return self.test_case == SkippedTest.ENTIRE_SUITE + + def skip_for_build_type(self, build_type): + if self.build_type is None: + return True; + + return self.build_type == build_type + +class TestTimeout(Exception): + pass + +class TestRunner: + TEST_DIRS = [ "WebKit2Gtk", "WebKit2", "JavaScriptCore", "WTF", "WebCore" ] + + SKIPPED = [ + SkippedTest("WebKit2Gtk/TestUIClient", "/webkit2/WebKitWebView/mouse-target", "Test times out after r150890", 117689), + SkippedTest("WebKit2Gtk/TestUIClient", "/webkit2/WebKitWebView/usermedia-permission-requests", "Test times out", 158257), + SkippedTest("WebKit2Gtk/TestUIClient", "/webkit2/WebKitWebView/audio-usermedia-permission-request", "Test times out", 158257), + SkippedTest("WebKit2Gtk/TestCookieManager", "/webkit2/WebKitCookieManager/persistent-storage", "Test is flaky", 134580), + SkippedTest("WebKit2Gtk/TestPrinting", "/webkit2/WebKitPrintOperation/custom-widget", "Test is flaky", 168196), + SkippedTest("WebKit2Gtk/TestWebViewEditor", "/webkit2/WebKitWebView/editable/editable", "Test hits an assertion in Debug builds", 151654, "Debug"), + SkippedTest("WebKit2Gtk/TestWebExtensions", "/webkit2/WebKitWebExtension/form-controls-associated-signal", "Test is flaky", 168194), + SkippedTest("WebKit2Gtk/TestWebExtensions", "/webkit2/WebKitWebView/install-missing-plugins-permission-request", "Test times out", 147822), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.MouseMoveAfterCrash", "Test is flaky", 85066), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.NewFirstVisuallyNonEmptyLayoutForImages", "Test is flaky", 85066), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.NewFirstVisuallyNonEmptyLayoutFrames", "Test fails", 85037), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.SpacebarScrolling", "Test fails", 84961), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.WKConnection", "Tests fail and time out out", 84959), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.ForceRepaint", "Test times out", 105532), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.ReloadPageAfterCrash", "Test flakily times out", 110129), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.DidAssociateFormControls", "Test times out", 120302), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.InjectedBundleFrameHitTest", "Test times out", 120303), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.TerminateTwice", "Test causes crash on the next test", 121970), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.GeolocationTransitionToHighAccuracy", "Test causes crash on the next test", 125068), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.GeolocationTransitionToLowAccuracy", "Test causes crash on the next test", 125068), + ] + + SLOW = [ + "WTF_Lock.ContendedShortSection", + "WTF_Lock.ContendedLongSection", + "WTF_WordLock.ContendedShortSection", + "WTF_WordLock.ContendedLongSection", + "WebKit2Gtk/TestInspectorServer", + ] + + def __init__(self, options, tests=[]): + self._options = options + + self._build_type = "Debug" if self._options.debug else "Release" + common.set_build_types((self._build_type,)) + self._port = Host().port_factory.get("gtk") + self._driver = self._create_driver() + + self._programs_path = common.binary_build_path() + self._tests = self._get_tests(tests) + self._skipped_tests = [skipped for skipped in TestRunner.SKIPPED if skipped.skip_for_build_type(self._build_type)] + self._disabled_tests = [] + + # These SPI daemons need to be active for the accessibility tests to work. + self._spi_registryd = None + self._spi_bus_launcher = None + + def _test_programs_base_dir(self): + return os.path.join(self._programs_path, "TestWebKitAPI") + + def _get_tests_from_dir(self, test_dir): + if not os.path.isdir(test_dir): + return [] + + tests = [] + for test_file in os.listdir(test_dir): + if not test_file.lower().startswith("test"): + continue + test_path = os.path.join(test_dir, test_file) + if os.path.isfile(test_path) and os.access(test_path, os.X_OK): + tests.append(test_path) + return tests + + def _get_tests(self, initial_tests): + tests = [] + for test in initial_tests: + if os.path.isdir(test): + tests.extend(self._get_tests_from_dir(test)) + else: + tests.append(test) + if tests: + return tests + + tests = [] + for test_dir in self.TEST_DIRS: + absolute_test_dir = os.path.join(self._test_programs_base_dir(), test_dir) + tests.extend(self._get_tests_from_dir(absolute_test_dir)) + return tests + + def _lookup_atspi2_binary(self, filename): + exec_prefix = common.pkg_config_file_variable('atspi-2', 'exec_prefix') + if not exec_prefix: + return None + for path in ['libexec', 'lib/at-spi2-core', 'lib32/at-spi2-core', 'lib64/at-spi2-core']: + filepath = os.path.join(exec_prefix, path, filename) + if os.path.isfile(filepath): + return filepath + + return None + + def _wait_for_accessibility_bus(self): + def timeout_accessibility_bus(): + self._accessibility_bus_found = False + sys.stderr.write("Timeout waiting for the accesibility bus.\n") + sys.stderr.flush() + loop.quit() + # Backup current environment, and temporally set the test one. + oldenv = dict(os.environ) + os.environ.clear() + os.environ.update(self._test_env) + # We spin a main loop until the bus name appears on DBus. + self._accessibility_bus_found = True + loop = GLib.MainLoop() + Gio.bus_watch_name(Gio.BusType.SESSION, 'org.a11y.Bus', Gio.BusNameWatcherFlags.NONE, + lambda *args: loop.quit(), None) + GLib.timeout_add_seconds(5, timeout_accessibility_bus) + loop.run() + # Restore previous environment. + os.environ.clear() + os.environ.update(oldenv) + return self._accessibility_bus_found + + def _start_accessibility_daemons(self): + spi_bus_launcher_path = self._lookup_atspi2_binary('at-spi-bus-launcher') + spi_registryd_path = self._lookup_atspi2_binary('at-spi2-registryd') + if not spi_bus_launcher_path or not spi_registryd_path: + return False + + try: + self._spi_bus_launcher = subprocess.Popen([spi_bus_launcher_path], env=self._test_env) + except: + sys.stderr.write("Failed to launch the accessibility bus\n") + sys.stderr.flush() + return False + + # We need to wait until the SPI bus is launched before trying to start the SPI registry. + if not self._wait_for_accessibility_bus(): + sys.stderr.write("Failed checking the accessibility bus within D-Bus\n") + sys.stderr.flush() + return False + + try: + self._spi_registryd = subprocess.Popen([spi_registryd_path], env=self._test_env) + except: + sys.stderr.write("Failed to launch the accessibility registry\n") + sys.stderr.flush() + return False + + return True + + def _create_driver(self, port_options=[]): + self._port._display_server = self._options.display_server + driver = self._port.create_driver(worker_number=0, no_timeout=True)._make_driver(pixel_tests=False) + if not driver.check_driver(self._port): + raise RuntimeError("Failed to check driver %s" %driver.__class__.__name__) + return driver + + def _setup_testing_environment(self): + self._test_env = self._driver._setup_environ_for_test() + self._test_env["TEST_WEBKIT_API_WEBKIT2_RESOURCES_PATH"] = common.top_level_path("Tools", "TestWebKitAPI", "Tests", "WebKit2") + self._test_env["TEST_WEBKIT_API_WEBKIT2_INJECTED_BUNDLE_PATH"] = common.library_build_path() + self._test_env["WEBKIT_EXEC_PATH"] = self._programs_path + + # If we cannot start the accessibility daemons, we can just skip the accessibility tests. + if not self._start_accessibility_daemons(): + print "Could not start accessibility bus, so disabling TestWebKitAccessibility" + self._disabled_tests.append("WebKit2APITests/TestWebKitAccessibility") + return True + + def _tear_down_testing_environment(self): + if self._spi_registryd: + self._spi_registryd.terminate() + if self._spi_bus_launcher: + self._spi_bus_launcher.terminate() + if self._driver: + self._driver.stop() + + def _test_cases_to_skip(self, test_program): + if self._options.skipped_action != 'skip': + return [] + + test_cases = [] + for skipped in self._skipped_tests: + if test_program.endswith(skipped.test) and not skipped.skip_entire_suite(): + test_cases.append(skipped.test_case) + return test_cases + + def _should_run_test_program(self, test_program): + for disabled_test in self._disabled_tests: + if test_program.endswith(disabled_test): + return False + + if self._options.skipped_action != 'skip': + return True + + for skipped in self._skipped_tests: + if test_program.endswith(skipped.test) and skipped.skip_entire_suite(): + return False + return True + + def _get_child_pid_from_test_output(self, output): + if not output: + return -1 + match = re.search(r'\(pid=(?P<child_pid>[0-9]+)\)', output) + if not match: + return -1 + return int(match.group('child_pid')) + + def _kill_process(self, pid): + try: + os.kill(pid, SIGKILL) + except OSError: + # Process already died. + pass + + def _run_test_command(self, command, timeout=-1): + def alarm_handler(signum, frame): + raise TestTimeout + + child_pid = [-1] + def parse_line(line, child_pid = child_pid): + if child_pid[0] == -1: + child_pid[0] = self._get_child_pid_from_test_output(line) + + sys.stdout.write(line) + + def waitpid(pid): + while True: + try: + return os.waitpid(pid, 0) + except (OSError, IOError) as e: + if e.errno == errno.EINTR: + continue + raise + + def return_code_from_exit_status(status): + if os.WIFSIGNALED(status): + return -os.WTERMSIG(status) + elif os.WIFEXITED(status): + return os.WEXITSTATUS(status) + else: + # Should never happen + raise RuntimeError("Unknown child exit status!") + + pid, fd = os.forkpty() + if pid == 0: + os.execvpe(command[0], command, self._test_env) + sys.exit(0) + + if timeout > 0: + signal(SIGALRM, alarm_handler) + alarm(timeout) + + try: + common.parse_output_lines(fd, parse_line) + if timeout > 0: + alarm(0) + except TestTimeout: + self._kill_process(pid) + if child_pid[0] > 0: + self._kill_process(child_pid[0]) + raise + + try: + dummy, status = waitpid(pid) + except OSError as e: + if e.errno != errno.ECHILD: + raise + # This happens if SIGCLD is set to be ignored or waiting + # for child processes has otherwise been disabled for our + # process. This child is dead, we can't get the status. + status = 0 + + return return_code_from_exit_status(status) + + def _run_test_glib(self, test_program): + tester_command = ['gtester', '-k'] + if self._options.verbose: + tester_command.append('--verbose') + for test_case in self._test_cases_to_skip(test_program): + tester_command.extend(['-s', test_case]) + tester_command.append(test_program) + # This timeout is supposed to be per test case, but in the case of GLib tests it affects all the tests cases of + # the same test program. Some test programs like TestLoaderClient, that have a lot of test cases, often time out + # in the bots because the timeout is not enough to run all the tests cases. So, we use a longer timeout for GLib + # tests (timeout * 2). + timeout = self._options.timeout * 2 + test = os.path.join(os.path.basename(os.path.dirname(test_program)), os.path.basename(test_program)) + if test in TestRunner.SLOW: + timeout *= 5 + + return self._run_test_command(tester_command, timeout) + + def _get_tests_from_google_test_suite(self, test_program): + try: + output = subprocess.check_output([test_program, '--gtest_list_tests'], env=self._test_env) + except subprocess.CalledProcessError: + sys.stderr.write("ERROR: could not list available tests for binary %s.\n" % (test_program)) + sys.stderr.flush() + return 1 + + skipped_test_cases = self._test_cases_to_skip(test_program) + + tests = [] + prefix = None + for line in output.split('\n'): + if not line.startswith(' '): + prefix = line + continue + else: + test_name = prefix + line.strip() + if not test_name in skipped_test_cases: + tests.append(test_name) + return tests + + def _run_google_test(self, test_program, subtest): + test_command = [test_program, '--gtest_filter=%s' % (subtest)] + timeout = self._options.timeout + if subtest in TestRunner.SLOW: + timeout *= 5 + + status = self._run_test_command(test_command, timeout) + if status == -SIGSEGV: + sys.stdout.write("**CRASH** %s\n" % subtest) + sys.stdout.flush() + return status + + def _run_google_test_suite(self, test_program): + retcode = 0 + for subtest in self._get_tests_from_google_test_suite(test_program): + if self._run_google_test(test_program, subtest): + retcode = 1 + return retcode + + def _run_test(self, test_program): + basedir = os.path.basename(os.path.dirname(test_program)) + if basedir in ["WebKit2Gtk", "WebKitGtk"]: + return self._run_test_glib(test_program) + + if basedir in ["WebKit2", "JavaScriptCore", "WTF", "WebCore", "WebCoreGtk"]: + return self._run_google_test_suite(test_program) + + return 1 + + def run_tests(self): + if not self._tests: + sys.stderr.write("ERROR: tests not found in %s.\n" % (self._test_programs_base_dir())) + sys.stderr.flush() + return 1 + + if not self._setup_testing_environment(): + return 1 + + # Remove skipped tests now instead of when we find them, because + # some tests might be skipped while setting up the test environment. + self._tests = [test for test in self._tests if self._should_run_test_program(test)] + + crashed_tests = [] + failed_tests = [] + timed_out_tests = [] + try: + for test in self._tests: + exit_status_code = 0 + try: + exit_status_code = self._run_test(test) + except TestTimeout: + sys.stdout.write("TEST: %s: TIMEOUT\n" % test) + sys.stdout.flush() + timed_out_tests.append(test) + + if exit_status_code == -SIGSEGV: + sys.stdout.write("TEST: %s: CRASHED\n" % test) + sys.stdout.flush() + crashed_tests.append(test) + elif exit_status_code != 0: + failed_tests.append(test) + finally: + self._tear_down_testing_environment() + + if failed_tests: + names = [test.replace(self._test_programs_base_dir(), '', 1) for test in failed_tests] + sys.stdout.write("Tests failed (%d): %s\n" % (len(names), ", ".join(names))) + sys.stdout.flush() + + if crashed_tests: + names = [test.replace(self._test_programs_base_dir(), '', 1) for test in crashed_tests] + sys.stdout.write("Tests that crashed (%d): %s\n" % (len(names), ", ".join(names))) + sys.stdout.flush() + + if timed_out_tests: + names = [test.replace(self._test_programs_base_dir(), '', 1) for test in timed_out_tests] + sys.stdout.write("Tests that timed out (%d): %s\n" % (len(names), ", ".join(names))) + sys.stdout.flush() + + if self._skipped_tests and self._options.skipped_action == 'skip': + sys.stdout.write("Tests skipped (%d):\n%s\n" % + (len(self._skipped_tests), + "\n".join([str(skipped) for skipped in self._skipped_tests]))) + sys.stdout.flush() + + return len(failed_tests) + len(timed_out_tests) + +if __name__ == "__main__": + if not jhbuildutils.enter_jhbuild_environment_if_available("gtk"): + print "***" + print "*** Warning: jhbuild environment not present. Run update-webkitgtk-libs before build-webkit to ensure proper testing." + print "***" + + option_parser = optparse.OptionParser(usage='usage: %prog [options] [test...]') + option_parser.add_option('-r', '--release', + action='store_true', dest='release', + help='Run in Release') + option_parser.add_option('-d', '--debug', + action='store_true', dest='debug', + help='Run in Debug') + option_parser.add_option('-v', '--verbose', + action='store_true', dest='verbose', + help='Run gtester in verbose mode') + option_parser.add_option('--skipped', action='store', dest='skipped_action', + choices=['skip', 'ignore', 'only'], default='skip', + metavar='skip|ignore|only', + help='Specifies how to treat the skipped tests') + option_parser.add_option('-t', '--timeout', + action='store', type='int', dest='timeout', default=10, + help='Time in seconds until a test times out') + option_parser.add_option('--display-server', choices=['xvfb', 'xorg', 'weston', 'wayland'], default='xvfb', + help='"xvfb": Use a virtualized X11 server. "xorg": Use the current X11 session. ' + '"weston": Use a virtualized Weston server. "wayland": Use the current wayland session.'), + options, args = option_parser.parse_args() + + logging.basicConfig(level=logging.INFO, format="%(message)s") + + runner = TestRunner(options, args) + sys.exit(runner.run_tests()) |