| # Copyright (C) 2010 Google Inc. All rights reserved. |
| # Copyright (C) 2013-2019 Apple 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 Google name 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. |
| |
| """Abstract base class of Port-specific entry points for the layout tests |
| test infrastructure (the Port and Driver classes).""" |
| |
| import difflib |
| import json |
| import logging |
| import os |
| import optparse |
| import re |
| import sys |
| |
| from collections import OrderedDict |
| from functools import partial |
| |
| from webkitpy.common import find_files |
| from webkitpy.common import read_checksum_from_png |
| from webkitpy.common.checkout.scm.detection import SCMDetector |
| from webkitpy.common.memoized import memoized |
| from webkitpy.common.prettypatch import PrettyPatch |
| from webkitpy.common.system import path, pemfile |
| from webkitpy.common.system.executive import ScriptError |
| from webkitpy.common.version_name_map import PUBLIC_TABLE, INTERNAL_TABLE, VersionNameMap |
| from webkitpy.common.wavediff import WaveDiff |
| from webkitpy.common.webkit_finder import WebKitFinder |
| from webkitpy.common.unicode_compatibility import encode_for, encode_if_necessary, decode_for |
| from webkitpy.layout_tests.models.test_configuration import TestConfiguration |
| from webkitpy.port import config as port_config |
| from webkitpy.port import driver |
| from webkitpy.port import image_diff |
| from webkitpy.port import server_process |
| from webkitpy.port.factory import PortFactory |
| from webkitpy.layout_tests.servers import apache_http_server, http_server, http_server_base |
| from webkitpy.layout_tests.servers import web_platform_test_server |
| from webkitpy.layout_tests.servers import websocket_server |
| |
| _log = logging.getLogger(__name__) |
| |
| |
| class Port(object): |
| """Abstract class for Port-specific hooks for the layout_test package.""" |
| |
| # Subclasses override this. This should indicate the basic implementation |
| # part of the port name, e.g., 'win', 'gtk'; there is probably (?) one unique value per class. |
| |
| # FIXME: We should probably rename this to something like 'implementation_name'. |
| port_name = None |
| |
| # Test names resemble unix relative paths, and use '/' as a directory separator. |
| TEST_PATH_SEPARATOR = '/' |
| |
| ALL_BUILD_TYPES = ('debug', 'release') |
| |
| DEFAULT_ARCHITECTURE = 'x86' |
| DEVICE_TYPE = None |
| DEFAULT_DEVICE_TYPES = [] |
| |
| @classmethod |
| def determine_full_port_name(cls, host, options, port_name): |
| """Return a fully-specified port name that can be used to construct objects.""" |
| # Subclasses will usually override this. |
| options = options or {} |
| assert port_name.startswith(cls.port_name) |
| if getattr(options, 'webkit_test_runner', False) and not '-wk2' in port_name: |
| return port_name + '-wk2' |
| return port_name |
| |
| def __init__(self, host, port_name, options=None, **kwargs): |
| |
| # This value may be different from cls.port_name by having version modifiers |
| # and other fields appended to it (for example, 'mac-wk2' or 'win'). |
| self._name = port_name |
| |
| # These are default values that should be overridden in a subclasses. |
| self._os_version = None |
| |
| # FIXME: This can be removed once default architectures for GTK and EFL EWS bots are set. |
| self.did_override_architecture = False |
| |
| # FIXME: Ideally we'd have a package-wide way to get a |
| # well-formed options object that had all of the necessary |
| # options defined on it. |
| self._options = options or optparse.Values() |
| |
| if self.get_option('architecture'): |
| self.did_override_architecture = True |
| else: |
| self.set_option('architecture', self.DEFAULT_ARCHITECTURE) |
| |
| if self._name and '-wk2' in self._name: |
| self._options.webkit_test_runner = True |
| |
| self.host = host |
| self._executive = host.executive |
| self._filesystem = host.filesystem |
| self._webkit_finder = WebKitFinder(host.filesystem) |
| self._config = port_config.Config(self._executive, self._filesystem, self.port_name) |
| self.pretty_patch = PrettyPatch(self._executive, self.path_from_webkit_base(), self._filesystem) |
| |
| self._helper = None |
| self._http_server = None |
| self._websocket_server = None |
| self._websocket_secure_server = None |
| self._web_platform_test_server = None |
| self._image_differ = None |
| self._server_process_constructor = server_process.ServerProcess # overridable for testing |
| self._test_runner_process_constructor = server_process.ServerProcess |
| |
| if not hasattr(options, 'configuration') or not options.configuration: |
| self.set_option_default('configuration', self.default_configuration()) |
| self._test_configuration = None |
| self._reftest_list = {} |
| self._results_directory = None |
| self._root_was_set = hasattr(options, 'root') and options.root |
| self._jhbuild_wrapper = [] |
| self._layout_tests_dir = hasattr(options, 'layout_tests_dir') and options.layout_tests_dir and self._filesystem.abspath(options.layout_tests_dir) |
| self._w3c_resource_files = None |
| |
| def target_host(self, worker_number=None): |
| return self.host |
| |
| def architecture(self): |
| return self.get_option('architecture') |
| |
| def set_architecture(self, arch): |
| self.did_override_architecture = True |
| self.set_option('architecture', arch) |
| |
| def additional_drt_flag(self): |
| return [] |
| |
| def supports_per_test_timeout(self): |
| return True |
| |
| def default_pixel_tests(self): |
| # FIXME: Disable until they are run by default on build.webkit.org. |
| return False |
| |
| def default_timeout_ms(self): |
| return 30 * 1000 |
| |
| def driver_stop_timeout(self): |
| """ Returns the amount of time in seconds to wait before killing the process in driver.stop().""" |
| # We want to wait for at least 3 seconds, but if we are really slow, we want to be slow on cleanup as |
| # well (for things like ASAN, Valgrind, etc.) |
| return 3.0 * float(self.get_option('time_out_ms', '0')) / self.default_timeout_ms() |
| |
| def should_retry_crashes(self): |
| return False |
| |
| def default_child_processes(self, **kwargs): |
| """Return the number of DumpRenderTree instances to use for this port.""" |
| return self._executive.cpu_count() |
| |
| def max_child_processes(self, device_type=None): |
| """Forbid the user from specifying more than this number of child processes""" |
| if device_type: |
| return 0 |
| return float('inf') |
| |
| def supported_device_types(self): |
| # An empty list would indicate a port was incapable of running tests. |
| return [None] |
| |
| def worker_startup_delay_secs(self): |
| # FIXME: If we start workers up too quickly, DumpRenderTree appears |
| # to thrash on something and time out its first few tests. Until |
| # we can figure out what's going on, sleep a bit in between |
| # workers. See https://bugs.webkit.org/show_bug.cgi?id=79147 . |
| return 0.1 |
| |
| def baseline_path(self): |
| """Return the absolute path to the directory to store new baselines in for this port.""" |
| # FIXME: remove once all callers are calling either baseline_version_dir() or baseline_platform_dir() |
| return self.baseline_version_dir() |
| |
| def baseline_platform_dir(self): |
| """Return the absolute path to the default (version-independent) platform-specific results.""" |
| return self._filesystem.join(self.layout_tests_dir(), 'platform', self.port_name) |
| |
| def baseline_version_dir(self): |
| """Return the absolute path to the platform-and-version-specific results.""" |
| baseline_search_paths = self.baseline_search_path() |
| return baseline_search_paths[0] |
| |
| def baseline_search_path(self, device_type=None): |
| return self.get_option('additional_platform_directory', []) + self._compare_baseline() + self.default_baseline_search_path(device_type=device_type) |
| |
| def default_baseline_search_path(self, device_type=None): |
| """Return a list of absolute paths to directories to search under for |
| baselines. The directories are searched in order.""" |
| search_paths = [] |
| if self.get_option('webkit_test_runner'): |
| search_paths.append(self._wk2_port_name()) |
| search_paths.append(self.name()) |
| if self.name() != self.port_name: |
| search_paths.append(self.port_name) |
| return list(map(self._webkit_baseline_path, search_paths)) |
| |
| @memoized |
| def _compare_baseline(self): |
| factory = PortFactory(self.host) |
| target_port = self.get_option('compare_port') |
| if target_port: |
| return factory.get(target_port).default_baseline_search_path() |
| return [] |
| |
| def check_build(self): |
| """This routine is used to ensure that the build is up to date |
| and all the needed binaries are present.""" |
| # If we're using a pre-built copy of WebKit (--root), we assume it also includes a build of DRT. |
| if not self._root_was_set and self.get_option('build') and not self._build_driver(): |
| return False |
| if self.get_option('install') and not self._check_driver(): |
| return False |
| if not self.check_image_diff(): |
| return self._build_image_diff() |
| return True |
| |
| def check_api_test_build(self, canonicalized_binaries=None): |
| if not canonicalized_binaries: |
| canonicalized_binaries = self.path_to_api_test_binaries().keys() |
| if not self._root_was_set and self.get_option('build') and not self._build_api_tests(wtf_only=(canonicalized_binaries == ['TestWTF'])): |
| return False |
| |
| for binary, path in self.path_to_api_test_binaries().items(): |
| if binary not in canonicalized_binaries: |
| continue |
| if not self._filesystem.exists(path): |
| _log.error('{} was not found at {}'.format(os.path.basename(path), path)) |
| return False |
| return True |
| |
| def environment_for_api_tests(self): |
| return self.setup_environ_for_server() |
| |
| def _check_driver(self): |
| driver_path = self._path_to_driver() |
| if not self._filesystem.exists(driver_path): |
| _log.error("%s was not found at %s" % (self.driver_name(), driver_path)) |
| return False |
| return True |
| |
| def check_sys_deps(self): |
| """If the port needs to do some runtime checks to ensure that the |
| tests can be run successfully, it should override this routine. |
| This step can be skipped with --nocheck-sys-deps. |
| |
| Returns whether the system is properly configured.""" |
| return True |
| |
| def check_image_diff(self, override_step=None, logging=True): |
| """This routine is used to check whether image_diff binary exists.""" |
| image_diff_path = self._path_to_default_image_diff() |
| if not self._filesystem.exists(image_diff_path): |
| if logging: |
| _log.error("ImageDiff was not found at %s" % image_diff_path) |
| return False |
| return True |
| |
| def check_httpd(self): |
| if self._uses_apache(): |
| httpd_path = self._path_to_apache() |
| else: |
| httpd_path = self._path_to_lighttpd() |
| |
| try: |
| server_name = self._filesystem.basename(httpd_path) |
| env = self.setup_environ_for_server(server_name) |
| if self._executive.run_command([httpd_path, "-v"], env=env, return_exit_code=True) != 0: |
| _log.error("httpd seems broken. Cannot run http tests.") |
| return False |
| return True |
| except OSError: |
| _log.error("No httpd found. Cannot run http tests.") |
| return False |
| |
| def do_text_results_differ(self, expected_text, actual_text): |
| return expected_text != actual_text |
| |
| def do_audio_results_differ(self, expected_audio, actual_audio): |
| if expected_audio == actual_audio: |
| return False |
| return not WaveDiff(expected_audio, actual_audio).filesAreIdenticalWithinTolerance() |
| |
| def diff_image(self, expected_contents, actual_contents, tolerance=None): |
| """Compare two images and return a tuple of an image diff, a percentage difference (0-100), and an error string. |
| |
| |tolerance| should be a percentage value (0.0 - 100.0). |
| If it is omitted, the port default tolerance value is used. |
| |
| If an error occurs (like ImageDiff isn't found, or crashes, we log an error and return True (for a diff). |
| """ |
| if not actual_contents and not expected_contents: |
| return (None, 0, None) |
| if not actual_contents or not expected_contents: |
| return (True, 0, None) |
| if not self._image_differ: |
| self._image_differ = image_diff.ImageDiffer(self) |
| self.set_option_default('tolerance', 0.1) |
| if tolerance is None: |
| tolerance = self.get_option('tolerance') |
| return self._image_differ.diff_image(expected_contents, actual_contents, tolerance) |
| |
| def diff_text(self, expected_text, actual_text, expected_filename, actual_filename): |
| """Returns a string containing the diff of the two text strings |
| in 'unified diff' format.""" |
| expected_filename = decode_for(encode_if_necessary(expected_filename), str) |
| actual_filename = decode_for(encode_if_necessary(actual_filename), str) |
| diff = difflib.unified_diff(expected_text.splitlines(True), |
| actual_text.splitlines(True), |
| expected_filename, |
| actual_filename) |
| result = "" |
| for line in diff: |
| result += line |
| if not line.endswith('\n'): |
| result += '\n No newline at end of file\n' |
| return result |
| |
| def check_for_leaks(self, process_name, process_id): |
| # Subclasses should check for leaks in the running process |
| # and print any necessary warnings if leaks are found. |
| # FIXME: We should consider moving much of this logic into |
| # Executive and make it platform-specific instead of port-specific. |
| pass |
| |
| def print_leaks_summary(self): |
| # Subclasses can override this to print a summary of leaks found |
| # while running the layout tests. |
| pass |
| |
| def driver_name(self): |
| if self.get_option('driver_name'): |
| return self.get_option('driver_name') |
| if self.get_option('webkit_test_runner'): |
| return 'WebKitTestRunner' |
| return 'DumpRenderTree' |
| |
| def expected_baselines_by_extension(self, test_name): |
| """Returns a dict mapping baseline suffix to relative path for each baseline in |
| a test. For reftests, it returns ".==" or ".!=" instead of the suffix.""" |
| # FIXME: The name similarity between this and expected_baselines() below, is unfortunate. |
| # We should probably rename them both. |
| baseline_dict = {} |
| reference_files = self.reference_files(test_name) |
| if reference_files: |
| # FIXME: How should this handle more than one type of reftest? |
| baseline_dict['.' + reference_files[0][0]] = self.relative_test_filename(reference_files[0][1]) |
| |
| for extension in self.baseline_extensions(): |
| path = self.expected_filename(test_name, extension, return_default=False) |
| baseline_dict[extension] = self.relative_test_filename(path) if path else path |
| |
| return baseline_dict |
| |
| def baseline_extensions(self): |
| """Returns a tuple of all of the non-reftest baseline extensions we use. The extensions include the leading '.'.""" |
| return ('.wav', '.webarchive', '.txt', '.png') |
| |
| def _expected_baselines_for_suffixes(self, test_name, suffixes, all_baselines=False, device_type=None): |
| baseline_search_path = self.baseline_search_path(device_type=device_type) + [self.layout_tests_dir()] |
| |
| baselines = [] |
| for platform_dir in baseline_search_path: |
| for suffix in suffixes: |
| baseline_filename = self._filesystem.splitext(test_name)[0] + '-expected' + suffix |
| if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)): |
| baselines.append((platform_dir, baseline_filename)) |
| |
| if not all_baselines and baselines: |
| return baselines |
| |
| if baselines: |
| return baselines |
| |
| for suffix in suffixes: |
| baselines.append((None, self._filesystem.splitext(test_name)[0] + '-expected' + suffix)) |
| return baselines |
| |
| def expected_baselines(self, test_name, suffix, all_baselines=False, device_type=None): |
| """Given a test name, finds where the baseline results are located. |
| |
| Args: |
| test_name: name of test file (usually a relative path under LayoutTests/) |
| suffix: file suffix of the expected results, including dot; e.g. |
| '.txt' or '.png'. This should not be None, but may be an empty |
| string. |
| all_baselines: If True, return an ordered list of all baseline paths |
| for the given platform. If False, return only the first one. |
| Returns |
| a list of ( platform_dir, results_filename ), where |
| platform_dir - abs path to the top of the results tree (or test |
| tree) |
| results_filename - relative path from top of tree to the results |
| file |
| (port.join() of the two gives you the full path to the file, |
| unless None was returned.) |
| Return values will be in the format appropriate for the current |
| platform (e.g., "\\" for path separators on Windows). If the results |
| file is not found, then None will be returned for the directory, |
| but the expected relative pathname will still be returned. |
| |
| This routine is generic but lives here since it is used in |
| conjunction with the other baseline and filename routines that are |
| platform specific. |
| """ |
| return self._expected_baselines_for_suffixes(test_name, [suffix], all_baselines=all_baselines, device_type=device_type) |
| |
| def expected_filename(self, test_name, suffix, return_default=True, device_type=None): |
| """Given a test name, returns an absolute path to its expected results. |
| |
| If no expected results are found in any of the searched directories, |
| the directory in which the test itself is located will be returned. |
| The return value is in the format appropriate for the platform |
| (e.g., "\\" for path separators on windows). |
| |
| Args: |
| test_name: name of test file (usually a relative path under LayoutTests/) |
| suffix: file suffix of the expected results, including dot; e.g. '.txt' |
| or '.png'. This should not be None, but may be an empty string. |
| platform: the most-specific directory name to use to build the |
| search list of directories; e.g. 'mountainlion-wk2' |
| return_default: if True, returns the path to the generic expectation if nothing |
| else is found; if False, returns None. |
| |
| This routine is generic but is implemented here to live alongside |
| the other baseline and filename manipulation routines. |
| """ |
| platform_dir, baseline_filename = self.expected_baselines(test_name, suffix, device_type=device_type)[0] |
| if platform_dir or return_default: |
| return self._filesystem.join(platform_dir or self.layout_tests_dir(), baseline_filename) |
| return None |
| |
| def expected_checksum(self, test_name, device_type=None): |
| """Returns the checksum of the image we expect the test to produce, or None if it is a text-only test.""" |
| png_path = self.expected_filename(test_name, '.png', device_type=device_type) |
| |
| if self._filesystem.exists(png_path): |
| with self._filesystem.open_binary_file_for_reading(png_path) as filehandle: |
| return read_checksum_from_png.read_checksum(filehandle) |
| |
| return None |
| |
| def expected_image(self, test_name, device_type=None): |
| """Returns the image we expect the test to produce.""" |
| baseline_path = self.expected_filename(test_name, '.png', device_type=device_type) |
| if not self._filesystem.exists(baseline_path): |
| return None |
| return self._filesystem.read_binary_file(baseline_path) |
| |
| def expected_audio(self, test_name, device_type=None): |
| baseline_path = self.expected_filename(test_name, '.wav', device_type=device_type) |
| if not self._filesystem.exists(baseline_path): |
| return None |
| return self._filesystem.read_binary_file(baseline_path) |
| |
| def expected_text(self, test_name, device_type=None): |
| """Returns the text output we expect the test to produce, or None |
| if we don't expect there to be any text output. |
| End-of-line characters are normalized to '\n'.""" |
| # FIXME: DRT output is actually utf-8, but since we don't decode the |
| # output from DRT (instead treating it as a binary string), we read the |
| # baselines as a binary string, too. |
| baseline_path = self.expected_filename(test_name, '.txt', device_type=device_type) |
| if not self._filesystem.exists(baseline_path): |
| baseline_path = self.expected_filename(test_name, '.webarchive', device_type=device_type) |
| if not self._filesystem.exists(baseline_path): |
| return None |
| text = decode_for(self._filesystem.read_binary_file(baseline_path), str) |
| return text.replace("\r\n", "\n") |
| |
| def _get_reftest_list(self, test_name): |
| dirname = self._filesystem.join(self.layout_tests_dir(), self._filesystem.dirname(test_name)) |
| if dirname not in self._reftest_list: |
| self._reftest_list[dirname] = Port._parse_reftest_list(self._filesystem, dirname) |
| return self._reftest_list[dirname] |
| |
| @staticmethod |
| def _parse_reftest_list(filesystem, test_dirpath): |
| reftest_list_path = filesystem.join(test_dirpath, 'reftest.list') |
| if not filesystem.isfile(reftest_list_path): |
| return None |
| reftest_list_file = filesystem.read_text_file(reftest_list_path) |
| |
| parsed_list = {} |
| for line in reftest_list_file.split('\n'): |
| line = re.sub('#.+$', '', line) |
| split_line = line.split() |
| if len(split_line) < 3: |
| continue |
| expectation_type, test_file, ref_file = split_line |
| parsed_list.setdefault(filesystem.join(test_dirpath, test_file), []).append((expectation_type, filesystem.join(test_dirpath, ref_file))) |
| return parsed_list |
| |
| def reference_files(self, test_name, device_type=None): |
| """Return a list of expectation (== or !=) and filename pairs""" |
| |
| if self.get_option('treat_ref_tests_as_pixel_tests'): |
| return [] |
| |
| result = self._get_reftest_list(test_name) |
| if result: |
| return result.get(self._filesystem.join(self.layout_tests_dir(), test_name), []) |
| |
| result = [] |
| suffixes = [] |
| for part1 in ['', '-mismatch']: |
| for part2 in self._supported_reference_extensions: |
| suffixes.append(part1 + part2) |
| for platform_dir, baseline_filename in self._expected_baselines_for_suffixes(test_name, suffixes, device_type=device_type): |
| if not platform_dir: |
| continue |
| result.append(( |
| '!=' if '-mismatch.' in baseline_filename else '==', |
| self._filesystem.join(platform_dir, baseline_filename), |
| )) |
| return result |
| |
| def potential_test_names_from_expected_file(self, path): |
| """Return potential test names if any from a potential expected file path, relative to LayoutTests directory.""" |
| |
| if not '-expected.' in path: |
| return None |
| |
| if path.startswith('platform' + self._filesystem.sep): |
| steps = path.split(self._filesystem.sep) |
| path = self._filesystem.join(self._filesystem.sep.join(steps[2:])) |
| |
| return [self.host.filesystem.relpath(test, self.layout_tests_dir()) for test in self._filesystem.glob(re.sub('-expected.*', '.*', self._filesystem.join(self.layout_tests_dir(), path))) if self._filesystem.isfile(test)] |
| |
| def tests(self, paths, device_type=None): |
| """Return the list of tests found. Both generic and platform-specific tests matching paths should be returned.""" |
| expanded_paths = self._expanded_paths(paths, device_type=device_type) |
| return self._real_tests(expanded_paths) |
| |
| def _expanded_paths(self, paths, device_type=None): |
| expanded_paths = [] |
| fs = self._filesystem |
| all_platform_dirs = [path for path in fs.glob(fs.join(self.layout_tests_dir(), 'platform', '*')) if fs.isdir(path)] |
| for path in paths: |
| expanded_paths.append(path) |
| if self.test_isdir(path) and not path.startswith('platform') and not fs.isabs(path): |
| for platform_dir in all_platform_dirs: |
| if fs.isdir(fs.join(platform_dir, path)) and platform_dir in self.baseline_search_path(device_type=device_type): |
| expanded_paths.append(self.relative_test_filename(fs.join(platform_dir, path))) |
| |
| return expanded_paths |
| |
| def _real_tests(self, paths): |
| # When collecting test cases, skip these directories |
| skipped_directories = set(['.svn', '_svn', 'resources', 'support', 'script-tests', 'reference', 'reftest']) |
| files = find_files.find(self._filesystem, self.layout_tests_dir(), paths, skipped_directories, partial(Port._is_test_file, self), self.test_key) |
| return [self.relative_test_filename(f) for f in files] |
| |
| # When collecting test cases, we include any file with these extensions. |
| _supported_test_extensions = set(['.html', '.shtml', '.xml', '.xhtml', '.pl', '.htm', '.php', '.svg', '.mht', '.xht']) |
| _supported_reference_extensions = set(['.html', '.xml', '.xhtml', '.htm', '.svg', '.xht']) |
| |
| def is_w3c_resource_file(self, filesystem, dirname, filename): |
| path = filesystem.join(dirname, filename) |
| w3c_path = filesystem.join(self.layout_tests_dir(), "imported", "w3c") |
| if not w3c_path in path: |
| return False |
| |
| if not self._w3c_resource_files: |
| filepath = filesystem.join(w3c_path, "resources", "resource-files.json") |
| json_data = filesystem.read_text_file(filepath) |
| self._w3c_resource_files = json.loads(json_data) |
| |
| subpath = path[len(w3c_path) + 1:].replace('\\', '/') |
| if subpath in self._w3c_resource_files["files"]: |
| return True |
| for dirpath in self._w3c_resource_files["directories"]: |
| if dirpath in subpath: |
| return True |
| return False |
| |
| @staticmethod |
| # If any changes are made here be sure to update the isUsedInReftest method in old-run-webkit-tests as well. |
| def is_reference_html_file(filesystem, dirname, filename): |
| if filename.startswith('ref-') or filename.startswith('notref-'): |
| return True |
| filename_wihout_ext, ext = filesystem.splitext(filename) |
| if ext not in Port._supported_reference_extensions: |
| return False |
| for suffix in ['-expected', '-expected-mismatch', '-ref', '-notref']: |
| if filename_wihout_ext.endswith(suffix): |
| return True |
| return False |
| |
| @staticmethod |
| def _has_supported_extension(filesystem, filename): |
| """Return true if filename is one of the file extensions we want to run a test on.""" |
| extension = filesystem.splitext(filename)[1] |
| return extension in Port._supported_test_extensions |
| |
| def _is_test_file(self, filesystem, dirname, filename): |
| if not Port._has_supported_extension(filesystem, filename): |
| return False |
| if Port.is_reference_html_file(filesystem, dirname, filename): |
| return False |
| if self.is_w3c_resource_file(filesystem, dirname, filename): |
| return False |
| return True |
| |
| def test_key(self, test_name): |
| """Turns a test name into a list with two sublists, the natural key of the |
| dirname, and the natural key of the basename. |
| |
| This can be used when sorting paths so that files in a directory. |
| directory are kept together rather than being mixed in with files in |
| subdirectories.""" |
| dirname, basename = self.split_test(test_name) |
| return (self._natural_sort_key(dirname + self.TEST_PATH_SEPARATOR), self._natural_sort_key(basename)) |
| |
| def _natural_sort_key(self, string_to_split): |
| """ Turns a string into a list of string and number chunks, i.e. "z23a" -> ["z", 23, "a"] |
| |
| This can be used to implement "natural sort" order. See: |
| http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html |
| http://nedbatchelder.com/blog/200712.html#e20071211T054956 |
| """ |
| def tryint(val): |
| try: |
| return int(val) |
| except ValueError: |
| return val |
| |
| return [tryint(chunk) for chunk in re.split('(\d+)', string_to_split)] |
| |
| def test_dirs(self): |
| """Returns the list of top-level test directories.""" |
| layout_tests_dir = self.layout_tests_dir() |
| return filter(lambda x: self._filesystem.isdir(self._filesystem.join(layout_tests_dir, x)), |
| self._filesystem.listdir(layout_tests_dir)) |
| |
| @memoized |
| def test_isfile(self, test_name): |
| """Return True if the test name refers to a directory of tests.""" |
| # Used by test_expectations.py to apply rules to whole directories. |
| return self._filesystem.isfile(self.abspath_for_test(test_name)) |
| |
| @memoized |
| def test_isdir(self, test_name): |
| """Return True if the test name refers to a directory of tests.""" |
| # Used by test_expectations.py to apply rules to whole directories. |
| return self._filesystem.isdir(self.abspath_for_test(test_name)) |
| |
| @memoized |
| def test_exists(self, test_name): |
| """Return True if the test name refers to an existing test or baseline.""" |
| # Used by test_expectations.py to determine if an entry refers to a |
| # valid test and by printing.py to determine if baselines exist. |
| return self.test_isfile(test_name) or self.test_isdir(test_name) |
| |
| def split_test(self, test_name): |
| """Splits a test name into the 'directory' part and the 'basename' part.""" |
| index = test_name.rfind(self.TEST_PATH_SEPARATOR) |
| if index < 1: |
| return ('', test_name) |
| return (test_name[0:index], test_name[index:]) |
| |
| def normalize_test_name(self, test_name): |
| """Returns a normalized version of the test name or test directory.""" |
| if test_name.endswith(self.TEST_PATH_SEPARATOR): |
| return test_name |
| if self.test_isdir(test_name): |
| return test_name + self.TEST_PATH_SEPARATOR |
| return test_name |
| |
| def driver_cmd_line_for_logging(self): |
| """Prints the DRT command line that will be used.""" |
| driver = self.create_driver(0) |
| return driver.cmd_line(self.get_option('pixel_tests'), []) |
| |
| def update_baseline(self, baseline_path, data): |
| """Updates the baseline for a test. |
| |
| Args: |
| baseline_path: the actual path to use for baseline, not the path to |
| the test. This function is used to update either generic or |
| platform-specific baselines, but we can't infer which here. |
| data: contents of the baseline. |
| """ |
| self._filesystem.write_binary_file(baseline_path, data) |
| |
| # FIXME: update callers to create a finder and call it instead of these next five routines (which should be protected). |
| def webkit_base(self): |
| return self._webkit_finder.webkit_base() |
| |
| def path_from_webkit_base(self, *comps): |
| return self._webkit_finder.path_from_webkit_base(*comps) |
| |
| def path_to_script(self, script_name): |
| return self._webkit_finder.path_to_script(script_name) |
| |
| def layout_tests_dir(self): |
| if self._layout_tests_dir: |
| return self._layout_tests_dir |
| return self._webkit_finder.layout_tests_dir() |
| |
| def perf_tests_dir(self): |
| return self._webkit_finder.perf_tests_dir() |
| |
| def skipped_layout_tests(self, test_list, device_type=None): |
| """Returns tests skipped outside of the TestExpectations files.""" |
| return set(self._tests_for_other_platforms(device_type=device_type)).union(self._skipped_tests_for_unsupported_features(test_list)) |
| |
| @memoized |
| def skipped_perf_tests(self): |
| filename = self._filesystem.join(self.perf_tests_dir(), "Skipped") |
| if not self._filesystem.exists(filename): |
| _log.debug("Skipped does not exist: %s" % filename) |
| return [] |
| |
| skipped_file_contents = self._filesystem.read_text_file(filename) |
| tests_to_skip = [] |
| for line_number, line in enumerate(skipped_file_contents.split('\n')): |
| match = re.match(r'^\s*(\[(?P<platforms>[\w ]*?)\])?\s*(?P<test>[\w\-\/\.]+?)?\s*(?P<comment>\#.*)?$', line) |
| if not match: |
| _log.error("Syntax error at line %d in %s: %s" % (line_number + 1, filename, line)) |
| else: |
| platform_names = list(filter(lambda token: token, match.group('platforms').lower().split(' '))) if match.group('platforms') else [] |
| test_name = match.group('test') |
| if test_name and (not platform_names or self.port_name in platform_names or self._name in platform_names): |
| tests_to_skip.append(test_name) |
| |
| return tests_to_skip |
| |
| def skips_perf_test(self, test_name): |
| for test_or_category in self.skipped_perf_tests(): |
| if test_or_category == test_name: |
| return True |
| category = self._filesystem.join(self.perf_tests_dir(), test_or_category) |
| if self._filesystem.isdir(category) and test_name.startswith(test_or_category): |
| return True |
| return False |
| |
| def name(self): |
| """Returns a name that uniquely identifies this particular type of port |
| (e.g., "mac-snowleopard" or "chromium-linux-x86_x64" and can be passed |
| to factory.get() to instantiate the port.""" |
| return self._name |
| |
| def operating_system(self): |
| # Subclasses should override this default implementation. |
| return 'mac' |
| |
| @memoized |
| def version_name(self): |
| """Returns a string indicating the version of a given platform, e.g. |
| 'leopard' or 'xp'. |
| |
| This is used to help identify the exact port when parsing test |
| expectations, determining search paths, and logging information.""" |
| if self._os_version is None: |
| return None |
| result = VersionNameMap.map(self.host.platform).to_name(self._os_version, table=PUBLIC_TABLE) |
| if not result: |
| result = VersionNameMap.map(self.host.platform).to_name(self._os_version, table=INTERNAL_TABLE) |
| return result |
| |
| def get_option(self, name, default_value=None): |
| return getattr(self._options, name, default_value) |
| |
| def set_option(self, name, value): |
| setattr(self._options, name, value) |
| return self.get_option(name) == value |
| |
| def set_option_default(self, name, default_value): |
| return self._options.ensure_value(name, default_value) |
| |
| @memoized |
| def path_to_generic_test_expectations_file(self): |
| return self._filesystem.join(self.layout_tests_dir(), 'TestExpectations') |
| |
| @memoized |
| def path_to_test_expectations_file(self): |
| """Update the test expectations to the passed-in string. |
| |
| This is used by the rebaselining tool. Raises NotImplementedError |
| if the port does not use expectations files.""" |
| |
| # FIXME: We need to remove this when we make rebaselining work with multiple files and just generalize expectations_files(). |
| |
| # test_expectations are always in mac/ not mac-leopard/ by convention, hence we use port_name instead of name(). |
| return self._filesystem.join(self._webkit_baseline_path(self.port_name), 'TestExpectations') |
| |
| def relative_test_filename(self, filename): |
| """Returns a test_name a relative unix-style path for a filename under the LayoutTests |
| directory. Ports may legitimately return abspaths here if no relpath makes sense.""" |
| # Ports that run on windows need to override this method to deal with |
| # filenames with backslashes in them. |
| if filename.startswith(self.layout_tests_dir()): |
| return self.host.filesystem.relpath(filename, self.layout_tests_dir()).replace(self.host.filesystem.sep, self.TEST_PATH_SEPARATOR) |
| else: |
| return self.host.filesystem.abspath(filename) |
| |
| @memoized |
| def abspath_for_test(self, test_name, target_host=None): |
| """Returns the full path to the file for a given test name. This is the |
| inverse of relative_test_filename() if no target_host is specified.""" |
| host = target_host or self.host |
| return host.filesystem.join(host.filesystem.map_base_host_path(self.layout_tests_dir()), test_name.replace(self.TEST_PATH_SEPARATOR, self.host.filesystem.sep)) |
| |
| def jsc_results_directory(self): |
| return self._build_path() |
| |
| def bindings_results_directory(self): |
| return self._build_path() |
| |
| def api_results_directory(self): |
| return self._build_path() |
| |
| def results_directory(self): |
| """Absolute path to the place to store the test results (uses --results-directory).""" |
| if not self._results_directory: |
| option_val = self.get_option('results_directory') or self.default_results_directory() |
| self._results_directory = self._filesystem.abspath(option_val) |
| return self._results_directory |
| |
| def perf_results_directory(self): |
| return self._build_path() |
| |
| def python_unittest_results_directory(self): |
| return self._build_path('python-unittest-results') |
| |
| def default_results_directory(self): |
| """Absolute path to the default place to store the test results.""" |
| # Results are store relative to the built products to make it easy |
| # to have multiple copies of webkit checked out and built. |
| return self._build_path('layout-test-results') |
| |
| def setup_test_run(self, device_type=None): |
| """Perform port-specific work at the beginning of a test run.""" |
| pass |
| |
| def clean_up_test_run(self): |
| """Perform port-specific work at the end of a test run.""" |
| if self._image_differ: |
| self._image_differ.stop() |
| self._image_differ = None |
| |
| # FIXME: os.environ access should be moved to onto a common/system class to be more easily mockable. |
| def _value_or_default_from_environ(self, name, default=None): |
| if name in os.environ: |
| return os.environ[name] |
| return default |
| |
| def _copy_value_from_environ_if_set(self, clean_env, name): |
| if name in os.environ: |
| clean_env[name] = os.environ[name] |
| |
| def setup_environ_for_server(self, server_name=None): |
| # We intentionally copy only a subset of os.environ when |
| # launching subprocesses to ensure consistent test results. |
| clean_env = {} |
| # Note: don't set here driver specific variables (related to X11, Wayland, etc.) |
| # Use the driver _setup_environ_for_test() method for that. |
| variables_to_copy = [ |
| # For Linux: |
| 'ALSA_CARD', |
| 'DBUS_SESSION_BUS_ADDRESS', |
| 'LANG', |
| 'LD_LIBRARY_PATH', |
| 'TERM', |
| 'XDG_DATA_DIRS', |
| 'XDG_RUNTIME_DIR', |
| |
| # Darwin: |
| 'DYLD_FRAMEWORK_PATH', |
| 'DYLD_LIBRARY_PATH', |
| '__XPC_DYLD_FRAMEWORK_PATH', |
| '__XPC_DYLD_LIBRARY_PATH', |
| |
| # CYGWIN: |
| 'HOMEDRIVE', |
| 'HOMEPATH', |
| '_NT_SYMBOL_PATH', |
| |
| # Windows: |
| 'COMSPEC', |
| 'SYSTEMDRIVE', |
| 'SYSTEMROOT', |
| 'WEBKIT_LIBRARIES', |
| |
| # Most ports (?): |
| 'HOME', |
| 'PATH', |
| 'WEBKIT_TESTFONTS', |
| 'WEBKIT_OUTPUTDIR', |
| |
| ] |
| for variable in variables_to_copy: |
| self._copy_value_from_environ_if_set(clean_env, variable) |
| |
| for string_variable in self.get_option('additional_env_var', []): |
| [name, value] = string_variable.split('=', 1) |
| clean_env[name] = value |
| |
| return clean_env |
| |
| def _clear_global_caches_and_temporary_files(self): |
| pass |
| |
| @staticmethod |
| def _append_value_colon_separated(env, name, value): |
| assert os.pathsep not in value |
| if name in env and env[name]: |
| env[name] = env[name] + os.pathsep + value |
| else: |
| env[name] = value |
| |
| def show_results_html_file(self, results_filename): |
| """This routine should display the HTML file pointed at by |
| results_filename in a users' browser.""" |
| return self.host.user.open_url(path.abspath_to_uri(self.host.platform, results_filename)) |
| |
| def create_driver(self, worker_number, no_timeout=False): |
| """Return a newly created Driver subclass for starting/stopping the test driver.""" |
| return driver.DriverProxy(self, worker_number, self._driver_class(), pixel_tests=self.get_option('pixel_tests'), no_timeout=no_timeout) |
| |
| def start_helper(self, pixel_tests=False): |
| """If a port needs to reconfigure graphics settings or do other |
| things to ensure a known test configuration, it should override this |
| method.""" |
| return True |
| |
| def reset_preferences(self): |
| """If a port needs to reset platform-specific persistent preference |
| storage, it should override this method.""" |
| pass |
| |
| def ports_to_forward(self): |
| ports = [] |
| if self._http_server: |
| ports.extend(self._http_server.ports_to_forward()) |
| if self._websocket_server: |
| ports.extend(self._websocket_server.ports_to_forward()) |
| if self._websocket_server: |
| ports.extend(self._websocket_secure_server.ports_to_forward()) |
| if self._web_platform_test_server: |
| ports.extend(self._web_platform_test_server.ports_to_forward()) |
| return ports |
| |
| def start_http_server(self, additional_dirs=None): |
| """Start a web server. Raise an error if it can't start or is already running. |
| |
| Ports can stub this out if they don't need a web server to be running.""" |
| assert not self._http_server, 'Already running an http server.' |
| if not self.check_httpd(): |
| return |
| |
| http_port = self.get_option('http_port') |
| if self._uses_apache(): |
| server = apache_http_server.LayoutTestApacheHttpd(self, self.results_directory(), additional_dirs=additional_dirs, port=http_port) |
| else: |
| server = http_server.Lighttpd(self, self.results_directory(), additional_dirs=additional_dirs, port=http_port) |
| |
| server.start() |
| self._http_server = server |
| |
| def is_http_server_running(self): |
| if self._http_server: |
| return True |
| return http_server_base.is_http_server_running() |
| |
| def is_websocket_server_running(self): |
| if self._websocket_server: |
| return True |
| return websocket_server.is_web_socket_server_running() |
| |
| def is_wpt_server_running(self): |
| if self._web_platform_test_server: |
| return True |
| return web_platform_test_server.is_wpt_server_running(self) |
| |
| def start_websocket_server(self): |
| """Start a web server. Raise an error if it can't start or is already running. |
| |
| Ports can stub this out if they don't need a websocket server to be running.""" |
| assert not self._websocket_server, 'Already running a websocket server.' |
| |
| server = websocket_server.PyWebSocket(self, self.results_directory()) |
| server.start() |
| self._websocket_server = server |
| |
| websocket_server_temporary_directory = self._filesystem.mkdtemp(prefix='webkitpy-websocket-server') |
| self._websocket_server_temporary_directory = websocket_server_temporary_directory |
| |
| pem_file = self._filesystem.join(self.layout_tests_dir(), "http", "conf", "webkit-httpd.pem") |
| pem = pemfile.load(self._filesystem, pem_file) |
| certificate_file = self._filesystem.join(str(websocket_server_temporary_directory), 'webkit-httpd.crt') |
| self._filesystem.write_text_file(certificate_file, pem.certificate) |
| private_key_file = self._filesystem.join(str(websocket_server_temporary_directory), 'webkit-httpd.key') |
| self._filesystem.write_text_file(private_key_file, pem.private_key) |
| |
| secure_server = self._websocket_secure_server = websocket_server.PyWebSocket(self, self.results_directory(), |
| use_tls=True, port=websocket_server.PyWebSocket.DEFAULT_WSS_PORT, private_key=private_key_file, certificate=certificate_file) |
| secure_server.start() |
| self._websocket_secure_server = secure_server |
| |
| def start_web_platform_test_server(self, additional_dirs=None, number_of_servers=None): |
| assert not self._web_platform_test_server, 'Already running a Web Platform Test server.' |
| |
| self._web_platform_test_server = web_platform_test_server.WebPlatformTestServer(self, "wptwk") |
| self._web_platform_test_server.start() |
| |
| def web_platform_test_server_doc_root(self): |
| return web_platform_test_server.doc_root(self) + self.TEST_PATH_SEPARATOR |
| |
| def web_platform_test_server_base_http_url(self): |
| return web_platform_test_server.base_http_url(self) |
| |
| def web_platform_test_server_base_https_url(self): |
| return web_platform_test_server.base_https_url(self) |
| |
| def http_server_supports_ipv6(self): |
| # Cygwin is the only platform to still use Apache 1.3, which only supports IPV4. |
| # Once it moves to Apache 2, we can drop this method altogether. |
| if self.host.platform.is_cygwin(): |
| return False |
| return True |
| |
| def stop_helper(self): |
| """Shut down the test helper if it is running. Do nothing if |
| it isn't, or it isn't available. If a port overrides start_helper() |
| it must override this routine as well.""" |
| pass |
| |
| def stop_http_server(self): |
| """Shut down the http server if it is running. Do nothing if it isn't.""" |
| if self._http_server: |
| self._http_server.stop() |
| self._http_server = None |
| |
| def stop_websocket_server(self): |
| """Shut down the websocket server if it is running. Do nothing if it isn't.""" |
| if self._websocket_server: |
| self._websocket_server.stop() |
| self._websocket_server = None |
| if self._websocket_secure_server: |
| self._websocket_secure_server.stop() |
| self._websocket_secure_server = None |
| if self._websocket_server_temporary_directory: |
| self._filesystem.rmtree(str(self._websocket_server_temporary_directory)) |
| |
| def stop_web_platform_test_server(self): |
| if self._web_platform_test_server: |
| self._web_platform_test_server.stop() |
| self._web_platform_test_server = None |
| |
| def exit_code_from_summarized_results(self, unexpected_results): |
| """Given summarized results, compute the exit code to be returned by new-run-webkit-tests. |
| Bots turn red when this function returns a non-zero value. By default, return the number of regressions |
| to avoid turning bots red by flaky failures, unexpected passes, and missing results""" |
| # Don't turn bots red for flaky failures, unexpected passes, and missing results. |
| return unexpected_results['num_regressions'] |
| |
| # |
| # TEST EXPECTATION-RELATED METHODS |
| # |
| |
| def test_configuration(self): |
| """Returns the current TestConfiguration for the port.""" |
| if not self._test_configuration: |
| self._test_configuration = TestConfiguration(self.version_name(), self.architecture(), self._options.configuration.lower()) |
| return self._test_configuration |
| |
| # FIXME: Belongs on a Platform object. |
| @memoized |
| def all_test_configurations(self): |
| """Returns a list of TestConfiguration instances, representing all available |
| test configurations for this port.""" |
| return self._generate_all_test_configurations() |
| |
| # FIXME: Belongs on a Platform object. |
| def configuration_specifier_macros(self): |
| """Ports may provide a way to abbreviate configuration specifiers to conveniently |
| refer to them as one term or alias specific values to more generic ones. For example: |
| |
| (xp, vista, win7) -> win # Abbreviate all Windows versions into one namesake. |
| (lucid) -> linux # Change specific name of the Linux distro to a more generic term. |
| |
| Returns a dictionary, each key representing a macro term ('win', for example), |
| and value being a list of valid configuration specifiers (such as ['xp', 'vista', 'win7']).""" |
| return {} |
| |
| def all_baseline_variants(self): |
| """Returns a list of platform names sufficient to cover all the baselines. |
| |
| The list should be sorted so that a later platform will reuse |
| an earlier platform's baselines if they are the same (e.g., |
| 'snowleopard' should precede 'leopard').""" |
| raise NotImplementedError |
| |
| def uses_test_expectations_file(self): |
| # This is different from checking test_expectations() is None, because |
| # some ports have Skipped files which are returned as part of test_expectations(). |
| for path in self.default_baseline_search_path(): |
| if self._filesystem.exists(self._filesystem.join(path, 'TestExpectations')): |
| return True |
| return False |
| |
| def warn_if_bug_missing_in_test_expectations(self): |
| return False |
| |
| def expectations_dict(self, device_type=None): |
| """Returns an OrderedDict of name -> expectations strings. |
| The names are expected to be (but not required to be) paths in the filesystem. |
| If the name is a path, the file can be considered updatable for things like rebaselining, |
| so don't use names that are paths if they're not paths. |
| Generally speaking the ordering should be files in the filesystem in cascade order |
| (TestExpectations followed by Skipped, if the port honors both formats), |
| then any built-in expectations (e.g., from compile-time exclusions), then --additional-expectations options.""" |
| # FIXME: rename this to test_expectations() once all the callers are updated to know about the ordered dict. |
| expectations = OrderedDict() |
| |
| for path in self.expectations_files(device_type=device_type): |
| if self._filesystem.exists(path): |
| expectations[path] = self._filesystem.read_text_file(path) |
| |
| for path in self.get_option('additional_expectations', []): |
| expanded_path = self._filesystem.expanduser(path) |
| if self._filesystem.exists(expanded_path): |
| _log.debug("reading additional_expectations from path '%s'" % path) |
| expectations[path] = self._filesystem.read_text_file(expanded_path) |
| else: |
| _log.warning("additional_expectations path '%s' does not exist" % path) |
| return expectations |
| |
| def _port_specific_expectations_files(self, **kwargs): |
| # Unlike baseline_search_path, we only want to search [WK2-PORT, PORT-VERSION, PORT] and any directories |
| # included via --additional-platform-directory, not the full casade. |
| search_paths = [self.port_name] |
| |
| non_wk2_name = self.name().replace('-wk2', '') |
| if non_wk2_name != self.port_name: |
| search_paths.append(non_wk2_name) |
| |
| if self.get_option('webkit_test_runner'): |
| # Because nearly all of the skipped tests for WebKit 2 are due to cross-platform |
| # issues, all wk2 ports share a skipped list under platform/wk2. |
| search_paths.extend(["wk2", self._wk2_port_name()]) |
| |
| search_paths.extend(self.get_option("additional_platform_directory", [])) |
| |
| return [self._filesystem.join(self._webkit_baseline_path(d), 'TestExpectations') for d in search_paths] |
| |
| def expectations_files(self, device_type=None): |
| return [self.path_to_generic_test_expectations_file()] + self._port_specific_expectations_files(device_type=device_type) |
| |
| def repository_paths(self): |
| """Returns a list of (repository_name, repository_path) tuples of its depending code base. |
| By default it returns a list that only contains a ('WebKit', <webkitRepositoryPath>) tuple.""" |
| |
| # We use LayoutTest directory here because webkit_base isn't a part of WebKit repository in Chromium port |
| # where turnk isn't checked out as a whole. |
| repository_paths = [('WebKit', self.layout_tests_dir())] |
| if self.get_option('additional_repository_name') and self.get_option('additional_repository_path'): |
| repository_paths += [(self._options.additional_repository_name, self._options.additional_repository_path)] |
| return repository_paths |
| |
| def allowed_hosts(self): |
| return self.get_option("allowed_host", []) |
| |
| def internal_feature(self): |
| return self.get_option("internal_feature", []) |
| |
| def experimental_feature(self): |
| return self.get_option("experimental_feature", []) |
| |
| def default_configuration(self): |
| return self._config.default_configuration() |
| |
| # |
| # PROTECTED ROUTINES |
| # |
| # The routines below should only be called by routines in this class |
| # or any of its subclasses. |
| # |
| |
| def _uses_apache(self): |
| return True |
| |
| # FIXME: This does not belong on the port object. |
| @memoized |
| def _path_to_apache(self): |
| """Returns the full path to the apache binary. |
| |
| This is needed only by ports that use the apache_http_server module.""" |
| # The Apache binary path can vary depending on OS and distribution |
| # See http://wiki.apache.org/httpd/DistrosDefaultLayout |
| for path in ["/usr/sbin/httpd", "/usr/sbin/apache2", "/app/bin/httpd"]: |
| if self._filesystem.exists(path): |
| return path |
| _log.error("Could not find apache. Not installed or unknown path.") |
| return None |
| |
| def _is_fedora_php_version_7(self): |
| if self._filesystem.exists("/etc/httpd/modules/libphp7.so"): |
| return True |
| return False |
| |
| def _is_darwin_php_version_7(self): |
| if self._filesystem.exists("/usr/libexec/apache2/libphp7.so"): |
| return True |
| return False |
| |
| # FIXME: This belongs on some platform abstraction instead of Port. |
| def _is_redhat_based(self): |
| return self._filesystem.exists('/etc/redhat-release') |
| |
| def _is_debian_based(self): |
| return self._filesystem.exists('/etc/debian_version') |
| |
| def _is_arch_based(self): |
| return self._filesystem.exists('/etc/arch-release') |
| |
| def _is_flatpak(self): |
| return self._filesystem.exists('/.flatpak-info') |
| |
| def _apache_version(self): |
| config = self._executive.run_command([self._path_to_apache(), '-v']) |
| return re.sub(r'(?:.|\n)*Server version: Apache/(\d+\.\d+)(?:.|\n)*', r'\1', config) |
| |
| def _debian_php_version(self): |
| prefix = "/usr/lib/apache2/modules/" |
| for version in ("7.0", "7.1", "7.2", "7.3"): |
| if self._filesystem.exists("%s/libphp%s.so" % (prefix, version)): |
| return "-php%s" % version |
| _log.error("No libphp7.x.so found in %s" % prefix) |
| return "" |
| |
| def _darwin_php_version(self): |
| if self._is_darwin_php_version_7(): |
| return "-php7" |
| return "" |
| |
| def _fedora_php_version(self): |
| if self._is_fedora_php_version_7(): |
| return "-php7" |
| return "" |
| |
| def _win_php_version(self): |
| root = os.environ.get('XAMPP_ROOT', 'C:\\xampp') |
| prefix = self._filesystem.join(root, 'php') |
| for version in ('5', '7'): |
| conf = self._filesystem.join(prefix, "php{}ts.dll".format(version)) |
| if self._filesystem.exists(conf): |
| return "-php{}".format(version) |
| _log.error("No php?ts.dll found in {}".format(prefix)) |
| return "" |
| |
| # We pass sys_platform into this method to make it easy to unit test. |
| def _apache_config_file_name_for_platform(self, sys_platform): |
| if sys_platform in ['cygwin', 'win32']: |
| return 'win-httpd-' + self._apache_version() + self._win_php_version() + '.conf' |
| if sys_platform == 'darwin': |
| return 'apache' + self._apache_version() + self._darwin_php_version() + '-httpd.conf' |
| if sys_platform.startswith('linux'): |
| if self._is_redhat_based(): |
| return 'fedora-httpd-' + self._apache_version() + self._fedora_php_version() + '.conf' |
| if self._is_debian_based(): |
| return 'debian-httpd-' + self._apache_version() + self._debian_php_version() + '.conf' |
| if self._is_arch_based(): |
| return 'archlinux-httpd.conf' |
| if self._is_flatpak(): |
| return 'flatpak-httpd.conf' |
| # All platforms use apache2 except for CYGWIN (and Mac OS X Tiger and prior, which we no longer support). |
| return 'apache' + self._apache_version() + '-httpd.conf' |
| |
| def _path_to_apache_config_file(self): |
| """Returns the full path to the apache configuration file. |
| |
| If the WEBKIT_HTTP_SERVER_CONF_PATH environment variable is set, its |
| contents will be used instead. |
| |
| This is needed only by ports that use the apache_http_server module.""" |
| config_file_from_env = os.environ.get('WEBKIT_HTTP_SERVER_CONF_PATH') |
| if config_file_from_env: |
| if not self._filesystem.exists(config_file_from_env): |
| raise IOError('%s was not found on the system' % config_file_from_env) |
| return config_file_from_env |
| |
| config_file_name = self._apache_config_file_name_for_platform(sys.platform) |
| return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', config_file_name) |
| |
| def _build_path(self, *comps): |
| root_directory = self.get_option('_cached_root') or self.get_option('root') |
| if not root_directory: |
| root_directory = self._config.build_directory(self.get_option('configuration')) |
| build_directory = self.get_option('build_directory') |
| if build_directory: |
| root_directory = self._filesystem.join(build_directory, root_directory.split('/')[-1]) |
| |
| # We take advantage of the behavior that self._options is passed by reference to worker |
| # subprocesses to use it as data store to cache the computed root directory path. This |
| # avoids making each worker subprocess compute this path again which is slow because of |
| # the call to config.build_directory(). |
| # |
| # FIXME: This is like decorating this function with @memoized, but more annoying and fragile; |
| # there should be another way to propagate precomputed values to workers without modifying |
| # the options list. |
| self.set_option('_cached_root', root_directory) |
| |
| if sys.platform.startswith('win') or sys.platform == 'cygwin': |
| return self._filesystem.join(root_directory, *comps) |
| |
| return self._filesystem.join(self._filesystem.abspath(root_directory), *comps) |
| |
| def _path_to_driver(self, configuration=None): |
| """Returns the full path to the test driver (DumpRenderTree).""" |
| local_driver_path = self._build_path(self.driver_name()) |
| if sys.platform.startswith('win'): |
| base = os.path.splitext(local_driver_path)[0] |
| local_driver_path = base + ".exe" |
| return local_driver_path |
| |
| def _driver_tempdir(self, target_host=None): |
| host = target_host or self.host |
| return host.filesystem.mkdtemp(prefix='{}s-'.format(self.driver_name())) |
| |
| def _path_to_user_cache_directory(self, suffix=None): |
| return None |
| |
| def _path_to_webcore_library(self): |
| """Returns the full path to a built copy of WebCore.""" |
| return None |
| |
| def _path_to_helper(self): |
| """Returns the full path to the layout_test_helper binary, which |
| is used to help configure the system for the test run, or None |
| if no helper is needed. |
| |
| This is likely only used by start/stop_helper().""" |
| return None |
| |
| def _path_to_default_image_diff(self): |
| """Returns the full path to the default ImageDiff binary, or None if it is not available.""" |
| return self._build_path('ImageDiff') |
| |
| @memoized |
| def _path_to_image_diff(self): |
| """Returns the full path to the image_diff binary, or None if it is not available. |
| |
| This is likely used only by diff_image()""" |
| default_image_diff = self._path_to_default_image_diff() |
| if self._filesystem.exists(default_image_diff): |
| return default_image_diff |
| built_image_diff = self._filesystem.join(self._config.build_directory(self.get_option('configuration'), for_host=True), 'ImageDiff') |
| _log.debug('ImageDiff not found at {}, using {} instead'.format(default_image_diff, built_image_diff)) |
| return built_image_diff |
| |
| API_TEST_BINARY_NAMES = ['TestWTF', 'TestWebKitAPI'] |
| |
| def path_to_api_test_binaries(self): |
| return {binary: self._build_path(binary) for binary in self.API_TEST_BINARY_NAMES} |
| |
| def _path_to_lighttpd(self): |
| """Returns the path to the LigHTTPd binary. |
| |
| This is needed only by ports that use the http_server.py module.""" |
| raise NotImplementedError('Port._path_to_lighttpd') |
| |
| def _path_to_lighttpd_modules(self): |
| """Returns the path to the LigHTTPd modules directory. |
| |
| This is needed only by ports that use the http_server.py module.""" |
| raise NotImplementedError('Port._path_to_lighttpd_modules') |
| |
| def _path_to_lighttpd_php(self): |
| """Returns the path to the LigHTTPd PHP executable. |
| |
| This is needed only by ports that use the http_server.py module.""" |
| raise NotImplementedError('Port._path_to_lighttpd_php') |
| |
| def _webkit_baseline_path(self, platform): |
| """Return the full path to the top of the baseline tree for a |
| given platform.""" |
| return self._filesystem.join(self.layout_tests_dir(), 'platform', platform) |
| |
| # FIXME: Belongs on a Platform object. |
| def _generate_all_test_configurations(self): |
| """Generates a list of TestConfiguration instances, representing configurations |
| for a platform across all OSes, architectures, build and graphics types.""" |
| raise NotImplementedError('Port._generate_test_configurations') |
| |
| def _driver_class(self): |
| """Returns the port's driver implementation.""" |
| return driver.Driver |
| |
| def path_to_crash_logs(self): |
| raise NotImplementedError |
| |
| def _get_crash_log(self, name, pid, stdout, stderr, newer_than, target_host=None): |
| name_str = name or '<unknown process name>' |
| pid_str = str(pid or '<unknown>') |
| stdout_lines = (stdout or '<empty>').decode('utf8', 'replace').splitlines() |
| stderr_lines = (stderr or '<empty>').decode('utf8', 'replace').splitlines() |
| return (stderr, 'crash log for %s (pid %s):\n%s\n%s\n' % (name_str, pid_str, |
| '\n'.join(('STDOUT: ' + l) for l in stdout_lines), |
| '\n'.join(('STDERR: ' + l) for l in stderr_lines))) |
| |
| def look_for_new_crash_logs(self, crashed_processes, start_time): |
| pass |
| |
| def look_for_new_samples(self, unresponsive_processes, start_time): |
| pass |
| |
| def sample_process(self, name, pid, target_host=None): |
| pass |
| |
| def should_run_as_pixel_test(self, test_input): |
| if not self._options.pixel_tests: |
| return False |
| if self._options.pixel_test_directories: |
| return any(test_input.test_name.startswith(directory) for directory in self._options.pixel_test_directories) |
| return self._should_run_as_pixel_test(test_input) |
| |
| def _should_run_as_pixel_test(self, test_input): |
| # Default behavior is to allow all test to run as pixel tests if --pixel-tests is on and |
| # --pixel-test-directory is not specified. |
| return True |
| |
| def _should_use_flatpak(self): |
| suffix = "" |
| if self.port_name: |
| suffix = self.port_name.upper() |
| return self._filesystem.exists(self.path_from_webkit_base('WebKitBuild', suffix, "FlatpakTree")) |
| |
| def _in_flatpak_sandbox(self): |
| return os.path.exists("/.flatpak-info") |
| |
| def _should_use_jhbuild(self): |
| if self._in_flatpak_sandbox(): |
| return False |
| |
| suffix = "" |
| if self.port_name: |
| suffix = self.port_name.upper() |
| return self._filesystem.exists(self.path_from_webkit_base('WebKitBuild', 'Dependencies%s' % suffix)) |
| |
| # FIXME: Eventually we should standarize port naming, and make this method smart enough |
| # to use for all port configurations (including architectures, graphics types, etc). |
| def _port_flag_for_scripts(self): |
| # This is overrriden by ports which need a flag passed to scripts to distinguish the use of that port. |
| return None |
| |
| # This is modeled after webkitdirs.pm argumentsForConfiguration() from old-run-webkit-tests |
| def _arguments_for_configuration(self): |
| config_args = [] |
| config_args.append(self._config.flag_for_configuration(self.get_option('configuration'))) |
| # FIXME: We may need to add support for passing --32-bit like old-run-webkit-tests had. |
| port_flag = self._port_flag_for_scripts() |
| if port_flag: |
| config_args.append(port_flag) |
| return config_args |
| |
| def _run_script(self, script_name, args=None, include_configuration_arguments=True, decode_output=True, env=None): |
| run_script_command = [self.path_to_script(script_name)] |
| if include_configuration_arguments: |
| run_script_command.extend(self._arguments_for_configuration()) |
| if args: |
| run_script_command.extend(args) |
| output = self._executive.run_command(run_script_command, cwd=self.webkit_base(), decode_output=decode_output, env=env) |
| _log.debug('Output of %s:\n%s' % (run_script_command, encode_for(output, str) if decode_output else output)) |
| return output |
| |
| def _build_driver(self): |
| environment = self.host.copy_current_environment() |
| env = environment.to_dictionary() |
| |
| # FIXME: We build both DumpRenderTree and WebKitTestRunner for WebKitTestRunner runs because |
| # DumpRenderTree includes TestNetscapePlugin. It should be factored out into its own project. |
| try: |
| self._run_script("build-dumprendertree", args=self._build_driver_flags(), env=env) |
| if self.get_option('webkit_test_runner'): |
| self._run_script("build-webkittestrunner", args=self._build_driver_flags(), env=env) |
| except ScriptError as e: |
| _log.error(e.message_with_output(output_limit=None)) |
| return False |
| return True |
| |
| def _build_api_tests(self, wtf_only=False): |
| environment = self.host.copy_current_environment().to_dictionary() |
| try: |
| self._run_script('build-api-tests', args=(['--wtf-only'] if wtf_only else []) + self._build_driver_flags(), env=environment) |
| except ScriptError as e: |
| _log.error(e.message_with_output(output_limit=None)) |
| return False |
| return True |
| |
| def _build_image_diff(self): |
| environment = self.host.copy_current_environment() |
| env = environment.to_dictionary() |
| try: |
| self._run_script("build-imagediff", env=env) |
| except ScriptError as e: |
| _log.error(e.message_with_output(output_limit=None)) |
| return False |
| return True |
| |
| def _build_driver_flags(self): |
| return [] |
| |
| def test_search_path(self, device_type=None): |
| return self.baseline_search_path(device_type=device_type) |
| |
| def _tests_for_other_platforms(self, device_type=None): |
| # By default we will skip any directory under LayoutTests/platform |
| # that isn't in our baseline search path (this mirrors what |
| # old-run-webkit-tests does in findTestsToRun()). |
| # Note this returns LayoutTests/platform/*, not platform/*/*. |
| entries = self._filesystem.glob(self._webkit_baseline_path('*')) |
| dirs_to_skip = [] |
| for entry in entries: |
| if self._filesystem.isdir(entry) and entry not in self.test_search_path(device_type=device_type): |
| basename = self._filesystem.basename(entry) |
| dirs_to_skip.append('platform/%s' % basename) |
| return dirs_to_skip |
| |
| def _skipped_tests_for_unsupported_features(self, test_list): |
| return [] |
| |
| def _wk2_port_name(self): |
| # By current convention, the WebKit2 name is always mac-wk2, win-wk2, not mac-leopard-wk2, etc, |
| return "%s-wk2" % self.port_name |
| |
| def logging_patterns_to_strip(self): |
| return [] |
| |
| def stderr_patterns_to_strip(self): |
| return [] |
| |
| def test_expectations_file_position(self): |
| # By default baseline search path schema is i.e. port-wk2 -> wk2 -> port -> generic, so port expectations file is at second to last position. |
| return 1 |
| |
| def did_spawn_worker(self, worker_number): |
| # This is overridden by ports that need to do work in the parent process after a worker subprocess is spawned, |
| # such as closing file descriptors that were implicitly cloned to the worker. |
| pass |
| |
| def configuration_for_upload(self, host=None): |
| from webkitpy.results.upload import Upload |
| |
| configuration = self.test_configuration() |
| host = self.host or host |
| |
| if self.get_option('guard_malloc'): |
| style = 'guard-malloc' |
| elif self._config.asan: |
| style = 'asan' |
| else: |
| style = configuration.build_type |
| |
| return Upload.create_configuration( |
| platform=host.platform.os_name, |
| version=str(host.platform.os_version), |
| version_name=host.platform.os_version_name(INTERNAL_TABLE) or host.platform.os_version_name(), |
| architecture=configuration.architecture, |
| style=style, |
| sdk=host.platform.build_version(), |
| flavor=self.get_option('result_report_flavor'), |
| ) |
| |
| @memoized |
| def commits_for_upload(self): |
| from webkitpy.results.upload import Upload |
| |
| repos = {} |
| if port_config.apple_additions() and getattr(port_config.apple_additions(), 'repos', False): |
| repos = port_config.apple_additions().repos() |
| |
| up = os.path.dirname |
| repos['webkit'] = up(up(up(up(up(os.path.abspath(__file__)))))) |
| |
| commits = [] |
| for repo_id, path in repos.items(): |
| scm = SCMDetector(self._filesystem, self._executive).detect_scm_system(path) |
| commits.append(Upload.create_commit( |
| repository_id=repo_id, |
| id=scm.native_revision(path), |
| branch=scm.native_branch(path), |
| )) |
| return commits |