diff options
author | bors-servo <lbergstrom+bors@mozilla.com> | 2016-10-18 08:15:11 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-10-18 08:15:11 -0500 |
commit | 80447a79c43c03cc13f041899b369c639ff621ce (patch) | |
tree | dc35b74800e40e052c68cb12c41b495623f72efa | |
parent | 89ced788ebb89b6cab82b15f4a1bab0b822e953c (diff) | |
parent | a9c2dda2997e18d691f0e6727cf65e52a1173f21 (diff) | |
download | servo-80447a79c43c03cc13f041899b369c639ff621ce.tar.gz servo-80447a79c43c03cc13f041899b369c639ff621ce.zip |
Auto merge of #13614 - jdm:wptrunnerup3, r=Ms2ger
Upgrade wptrunner
This gets us back to a pristine local copy and allows us to start experimenting with webdriver tests.
<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/13614)
<!-- Reviewable:end -->
24 files changed, 480 insertions, 142 deletions
diff --git a/python/servo/devenv_commands.py b/python/servo/devenv_commands.py index 8163340c501..8618041b756 100644 --- a/python/servo/devenv_commands.py +++ b/python/servo/devenv_commands.py @@ -170,25 +170,28 @@ class MachCommands(CommandBase): print(cargo_path) call(["cargo", "fetch"], env=self.build_env()) - @Command('wpt-upgrade', + @Command('wptrunner-upgrade', description='upgrade wptrunner.', category='devenv') def upgrade_wpt_runner(self): + env = self.build_env() with cd(path.join(self.context.topdir, 'tests', 'wpt', 'harness')): - code = call(["git", "init"], env=self.build_env()) + code = call(["git", "init"], env=env) if code: return code + # No need to report an error if this fails, as it will for the first use + call(["git", "remote", "rm", "upstream"], env=env) code = call( - ["git", "remote", "add", "upstream", "https://github.com/w3c/wptrunner.git"], env=self.build_env()) + ["git", "remote", "add", "upstream", "https://github.com/w3c/wptrunner.git"], env=env) if code: return code - code = call(["git", "fetch", "upstream"], env=self.build_env()) + code = call(["git", "fetch", "upstream"], env=env) if code: return code - code = call(["git", "reset", "--hard", "remotes/upstream/master"], env=self.build_env()) + code = call(["git", "reset", "--hard", "remotes/upstream/master"], env=env) if code: return code - code = call(["rm", "-rf", ".git"], env=self.build_env()) + code = call(["rm", "-rf", ".git"], env=env) if code: return code return 0 diff --git a/tests/wpt/harness/.travis.yml b/tests/wpt/harness/.travis.yml new file mode 100644 index 00000000000..add6efd12ca --- /dev/null +++ b/tests/wpt/harness/.travis.yml @@ -0,0 +1,20 @@ +language: python +python: 2.7 + +sudo: false + +cache: + directories: + - $HOME/.cache/pip + +env: + - TOXENV="{py27,pypy}-base" + - TOXENV="{py27,pypy}-chrome" + - TOXENV="{py27,pypy}-firefox" + - TOXENV="{py27,pypy}-servo" + +install: + - pip install -U tox + +script: + - tox diff --git a/tests/wpt/harness/tox.ini b/tests/wpt/harness/tox.ini new file mode 100644 index 00000000000..844a8d05b20 --- /dev/null +++ b/tests/wpt/harness/tox.ini @@ -0,0 +1,15 @@ +[pytest] +xfail_strict=true + +[tox] +envlist = {py27,pypy}-{base,b2g,chrome,firefox,servo} + +[testenv] +deps = + pytest>=2.9 + -r{toxinidir}/requirements.txt + chrome: -r{toxinidir}/requirements_chrome.txt + firefox: -r{toxinidir}/requirements_firefox.txt + servo: -r{toxinidir}/requirements_servo.txt + +commands = py.test [] diff --git a/tests/wpt/harness/wptrunner/browsers/__init__.py b/tests/wpt/harness/wptrunner/browsers/__init__.py index ffc5aedc830..8b34cc3963f 100644 --- a/tests/wpt/harness/wptrunner/browsers/__init__.py +++ b/tests/wpt/harness/wptrunner/browsers/__init__.py @@ -26,8 +26,8 @@ All classes and functions named in the above dict must be imported into the module global scope. """ -product_list = ["b2g", - "chrome", +product_list = ["chrome", + "edge", "firefox", "servo", "servodriver"] diff --git a/tests/wpt/harness/wptrunner/browsers/edge.py b/tests/wpt/harness/wptrunner/browsers/edge.py new file mode 100644 index 00000000000..7c993fce1ac --- /dev/null +++ b/tests/wpt/harness/wptrunner/browsers/edge.py @@ -0,0 +1,71 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from .base import Browser, ExecutorBrowser, require_arg +from ..webdriver_server import EdgeDriverServer +from ..executors import executor_kwargs as base_executor_kwargs +from ..executors.executorselenium import (SeleniumTestharnessExecutor, + SeleniumRefTestExecutor) + +__wptrunner__ = {"product": "edge", + "check_args": "check_args", + "browser": "EdgeBrowser", + "executor": {"testharness": "SeleniumTestharnessExecutor", + "reftest": "SeleniumRefTestExecutor"}, + "browser_kwargs": "browser_kwargs", + "executor_kwargs": "executor_kwargs", + "env_options": "env_options"} + + +def check_args(**kwargs): + require_arg(kwargs, "webdriver_binary") + +def browser_kwargs(**kwargs): + return {"webdriver_binary": kwargs["webdriver_binary"]} + +def executor_kwargs(test_type, server_config, cache_manager, run_info_data, + **kwargs): + from selenium.webdriver import DesiredCapabilities + + executor_kwargs = base_executor_kwargs(test_type, server_config, + cache_manager, **kwargs) + executor_kwargs["close_after_done"] = True + executor_kwargs["capabilities"] = dict(DesiredCapabilities.EDGE.items()) + return executor_kwargs + +def env_options(): + return {"host": "web-platform.test", + "bind_hostname": "true", + "supports_debugger": False} + +class EdgeBrowser(Browser): + used_ports = set() + + def __init__(self, logger, webdriver_binary): + Browser.__init__(self, logger) + self.server = EdgeDriverServer(self.logger, binary=webdriver_binary) + self.webdriver_host = "localhost" + self.webdriver_port = self.server.port + + def start(self): + print self.server.url + self.server.start() + + def stop(self): + self.server.stop() + + def pid(self): + return self.server.pid + + def is_alive(self): + # TODO(ato): This only indicates the server is alive, + # and doesn't say anything about whether a browser session + # is active. + return self.server.is_alive() + + def cleanup(self): + self.stop() + + def executor_browser(self): + return ExecutorBrowser, {"webdriver_url": self.server.url} diff --git a/tests/wpt/harness/wptrunner/browsers/firefox.py b/tests/wpt/harness/wptrunner/browsers/firefox.py index dcd7a4f1d17..a3c6f3f5753 100644 --- a/tests/wpt/harness/wptrunner/browsers/firefox.py +++ b/tests/wpt/harness/wptrunner/browsers/firefox.py @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. import os +import platform import subprocess import sys @@ -128,10 +129,16 @@ class FirefoxBrowser(Browser): self.profile.set_preferences({"marionette.defaultPrefs.enabled": True, "marionette.defaultPrefs.port": self.marionette_port, "dom.disable_open_during_load": False, - "network.dns.localDomains": ",".join(hostnames)}) + "network.dns.localDomains": ",".join(hostnames), + "places.history.enabled": False}) if self.e10s: self.profile.set_preferences({"browser.tabs.remote.autostart": True}) + # Bug 1262954: winxp + e10s, disable hwaccel + if (self.e10s and platform.system() in ("Windows", "Microsoft") and + '5.1' in platform.version()): + self.profile.set_preferences({"layers.acceleration.disabled": True}) + if self.ca_certificate_path is not None: self.setup_ssl() diff --git a/tests/wpt/harness/wptrunner/browsers/servo.py b/tests/wpt/harness/wptrunner/browsers/servo.py index 1ea017377fe..2eeb5aaa158 100644 --- a/tests/wpt/harness/wptrunner/browsers/servo.py +++ b/tests/wpt/harness/wptrunner/browsers/servo.py @@ -6,7 +6,7 @@ import os from .base import NullBrowser, ExecutorBrowser, require_arg from ..executors import executor_kwargs as base_executor_kwargs -from ..executors.executorservo import ServoTestharnessExecutor, ServoRefTestExecutor +from ..executors.executorservo import ServoTestharnessExecutor, ServoRefTestExecutor, ServoWdspecExecutor here = os.path.join(os.path.split(__file__)[0]) @@ -14,7 +14,8 @@ __wptrunner__ = {"product": "servo", "check_args": "check_args", "browser": "ServoBrowser", "executor": {"testharness": "ServoTestharnessExecutor", - "reftest": "ServoRefTestExecutor"}, + "reftest": "ServoRefTestExecutor", + "wdspec": "ServoWdspecExecutor"}, "browser_kwargs": "browser_kwargs", "executor_kwargs": "executor_kwargs", "env_options": "env_options", diff --git a/tests/wpt/harness/wptrunner/executors/executormarionette.py b/tests/wpt/harness/wptrunner/executors/executormarionette.py index c4b1cba689a..8495d3b45ee 100644 --- a/tests/wpt/harness/wptrunner/executors/executormarionette.py +++ b/tests/wpt/harness/wptrunner/executors/executormarionette.py @@ -17,11 +17,11 @@ from ..wpttest import WdspecResult, WdspecSubtestResult errors = None marionette = None +pytestrunner = None webdriver = None here = os.path.join(os.path.split(__file__)[0]) -from . import pytestrunner from .base import (ExecutorException, Protocol, RefTestExecutor, @@ -41,7 +41,7 @@ extra_timeout = 5 # seconds def do_delayed_imports(): - global errors, marionette, webdriver + global errors, marionette # Marionette client used to be called marionette, recently it changed # to marionette_driver for unfathomable reasons @@ -51,8 +51,6 @@ def do_delayed_imports(): except ImportError: from marionette_driver import marionette, errors - import webdriver - class MarionetteProtocol(Protocol): def __init__(self, executor, browser): @@ -292,7 +290,7 @@ class RemoteMarionetteProtocol(Protocol): class ExecuteAsyncScriptRun(object): def __init__(self, logger, func, marionette, url, timeout): self.logger = logger - self.result = None + self.result = (None, None) self.marionette = marionette self.func = func self.url = url @@ -323,11 +321,9 @@ class ExecuteAsyncScriptRun(object): wait_timeout = None flag = self.result_flag.wait(wait_timeout) - if self.result is None: + if self.result[1] is None: self.logger.debug("Timed out waiting for a result") - assert not flag self.result = False, ("EXTERNAL-TIMEOUT", None) - return self.result def _run(self): @@ -409,7 +405,8 @@ class MarionetteTestharnessExecutor(TestharnessExecutor): "timeout": timeout_ms, "explicit_timeout": timeout is None} - return marionette.execute_async_script(script, new_sandbox=False) + rv = marionette.execute_async_script(script, new_sandbox=False) + return rv class MarionetteRefTestExecutor(RefTestExecutor): @@ -487,7 +484,7 @@ class MarionetteRefTestExecutor(RefTestExecutor): class WdspecRun(object): def __init__(self, func, session, path, timeout): self.func = func - self.result = None + self.result = (None, None) self.session = session self.path = path self.timeout = timeout @@ -504,8 +501,7 @@ class WdspecRun(object): executor.start() flag = self.result_flag.wait(self.timeout) - if self.result is None: - assert not flag + if self.result[1] is None: self.result = False, ("EXTERNAL-TIMEOUT", None) return self.result @@ -528,6 +524,7 @@ class WdspecRun(object): class MarionetteWdspecExecutor(WdspecExecutor): def __init__(self, browser, server_config, webdriver_binary, timeout_multiplier=1, close_after_done=True, debug_info=None): + self.do_delayed_imports() WdspecExecutor.__init__(self, browser, server_config, timeout_multiplier=timeout_multiplier, debug_info=debug_info) @@ -557,3 +554,8 @@ class MarionetteWdspecExecutor(WdspecExecutor): harness_result = ("OK", None) subtest_results = pytestrunner.run(path, session, timeout=timeout) return (harness_result, subtest_results) + + def do_delayed_imports(self): + global pytestrunner, webdriver + from . import pytestrunner + import webdriver diff --git a/tests/wpt/harness/wptrunner/executors/executorselenium.py b/tests/wpt/harness/wptrunner/executors/executorselenium.py index 587c49c7c92..f5d65f499b0 100644 --- a/tests/wpt/harness/wptrunner/executors/executorselenium.py +++ b/tests/wpt/harness/wptrunner/executors/executorselenium.py @@ -22,20 +22,21 @@ from .base import (ExecutorException, strip_server) from ..testrunner import Stop - here = os.path.join(os.path.split(__file__)[0]) webdriver = None exceptions = None +RemoteConnection = None extra_timeout = 5 def do_delayed_imports(): global webdriver global exceptions + global RemoteConnection from selenium import webdriver from selenium.common import exceptions - + from selenium.webdriver.remote.remote_connection import RemoteConnection class SeleniumProtocol(Protocol): def __init__(self, executor, browser, capabilities, **kwargs): @@ -53,8 +54,9 @@ class SeleniumProtocol(Protocol): session_started = False try: - self.webdriver = webdriver.Remote( - self.url, desired_capabilities=self.capabilities) + self.webdriver = webdriver.Remote(command_executor=RemoteConnection(self.url.strip("/"), + resolve_ip=False), + desired_capabilities=self.capabilities) except: self.logger.warning( "Connecting to Selenium failed:\n%s" % traceback.format_exc()) @@ -231,17 +233,7 @@ class SeleniumRefTestExecutor(RefTestExecutor): def do_test(self, test): self.logger.info("Test requires OS-level window focus") - if self.close_after_done and self.has_window: - self.protocol.webdriver.close() - self.protocol.webdriver.switch_to_window( - self.protocol.webdriver.window_handles[-1]) - self.has_window = False - - if not self.has_window: - self.protocol.webdriver.execute_script(self.script) - self.protocol.webdriver.switch_to_window( - self.protocol.webdriver.window_handles[-1]) - self.has_window = True + self.protocol.webdriver.set_window_size(600, 600) result = self.implementation.run_test(test) diff --git a/tests/wpt/harness/wptrunner/executors/executorservo.py b/tests/wpt/harness/wptrunner/executors/executorservo.py index 15092056a93..b627223a7df 100644 --- a/tests/wpt/harness/wptrunner/executors/executorservo.py +++ b/tests/wpt/harness/wptrunner/executors/executorservo.py @@ -4,11 +4,13 @@ import base64 import hashlib +import httplib import json import os import subprocess import tempfile import threading +import traceback import urlparse import uuid from collections import defaultdict @@ -19,11 +21,19 @@ from .base import (ExecutorException, Protocol, RefTestImplementation, testharness_result_converter, - reftest_result_converter) + reftest_result_converter, + WdspecExecutor) from .process import ProcessTestExecutor from ..browsers.base import browser_command +from ..wpttest import WdspecResult, WdspecSubtestResult +from ..webdriver_server import ServoDriverServer +from .executormarionette import WdspecRun + +pytestrunner = None render_arg = None +webdriver = None +extra_timeout = 5 # seconds def do_delayed_imports(): global render_arg @@ -205,7 +215,7 @@ class ServoRefTestExecutor(ProcessTestExecutor): self.binary, [render_arg(self.browser.render_backend), "--hard-fail", "--exit", "-u", "Servo/wptrunner", "-Z", "disable-text-aa,load-webfonts-synchronously,replace-surrogates", - "--output=%s" % output_path, full_url], + "--output=%s" % output_path, full_url] + self.browser.binary_args, self.debug_info) for stylesheet in self.browser.user_stylesheets: @@ -214,10 +224,7 @@ class ServoRefTestExecutor(ProcessTestExecutor): for pref, value in test.environment.get('prefs', {}).iteritems(): command += ["--pref", "%s=%s" % (pref, value)] - if viewport_size: - command += ["--resolution", viewport_size] - else: - command += ["--resolution", "800x600"] + command += ["--resolution", viewport_size or "800x600"] if dpi: command += ["--device-pixel-ratio", dpi] @@ -278,3 +285,83 @@ class ServoRefTestExecutor(ProcessTestExecutor): self.logger.process_output(self.proc.pid, line, " ".join(self.command)) + +class ServoWdspecProtocol(Protocol): + def __init__(self, executor, browser): + self.do_delayed_imports() + Protocol.__init__(self, executor, browser) + self.session = None + self.server = None + + def setup(self, runner): + try: + self.server = ServoDriverServer(self.logger, binary=self.browser.binary, binary_args=self.browser.binary_args, render_backend=self.browser.render_backend) + self.server.start(block=False) + self.logger.info( + "WebDriver HTTP server listening at %s" % self.server.url) + + self.logger.info( + "Establishing new WebDriver session with %s" % self.server.url) + self.session = webdriver.Session( + self.server.host, self.server.port, self.server.base_path) + except Exception: + self.logger.error(traceback.format_exc()) + self.executor.runner.send_message("init_failed") + else: + self.executor.runner.send_message("init_succeeded") + + def teardown(self): + if self.server is not None: + try: + if self.session.session_id is not None: + self.session.end() + except Exception: + pass + if self.server.is_alive: + self.server.stop() + + @property + def is_alive(self): + conn = httplib.HTTPConnection(self.server.host, self.server.port) + conn.request("HEAD", self.server.base_path + "invalid") + res = conn.getresponse() + return res.status == 404 + + def do_delayed_imports(self): + global pytestrunner, webdriver + from . import pytestrunner + import webdriver + + +class ServoWdspecExecutor(WdspecExecutor): + def __init__(self, browser, server_config, + timeout_multiplier=1, close_after_done=True, debug_info=None, + **kwargs): + WdspecExecutor.__init__(self, browser, server_config, + timeout_multiplier=timeout_multiplier, + debug_info=debug_info) + self.protocol = ServoWdspecProtocol(self, browser) + + def is_alive(self): + return self.protocol.is_alive + + def on_environment_change(self, new_environment): + pass + + def do_test(self, test): + timeout = test.timeout * self.timeout_multiplier + extra_timeout + + success, data = WdspecRun(self.do_wdspec, + self.protocol.session, + test.path, + timeout).run() + + if success: + return self.convert_result(test, data) + + return (test.result_cls(*data), []) + + def do_wdspec(self, session, path, timeout): + harness_result = ("OK", None) + subtest_results = pytestrunner.run(path, session, timeout=timeout) + return (harness_result, subtest_results) diff --git a/tests/wpt/harness/wptrunner/executors/executorservodriver.py b/tests/wpt/harness/wptrunner/executors/executorservodriver.py index fceeb58fad2..279658d0cd1 100644 --- a/tests/wpt/harness/wptrunner/executors/executorservodriver.py +++ b/tests/wpt/harness/wptrunner/executors/executorservodriver.py @@ -14,7 +14,6 @@ from .base import (Protocol, RefTestImplementation, TestharnessExecutor, strip_server) -from .. import webdriver from ..testrunner import Stop webdriver = None @@ -26,7 +25,7 @@ extra_timeout = 5 def do_delayed_imports(): global webdriver - import webdriver + from tools import webdriver class ServoWebDriverProtocol(Protocol): diff --git a/tests/wpt/harness/wptrunner/executors/pytestrunner/fixtures.py b/tests/wpt/harness/wptrunner/executors/pytestrunner/fixtures.py index 77afb4a3684..81796d69883 100644 --- a/tests/wpt/harness/wptrunner/executors/pytestrunner/fixtures.py +++ b/tests/wpt/harness/wptrunner/executors/pytestrunner/fixtures.py @@ -3,6 +3,10 @@ # You can obtain one at http://mozilla.org/MPL/2.0/. import pytest +import webdriver + +import contextlib +import httplib """pytest fixtures for use in Python-based WPT tests. @@ -17,7 +21,7 @@ class Session(object): in tests. The session is not created by default to enable testing of session - creation. However, a module-scoped session will be implicitly created + creation. However, a function-scoped session will be implicitly created at the first call to a WebDriver command. This means methods such as `session.send_command` and `session.session_id` are possible to use without having a session. @@ -45,14 +49,88 @@ class Session(object): def test_something(setup, session): assert session.url == "https://example.org" - The session is closed when the test module goes out of scope by an - implicit call to `session.end`. + When the test function goes out of scope, any remaining user prompts + and opened windows are closed, and the current browsing context is + switched back to the top-level browsing context. """ def __init__(self, client): self.client = client - @pytest.fixture(scope="module") + @pytest.fixture(scope="function") def session(self, request): - request.addfinalizer(self.client.end) + # finalisers are popped off a stack, + # making their ordering reverse + request.addfinalizer(self.switch_to_top_level_browsing_context) + request.addfinalizer(self.restore_windows) + request.addfinalizer(self.dismiss_user_prompts) + return self.client + + def dismiss_user_prompts(self): + """Dismisses any open user prompts in windows.""" + current_window = self.client.window_handle + + for window in self.windows(): + self.client.window_handle = window + try: + self.client.alert.dismiss() + except webdriver.NoSuchAlertException: + pass + + self.client.window_handle = current_window + + def restore_windows(self): + """Closes superfluous windows opened by the test without ending + the session implicitly by closing the last window. + """ + current_window = self.client.window_handle + + for window in self.windows(exclude=[current_window]): + self.client.window_handle = window + if len(self.client.window_handles) > 1: + self.client.close() + + self.client.window_handle = current_window + + def switch_to_top_level_browsing_context(self): + """If the current browsing context selected by WebDriver is a + `<frame>` or an `<iframe>`, switch it back to the top-level + browsing context. + """ + self.client.switch_frame(None) + + def windows(self, exclude=None): + """Set of window handles, filtered by an `exclude` list if + provided. + """ + if exclude is None: + exclude = [] + wins = [w for w in self.client.handles if w not in exclude] + return set(wins) + + +class HTTPRequest(object): + def __init__(self, host, port): + self.host = host + self.port = port + + def head(self, path): + return self._request("HEAD", path) + + def get(self, path): + return self._request("GET", path) + + @contextlib.contextmanager + def _request(self, method, path): + conn = httplib.HTTPConnection(self.host, self.port) + try: + conn.request(method, path) + yield conn.getresponse() + finally: + conn.close() + + +@pytest.fixture(scope="module") +def http(session): + return HTTPRequest(session.transport.host, session.transport.port) diff --git a/tests/wpt/harness/wptrunner/executors/pytestrunner/runner.py b/tests/wpt/harness/wptrunner/executors/pytestrunner/runner.py index 8aa575ff8b7..4f2ead1b6d9 100644 --- a/tests/wpt/harness/wptrunner/executors/pytestrunner/runner.py +++ b/tests/wpt/harness/wptrunner/executors/pytestrunner/runner.py @@ -45,6 +45,7 @@ def run(path, session, timeout=0): recorder = SubtestResultRecorder() plugins = [recorder, + fixtures, fixtures.Session(session)] # TODO(ato): Deal with timeouts diff --git a/tests/wpt/harness/wptrunner/executors/testharness_webdriver.js b/tests/wpt/harness/wptrunner/executors/testharness_webdriver.js index 7ca8d573703..f78f9aef05b 100644 --- a/tests/wpt/harness/wptrunner/executors/testharness_webdriver.js +++ b/tests/wpt/harness/wptrunner/executors/testharness_webdriver.js @@ -5,14 +5,18 @@ var callback = arguments[arguments.length - 1]; window.timeout_multiplier = %(timeout_multiplier)d; -window.addEventListener("message", function(event) { - var tests = event.data[0]; - var status = event.data[1]; +window.addEventListener("message", function f(event) { + if (event.data.type != "complete") { + return; + } + window.removeEventListener("message", f); + + var tests = event.data.tests; + var status = event.data.status; var subtest_results = tests.map(function(x) { - return [x.name, x.status, x.message, x.stack] + return [x.name, x.status, x.message, x.stack] }); - clearTimeout(timer); callback(["%(url)s", status.status, diff --git a/tests/wpt/harness/wptrunner/testloader.py b/tests/wpt/harness/wptrunner/testloader.py index 29a07c45522..e44e11106a3 100644 --- a/tests/wpt/harness/wptrunner/testloader.py +++ b/tests/wpt/harness/wptrunner/testloader.py @@ -27,6 +27,7 @@ class TestChunker(object): self.chunk_number = chunk_number assert self.chunk_number <= self.total_chunks self.logger = structured.get_default_logger() + assert self.logger def __call__(self, manifest): raise NotImplementedError diff --git a/tests/wpt/harness/wptrunner/testrunner.py b/tests/wpt/harness/wptrunner/testrunner.py index 77d2a885083..38cc4220310 100644 --- a/tests/wpt/harness/wptrunner/testrunner.py +++ b/tests/wpt/harness/wptrunner/testrunner.py @@ -168,7 +168,7 @@ class TestRunnerManager(threading.Thread): def __init__(self, suite_name, test_queue, test_source_cls, browser_cls, browser_kwargs, executor_cls, executor_kwargs, stop_flag, pause_after_test=False, - pause_on_unexpected=False, debug_info=None): + pause_on_unexpected=False, restart_on_unexpected=True, debug_info=None): """Thread that owns a single TestRunner process and any processes required by the TestRunner (e.g. the Firefox binary). @@ -207,6 +207,7 @@ class TestRunnerManager(threading.Thread): self.pause_after_test = pause_after_test self.pause_on_unexpected = pause_on_unexpected + self.restart_on_unexpected = restart_on_unexpected self.debug_info = debug_info self.manager_number = next_manager_number() @@ -526,7 +527,8 @@ class TestRunnerManager(threading.Thread): restart_before_next = (test.restart_after or file_result.status in ("CRASH", "EXTERNAL-TIMEOUT") or - subtest_unexpected or is_unexpected) + ((subtest_unexpected or is_unexpected) + and self.restart_on_unexpected)) if (self.pause_after_test or (self.pause_on_unexpected and (subtest_unexpected or is_unexpected))): @@ -593,6 +595,7 @@ class ManagerGroup(object): executor_cls, executor_kwargs, pause_after_test=False, pause_on_unexpected=False, + restart_on_unexpected=True, debug_info=None): """Main thread object that owns all the TestManager threads.""" self.suite_name = suite_name @@ -605,6 +608,7 @@ class ManagerGroup(object): self.executor_kwargs = executor_kwargs self.pause_after_test = pause_after_test self.pause_on_unexpected = pause_on_unexpected + self.restart_on_unexpected = restart_on_unexpected self.debug_info = debug_info self.pool = set() @@ -643,6 +647,7 @@ class ManagerGroup(object): self.stop_flag, self.pause_after_test, self.pause_on_unexpected, + self.restart_on_unexpected, self.debug_info) manager.start() self.pool.add(manager) diff --git a/tests/wpt/harness/wptrunner/tests/test_chunker.py b/tests/wpt/harness/wptrunner/tests/test_chunker.py index d46ad258783..60b34b804a2 100644 --- a/tests/wpt/harness/wptrunner/tests/test_chunker.py +++ b/tests/wpt/harness/wptrunner/tests/test_chunker.py @@ -4,9 +4,16 @@ import unittest import sys -sys.path.insert(0, "..") +from os.path import join, dirname +from mozlog import structured -from wptrunner import wptrunner +import pytest + +sys.path.insert(0, join(dirname(__file__), "..", "..")) + +from wptrunner.testloader import EqualTimeChunker + +structured.set_default_logger(structured.structuredlog.StructuredLogger("TestChunker")) class MockTest(object): def __init__(self, id, timeout=10): @@ -28,9 +35,9 @@ class TestEqualTimeChunker(unittest.TestCase): def test_include_all(self): tests = make_mock_manifest(("a", 10), ("a/b", 10), ("c", 10)) - chunk_1 = list(wptrunner.EqualTimeChunker(3, 1)(tests)) - chunk_2 = list(wptrunner.EqualTimeChunker(3, 2)(tests)) - chunk_3 = list(wptrunner.EqualTimeChunker(3, 3)(tests)) + chunk_1 = list(EqualTimeChunker(3, 1)(tests)) + chunk_2 = list(EqualTimeChunker(3, 2)(tests)) + chunk_3 = list(EqualTimeChunker(3, 3)(tests)) self.assertEquals(tests[:10], chunk_1) self.assertEquals(tests[10:20], chunk_2) @@ -39,9 +46,9 @@ class TestEqualTimeChunker(unittest.TestCase): def test_include_all_1(self): tests = make_mock_manifest(("a", 5), ("a/b", 5), ("c", 10), ("d", 10)) - chunk_1 = list(wptrunner.EqualTimeChunker(3, 1)(tests)) - chunk_2 = list(wptrunner.EqualTimeChunker(3, 2)(tests)) - chunk_3 = list(wptrunner.EqualTimeChunker(3, 3)(tests)) + chunk_1 = list(EqualTimeChunker(3, 1)(tests)) + chunk_2 = list(EqualTimeChunker(3, 2)(tests)) + chunk_3 = list(EqualTimeChunker(3, 3)(tests)) self.assertEquals(tests[:10], chunk_1) self.assertEquals(tests[10:20], chunk_2) @@ -50,9 +57,9 @@ class TestEqualTimeChunker(unittest.TestCase): def test_long(self): tests = make_mock_manifest(("a", 100), ("a/b", 1), ("c", 1)) - chunk_1 = list(wptrunner.EqualTimeChunker(3, 1)(tests)) - chunk_2 = list(wptrunner.EqualTimeChunker(3, 2)(tests)) - chunk_3 = list(wptrunner.EqualTimeChunker(3, 3)(tests)) + chunk_1 = list(EqualTimeChunker(3, 1)(tests)) + chunk_2 = list(EqualTimeChunker(3, 2)(tests)) + chunk_3 = list(EqualTimeChunker(3, 3)(tests)) self.assertEquals(tests[:100], chunk_1) self.assertEquals(tests[100:101], chunk_2) @@ -61,9 +68,9 @@ class TestEqualTimeChunker(unittest.TestCase): def test_long_1(self): tests = make_mock_manifest(("a", 1), ("a/b", 100), ("c", 1)) - chunk_1 = list(wptrunner.EqualTimeChunker(3, 1)(tests)) - chunk_2 = list(wptrunner.EqualTimeChunker(3, 2)(tests)) - chunk_3 = list(wptrunner.EqualTimeChunker(3, 3)(tests)) + chunk_1 = list(EqualTimeChunker(3, 1)(tests)) + chunk_2 = list(EqualTimeChunker(3, 2)(tests)) + chunk_3 = list(EqualTimeChunker(3, 3)(tests)) self.assertEquals(tests[:1], chunk_1) self.assertEquals(tests[1:101], chunk_2) @@ -72,7 +79,7 @@ class TestEqualTimeChunker(unittest.TestCase): def test_too_few_dirs(self): with self.assertRaises(ValueError): tests = make_mock_manifest(("a", 1), ("a/b", 100), ("c", 1)) - list(wptrunner.EqualTimeChunker(4, 1)(tests)) + list(EqualTimeChunker(4, 1)(tests)) if __name__ == "__main__": diff --git a/tests/wpt/harness/wptrunner/tests/test_hosts.py b/tests/wpt/harness/wptrunner/tests/test_hosts.py index 4f122640d58..3f9006273df 100644 --- a/tests/wpt/harness/wptrunner/tests/test_hosts.py +++ b/tests/wpt/harness/wptrunner/tests/test_hosts.py @@ -4,11 +4,12 @@ import unittest import sys +from os.path import join, dirname from cStringIO import StringIO -sys.path.insert(0, "..") +sys.path.insert(0, join(dirname(__file__), "..", "..")) -import hosts +from wptrunner import hosts class HostsTest(unittest.TestCase): diff --git a/tests/wpt/harness/wptrunner/tests/test_update.py b/tests/wpt/harness/wptrunner/tests/test_update.py index 2d77b326d61..50878ac56dc 100644 --- a/tests/wpt/harness/wptrunner/tests/test_update.py +++ b/tests/wpt/harness/wptrunner/tests/test_update.py @@ -5,6 +5,8 @@ import unittest import StringIO +import pytest + from .. import metadata, manifestupdate from mozlog import structuredlog, handlers, formatters @@ -51,6 +53,7 @@ class TestExpectedUpdater(unittest.TestCase): subtest.coalesce_expected() test.coalesce_expected() + @pytest.mark.xfail def test_update_0(self): prev_data = [("path/to/test.htm.ini", ["/path/to/test.htm"], """[test.htm] type: testharness @@ -71,6 +74,7 @@ class TestExpectedUpdater(unittest.TestCase): self.coalesce_results([new_manifest]) self.assertTrue(new_manifest.is_empty) + @pytest.mark.xfail def test_update_1(self): test_id = "/path/to/test.htm" prev_data = [("path/to/test.htm.ini", [test_id], """[test.htm] @@ -93,6 +97,7 @@ class TestExpectedUpdater(unittest.TestCase): self.assertFalse(new_manifest.is_empty) self.assertEquals(new_manifest.get_test(test_id).children[0].get("expected"), "FAIL") + @pytest.mark.xfail def test_new_subtest(self): test_id = "/path/to/test.htm" prev_data = [("path/to/test.htm.ini", [test_id], """[test.htm] @@ -120,6 +125,7 @@ class TestExpectedUpdater(unittest.TestCase): self.assertEquals(new_manifest.get_test(test_id).children[0].get("expected"), "FAIL") self.assertEquals(new_manifest.get_test(test_id).children[1].get("expected"), "FAIL") + @pytest.mark.xfail def test_update_multiple_0(self): test_id = "/path/to/test.htm" prev_data = [("path/to/test.htm.ini", [test_id], """[test.htm] @@ -159,6 +165,7 @@ class TestExpectedUpdater(unittest.TestCase): self.assertEquals(new_manifest.get_test(test_id).children[0].get( "expected", {"debug": False, "os": "linux"}), "TIMEOUT") + @pytest.mark.xfail def test_update_multiple_1(self): test_id = "/path/to/test.htm" prev_data = [("path/to/test.htm.ini", [test_id], """[test.htm] @@ -200,6 +207,7 @@ class TestExpectedUpdater(unittest.TestCase): self.assertEquals(new_manifest.get_test(test_id).children[0].get( "expected", {"debug": False, "os": "windows"}), "FAIL") + @pytest.mark.xfail def test_update_multiple_2(self): test_id = "/path/to/test.htm" prev_data = [("path/to/test.htm.ini", [test_id], """[test.htm] @@ -239,6 +247,7 @@ class TestExpectedUpdater(unittest.TestCase): self.assertEquals(new_manifest.get_test(test_id).children[0].get( "expected", {"debug": True, "os": "osx"}), "TIMEOUT") + @pytest.mark.xfail def test_update_multiple_3(self): test_id = "/path/to/test.htm" prev_data = [("path/to/test.htm.ini", [test_id], """[test.htm] @@ -280,6 +289,7 @@ class TestExpectedUpdater(unittest.TestCase): self.assertEquals(new_manifest.get_test(test_id).children[0].get( "expected", {"debug": True, "os": "osx"}), "TIMEOUT") + @pytest.mark.xfail def test_update_ignore_existing(self): test_id = "/path/to/test.htm" prev_data = [("path/to/test.htm.ini", [test_id], """[test.htm] diff --git a/tests/wpt/harness/wptrunner/webdriver_server.py b/tests/wpt/harness/wptrunner/webdriver_server.py index 68d3fb7a3bf..3b2b095a4a5 100644 --- a/tests/wpt/harness/wptrunner/webdriver_server.py +++ b/tests/wpt/harness/wptrunner/webdriver_server.py @@ -16,7 +16,8 @@ import mozprocess __all__ = ["SeleniumServer", "ChromeDriverServer", - "GeckoDriverServer", "WebDriverServer"] + "GeckoDriverServer", "ServoDriverServer", + "WebDriverServer"] class WebDriverServer(object): @@ -44,7 +45,7 @@ class WebDriverServer(object): def make_command(self): """Returns the full command for starting the server process as a list.""" - def start(self, block=True): + def start(self, block=False): try: self._run(block) except KeyboardInterrupt: @@ -86,9 +87,7 @@ class WebDriverServer(object): @property def is_alive(self): - return (self._proc is not None and - self._proc.proc is not None and - self._proc.poll() is None) + return hasattr(self._proc, "proc") and self._proc.poll() is None def on_output(self, line): self.logger.process_output(self.pid, @@ -138,6 +137,17 @@ class ChromeDriverServer(WebDriverServer): cmd_arg("url-base", self.base_path) if self.base_path else ""] +class EdgeDriverServer(WebDriverServer): + def __init__(self, logger, binary="MicrosoftWebDriver.exe", port=None, + base_path="", host="localhost"): + WebDriverServer.__init__( + self, logger, binary, host=host, port=port) + + def make_command(self): + return [self.binary, + "--port=%s" % str(self.port)] + + class GeckoDriverServer(WebDriverServer): def __init__(self, logger, marionette_port=2828, binary="wires", host="127.0.0.1", port=None): @@ -150,8 +160,30 @@ class GeckoDriverServer(WebDriverServer): return [self.binary, "--connect-existing", "--marionette-port", str(self.marionette_port), - "--webdriver-host", self.host, - "--webdriver-port", str(self.port)] + "--host", self.host, + "--port", str(self.port)] + + +class ServoDriverServer(WebDriverServer): + def __init__(self, logger, binary="servo", binary_args=None, host="127.0.0.1", port=None, render_backend=None): + env = os.environ.copy() + env["RUST_BACKTRACE"] = "1" + WebDriverServer.__init__(self, logger, binary, host=host, port=port, env=env) + self.binary_args = binary_args + self.render_backend = render_backend + + def make_command(self): + command = [self.binary, + "--webdriver", str(self.port), + "--hard-fail", + "--headless"] + if self.binary_args: + command += self.binary_args + if self.render_backend == "cpu": + command += ["--cpu"] + elif self.render_backend == "webrender": + command += ["--webrender"] + return command def cmd_arg(name, value=None): diff --git a/tests/wpt/harness/wptrunner/wptcommandline.py b/tests/wpt/harness/wptrunner/wptcommandline.py index c068a69d4f6..38e7b8f5363 100644 --- a/tests/wpt/harness/wptrunner/wptcommandline.py +++ b/tests/wpt/harness/wptrunner/wptcommandline.py @@ -45,61 +45,33 @@ def create_parser(product_choices=None): config_data = config.load() product_choices = products.products_enabled(config_data) - parser = argparse.ArgumentParser(description="Runner for web-platform-tests tests.") - parser.add_argument("--metadata", action="store", type=abs_path, dest="metadata_root", - help="Path to the folder containing test metadata"), - parser.add_argument("--tests", action="store", type=abs_path, dest="tests_root", - help="Path to test files"), - parser.add_argument("--run-info", action="store", type=abs_path, - help="Path to directory containing extra json files to add to run info") - parser.add_argument("--config", action="store", type=abs_path, dest="config", - help="Path to config file") + parser = argparse.ArgumentParser(description="""Runner for web-platform-tests tests.""", + usage="""%(prog)s [OPTION]... [TEST]... +TEST is either the full path to a test file to run, or the URL of a test excluding +scheme host and port.""") parser.add_argument("--manifest-update", action="store_true", default=False, - help="Force regeneration of the test manifest") - - parser.add_argument("--binary", action="store", - type=abs_path, help="Binary to run tests against") - parser.add_argument('--binary-arg', - default=[], action="append", dest="binary_args", - help="Extra argument for the binary (servo)") - parser.add_argument("--webdriver-binary", action="store", metavar="BINARY", - type=abs_path, help="WebDriver server binary to use") - parser.add_argument("--processes", action="store", type=int, default=None, - help="Number of simultaneous processes to use") + help="Regenerate the test manifest.") + parser.add_argument("--timeout-multiplier", action="store", type=float, default=None, + help="Multiplier relative to standard test timeout to use") parser.add_argument("--run-by-dir", type=int, nargs="?", default=False, help="Split run into groups by directories. With a parameter," "limit the depth of splits e.g. --run-by-dir=1 to split by top-level" "directory") - - parser.add_argument("--timeout-multiplier", action="store", type=float, default=None, - help="Multiplier relative to standard test timeout to use") - parser.add_argument("--repeat", action="store", type=int, default=1, - help="Number of times to run the tests") - parser.add_argument("--repeat-until-unexpected", action="store_true", default=None, - help="Run tests in a loop until one returns an unexpected result") + parser.add_argument("--processes", action="store", type=int, default=None, + help="Number of simultaneous processes to use") parser.add_argument("--no-capture-stdio", action="store_true", default=False, help="Don't capture stdio and write to logging") - parser.add_argument("--product", action="store", choices=product_choices, - default=None, help="Browser against which to run tests") - - parser.add_argument("--list-test-groups", action="store_true", - default=False, - help="List the top level directories containing tests that will run.") - parser.add_argument("--list-disabled", action="store_true", - default=False, - help="List the tests that are disabled on the current platform") - - build_type = parser.add_mutually_exclusive_group() - build_type.add_argument("--debug-build", dest="debug", action="store_true", - default=None, - help="Build is a debug build (overrides any mozinfo file)") - build_type.add_argument("--release-build", dest="debug", action="store_false", - default=None, - help="Build is a release (overrides any mozinfo file)") + mode_group = parser.add_argument_group("Mode") + mode_group.add_argument("--list-test-groups", action="store_true", + default=False, + help="List the top level directories containing tests that will run.") + mode_group.add_argument("--list-disabled", action="store_true", + default=False, + help="List the tests that are disabled on the current platform") test_selection_group = parser.add_argument_group("Test Selection") test_selection_group.add_argument("--test-types", action="store", @@ -119,7 +91,10 @@ def create_parser(product_choices=None): debugging_group.add_argument('--debugger', const="__default__", nargs="?", help="run under a debugger, e.g. gdb or valgrind") debugging_group.add_argument('--debugger-args', help="arguments to the debugger") - + debugging_group.add_argument("--repeat", action="store", type=int, default=1, + help="Number of times to run the tests") + debugging_group.add_argument("--repeat-until-unexpected", action="store_true", default=None, + help="Run tests in a loop until one returns an unexpected result") debugging_group.add_argument('--pause-after-test', action="store_true", default=None, help="Halt the test runner after each test (this happens by default if only a single test is run)") debugging_group.add_argument('--no-pause-after-test', dest="pause_after_test", action="store_false", @@ -127,12 +102,47 @@ def create_parser(product_choices=None): debugging_group.add_argument('--pause-on-unexpected', action="store_true", help="Halt the test runner when an unexpected result is encountered") + debugging_group.add_argument('--no-restart-on-unexpected', dest="restart_on_unexpected", + default=True, action="store_false", + help="Don't restart on an unexpected result") debugging_group.add_argument("--symbols-path", action="store", type=url_or_path, help="Path or url to symbols file used to analyse crash minidumps.") debugging_group.add_argument("--stackwalk-binary", action="store", type=abs_path, help="Path to stackwalker program used to analyse minidumps.") + debugging_group.add_argument("--pdb", action="store_true", + help="Drop into pdb on python exception") + + config_group = parser.add_argument_group("Configuration") + config_group.add_argument("--binary", action="store", + type=abs_path, help="Binary to run tests against") + config_group.add_argument('--binary-arg', + default=[], action="append", dest="binary_args", + help="Extra argument for the binary (servo)") + config_group.add_argument("--webdriver-binary", action="store", metavar="BINARY", + type=abs_path, help="WebDriver server binary to use") + + config_group.add_argument("--metadata", action="store", type=abs_path, dest="metadata_root", + help="Path to root directory containing test metadata"), + config_group.add_argument("--tests", action="store", type=abs_path, dest="tests_root", + help="Path to root directory containing test files"), + config_group.add_argument("--run-info", action="store", type=abs_path, + help="Path to directory containing extra json files to add to run info") + config_group.add_argument("--product", action="store", choices=product_choices, + default=None, help="Browser against which to run tests") + config_group.add_argument("--config", action="store", type=abs_path, dest="config", + help="Path to config file") + + build_type = parser.add_mutually_exclusive_group() + build_type.add_argument("--debug-build", dest="debug", action="store_true", + default=None, + help="Build is a debug build (overrides any mozinfo file)") + build_type.add_argument("--release-build", dest="debug", action="store_false", + default=None, + help="Build is a release (overrides any mozinfo file)") + + chunking_group = parser.add_argument_group("Test Chunking") chunking_group.add_argument("--total-chunks", action="store", type=int, default=1, help="Total number of chunks to use") @@ -164,10 +174,6 @@ def create_parser(product_choices=None): gecko_group.add_argument("--disable-e10s", dest="gecko_e10s", action="store_false", default=True, help="Run tests without electrolysis preferences") - b2g_group = parser.add_argument_group("B2G-specific") - b2g_group.add_argument("--b2g-no-backup", action="store_true", default=False, - help="Don't backup device before testrun with --product=b2g") - servo_group = parser.add_argument_group("Servo-specific") servo_group.add_argument("--user-stylesheet", default=[], action="append", dest="user_stylesheets", diff --git a/tests/wpt/harness/wptrunner/wptmanifest/tests/test_serializer.py b/tests/wpt/harness/wptrunner/wptmanifest/tests/test_serializer.py index ec4d6e2d737..62a462c44e7 100644 --- a/tests/wpt/harness/wptrunner/wptmanifest/tests/test_serializer.py +++ b/tests/wpt/harness/wptrunner/wptmanifest/tests/test_serializer.py @@ -2,10 +2,13 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. +import sys import unittest from cStringIO import StringIO +import pytest + from .. import parser, serializer @@ -196,6 +199,7 @@ class TokenizerTest(unittest.TestCase): r"""key: "#" """) + @pytest.mark.xfail(sys.maxunicode == 0xFFFF, reason="narrow unicode") def test_escape_9(self): self.compare(r"""key: \U10FFFFabc""", u"""key: \U0010FFFFabc diff --git a/tests/wpt/harness/wptrunner/wptrunner.py b/tests/wpt/harness/wptrunner/wptrunner.py index 47560c83a35..d955131c581 100644 --- a/tests/wpt/harness/wptrunner/wptrunner.py +++ b/tests/wpt/harness/wptrunner/wptrunner.py @@ -204,6 +204,7 @@ def run_tests(config, test_paths, product, **kwargs): executor_kwargs, kwargs["pause_after_test"], kwargs["pause_on_unexpected"], + kwargs["restart_on_unexpected"], kwargs["debug_info"]) as manager_group: try: manager_group.run(test_type, test_loader.tests) diff --git a/tests/wpt/harness/wptrunner/wpttest.py b/tests/wpt/harness/wptrunner/wpttest.py index 9832f72654e..97dd11bae9a 100644 --- a/tests/wpt/harness/wptrunner/wpttest.py +++ b/tests/wpt/harness/wptrunner/wpttest.py @@ -68,10 +68,7 @@ class WdspecSubtestResult(SubtestResult): def get_run_info(metadata_root, product, **kwargs): - if product == "b2g": - return B2GRunInfo(metadata_root, product, **kwargs) - else: - return RunInfo(metadata_root, product, **kwargs) + return RunInfo(metadata_root, product, **kwargs) class RunInfo(dict): @@ -101,12 +98,6 @@ class RunInfo(dict): mozinfo.find_and_update_from_json(*dirs) -class B2GRunInfo(RunInfo): - def __init__(self, *args, **kwargs): - RunInfo.__init__(self, *args, **kwargs) - self["os"] = "b2g" - - class Test(object): result_cls = None subtest_result_cls = None @@ -131,7 +122,7 @@ class Test(object): inherit_metadata, test_metadata, timeout=timeout, - path=manifest_item.path, + path=manifest_item.source_file.path, protocol="https" if hasattr(manifest_item, "https") and manifest_item.https else "http") @property |