Allow running testharness/testdriver/reftests in servodriver (#34550)

* Make servodriver a thin wrapper over the base webdriver browser/executor classes.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* Make ServoWebDriverRefTestExecutor a thin shell over the webdriver reftest executor.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* Wait for the initial load to complete when opening a new tab via webdriver.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* Remove assumption of a single tab from the webdriver server.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* Serialize all keys of JS objects when converting to webdriver values.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* Formatting.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* Cleanup, docs, etc.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* Use webview terminology more consistently.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* Fix flake8 errors.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

---------

Signed-off-by: Josh Matthews <josh@joshmatthews.net>
This commit is contained in:
Josh Matthews 2024-12-11 14:18:44 -05:00 committed by GitHub
parent 25f242b652
commit 7b160700d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 336 additions and 407 deletions

View file

@ -499057,7 +499057,7 @@
[]
],
"servodriver.py": [
"2cb638be1569c91a963bf2cb3824f2c07788f8cc",
"1e9a2f3090ef1ff826dac4ca3a88a958242ffae6",
[]
],
"webkit.py": [
@ -499123,7 +499123,7 @@
[]
],
"executorservodriver.py": [
"5d7d55f30b551f59bc0b16aacc8641c0fc24e39c",
"41b8ed9ac1891edb7ec332c61923314f8b1e5f19",
[]
],
"executorwebdriver.py": [
@ -499269,10 +499269,6 @@
"d6616739e6ed63a79e8b2f5d8aee1d5b2ced7f49",
[]
],
"testharnessreport-servodriver.js": [
"7819538dbb8f4a807d5db2649c2540854996c865",
[]
],
"testharnessreport-wktr.js": [
"b7d350a4262cb6f0d38337b17311fea7bd73eb70",
[]

View file

@ -1,19 +1,13 @@
# mypy: allow-untyped-defs
import os
import subprocess
import tempfile
from mozprocess import ProcessHandler
from tools.serve.serve import make_hosts_file
from .base import (Browser,
ExecutorBrowser,
OutputHandler,
from .base import (WebDriverBrowser,
require_arg,
get_free_port,
browser_command)
get_free_port)
from .base import get_timeout_multiplier # noqa: F401
from ..executors import executor_kwargs as base_executor_kwargs
from ..executors.executorservodriver import (ServoWebDriverTestharnessExecutor, # noqa: F401
@ -64,8 +58,7 @@ def env_extras(**kwargs):
def env_options():
return {"server_host": "127.0.0.1",
"testharnessreport": "testharnessreport-servodriver.js",
"supports_debugger": True}
"supports_debugger": False}
def update_properties():
@ -79,107 +72,40 @@ def write_hosts_file(config):
return hosts_path
class ServoWebDriverBrowser(Browser):
class ServoWebDriverBrowser(WebDriverBrowser):
init_timeout = 300 # Large timeout for cases where we're booting an Android emulator
def __init__(self, logger, binary, debug_info=None, webdriver_host="127.0.0.1",
server_config=None, binary_args=None,
user_stylesheets=None, headless=None, **kwargs):
Browser.__init__(self, logger)
self.binary = binary
self.binary_args = binary_args or []
self.webdriver_host = webdriver_host
self.webdriver_port = None
self.proc = None
self.debug_info = debug_info
self.hosts_path = write_hosts_file(server_config)
self.server_ports = server_config.ports if server_config else {}
self.command = None
self.user_stylesheets = user_stylesheets if user_stylesheets else []
self.headless = headless if headless else False
self.ca_certificate_path = server_config.ssl_config["ca_cert_path"]
self.output_handler = None
def start(self, **kwargs):
self.webdriver_port = get_free_port()
hosts_path = write_hosts_file(server_config)
port = get_free_port()
env = os.environ.copy()
env["HOST_FILE"] = self.hosts_path
env["HOST_FILE"] = hosts_path
env["RUST_BACKTRACE"] = "1"
env["EMULATOR_REVERSE_FORWARD_PORTS"] = ",".join(
str(port)
for _protocol, ports in self.server_ports.items()
for port in ports
if port
)
debug_args, command = browser_command(
self.binary,
self.binary_args + [
"--hard-fail",
"--webdriver=%s" % self.webdriver_port,
"about:blank",
],
self.debug_info
)
args = [
"--hard-fail",
"--webdriver=%s" % port,
"about:blank",
]
if self.headless:
command += ["--headless"]
ca_cert_path = server_config.ssl_config["ca_cert_path"]
if ca_cert_path:
args += ["--certificate-path", ca_cert_path]
if binary_args:
args += binary_args
if user_stylesheets:
for stylesheet in user_stylesheets:
args += ["--user-stylesheet", stylesheet]
if headless:
args += ["--headless"]
if self.ca_certificate_path:
command += ["--certificate-path", self.ca_certificate_path]
for stylesheet in self.user_stylesheets:
command += ["--user-stylesheet", stylesheet]
self.command = command
self.command = debug_args + self.command
if not self.debug_info or not self.debug_info.interactive:
self.output_handler = OutputHandler(self.logger, self.command)
self.proc = ProcessHandler(self.command,
processOutputLine=[self.on_output],
env=env,
storeOutput=False)
self.proc.run()
self.output_handler.after_process_start(self.proc.pid)
self.output_handler.start()
else:
self.proc = subprocess.Popen(self.command, env=env)
self.logger.debug("Servo Started")
def stop(self, force=False):
self.logger.debug("Stopping browser")
if self.proc is not None:
try:
self.proc.kill()
except OSError:
# This can happen on Windows if the process is already dead
pass
if self.output_handler is not None:
self.output_handler.after_process_stop()
@property
def pid(self):
if self.proc is None:
return None
try:
return self.proc.pid
except AttributeError:
return None
def is_alive(self):
return self.proc.poll() is None
WebDriverBrowser.__init__(self, env=env, logger=logger, host=webdriver_host, port=port,
supports_pac=False, webdriver_binary=binary, webdriver_args=args,
binary=binary)
self.hosts_path = hosts_path
def cleanup(self):
self.stop()
WebDriverBrowser.cleanup(self)
os.remove(self.hosts_path)
def executor_browser(self):
assert self.webdriver_port is not None
return ExecutorBrowser, {"webdriver_host": self.webdriver_host,
"webdriver_port": self.webdriver_port,
"init_timeout": self.init_timeout}

View file

@ -1,18 +1,8 @@
# mypy: allow-untyped-defs
import json
import os
import socket
import traceback
from .base import (Protocol,
RefTestExecutor,
RefTestImplementation,
TestharnessExecutor,
TimedRunner,
strip_server)
from .protocol import BaseProtocolPart
from ..environment import wait_for_service
from .executorwebdriver import WebDriverProtocol, WebDriverTestharnessExecutor, WebDriverRefTestExecutor
webdriver = None
ServoCommandExtensions = None
@ -64,240 +54,57 @@ def parse_pref_value(value):
return value
class ServoBaseProtocolPart(BaseProtocolPart):
def execute_script(self, script, asynchronous=False):
pass
def set_timeout(self, timeout):
pass
def wait(self):
return False
def set_window(self, handle):
pass
def window_handles(self):
return []
def load(self, url):
pass
class ServoWebDriverProtocol(Protocol):
implements = [ServoBaseProtocolPart]
class ServoWebDriverProtocol(WebDriverProtocol):
def __init__(self, executor, browser, capabilities, **kwargs):
do_delayed_imports()
Protocol.__init__(self, executor, browser)
self.capabilities = capabilities
self.host = browser.webdriver_host
self.port = browser.webdriver_port
self.init_timeout = browser.init_timeout
self.session = None
WebDriverProtocol.__init__(self, executor, browser, capabilities, **kwargs)
def connect(self):
"""Connect to browser via WebDriver."""
wait_for_service(self.logger, self.host, self.port, timeout=self.init_timeout)
"""Connect to browser via WebDriver and crete a WebDriver session."""
self.logger.debug("Connecting to WebDriver on URL: %s" % self.url)
self.session = webdriver.Session(self.host, self.port, extension=ServoCommandExtensions)
self.session.start()
host, port = self.url.split(":")[1].strip("/"), self.url.split(':')[-1].strip("/")
def after_connect(self):
pass
def teardown(self):
self.logger.debug("Hanging up on WebDriver session")
try:
self.session.end()
except Exception:
pass
def is_alive(self):
try:
# Get a simple property over the connection
self.session.window_handle
# TODO what exception?
except Exception:
return False
return True
def wait(self):
while True:
try:
return self.session.execute_async_script("""let callback = arguments[arguments.length - 1];
addEventListener("__test_restart", e => {e.preventDefault(); callback(true)})""")
except webdriver.TimeoutException:
pass
except (socket.timeout, OSError):
break
except Exception:
self.logger.error(traceback.format_exc())
break
return False
capabilities = {"alwaysMatch": self.capabilities}
self.webdriver = webdriver.Session(host, port,
capabilities=capabilities,
enable_bidi=self.enable_bidi,
extension=ServoCommandExtensions)
self.webdriver.start()
class ServoWebDriverRun(TimedRunner):
def set_timeout(self):
pass
def run_func(self):
try:
self.result = True, self.func(self.protocol.session, self.url, self.timeout)
except webdriver.TimeoutException:
self.result = False, ("EXTERNAL-TIMEOUT", None)
except (socket.timeout, OSError):
self.result = False, ("CRASH", None)
except Exception as e:
message = getattr(e, "message", "")
if message:
message += "\n"
message += traceback.format_exc()
self.result = False, ("INTERNAL-ERROR", e)
finally:
self.result_flag.set()
class ServoWebDriverTestharnessExecutor(TestharnessExecutor):
class ServoWebDriverTestharnessExecutor(WebDriverTestharnessExecutor):
supports_testdriver = True
protocol_cls = ServoWebDriverProtocol
def __init__(self, logger, browser, server_config, timeout_multiplier=1,
close_after_done=True, capabilities=None, debug_info=None,
close_after_done=True, capabilities={}, debug_info=None,
**kwargs):
TestharnessExecutor.__init__(self, logger, browser, server_config, timeout_multiplier=1,
debug_info=None)
self.protocol = ServoWebDriverProtocol(self, browser, capabilities=capabilities)
with open(os.path.join(here, "testharness_servodriver.js")) as f:
self.script = f.read()
self.timeout = None
def on_protocol_change(self, new_protocol):
pass
def is_alive(self):
return self.protocol.is_alive()
def do_test(self, test):
url = self.test_url(test)
timeout = test.timeout * self.timeout_multiplier + self.extra_timeout
if timeout != self.timeout:
try:
self.protocol.session.timeouts.script = timeout
self.timeout = timeout
except OSError:
msg = "Lost WebDriver connection"
self.logger.error(msg)
return ("INTERNAL-ERROR", msg)
success, data = ServoWebDriverRun(self.logger,
self.do_testharness,
self.protocol,
url,
timeout,
self.extra_timeout).run()
if success:
return self.convert_result(test, data)
return (test.make_result(*data), [])
def do_testharness(self, session, url, timeout):
session.url = url
result = json.loads(
session.execute_async_script(
self.script % {"abs_url": url,
"url": strip_server(url),
"timeout_multiplier": self.timeout_multiplier,
"timeout": timeout * 1000}))
# Prevent leaking every page in history until Servo develops a more sane
# page cache
session.back()
return result
WebDriverTestharnessExecutor.__init__(self, logger, browser, server_config,
timeout_multiplier, capabilities=capabilities,
debug_info=debug_info, close_after_done=close_after_done,
cleanup_after_test=False)
def on_environment_change(self, new_environment):
self.protocol.session.extension.change_prefs(
self.protocol.webdriver.extension.change_prefs(
self.last_environment.get("prefs", {}),
new_environment.get("prefs", {})
)
class TimeoutError(Exception):
pass
class ServoWebDriverRefTestExecutor(WebDriverRefTestExecutor):
protocol_cls = ServoWebDriverProtocol
class ServoWebDriverRefTestExecutor(RefTestExecutor):
def __init__(self, logger, browser, server_config, timeout_multiplier=1,
screenshot_cache=None, capabilities=None, debug_info=None,
screenshot_cache=None, capabilities={}, debug_info=None,
**kwargs):
"""Selenium WebDriver-based executor for reftests"""
RefTestExecutor.__init__(self,
logger,
browser,
server_config,
screenshot_cache=screenshot_cache,
timeout_multiplier=timeout_multiplier,
debug_info=debug_info)
self.protocol = ServoWebDriverProtocol(self, browser,
capabilities=capabilities)
self.implementation = RefTestImplementation(self)
self.timeout = None
with open(os.path.join(here, "test-wait.js")) as f:
self.wait_script = f.read() % {"classname": "reftest-wait"}
def reset(self):
self.implementation.reset()
def is_alive(self):
return self.protocol.is_alive()
def do_test(self, test):
try:
result = self.implementation.run_test(test)
return self.convert_result(test, result)
except OSError:
return test.make_result("CRASH", None), []
except TimeoutError:
return test.make_result("TIMEOUT", None), []
except Exception as e:
message = getattr(e, "message", "")
if message:
message += "\n"
message += traceback.format_exc()
return test.make_result("INTERNAL-ERROR", message), []
def screenshot(self, test, viewport_size, dpi, page_ranges):
# https://github.com/web-platform-tests/wpt/issues/7135
assert viewport_size is None
assert dpi is None
timeout = (test.timeout * self.timeout_multiplier + self.extra_timeout
if self.debug_info is None else None)
if self.timeout != timeout:
try:
self.protocol.session.timeouts.script = timeout
self.timeout = timeout
except OSError:
msg = "Lost webdriver connection"
self.logger.error(msg)
return ("INTERNAL-ERROR", msg)
return ServoWebDriverRun(self.logger,
self._screenshot,
self.protocol,
self.test_url(test),
timeout,
self.extra_timeout).run()
def _screenshot(self, session, url, timeout):
session.url = url
session.execute_async_script(self.wait_script)
return session.screenshot()
WebDriverRefTestExecutor.__init__(self, logger, browser, server_config,
timeout_multiplier, screenshot_cache,
capabilities=capabilities,
debug_info=debug_info)
def on_environment_change(self, new_environment):
self.protocol.session.extension.change_prefs(
self.protocol.webdriver.extension.change_prefs(
self.last_environment.get("prefs", {}),
new_environment.get("prefs", {})
)

View file

@ -1,23 +0,0 @@
setup({output:%(output)d, debug: %(debug)s});
add_completion_callback(function() {
add_completion_callback(function (tests, status) {
var subtest_results = tests.map(function(x) {
return [x.name, x.status, x.message, x.stack]
});
var id = location.pathname + location.search + location.hash;
var results = JSON.stringify([id,
status.status,
status.message,
status.stack,
subtest_results]);
(function done() {
if (window.__wd_results_callback__) {
clearTimeout(__wd_results_timer__);
__wd_results_callback__(results)
} else {
setTimeout(done, 20);
}
})()
})
});