blob: f76a8e5271ebfe8cfe432d7a9d8bd94085ef4af8 [file] [log] [blame]
#!/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 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
class SkippedTest:
ENTIRE_SUITE = None
def __init__(self, test, test_case, reason, bug=None):
self.test = test
self.test_case = test_case
self.reason = reason
self.bug = bug
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 " % self.reason
if self.bug is not None:
skipped_test_str += "(https://bugs.webkit.org/show_bug.cgi?id=%d)" % self.bug
return skipped_test_str
def skip_entire_suite(self):
return self.test_case == SkippedTest.ENTIRE_SUITE
class TestTimeout(Exception):
pass
class TestRunner:
TEST_DIRS = [ "unittests", "WebKit2APITests", "TestWebKitAPI" ]
SKIPPED = [
SkippedTest("unittests/testdownload", "/webkit/download/not-found", "Test fails in GTK Linux 64-bit Release bot", 82329),
SkippedTest("unittests/testwebinspector", "/webkit/webinspector/close-and-inspect", "Test is flaky in GTK Linux 32-bit Release bot", 82869),
SkippedTest("unittests/testwebresource", "/webkit/webresource/loading", "Test fails", 104689),
SkippedTest("unittests/testwebresource", "/webkit/webresource/sub_resource_loading", "Test fails in GTK Linux 64-bit Release bot", 82330),
SkippedTest("unittests/testwebview", "/webkit/webview/icon-uri", "Test times out in GTK Linux 64-bit Release bot", 82328),
SkippedTest("WebKit2APITests/TestResources", "/webkit2/WebKitWebView/resources", "Test is flaky in GTK Linux 32-bit Release bot", 82868),
SkippedTest("WebKit2APITests/TestWebKitWebView", SkippedTest.ENTIRE_SUITE, "Test times out after r150890", 117689),
SkippedTest("WebKit2APITests/TestContextMenu", SkippedTest.ENTIRE_SUITE, "Test times out after r150890", 117689),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.MouseMoveAfterCrash", "Test is flaky", 85066),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.NewFirstVisuallyNonEmptyLayoutForImages", "Test is flaky", 85066),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.NewFirstVisuallyNonEmptyLayoutFrames", "Test fails", 85037),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.RestoreSessionStateContainingFormData", "Session State is not implemented in GTK+ port", 84960),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.SpacebarScrolling", "Test fails", 84961),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.WKConnection", "Tests fail and time out out", 84959),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.WKPageGetScaleFactorNotZero", "Test fails and times out", 88455),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.ForceRepaint", "Test times out", 105532),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.ReloadPageAfterCrash", "Test flakily times out", 110129),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.DidAssociateFormControls", "Test times out", 120302),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.InjectedBundleFrameHitTest", "Test times out", 120303),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.ResizeReversePaginatedWebView", "Test fails", 120305),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.ScrollPinningBehaviors", "Test fails", 120306),
SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.TerminateTwice", "Test causes crash on the next test", 121970),
SkippedTest("WebKit2APITests/TestInspectorServer", SkippedTest.ENTIRE_SUITE, "Timing out on the bot", 122571),
]
def __init__(self, options, tests=[]):
self._options = options
self._build_type = "Debug" if self._options.debug else "Release"
self._programs_path = common.build_path_for_build_types((self._build_type,), "Programs")
self._tests = self._get_tests(tests)
self._skipped_tests = TestRunner.SKIPPED
self._disabled_tests = []
if not sys.stdout.isatty():
self._tty_colors_pattern = re.compile("\033\[[0-9;]*m")
# These SPI daemons need to be active for the accessibility tests to work.
self._spi_registryd = None
self._spi_bus_launcher = None
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._programs_path, 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 _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, so we spin a main loop until the bus name appears on DBus.
loop = GLib.MainLoop()
Gio.bus_watch_name(Gio.BusType.SESSION, 'org.a11y.Bus', Gio.BusNameWatcherFlags.NONE,
lambda *args: loop.quit(), None)
loop.run()
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 _run_xvfb(self):
self._xvfb = None
if not self._options.use_xvfb:
return True
self._test_env["DISPLAY"] = self._options.display
try:
self._xvfb = subprocess.Popen(["Xvfb", self._options.display, "-screen", "0", "800x600x24", "-nolisten", "tcp"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except Exception as e:
sys.stderr.write("Failed to run Xvfb: %s\n" % e)
sys.stderr.flush()
return False
return True
def _setup_testing_environment(self):
self._test_env = os.environ
self._test_env['GSETTINGS_BACKEND'] = 'memory'
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.build_path_for_build_types((self._build_type,), "Libraries")
self._test_env["WEBKIT_EXEC_PATH"] = self._programs_path
if not self._run_xvfb():
return False
# 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._xvfb:
self._xvfb.terminate()
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)
if sys.stdout.isatty():
sys.stdout.write(line)
else:
sys.stdout.write(self._tty_colors_pattern.sub('', line.replace('\r', '')))
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']
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)
return self._run_test_command(tester_command, self._options.timeout)
def _run_test_google(self, test_program):
tester_command = [test_program]
skipped_tests_cases = self._test_cases_to_skip(test_program)
if skipped_tests_cases:
tester_command.append("--gtest_filter=-%s" % ":".join(skipped_tests_cases))
return self._run_test_command(tester_command, self._options.timeout)
def _run_test(self, test_program):
if "unittests" in test_program or "WebKit2APITests" in test_program:
return self._run_test_glib(test_program)
if "TestWebKitAPI" in test_program:
return self._run_test_google(test_program)
return 1
def run_tests(self):
if not self._tests:
sys.stderr.write("ERROR: tests not found in %s.\n" % (self._programs_path))
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._programs_path, '', 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._programs_path, '', 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._programs_path, '', 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('--display', action='store', dest='display', default=':55',
help='Display to run Xvfb')
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('--no-xvfb', action='store_false', dest='use_xvfb', default=True,
help='Do not run tests under Xvfb')
options, args = option_parser.parse_args()
sys.exit(TestRunner(options, args).run_tests())