Skip to content

Commit

Permalink
[wptrunner] Split message-queue.js from testharnessreport.js
Browse files Browse the repository at this point in the history
This will allow non-testharness tests to use `testdriver.js` without
needing extra scripts. Evaluating `message-queue.js` is idempotent so
that, when using testharness with testdriver, the second inclusion is a
no-op.

Because resource scripts are cached, the size increase should not
meaningfully affect test performance.
  • Loading branch information
jonathan-j-lee committed Nov 11, 2024
1 parent 51674f3 commit 8469799
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 115 deletions.
67 changes: 31 additions & 36 deletions tools/wptrunner/wptrunner/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
sys.path.insert(0, repo_root)
from tools import localpaths # noqa: F401

from wptserve.handlers import StringHandler

serve = None


Expand Down Expand Up @@ -225,29 +223,47 @@ def get_routes(self):
self.config.aliases,
self.config)

testharnessreport_format_args = {
"output": self.pause_after_test,
"timeout_multiplier": self.testharness_timeout_multipler,
"explicit_timeout": "true" if self.debug_info is not None else "false",
"debug": "true" if self.debug_test else "false",
}
for path, format_args, content_type, route in [
("testharness_runner.html", {}, "text/html", "/testharness_runner.html"),
("print_pdf_runner.html", {}, "text/html", "/print_pdf_runner.html"),
(os.path.join(here, "..", "..", "third_party", "pdf_js", "pdf.js"), None,
(os.path.join(here, "..", "..", "third_party", "pdf_js", "pdf.js"), {},
"text/javascript", "/_pdf_js/pdf.js"),
(os.path.join(here, "..", "..", "third_party", "pdf_js", "pdf.worker.js"), None,
(os.path.join(here, "..", "..", "third_party", "pdf_js", "pdf.worker.js"), {},
"text/javascript", "/_pdf_js/pdf.worker.js"),
(self.options.get("testharnessreport", "testharnessreport.js"),
{"output": self.pause_after_test,
"timeout_multiplier": self.testharness_timeout_multipler,
"explicit_timeout": "true" if self.debug_info is not None else "false",
"debug": "true" if self.debug_test else "false"},
"text/javascript;charset=utf8",
"/resources/testharnessreport.js")]:
path = os.path.normpath(os.path.join(here, path))
(
self.options.get("testharnessreport", [
# All testharness tests, even those that don't use testdriver, require
# `message-queue.js` to signal completion.
os.path.join("executors", "message-queue.js"),
"testharnessreport.js"]),
testharnessreport_format_args,
"text/javascript;charset=utf8",
"/resources/testharnessreport.js",
),
(
[os.path.join(repo_root, "resources", "testdriver.js"),
# Include `message-queue.js` to support testdriver in non-testharness tests.
os.path.join("executors", "message-queue.js"),
"testdriver-extra.js"],
{},
"text/javascript",
"/resources/testdriver.js",
),
]:
paths = [path] if isinstance(path, str) else path
abs_paths = [os.path.normpath(os.path.join(here, path)) for path in paths]
# Note that .headers. files don't apply to static routes, so we need to
# readd any static headers here.
headers = {"Cache-Control": "max-age=3600"}
route_builder.add_static(path, format_args, content_type, route,
route_builder.add_static(abs_paths, format_args, content_type, route,
headers=headers)

route_builder.add_handler("GET", "/resources/testdriver.js", TestdriverLoader())

for url_base, test_root in self.test_paths.items():
if url_base == "/":
continue
Expand Down Expand Up @@ -315,27 +331,6 @@ def test_servers(self):
return failed, pending


class TestdriverLoader:
"""A special static handler for serving `/resources/testdriver.js`.
This handler lazily reads `testdriver{,-extra}.js` so that wptrunner doesn't
need to pass the entire file contents to child `wptserve` processes, which
can slow `wptserve` startup by several seconds (crbug.com/1479850).
"""
def __init__(self):
self._handler = None

def __call__(self, request, response):
if not self._handler:
data = b""
with open(os.path.join(repo_root, "resources", "testdriver.js"), "rb") as fp:
data += fp.read()
with open(os.path.join(here, "testdriver-extra.js"), "rb") as fp:
data += fp.read()
self._handler = StringHandler(data, "text/javascript")
return self._handler(request, response)


def wait_for_service(logger: StructuredLogger,
host: str,
port: int,
Expand Down
77 changes: 77 additions & 0 deletions tools/wptrunner/wptrunner/executors/message-queue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
(function() {
if (window.__wptrunner_message_queue && window.__wptrunner_process_next_event) {
// Another script already set up the testdriver infrastructure.
return;
}

class MessageQueue {
constructor() {
this.item_id = 0;
this._queue = [];
}

push(item) {
let cmd_id = this.item_id++;
item.id = cmd_id;
this._queue.push(item);
__wptrunner_process_next_event();
return cmd_id;
}

shift() {
return this._queue.shift();
}
}

window.__wptrunner_testdriver_callback = null;
window.__wptrunner_message_queue = new MessageQueue();
window.__wptrunner_url = null;

window.__wptrunner_process_next_event = function() {
/* This function handles the next testdriver event. The presence of
window.testdriver_callback is used as a switch; when that function
is present we are able to handle the next event and when is is not
present we must wait. Therefore to drive the event processing, this
function must be called in two circumstances:
* Every time there is a new event that we may be able to handle
* Every time we set the callback function
This function unsets the callback, so no further testdriver actions
will be run until it is reset, which wptrunner does after it has
completed handling the current action.
*/

if (!window.__wptrunner_testdriver_callback) {
return;
}
var data = window.__wptrunner_message_queue.shift();
if (!data) {
return;
}

var payload = undefined;

switch(data.type) {
case "complete":
var tests = data.tests;
var status = data.status;

var subtest_results = tests.map(function(x) {
return [x.name, x.status, x.message, x.stack];
});
payload = [status.status,
status.message,
status.stack,
subtest_results];
clearTimeout(window.__wptrunner_timer);
break;
case "action":
payload = data;
break;
default:
return;
}
var callback = window.__wptrunner_testdriver_callback;
window.__wptrunner_testdriver_callback = null;
callback([__wptrunner_url, data.type, payload]);
};
})();
4 changes: 2 additions & 2 deletions tools/wptrunner/wptrunner/testdriver-extra.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
});

function is_test_context() {
return window.__wptrunner_message_queue !== undefined;
return !!window.__wptrunner_is_test_context;
}

// Code copied from /common/utils.js
Expand Down Expand Up @@ -226,7 +226,7 @@
};

window.test_driver_internal.set_test_context = function(context) {
if (window.__wptrunner_message_queue) {
if (is_test_context()) {
throw new Error("Tried to set testharness context in a window containing testharness.js");
}
testharness_context = context;
Expand Down
77 changes: 5 additions & 72 deletions tools/wptrunner/wptrunner/testharnessreport.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,9 @@
class MessageQueue {
constructor() {
this.item_id = 0;
this._queue = [];
}

push(item) {
let cmd_id = this.item_id++;
item.id = cmd_id;
this._queue.push(item);
__wptrunner_process_next_event();
return cmd_id;
}

shift() {
return this._queue.shift();
}
}

window.__wptrunner_testdriver_callback = null;
window.__wptrunner_message_queue = new MessageQueue();
window.__wptrunner_url = null;

window.__wptrunner_process_next_event = function() {
/* This function handles the next testdriver event. The presence of
window.testdriver_callback is used as a switch; when that function
is present we are able to handle the next event and when is is not
present we must wait. Therefore to drive the event processing, this
function must be called in two circumstances:
* Every time there is a new event that we may be able to handle
* Every time we set the callback function
This function unsets the callback, so no further testdriver actions
will be run until it is reset, which wptrunner does after it has
completed handling the current action.
*/

if (!window.__wptrunner_testdriver_callback) {
return;
}
var data = window.__wptrunner_message_queue.shift();
if (!data) {
return;
}

var payload = undefined;

switch(data.type) {
case "complete":
var tests = data.tests;
var status = data.status;

var subtest_results = tests.map(function(x) {
return [x.name, x.status, x.message, x.stack];
});
payload = [status.status,
status.message,
status.stack,
subtest_results];
clearTimeout(window.__wptrunner_timer);
break;
case "action":
payload = data;
break;
default:
return;
}
var callback = window.__wptrunner_testdriver_callback;
window.__wptrunner_testdriver_callback = null;
callback([__wptrunner_url, data.type, payload]);
};

(function() {
// Signal to `testdriver.js` that this is the "main" test browsing context,
// meaning testdriver actions should be queued for retrieval instead of
// `postMessage()`d elsewhere.
window.__wptrunner_is_test_context = true;

var props = {output: %(output)d,
timeout_multiplier: %(timeout_multiplier)s,
explicit_timeout: %(explicit_timeout)s,
Expand All @@ -85,4 +19,3 @@ window.__wptrunner_process_next_event = function() {
});
setup(props);
})();

14 changes: 9 additions & 5 deletions tools/wptserve/wptserve/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,19 +513,21 @@ def __init__(self, path, format_args, content_type, **headers):
Note that *.headers files have no effect in this handler.
:param path: Path to the template file to use
:param path: Path(s) to template files to use. If a sequence of paths is provided instead
of a single path, the contents of each file will be concatenated together before the
`format_args` are interpolated.
:param format_args: Dictionary of values to substitute into the template file
:param content_type: Content type header to server the response with
:param headers: List of headers to send with responses"""
self._path = path
self._paths = [path] if isinstance(path, str) else path
self._format_args = format_args
self._content_type = content_type
self._headers = headers
self._handler = None

def __getnewargs_ex__(self):
# Do not pickle `self._handler`, which can be arbitrarily large.
args = self._path, self._format_args, self._content_type
args = self._paths, self._format_args, self._content_type
return args, self._headers

def __call__(self, request, response):
Expand All @@ -534,8 +536,10 @@ def __call__(self, request, response):
# contents across processes can slow `wptserve` startup by several
# seconds (crbug.com/1479850).
if not self._handler:
with open(self._path) as f:
data = f.read()
data = ""
for path in self._paths:
with open(path) as f:
data += f.read()
if self._format_args:
data = data % self._format_args
self._handler = StringHandler(data, self._content_type, **self._headers)
Expand Down

0 comments on commit 8469799

Please sign in to comment.