Auto merge of #10631 - servo:wptrunner-20160415, r=KiChjang

Update wptrunner.

Fixes #10540.
Fixes #10392.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="35" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/10631)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2016-04-21 13:54:19 +05:30
commit 9c172f49d0
22 changed files with 732 additions and 68 deletions

View file

@ -152,11 +152,17 @@ class MachCommands(CommandBase):
code = call(["git", "init"], env=self.build_env()) code = call(["git", "init"], env=self.build_env())
if code: if code:
return code return code
call( 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=self.build_env())
if code:
return code
code = call(["git", "fetch", "upstream"], env=self.build_env()) code = call(["git", "fetch", "upstream"], env=self.build_env())
if code: if code:
return code return code
code = call(["git", "reset", '--', "hard", "remotes/upstream/master"], env=self.build_env()) code = call(["git", "reset", "--hard", "remotes/upstream/master"], env=self.build_env())
if code: if code:
return code return code
code = call(["rm", "-rf", ".git"], env=self.build_env())
if code:
return code
return 0

View file

@ -231,7 +231,7 @@ The web-platform-test harness knows about several keys:
Any value indicates that the test is disabled. Any value indicates that the test is disabled.
`type` `type`
The test type e.g. `testharness` or `reftest`. The test type e.g. `testharness`, `reftest`, or `wdspec`.
`reftype` `reftype`
The type of comparison for reftests; either `==` or `!=`. The type of comparison for reftests; either `==` or `!=`.

View file

@ -203,6 +203,10 @@ When used for expectation data, manifests have the following format:
the (sub)test is disabled and should either not be run (for tests) the (sub)test is disabled and should either not be run (for tests)
or that its results should be ignored (subtests). or that its results should be ignored (subtests).
* A key ``restart-after`` which can be set to any value to indicate that
the runner should restart the browser after running this test (e.g. to
clear out unwanted state).
* Variables ``debug``, ``os``, ``version``, ``processor`` and * Variables ``debug``, ``os``, ``version``, ``processor`` and
``bits`` that describe the configuration of the browser under ``bits`` that describe the configuration of the browser under
test. ``debug`` is a boolean indicating whether a build is a debug test. ``debug`` is a boolean indicating whether a build is a debug

View file

@ -3,7 +3,7 @@
# You can obtain one at http://mozilla.org/MPL/2.0/. # You can obtain one at http://mozilla.org/MPL/2.0/.
from .base import Browser, ExecutorBrowser, require_arg from .base import Browser, ExecutorBrowser, require_arg
from .webdriver import ChromedriverLocalServer from ..webdriver_server import ChromeDriverServer
from ..executors import executor_kwargs as base_executor_kwargs from ..executors import executor_kwargs as base_executor_kwargs
from ..executors.executorselenium import (SeleniumTestharnessExecutor, from ..executors.executorselenium import (SeleniumTestharnessExecutor,
SeleniumRefTestExecutor) SeleniumRefTestExecutor)
@ -49,32 +49,33 @@ def env_options():
class ChromeBrowser(Browser): class ChromeBrowser(Browser):
"""Chrome is backed by chromedriver, which is supplied through """Chrome is backed by chromedriver, which is supplied through
``browsers.webdriver.ChromedriverLocalServer``.""" ``wptrunner.webdriver.ChromeDriverServer``.
"""
def __init__(self, logger, binary, webdriver_binary="chromedriver"): def __init__(self, logger, binary, webdriver_binary="chromedriver"):
"""Creates a new representation of Chrome. The `binary` argument gives """Creates a new representation of Chrome. The `binary` argument gives
the browser binary to use for testing.""" the browser binary to use for testing."""
Browser.__init__(self, logger) Browser.__init__(self, logger)
self.binary = binary self.binary = binary
self.driver = ChromedriverLocalServer(self.logger, binary=webdriver_binary) self.server = ChromeDriverServer(self.logger, binary=webdriver_binary)
def start(self): def start(self):
self.driver.start() self.server.start(block=False)
def stop(self): def stop(self):
self.driver.stop() self.server.stop()
def pid(self): def pid(self):
return self.driver.pid return self.server.pid
def is_alive(self): def is_alive(self):
# TODO(ato): This only indicates the driver is alive, # TODO(ato): This only indicates the driver is alive,
# and doesn't say anything about whether a browser session # and doesn't say anything about whether a browser session
# is active. # is active.
return self.driver.is_alive() return self.server.is_alive()
def cleanup(self): def cleanup(self):
self.stop() self.stop()
def executor_browser(self): def executor_browser(self):
return ExecutorBrowser, {"webdriver_url": self.driver.url} return ExecutorBrowser, {"webdriver_url": self.server.url}

View file

@ -13,18 +13,27 @@ from mozprofile.permissions import ServerLocations
from mozrunner import FirefoxRunner from mozrunner import FirefoxRunner
from mozcrash import mozcrash from mozcrash import mozcrash
from .base import get_free_port, Browser, ExecutorBrowser, require_arg, cmd_arg, browser_command from .base import (get_free_port,
Browser,
ExecutorBrowser,
require_arg,
cmd_arg,
browser_command)
from ..executors import executor_kwargs as base_executor_kwargs from ..executors import executor_kwargs as base_executor_kwargs
from ..executors.executormarionette import MarionetteTestharnessExecutor, MarionetteRefTestExecutor from ..executors.executormarionette import (MarionetteTestharnessExecutor,
MarionetteRefTestExecutor,
MarionetteWdspecExecutor)
from ..environment import hostnames from ..environment import hostnames
here = os.path.join(os.path.split(__file__)[0]) here = os.path.join(os.path.split(__file__)[0])
__wptrunner__ = {"product": "firefox", __wptrunner__ = {"product": "firefox",
"check_args": "check_args", "check_args": "check_args",
"browser": "FirefoxBrowser", "browser": "FirefoxBrowser",
"executor": {"testharness": "MarionetteTestharnessExecutor", "executor": {"testharness": "MarionetteTestharnessExecutor",
"reftest": "MarionetteRefTestExecutor"}, "reftest": "MarionetteRefTestExecutor",
"wdspec": "MarionetteWdspecExecutor"},
"browser_kwargs": "browser_kwargs", "browser_kwargs": "browser_kwargs",
"executor_kwargs": "executor_kwargs", "executor_kwargs": "executor_kwargs",
"env_options": "env_options", "env_options": "env_options",
@ -62,6 +71,8 @@ def executor_kwargs(test_type, server_config, cache_manager, run_info_data,
executor_kwargs["timeout_multiplier"] = 2 executor_kwargs["timeout_multiplier"] = 2
elif run_info_data["debug"] or run_info_data.get("asan"): elif run_info_data["debug"] or run_info_data.get("asan"):
executor_kwargs["timeout_multiplier"] = 3 executor_kwargs["timeout_multiplier"] = 3
if test_type == "wdspec":
executor_kwargs["webdriver_binary"] = kwargs.get("webdriver_binary")
return executor_kwargs return executor_kwargs
@ -80,6 +91,7 @@ def run_info_extras(**kwargs):
def update_properties(): def update_properties():
return ["debug", "e10s", "os", "version", "processor", "bits"], {"debug", "e10s"} return ["debug", "e10s", "os", "version", "processor", "bits"], {"debug", "e10s"}
class FirefoxBrowser(Browser): class FirefoxBrowser(Browser):
used_ports = set() used_ports = set()
init_timeout = 60 init_timeout = 60

View file

@ -32,7 +32,6 @@ def do_delayed_imports(logger, test_paths):
global serve, sslutils global serve, sslutils
serve_root = serve_path(test_paths) serve_root = serve_path(test_paths)
sys.path.insert(0, serve_root) sys.path.insert(0, serve_root)
failed = [] failed = []
@ -45,7 +44,6 @@ def do_delayed_imports(logger, test_paths):
try: try:
import sslutils import sslutils
except ImportError: except ImportError:
raise
failed.append("sslutils") failed.append("sslutils")
if failed: if failed:

View file

@ -63,6 +63,7 @@ class TestharnessResultConverter(object):
[test.subtest_result_cls(name, self.test_codes[status], message, stack) [test.subtest_result_cls(name, self.test_codes[status], message, stack)
for name, status, message, stack in subtest_results]) for name, status, message, stack in subtest_results])
testharness_result_converter = TestharnessResultConverter() testharness_result_converter = TestharnessResultConverter()
@ -71,11 +72,24 @@ def reftest_result_converter(self, test, result):
extra=result.get("extra")), []) extra=result.get("extra")), [])
def pytest_result_converter(self, test, data):
harness_data, subtest_data = data
if subtest_data is None:
subtest_data = []
harness_result = test.result_cls(*harness_data)
subtest_results = [test.subtest_result_cls(*item) for item in subtest_data]
return (harness_result, subtest_results)
class ExecutorException(Exception): class ExecutorException(Exception):
def __init__(self, status, message): def __init__(self, status, message):
self.status = status self.status = status
self.message = message self.message = message
class TestExecutor(object): class TestExecutor(object):
__metaclass__ = ABCMeta __metaclass__ = ABCMeta
@ -116,11 +130,13 @@ class TestExecutor(object):
:param runner: TestRunner instance that is going to run the tests""" :param runner: TestRunner instance that is going to run the tests"""
self.runner = runner self.runner = runner
self.protocol.setup(runner) if self.protocol is not None:
self.protocol.setup(runner)
def teardown(self): def teardown(self):
"""Run cleanup steps after tests have finished""" """Run cleanup steps after tests have finished"""
self.protocol.teardown() if self.protocol is not None:
self.protocol.teardown()
def run_test(self, test): def run_test(self, test):
"""Run a particular test. """Run a particular test.
@ -137,6 +153,7 @@ class TestExecutor(object):
if result is Stop: if result is Stop:
return result return result
# log result of parent test
if result[0].status == "ERROR": if result[0].status == "ERROR":
self.logger.debug(result[0].message) self.logger.debug(result[0].message)
@ -144,7 +161,6 @@ class TestExecutor(object):
self.runner.send_message("test_ended", test, result) self.runner.send_message("test_ended", test, result)
def server_url(self, protocol): def server_url(self, protocol):
return "%s://%s:%s" % (protocol, return "%s://%s:%s" % (protocol,
self.server_config["host"], self.server_config["host"],
@ -191,6 +207,7 @@ class RefTestExecutor(TestExecutor):
self.screenshot_cache = screenshot_cache self.screenshot_cache = screenshot_cache
class RefTestImplementation(object): class RefTestImplementation(object):
def __init__(self, executor): def __init__(self, executor):
self.timeout_multiplier = executor.timeout_multiplier self.timeout_multiplier = executor.timeout_multiplier
@ -288,6 +305,11 @@ class RefTestImplementation(object):
self.screenshot_cache[key] = hash_val, data self.screenshot_cache[key] = hash_val, data
return True, data return True, data
class WdspecExecutor(TestExecutor):
convert_result = pytest_result_converter
class Protocol(object): class Protocol(object):
def __init__(self, executor, browser): def __init__(self, executor, browser):
self.executor = executor self.executor = executor

View file

@ -3,9 +3,9 @@
# You can obtain one at http://mozilla.org/MPL/2.0/. # You can obtain one at http://mozilla.org/MPL/2.0/.
import hashlib import hashlib
import httplib
import os import os
import socket import socket
import sys
import threading import threading
import time import time
import traceback import traceback
@ -13,10 +13,15 @@ import urlparse
import uuid import uuid
from collections import defaultdict from collections import defaultdict
from ..wpttest import WdspecResult, WdspecSubtestResult
errors = None
marionette = None marionette = None
webdriver = None
here = os.path.join(os.path.split(__file__)[0]) here = os.path.join(os.path.split(__file__)[0])
from . import pytestrunner
from .base import (ExecutorException, from .base import (ExecutorException,
Protocol, Protocol,
RefTestExecutor, RefTestExecutor,
@ -25,22 +30,29 @@ from .base import (ExecutorException,
TestharnessExecutor, TestharnessExecutor,
testharness_result_converter, testharness_result_converter,
reftest_result_converter, reftest_result_converter,
strip_server) strip_server,
WdspecExecutor)
from ..testrunner import Stop from ..testrunner import Stop
from ..webdriver_server import GeckoDriverServer
# Extra timeout to use after internal test timeout at which the harness # Extra timeout to use after internal test timeout at which the harness
# should force a timeout # should force a timeout
extra_timeout = 5 # seconds extra_timeout = 5 # seconds
def do_delayed_imports(): def do_delayed_imports():
global marionette global errors, marionette, webdriver
global errors
# Marionette client used to be called marionette, recently it changed
# to marionette_driver for unfathomable reasons
try: try:
import marionette import marionette
from marionette import errors from marionette import errors
except ImportError: except ImportError:
from marionette_driver import marionette, errors from marionette_driver import marionette, errors
import webdriver
class MarionetteProtocol(Protocol): class MarionetteProtocol(Protocol):
def __init__(self, executor, browser): def __init__(self, executor, browser):
@ -54,8 +66,10 @@ class MarionetteProtocol(Protocol):
"""Connect to browser via Marionette.""" """Connect to browser via Marionette."""
Protocol.setup(self, runner) Protocol.setup(self, runner)
self.logger.debug("Connecting to marionette on port %i" % self.marionette_port) self.logger.debug("Connecting to Marionette on port %i" % self.marionette_port)
self.marionette = marionette.Marionette(host='localhost', port=self.marionette_port) self.marionette = marionette.Marionette(host='localhost',
port=self.marionette_port,
socket_timeout=None)
# XXX Move this timeout somewhere # XXX Move this timeout somewhere
self.logger.debug("Waiting for Marionette connection") self.logger.debug("Waiting for Marionette connection")
@ -97,10 +111,10 @@ class MarionetteProtocol(Protocol):
pass pass
del self.marionette del self.marionette
@property
def is_alive(self): def is_alive(self):
"""Check if the marionette connection is still active""" """Check if the Marionette connection is still active."""
try: try:
# Get a simple property over the connection
self.marionette.current_window_handle self.marionette.current_window_handle
except Exception: except Exception:
return False return False
@ -126,12 +140,18 @@ class MarionetteProtocol(Protocol):
"document.title = '%s'" % threading.current_thread().name.replace("'", '"')) "document.title = '%s'" % threading.current_thread().name.replace("'", '"'))
def wait(self): def wait(self):
socket_timeout = self.marionette.client.sock.gettimeout()
if socket_timeout:
self.marionette.set_script_timeout((socket_timeout / 2) * 1000)
while True: while True:
try: try:
self.marionette.execute_async_script(""); self.marionette.execute_async_script("")
except errors.ScriptTimeoutException: except errors.ScriptTimeoutException:
self.logger.debug("Script timed out")
pass pass
except (socket.timeout, IOError): except (socket.timeout, IOError):
self.logger.debug("Socket closed")
break break
except Exception as e: except Exception as e:
self.logger.error(traceback.format_exc(e)) self.logger.error(traceback.format_exc(e))
@ -213,7 +233,63 @@ class MarionetteProtocol(Protocol):
with self.marionette.using_context(self.marionette.CONTEXT_CHROME): with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
self.marionette.execute_script(script) self.marionette.execute_script(script)
class MarionetteRun(object):
class RemoteMarionetteProtocol(Protocol):
def __init__(self, executor, browser):
do_delayed_imports()
Protocol.__init__(self, executor, browser)
self.session = None
self.webdriver_binary = executor.webdriver_binary
self.marionette_port = browser.marionette_port
self.server = None
def setup(self, runner):
"""Connect to browser via the Marionette HTTP server."""
try:
self.server = GeckoDriverServer(
self.logger, self.marionette_port, binary=self.webdriver_binary)
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):
try:
if self.session.session_id is not None:
self.session.end()
except Exception:
pass
if self.server is not None and self.server.is_alive:
self.server.stop()
@property
def is_alive(self):
"""Test that the Marionette connection is still alive.
Because the remote communication happens over HTTP we need to
make an explicit request to the remote. It is allowed for
WebDriver spec tests to not have a WebDriver session, since this
may be what is tested.
An HTTP request to an invalid path that results in a 404 is
proof enough to us that the server is alive and kicking.
"""
conn = httplib.HTTPConnection(self.server.host, self.server.port)
conn.request("HEAD", self.server.base_path + "invalid")
res = conn.getresponse()
return res.status == 404
class ExecuteAsyncScriptRun(object):
def __init__(self, logger, func, marionette, url, timeout): def __init__(self, logger, func, marionette, url, timeout):
self.logger = logger self.logger = logger
self.result = None self.result = None
@ -277,8 +353,8 @@ class MarionetteRun(object):
class MarionetteTestharnessExecutor(TestharnessExecutor): class MarionetteTestharnessExecutor(TestharnessExecutor):
def __init__(self, browser, server_config, timeout_multiplier=1, close_after_done=True, def __init__(self, browser, server_config, timeout_multiplier=1,
debug_info=None): close_after_done=True, debug_info=None, **kwargs):
"""Marionette-based executor for testharness.js tests""" """Marionette-based executor for testharness.js tests"""
TestharnessExecutor.__init__(self, browser, server_config, TestharnessExecutor.__init__(self, browser, server_config,
timeout_multiplier=timeout_multiplier, timeout_multiplier=timeout_multiplier,
@ -295,7 +371,7 @@ class MarionetteTestharnessExecutor(TestharnessExecutor):
do_delayed_imports() do_delayed_imports()
def is_alive(self): def is_alive(self):
return self.protocol.is_alive() return self.protocol.is_alive
def on_environment_change(self, new_environment): def on_environment_change(self, new_environment):
self.protocol.on_environment_change(self.last_environment, new_environment) self.protocol.on_environment_change(self.last_environment, new_environment)
@ -307,11 +383,11 @@ class MarionetteTestharnessExecutor(TestharnessExecutor):
timeout = (test.timeout * self.timeout_multiplier if self.debug_info is None timeout = (test.timeout * self.timeout_multiplier if self.debug_info is None
else None) else None)
success, data = MarionetteRun(self.logger, success, data = ExecuteAsyncScriptRun(self.logger,
self.do_testharness, self.do_testharness,
self.protocol.marionette, self.protocol.marionette,
self.test_url(test), self.test_url(test),
timeout).run() timeout).run()
if success: if success:
return self.convert_result(test, data) return self.convert_result(test, data)
@ -338,7 +414,9 @@ class MarionetteTestharnessExecutor(TestharnessExecutor):
class MarionetteRefTestExecutor(RefTestExecutor): class MarionetteRefTestExecutor(RefTestExecutor):
def __init__(self, browser, server_config, timeout_multiplier=1, def __init__(self, browser, server_config, timeout_multiplier=1,
screenshot_cache=None, close_after_done=True, debug_info=None): screenshot_cache=None, close_after_done=True,
debug_info=None, **kwargs):
"""Marionette-based executor for reftests""" """Marionette-based executor for reftests"""
RefTestExecutor.__init__(self, RefTestExecutor.__init__(self,
browser, browser,
@ -358,7 +436,7 @@ class MarionetteRefTestExecutor(RefTestExecutor):
self.wait_script = f.read() self.wait_script = f.read()
def is_alive(self): def is_alive(self):
return self.protocol.is_alive() return self.protocol.is_alive
def on_environment_change(self, new_environment): def on_environment_change(self, new_environment):
self.protocol.on_environment_change(self.last_environment, new_environment) self.protocol.on_environment_change(self.last_environment, new_environment)
@ -376,7 +454,6 @@ class MarionetteRefTestExecutor(RefTestExecutor):
self.has_window = True self.has_window = True
result = self.implementation.run_test(test) result = self.implementation.run_test(test)
return self.convert_result(test, result) return self.convert_result(test, result)
def screenshot(self, test, viewport_size, dpi): def screenshot(self, test, viewport_size, dpi):
@ -388,7 +465,7 @@ class MarionetteRefTestExecutor(RefTestExecutor):
test_url = self.test_url(test) test_url = self.test_url(test)
return MarionetteRun(self.logger, return ExecuteAsyncScriptRun(self.logger,
self._screenshot, self._screenshot,
self.protocol.marionette, self.protocol.marionette,
test_url, test_url,
@ -405,3 +482,78 @@ class MarionetteRefTestExecutor(RefTestExecutor):
screenshot = screenshot.split(",", 1)[1] screenshot = screenshot.split(",", 1)[1]
return screenshot return screenshot
class WdspecRun(object):
def __init__(self, func, session, path, timeout):
self.func = func
self.result = None
self.session = session
self.path = path
self.timeout = timeout
self.result_flag = threading.Event()
def run(self):
"""Runs function in a thread and interrupts it if it exceeds the
given timeout. Returns (True, (Result, [SubtestResult ...])) in
case of success, or (False, (status, extra information)) in the
event of failure.
"""
executor = threading.Thread(target=self._run)
executor.start()
flag = self.result_flag.wait(self.timeout)
if self.result is None:
assert not flag
self.result = False, ("EXTERNAL-TIMEOUT", None)
return self.result
def _run(self):
try:
self.result = True, self.func(self.session, self.path, self.timeout)
except (socket.timeout, IOError):
self.result = False, ("CRASH", None)
except Exception as e:
message = getattr(e, "message")
if message:
message += "\n"
message += traceback.format_exc(e)
self.result = False, ("ERROR", message)
finally:
self.result_flag.set()
class MarionetteWdspecExecutor(WdspecExecutor):
def __init__(self, browser, server_config, webdriver_binary,
timeout_multiplier=1, close_after_done=True, debug_info=None):
WdspecExecutor.__init__(self, browser, server_config,
timeout_multiplier=timeout_multiplier,
debug_info=debug_info)
self.webdriver_binary = webdriver_binary
self.protocol = RemoteMarionetteProtocol(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)

View file

@ -71,7 +71,7 @@ class ServoTestharnessExecutor(ProcessTestExecutor):
self.result_flag = threading.Event() self.result_flag = threading.Event()
args = [render_arg(self.browser.render_backend), "--hard-fail", "-u", "Servo/wptrunner", args = [render_arg(self.browser.render_backend), "--hard-fail", "-u", "Servo/wptrunner",
"-z", self.test_url(test)] "-Z", "replace-surrogates", "-z", self.test_url(test)]
for stylesheet in self.browser.user_stylesheets: for stylesheet in self.browser.user_stylesheets:
args += ["--user-stylesheet", stylesheet] args += ["--user-stylesheet", stylesheet]
for pref, value in test.environment.get('prefs', {}).iteritems(): for pref, value in test.environment.get('prefs', {}).iteritems():
@ -204,7 +204,7 @@ class ServoRefTestExecutor(ProcessTestExecutor):
debug_args, command = browser_command( debug_args, command = browser_command(
self.binary, self.binary,
[render_arg(self.browser.render_backend), "--hard-fail", "--exit", [render_arg(self.browser.render_backend), "--hard-fail", "--exit",
"-u", "Servo/wptrunner", "-Z", "disable-text-aa,load-webfonts-synchronously", "-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.debug_info) self.debug_info)

View file

@ -14,16 +14,24 @@ from .base import (Protocol,
RefTestImplementation, RefTestImplementation,
TestharnessExecutor, TestharnessExecutor,
strip_server) strip_server)
import webdriver from .. import webdriver
from ..testrunner import Stop from ..testrunner import Stop
webdriver = None
here = os.path.join(os.path.split(__file__)[0]) here = os.path.join(os.path.split(__file__)[0])
extra_timeout = 5 extra_timeout = 5
def do_delayed_imports():
global webdriver
import webdriver
class ServoWebDriverProtocol(Protocol): class ServoWebDriverProtocol(Protocol):
def __init__(self, executor, browser, capabilities, **kwargs): def __init__(self, executor, browser, capabilities, **kwargs):
do_delayed_imports()
Protocol.__init__(self, executor, browser) Protocol.__init__(self, executor, browser)
self.capabilities = capabilities self.capabilities = capabilities
self.host = browser.webdriver_host self.host = browser.webdriver_host
@ -34,10 +42,11 @@ class ServoWebDriverProtocol(Protocol):
"""Connect to browser via WebDriver.""" """Connect to browser via WebDriver."""
self.runner = runner self.runner = runner
url = "http://%s:%d" % (self.host, self.port)
session_started = False session_started = False
try: try:
self.session = webdriver.Session(self.host, self.port, self.session = webdriver.Session(self.host, self.port,
extension=webdriver.ServoExtensions) extension=webdriver.servo.ServoCommandExtensions)
self.session.start() self.session.start()
except: except:
self.logger.warning( self.logger.warning(
@ -62,7 +71,7 @@ class ServoWebDriverProtocol(Protocol):
def is_alive(self): def is_alive(self):
try: try:
# Get a simple property over the connection # Get a simple property over the connection
self.session.handle self.session.window_handle
# TODO what exception? # TODO what exception?
except Exception: except Exception:
return False return False

View file

@ -0,0 +1,6 @@
# 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 . import fixtures
from .runner import run

View file

@ -0,0 +1,58 @@
# 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/.
import pytest
"""pytest fixtures for use in Python-based WPT tests.
The purpose of test fixtures is to provide a fixed baseline upon which
tests can reliably and repeatedly execute.
"""
class Session(object):
"""Fixture to allow access to wptrunner's existing WebDriver session
in tests.
The session is not created by default to enable testing of session
creation. However, a module-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.
To illustrate implicit session creation::
def test_session_scope(session):
# at this point there is no session
assert session.session_id is None
# window_id is a WebDriver command,
# and implicitly creates the session for us
assert session.window_id is not None
# we now have a session
assert session.session_id is not None
You can also access the session in custom fixtures defined in the
tests, such as a setup function::
@pytest.fixture(scope="function")
def setup(request, session):
session.url = "https://example.org"
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`.
"""
def __init__(self, client):
self.client = client
@pytest.fixture(scope="module")
def session(self, request):
request.addfinalizer(self.client.end)
return self.client

View file

@ -0,0 +1,113 @@
# 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/.
"""Provides interface to deal with pytest.
Usage::
session = webdriver.client.Session("127.0.0.1", "4444", "/")
harness_result = ("OK", None)
subtest_results = pytestrunner.run("/path/to/test", session.url)
return (harness_result, subtest_results)
"""
import errno
import shutil
import tempfile
from . import fixtures
pytest = None
def do_delayed_imports():
global pytest
import pytest
def run(path, session, timeout=0):
"""Run Python test at ``path`` in pytest. The provided ``session``
is exposed as a fixture available in the scope of the test functions.
:param path: Path to the test file.
:param session: WebDriver session to expose.
:param timeout: Duration before interrupting potentially hanging
tests. If 0, there is no timeout.
:returns: List of subtest results, which are tuples of (test id,
status, message, stacktrace).
"""
if pytest is None:
do_delayed_imports()
recorder = SubtestResultRecorder()
plugins = [recorder,
fixtures.Session(session)]
# TODO(ato): Deal with timeouts
with TemporaryDirectory() as cache:
pytest.main(["--strict", # turn warnings into errors
"--verbose", # show each individual subtest
"--capture", "no", # enable stdout/stderr from tests
"--basetemp", cache, # temporary directory
path],
plugins=plugins)
return recorder.results
class SubtestResultRecorder(object):
def __init__(self):
self.results = []
def pytest_runtest_logreport(self, report):
if report.passed and report.when == "call":
self.record_pass(report)
elif report.failed:
if report.when != "call":
self.record_error(report)
else:
self.record_fail(report)
elif report.skipped:
self.record_skip(report)
def record_pass(self, report):
self.record(report.nodeid, "PASS")
def record_fail(self, report):
self.record(report.nodeid, "FAIL", stack=report.longrepr)
def record_error(self, report):
# error in setup/teardown
if report.when != "call":
message = "%s error" % report.when
self.record(report.nodeid, "ERROR", message, report.longrepr)
def record_skip(self, report):
self.record(report.nodeid, "ERROR",
"In-test skip decorators are disallowed, "
"please use WPT metadata to ignore tests.")
def record(self, test, status, message=None, stack=None):
if stack is not None:
stack = str(stack)
new_result = (test, status, message, stack)
self.results.append(new_result)
class TemporaryDirectory(object):
def __enter__(self):
self.path = tempfile.mkdtemp(prefix="pytest-")
return self.path
def __exit__(self, *args):
try:
shutil.rmtree(self.path)
except OSError as e:
# no such file or directory
if e.errno != errno.ENOENT:
raise

View file

@ -29,10 +29,10 @@ def data_cls_getter(output_node, visited_node):
raise ValueError raise ValueError
def disabled(node): def bool_prop(name, node):
"""Boolean indicating whether the test is disabled""" """Boolean property"""
try: try:
return node.get("disabled") return node.get(name)
except KeyError: except KeyError:
return None return None
@ -109,7 +109,11 @@ class ExpectedManifest(ManifestItem):
@property @property
def disabled(self): def disabled(self):
return disabled(self) return bool_prop("disabled", self)
@property
def restart_after(self):
return bool_prop("restart-after", self)
@property @property
def tags(self): def tags(self):
@ -123,7 +127,11 @@ class ExpectedManifest(ManifestItem):
class DirectoryManifest(ManifestItem): class DirectoryManifest(ManifestItem):
@property @property
def disabled(self): def disabled(self):
return disabled(self) return bool_prop("disabled", self)
@property
def restart_after(self):
return bool_prop("restart-after", self)
@property @property
def tags(self): def tags(self):
@ -164,7 +172,11 @@ class TestNode(ManifestItem):
@property @property
def disabled(self): def disabled(self):
return disabled(self) return bool_prop("disabled", self)
@property
def restart_after(self):
return bool_prop("restart-after", self)
@property @property
def tags(self): def tags(self):

View file

@ -524,7 +524,8 @@ class TestRunnerManager(threading.Thread):
self.test = None self.test = None
restart_before_next = (file_result.status in ("CRASH", "EXTERNAL-TIMEOUT") or 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)
if (self.pause_after_test or if (self.pause_after_test or

View file

@ -0,0 +1,206 @@
# 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/.
import abc
import errno
import os
import platform
import socket
import threading
import time
import traceback
import urlparse
import mozprocess
__all__ = ["SeleniumServer", "ChromeDriverServer",
"GeckoDriverServer", "WebDriverServer"]
class WebDriverServer(object):
__metaclass__ = abc.ABCMeta
default_base_path = "/"
_used_ports = set()
def __init__(self, logger, binary, host="127.0.0.1", port=None,
base_path="", env=None):
self.logger = logger
self.binary = binary
self.host = host
if base_path == "":
self.base_path = self.default_base_path
else:
self.base_path = base_path
self.env = os.environ.copy() if env is None else env
self._port = port
self._cmd = None
self._proc = None
@abc.abstractmethod
def make_command(self):
"""Returns the full command for starting the server process as a list."""
def start(self, block=True):
try:
self._run(block)
except KeyboardInterrupt:
self.stop()
def _run(self, block):
self._cmd = self.make_command()
self._proc = mozprocess.ProcessHandler(
self._cmd,
processOutputLine=self.on_output,
env=self.env,
storeOutput=False)
try:
self._proc.run()
except OSError as e:
if e.errno == errno.ENOENT:
raise IOError(
"WebDriver HTTP server executable not found: %s" % self.binary)
raise
self.logger.debug(
"Waiting for server to become accessible: %s" % self.url)
try:
wait_for_service((self.host, self.port))
except:
self.logger.error(
"WebDriver HTTP server was not accessible "
"within the timeout:\n%s" % traceback.format_exc())
raise
if block:
self._proc.wait()
def stop(self):
if self.is_alive:
return self._proc.kill()
return not self.is_alive
@property
def is_alive(self):
return (self._proc is not None and
self._proc.proc is not None and
self._proc.poll() is None)
def on_output(self, line):
self.logger.process_output(self.pid,
line.decode("utf8", "replace"),
command=" ".join(self._cmd))
@property
def pid(self):
if self._proc is not None:
return self._proc.pid
@property
def url(self):
return "http://%s:%i%s" % (self.host, self.port, self.base_path)
@property
def port(self):
if self._port is None:
self._port = self._find_next_free_port()
return self._port
@staticmethod
def _find_next_free_port():
port = get_free_port(4444, exclude=WebDriverServer._used_ports)
WebDriverServer._used_ports.add(port)
return port
class SeleniumServer(WebDriverServer):
default_base_path = "/wd/hub"
def make_command(self):
return ["java", "-jar", self.binary, "-port", str(self.port)]
class ChromeDriverServer(WebDriverServer):
default_base_path = "/wd/hub"
def __init__(self, logger, binary="chromedriver", port=None,
base_path=""):
WebDriverServer.__init__(
self, logger, binary, port=port, base_path=base_path)
def make_command(self):
return [self.binary,
cmd_arg("port", str(self.port)),
cmd_arg("url-base", self.base_path) if self.base_path else ""]
class GeckoDriverServer(WebDriverServer):
def __init__(self, logger, marionette_port=2828, binary="wires",
host="127.0.0.1", port=None):
env = os.environ.copy()
env["RUST_BACKTRACE"] = "1"
WebDriverServer.__init__(self, logger, binary, host=host, port=port, env=env)
self.marionette_port = marionette_port
def make_command(self):
return [self.binary,
"--connect-existing",
"--marionette-port", str(self.marionette_port),
"--webdriver-host", self.host,
"--webdriver-port", str(self.port)]
def cmd_arg(name, value=None):
prefix = "-" if platform.system() == "Windows" else "--"
rv = prefix + name
if value is not None:
rv += "=" + value
return rv
def get_free_port(start_port, exclude=None):
"""Get the first port number after start_port (inclusive) that is
not currently bound.
:param start_port: Integer port number at which to start testing.
:param exclude: Set of port numbers to skip"""
port = start_port
while True:
if exclude and port in exclude:
port += 1
continue
s = socket.socket()
try:
s.bind(("127.0.0.1", port))
except socket.error:
port += 1
else:
return port
finally:
s.close()
def wait_for_service(addr, timeout=15):
"""Waits until network service given as a tuple of (host, port) becomes
available or the `timeout` duration is reached, at which point
``socket.error`` is raised."""
end = time.time() + timeout
while end > time.time():
so = socket.socket()
try:
so.connect(addr)
except socket.timeout:
pass
except socket.error as e:
if e[0] != errno.ECONNREFUSED:
raise
else:
return True
finally:
so.close()
time.sleep(0.5)
raise socket.error("Service is unavailable: %s:%i" % addr)

View file

@ -10,6 +10,7 @@ from collections import OrderedDict
from distutils.spawn import find_executable from distutils.spawn import find_executable
import config import config
import wpttest
def abs_path(path): def abs_path(path):
@ -25,6 +26,7 @@ def url_or_path(path):
else: else:
return abs_path(path) return abs_path(path)
def require_arg(kwargs, name, value_func=None): def require_arg(kwargs, name, value_func=None):
if value_func is None: if value_func is None:
value_func = lambda x: x is not None value_func = lambda x: x is not None
@ -101,8 +103,8 @@ def create_parser(product_choices=None):
test_selection_group = parser.add_argument_group("Test Selection") test_selection_group = parser.add_argument_group("Test Selection")
test_selection_group.add_argument("--test-types", action="store", test_selection_group.add_argument("--test-types", action="store",
nargs="*", default=["testharness", "reftest"], nargs="*", default=wpttest.enabled_tests,
choices=["testharness", "reftest"], choices=wpttest.enabled_tests,
help="Test types to run") help="Test types to run")
test_selection_group.add_argument("--include", action="append", test_selection_group.add_argument("--include", action="append",
help="URL prefix to include") help="URL prefix to include")
@ -159,8 +161,8 @@ def create_parser(product_choices=None):
gecko_group = parser.add_argument_group("Gecko-specific") gecko_group = parser.add_argument_group("Gecko-specific")
gecko_group.add_argument("--prefs-root", dest="prefs_root", action="store", type=abs_path, gecko_group.add_argument("--prefs-root", dest="prefs_root", action="store", type=abs_path,
help="Path to the folder containing browser prefs") help="Path to the folder containing browser prefs")
gecko_group.add_argument("--e10s", dest="gecko_e10s", action="store_true", gecko_group.add_argument("--disable-e10s", dest="gecko_e10s", action="store_false", default=True,
help="Run tests with electrolysis preferences") help="Run tests without electrolysis preferences")
b2g_group = parser.add_argument_group("B2G-specific") b2g_group = parser.add_argument_group("B2G-specific")
b2g_group.add_argument("--b2g-no-backup", action="store_true", default=False, b2g_group.add_argument("--b2g-no-backup", action="store_true", default=False,
@ -343,12 +345,14 @@ def check_args(kwargs):
return kwargs return kwargs
def check_args_update(kwargs): def check_args_update(kwargs):
set_from_config(kwargs) set_from_config(kwargs)
if kwargs["product"] is None: if kwargs["product"] is None:
kwargs["product"] = "firefox" kwargs["product"] = "firefox"
def create_parser_update(product_choices=None): def create_parser_update(product_choices=None):
from mozlog.structured import commandline from mozlog.structured import commandline

View file

@ -74,6 +74,7 @@ class LoggingWrapper(StringIO):
instead""" instead"""
def __init__(self, queue, prefix=None): def __init__(self, queue, prefix=None):
StringIO.__init__(self)
self.queue = queue self.queue = queue
self.prefix = prefix self.prefix = prefix
@ -94,6 +95,7 @@ class LoggingWrapper(StringIO):
def flush(self): def flush(self):
pass pass
class CaptureIO(object): class CaptureIO(object):
def __init__(self, logger, do_capture): def __init__(self, logger, do_capture):
self.logger = logger self.logger = logger

View file

@ -12,6 +12,8 @@ import mozinfo
from wptmanifest.parser import atoms from wptmanifest.parser import atoms
atom_reset = atoms["Reset"] atom_reset = atoms["Reset"]
enabled_tests = set(["testharness", "reftest", "wdspec"])
class Result(object): class Result(object):
def __init__(self, status, message, expected=None, extra=None): def __init__(self, status, message, expected=None, extra=None):
@ -22,6 +24,9 @@ class Result(object):
self.expected = expected self.expected = expected
self.extra = extra self.extra = extra
def __repr__(self):
return "<%s.%s %s>" % (self.__module__, self.__class__.__name__, self.status)
class SubtestResult(object): class SubtestResult(object):
def __init__(self, name, status, message, stack=None, expected=None): def __init__(self, name, status, message, stack=None, expected=None):
@ -33,20 +38,33 @@ class SubtestResult(object):
self.stack = stack self.stack = stack
self.expected = expected self.expected = expected
def __repr__(self):
return "<%s.%s %s %s>" % (self.__module__, self.__class__.__name__, self.name, self.status)
class TestharnessResult(Result): class TestharnessResult(Result):
default_expected = "OK" default_expected = "OK"
statuses = set(["OK", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"]) statuses = set(["OK", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
class TestharnessSubtestResult(SubtestResult):
default_expected = "PASS"
statuses = set(["PASS", "FAIL", "TIMEOUT", "NOTRUN"])
class ReftestResult(Result): class ReftestResult(Result):
default_expected = "PASS" default_expected = "PASS"
statuses = set(["PASS", "FAIL", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"]) statuses = set(["PASS", "FAIL", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
class TestharnessSubtestResult(SubtestResult): class WdspecResult(Result):
default_expected = "OK"
statuses = set(["OK", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
class WdspecSubtestResult(SubtestResult):
default_expected = "PASS" default_expected = "PASS"
statuses = set(["PASS", "FAIL", "TIMEOUT", "NOTRUN"]) statuses = set(["PASS", "FAIL", "ERROR"])
def get_run_info(metadata_root, product, **kwargs): def get_run_info(metadata_root, product, **kwargs):
@ -82,6 +100,7 @@ class RunInfo(dict):
mozinfo.find_and_update_from_json(*dirs) mozinfo.find_and_update_from_json(*dirs)
class B2GRunInfo(RunInfo): class B2GRunInfo(RunInfo):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
RunInfo.__init__(self, *args, **kwargs) RunInfo.__init__(self, *args, **kwargs)
@ -115,7 +134,6 @@ class Test(object):
path=manifest_item.path, path=manifest_item.path,
protocol="https" if hasattr(manifest_item, "https") and manifest_item.https else "http") protocol="https" if hasattr(manifest_item, "https") and manifest_item.https else "http")
@property @property
def id(self): def id(self):
return self.url return self.url
@ -141,7 +159,6 @@ class Test(object):
if subtest_meta is not None: if subtest_meta is not None:
yield subtest_meta yield subtest_meta
def disabled(self, subtest=None): def disabled(self, subtest=None):
for meta in self.itermeta(subtest): for meta in self.itermeta(subtest):
disabled = meta.disabled disabled = meta.disabled
@ -149,6 +166,14 @@ class Test(object):
return disabled return disabled
return None return None
@property
def restart_after(self):
for meta in self.itermeta(None):
restart_after = meta.restart_after
if restart_after is not None:
return True
return False
@property @property
def tags(self): def tags(self):
tags = set() tags = set()
@ -191,6 +216,9 @@ class Test(object):
except KeyError: except KeyError:
return default return default
def __repr__(self):
return "<%s.%s %s>" % (self.__module__, self.__class__.__name__, self.id)
class TestharnessTest(Test): class TestharnessTest(Test):
result_cls = TestharnessResult result_cls = TestharnessResult
@ -293,12 +321,18 @@ class ReftestTest(Test):
return ("reftype", "refurl") return ("reftype", "refurl")
class WdspecTest(Test):
result_cls = WdspecResult
subtest_result_cls = WdspecSubtestResult
test_type = "wdspec"
manifest_test_cls = {"reftest": ReftestTest, manifest_test_cls = {"reftest": ReftestTest,
"testharness": TestharnessTest, "testharness": TestharnessTest,
"manual": ManualTest} "manual": ManualTest,
"wdspec": WdspecTest}
def from_manifest(manifest_test, inherit_metadata, test_metadata): def from_manifest(manifest_test, inherit_metadata, test_metadata):
test_cls = manifest_test_cls[manifest_test.item_type] test_cls = manifest_test_cls[manifest_test.item_type]
return test_cls.from_manifest(manifest_test, inherit_metadata, test_metadata) return test_cls.from_manifest(manifest_test, inherit_metadata, test_metadata)

View file

@ -1,3 +1,14 @@
[escape.htm] [escape.htm]
type: testharness type: testharness
expected: CRASH [Null bytes]
expected: FAIL
bug: https://github.com/servo/servo/issues/10685
[Various tests]
expected: FAIL
bug: https://github.com/servo/servo/issues/10685
[Surrogates]
expected: FAIL
bug: https://github.com/servo/servo/issues/6564

View file

@ -1,3 +0,0 @@
[invalid-UTF-16.html]
type: testharness
expected: CRASH

View file

@ -1,3 +1,19 @@
[storage_setitem.html] [storage_setitem.html]
type: testharness type: testharness
expected: CRASH expected: TIMEOUT
[localStorage[\] = "<22>"]
expected: FAIL
bug: https://github.com/servo/servo/issues/6564
[localStorage[\] = "<22>a"]
expected: FAIL
bug: https://github.com/servo/servo/issues/6564
[localStorage[\] = "a<>"]
expected: FAIL
bug: https://github.com/servo/servo/issues/6564
[localStorage["0"\]]
expected: TIMEOUT
bug: https://github.com/servo/servo/issues/10686