From 8469799b90243448198c8cd52547e7afd20614a5 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 4 Oct 2024 22:38:51 -0400 Subject: [PATCH] [wptrunner] Split `message-queue.js` from `testharnessreport.js` 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. --- tools/wptrunner/wptrunner/environment.py | 67 ++++++++-------- .../wptrunner/executors/message-queue.js | 77 +++++++++++++++++++ tools/wptrunner/wptrunner/testdriver-extra.js | 4 +- .../wptrunner/wptrunner/testharnessreport.js | 77 ++----------------- tools/wptserve/wptserve/handlers.py | 14 ++-- 5 files changed, 124 insertions(+), 115 deletions(-) create mode 100644 tools/wptrunner/wptrunner/executors/message-queue.js diff --git a/tools/wptrunner/wptrunner/environment.py b/tools/wptrunner/wptrunner/environment.py index f8538b7da1d7f4..5b02354b9d29f5 100644 --- a/tools/wptrunner/wptrunner/environment.py +++ b/tools/wptrunner/wptrunner/environment.py @@ -23,8 +23,6 @@ sys.path.insert(0, repo_root) from tools import localpaths # noqa: F401 -from wptserve.handlers import StringHandler - serve = None @@ -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 @@ -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, diff --git a/tools/wptrunner/wptrunner/executors/message-queue.js b/tools/wptrunner/wptrunner/executors/message-queue.js new file mode 100644 index 00000000000000..bbad17180e0a82 --- /dev/null +++ b/tools/wptrunner/wptrunner/executors/message-queue.js @@ -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]); + }; +})(); diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index 87ae9e1f33d02f..079081c3a6d44d 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -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 @@ -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; diff --git a/tools/wptrunner/wptrunner/testharnessreport.js b/tools/wptrunner/wptrunner/testharnessreport.js index d385692445c508..fb9e8c678de63a 100644 --- a/tools/wptrunner/wptrunner/testharnessreport.js +++ b/tools/wptrunner/wptrunner/testharnessreport.js @@ -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, @@ -85,4 +19,3 @@ window.__wptrunner_process_next_event = function() { }); setup(props); })(); - diff --git a/tools/wptserve/wptserve/handlers.py b/tools/wptserve/wptserve/handlers.py index 62faf47d645692..cde04b13cbb088 100644 --- a/tools/wptserve/wptserve/handlers.py +++ b/tools/wptserve/wptserve/handlers.py @@ -513,11 +513,13 @@ 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 @@ -525,7 +527,7 @@ def __init__(self, path, format_args, content_type, **headers): 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): @@ -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)