Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wptrunner] Lazily post testdriver result while polling the next message #49513

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions tools/wptrunner/wptrunner/executors/executorchrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@

from webdriver import error

from .base import strip_server
from .executorwebdriver import (
WebDriverBaseProtocolPart,
WebDriverCrashtestExecutor,
WebDriverFedCMProtocolPart,
WebDriverPrintRefTestExecutor,
WebDriverProtocol,
WebDriverRefTestExecutor,
WebDriverTestDriverProtocolPart,
WebDriverTestharnessExecutor,
WebDriverTestharnessProtocolPart,
)
Expand Down Expand Up @@ -94,6 +96,37 @@ def get_counters(self) -> Mapping[str, int]:
return counters


class ChromeDriverTestDriverProtocolPart(WebDriverTestDriverProtocolPart):
"""An interface to the browser-side testdriver infrastructure that lazily settles calls."""

def setup(self):
super().setup()
self._pending_message = ""

def send_message(self, cmd_id, message_type, status, message=None):
message_script = self._format_send_message_script(cmd_id, message_type, status, message)
if message_type == "complete":
assert not self._pending_message, self._pending_message
self._pending_message = message_script
else:
self.webdriver.execute_script(message_script)

jonathan-j-lee marked this conversation as resolved.
Show resolved Hide resolved
def _get_next_message_classic(self, url):
try:
message_script, self._pending_message = self._pending_message, ""
return self.parent.base.execute_script(message_script + self.script_resume,
asynchronous=True,
args=[strip_server(url)])
except error.JavascriptErrorException as js_error:
# TODO(crbug.com/340662810): Cycle testdriver event loop to work
# around `testharnessreport.js` flakily not loaded.
if re.search(r'window\.__wptrunner_process_next_event is not a function',
js_error.message):
time.sleep(0.05)
return None
raise


class ChromeDriverTestharnessProtocolPart(WebDriverTestharnessProtocolPart):
"""Implementation of `testharness.js` tests controlled by ChromeDriver.

Expand Down Expand Up @@ -156,6 +189,7 @@ class ChromeDriverProtocol(WebDriverProtocol):
ChromeDriverBaseProtocolPart,
ChromeDriverDevToolsProtocolPart,
ChromeDriverFedCMProtocolPart,
ChromeDriverTestDriverProtocolPart,
ChromeDriverTestharnessProtocolPart,
]
for base_part in WebDriverProtocol.implements:
Expand Down Expand Up @@ -246,18 +280,6 @@ def get_or_create_test_window(self, protocol):
self.protocol.testharness.persistent_test_window = test_window
return test_window

def _get_next_message_classic(self, protocol, url, test_window):
try:
return super()._get_next_message_classic(protocol, url, test_window)
except error.JavascriptErrorException as js_error:
# TODO(crbug.com/340662810): Cycle testdriver event loop to work
# around `testharnessreport.js` flakily not loaded.
if re.search(r'window\.__wptrunner_process_next_event is not a function',
js_error.message):
time.sleep(0.05)
return None
raise


@_evaluate_sanitized_result
class ChromeDriverPrintRefTestExecutor(WebDriverPrintRefTestExecutor):
Expand Down
121 changes: 64 additions & 57 deletions tools/wptrunner/wptrunner/executors/executorwebdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,16 +433,70 @@ def release(self):
class WebDriverTestDriverProtocolPart(TestDriverProtocolPart):
def setup(self):
self.webdriver = self.parent.webdriver
with open(os.path.join(here, "testharness_webdriver_resume.js")) as f:
self.script_resume = f.read()

def get_next_message(self, url, test_window):
if hasattr(self.parent, "bidi_script"):
# If `bidi_script` is available, the messages can be handled via BiDi.
return self._get_next_message_bidi(url, test_window)
else:
return self._get_next_message_classic(url)

def _get_next_message_classic(self, url):
"""
Get the next message from the test_driver using the classic WebDriver async script execution. This will block
the event loop until the test_driver send a message.
"""
return self.parent.base.execute_script(self.script_resume, asynchronous=True, args=[strip_server(url)])

def _get_next_message_bidi(self, url, test_window):
"""
Get the next message from the test_driver using async call. This will not block the event loop, which allows for
processing the events from the test_runner to test_driver while waiting for the next test_driver commands.
"""
# As long as we want to be able to use scripts both in bidi and in classic mode, the script should
# be wrapped to some harness to emulate the WebDriver Classic async script execution. The script
# will be provided with the `resolve` delegate, which finishes the execution. After that the
# coroutine is finished as well.
wrapped_script = """async function(...args){
return new Promise((resolve, reject) => {
args.push(resolve);
(async function(){
%s
}).apply(null, args);
})
}""" % self.script_resume

bidi_url_argument = {
"type": "string",
"value": strip_server(url)
}

# `run_until_complete` allows processing BiDi events in the same loop while waiting for the next message.
message = self.parent.loop.run_until_complete(self.parent.bidi_script.call_function(
wrapped_script, target={
"context": test_window
},
arguments=[bidi_url_argument]))
# The message is in WebDriver BiDi format. Deserialize it.
deserialized_message = bidi_deserialize(message)
return deserialized_message

def send_message(self, cmd_id, message_type, status, message=None):
self.webdriver.execute_script(
self._format_send_message_script(cmd_id, message_type, status, message))

def _format_send_message_script(self, cmd_id, message_type, status, message=None):
obj = {
"cmd_id": cmd_id,
"type": "testdriver-%s" % str(message_type),
"type": f"testdriver-{message_type}",
"status": str(status)
}
if message:
obj["message"] = str(message)
self.webdriver.execute_script("window.postMessage(%s, '*')" % json.dumps(obj))
return f"window.postMessage({json.dumps(obj)}, '*');"


def _switch_to_frame(self, index_or_elem):
try:
Expand Down Expand Up @@ -782,7 +836,6 @@ def run_func(self):
class WebDriverTestharnessExecutor(TestharnessExecutor):
supports_testdriver = True
protocol_cls = WebDriverProtocol
_get_next_message = None

def __init__(self, logger, browser, server_config, timeout_multiplier=1,
close_after_done=True, capabilities=None, debug_info=None,
Expand All @@ -792,16 +845,9 @@ def __init__(self, logger, browser, server_config, timeout_multiplier=1,
timeout_multiplier=timeout_multiplier,
debug_info=debug_info)
self.protocol = self.protocol_cls(self, browser, capabilities)
with open(os.path.join(here, "testharness_webdriver_resume.js")) as f:
self.script_resume = f.read()
with open(os.path.join(here, "window-loaded.js")) as f:
self.window_loaded_script = f.read()

if hasattr(self.protocol, 'bidi_script'):
# If `bidi_script` is available, the messages can be handled via BiDi.
self._get_next_message = self._get_next_message_bidi
else:
self._get_next_message = self._get_next_message_classic

self.close_after_done = close_after_done
self.cleanup_after_test = cleanup_after_test
Expand Down Expand Up @@ -855,11 +901,13 @@ async def process_bidi_event(method, params):
self.logger.debug(f"Received bidi event: {method}, {params}")
if hasattr(protocol, 'bidi_browsing_context') and method == "browsingContext.userPromptOpened" and \
params["context"] == test_window:
# User prompts of the test window are handled separately. In classic implementation, this user
# prompt always causes an exception when `_get_next_message` is called. In BiDi it's not a case,
# as the BiDi protocol allows sending commands even with the user prompt opened. However, the
# user prompt can block the testdriver JS execution and cause the dead loop. To overcome this
# issue, the user prompt of the test window is always dismissed and the test is failing.
# User prompts of the test window are handled separately. In classic
# implementation, this user prompt always causes an exception when
# `protocol.testdriver.get_next_message()` is called. In BiDi it's not the
# case, as the BiDi protocol allows sending commands even with the user
# prompt opened. However, the user prompt can block the testdriver JS
# execution and cause a dead loop. To overcome this issue, the user prompt
# of the test window is always dismissed and the test is failing.
try:
await protocol.bidi_browsing_context.handle_user_prompt(params["context"])
except Exception as e:
Expand Down Expand Up @@ -896,7 +944,7 @@ async def process_bidi_event(method, params):
# TODO: what to do if there are more then 1 unexpected exceptions?
raise unexpected_exceptions[0]

test_driver_message = self._get_next_message(protocol, url, test_window)
test_driver_message = protocol.testdriver.get_next_message(url, test_window)
self.logger.debug("Receive message from testdriver: %s" % test_driver_message)
jonathan-j-lee marked this conversation as resolved.
Show resolved Hide resolved

# As of 2019-03-29, WebDriver does not define expected behavior for
Expand Down Expand Up @@ -960,47 +1008,6 @@ async def process_bidi_event(method, params):
def get_or_create_test_window(self, protocol):
return protocol.base.create_window()

def _get_next_message_classic(self, protocol, url, _):
"""
Get the next message from the test_driver using the classic WebDriver async script execution. This will block
the event loop until the test_driver send a message.
:param window:
"""
return protocol.base.execute_script(self.script_resume, asynchronous=True, args=[strip_server(url)])

def _get_next_message_bidi(self, protocol, url, test_window):
"""
Get the next message from the test_driver using async call. This will not block the event loop, which allows for
processing the events from the test_runner to test_driver while waiting for the next test_driver commands.
"""
# As long as we want to be able to use scripts both in bidi and in classic mode, the script should
# be wrapped to some harness to emulate the WebDriver Classic async script execution. The script
# will be provided with the `resolve` delegate, which finishes the execution. After that the
# coroutine is finished as well.
wrapped_script = """async function(...args){
return new Promise((resolve, reject) => {
args.push(resolve);
(async function(){
%s
}).apply(null, args);
})
}""" % self.script_resume

bidi_url_argument = {
"type": "string",
"value": strip_server(url)
}

# `run_until_complete` allows processing BiDi events in the same loop while waiting for the next message.
message = protocol.loop.run_until_complete(protocol.bidi_script.call_function(
wrapped_script, target={
"context": test_window
},
arguments=[bidi_url_argument]))
# The message is in WebDriver BiDi format. Deserialize it.
deserialized_message = bidi_deserialize(message)
return deserialized_message


class WebDriverRefTestExecutor(RefTestExecutor):
protocol_cls = WebDriverProtocol
Expand Down
Loading