From b0aea868675eebb1d08ce482a1e868d58398ef98 Mon Sep 17 00:00:00 2001 From: James Scott Date: Thu, 18 Dec 2025 14:02:12 +0000 Subject: [PATCH 1/3] feat(serve): Add server-side handlers for Test262 tests This commit introduces the necessary server-side handlers within `wptserve` to dynamically generate HTML wrappers for Test262 JavaScript tests. This is needed to enable Test262 execution within WPT. Key changes and their purpose: - Introduction of several new `HtmlWrapperHandler` and `WrapperHandler` subclasses (e.g., `Test262WindowHandler`, `Test262WindowTestHandler`, `Test262StrictHandler`). These handlers are responsible for: - Identifying Test262 test requests based on URL patterns. - Dynamically constructing an HTML page that loads the Test262 `.js` test file within an isolated `iframe`. - Injecting the required Test262 harness files (`assert.js`, `sta.js`) and the WPT-specific `testharness-client.js` and `harness-adapter.js` into the generated HTML. - Processing Test262-specific metadata (like `includes` and `negative` expectations) extracted by the manifest tooling from PR 1. - Updates to `RoutesBuilder` in `serve.py` to map incoming requests for Test262 test URLs to the appropriate new handler. - Unit tests in `test_serve.py` to validate the correct behavior of these new handlers, including URL rewriting, metadata processing, and the structure of the generated HTML wrappers. This work directly supports the integration of Test262 into WPT as detailed in the RFC: https://github.com/web-platform-tests/rfcs/pull/229 This commit is the second in a series of smaller PRs split from the larger, original implementation in https://github.com/web-platform-tests/wpt/pull/55997. --- tools/serve/serve.py | 104 +++++++++++++++++++- tools/serve/test_serve.py | 195 +++++++++++++++++++++++++++++++++++++- 2 files changed, 297 insertions(+), 2 deletions(-) diff --git a/tools/serve/serve.py b/tools/serve/serve.py index e8c42129702ae1..a02bdbea6574bf 100644 --- a/tools/serve/serve.py +++ b/tools/serve/serve.py @@ -19,11 +19,12 @@ from io import IOBase from itertools import chain, product from html5lib import html5parser -from typing import ClassVar, List, Optional, Set, Tuple +from typing import ClassVar, List, Optional, Set, Tuple, Union from localpaths import repo_root # type: ignore from manifest.sourcefile import read_script_metadata, js_meta_re, parse_variants # type: ignore +from manifest.test262 import parse as test262_parse # type: ignore from wptserve import server as wptserve, handlers from wptserve import stash from wptserve import config @@ -327,6 +328,75 @@ class ExtensionHandler(HtmlWrapperHandler): """ +class Test262WindowHandler(HtmlWrapperHandler): + path_replace = [(".test262.html", ".js", ".test262-test.html")] + wrapper = """ + +Test + + +%(meta)s +%(script)s +
+""" + + +class Test262WindowTestHandler(HtmlWrapperHandler): + # For SHAB + headers = [('Cross-Origin-Opener-Policy', 'same-origin'), + ('Cross-Origin-Embedder-Policy', 'require-corp')] + + path_replace: Union[List[Tuple[str, str]], List[Tuple[str, str, str]]] = [(".test262-test.html", ".js")] + + pre_wrapper = """ + +Test + + + + +%(meta)s +%(script)s""" + wrapper = pre_wrapper + """ + +""" + + def _get_metadata(self, request): + path = self._get_filesystem_path(request) + with open(path, encoding='utf-8') as f: + test_record = test262_parse(logging.getLogger(), f.read(), path) + yield from (('script', "/third_party/test262/harness/%s" % filename) + for filename in (test_record.includes or [])) + + expected_error = (test_record.negative or {}).get('type', None) + if expected_error is not None: + yield ('negative', expected_error) + + def _meta_replacement(self, key: str, value: str) -> Optional[str]: + if key == 'negative': + return """""" % value + return None + + +class Test262WindowModuleHandler(Test262WindowHandler): + path_replace = [(".test262-module.html", ".js", ".test262-module-test.html")] + +class Test262WindowModuleTestHandler(Test262WindowTestHandler): + path_replace = [(".test262-module-test.html", ".js")] + wrapper = Test262WindowTestHandler.pre_wrapper + """""" + + +class Test262StrictWindowHandler(Test262WindowHandler): + path_replace = [(".test262.strict.html", ".js", ".test262-test.strict.html")] + +class Test262StrictWindowTestHandler(Test262WindowTestHandler): + path_replace = [(".test262-test.strict.html", ".js", ".test262.strict.js")] + + class WindowModulesHandler(HtmlWrapperHandler): global_type = "window-module" path_replace = [(".any.window-module.html", ".any.js")] @@ -574,6 +644,31 @@ class ShadowRealmInAudioWorkletHandler(HtmlWrapperHandler): """ +class Test262StrictHandler(WrapperHandler): + path_replace = [(".test262.strict.js", ".js")] + headers = [('Content-Type', 'text/javascript')] + wrapper = """ +"use strict"; +%(script)s +""" + + def _meta_replacement(self, key, value): + return None + + def _get_metadata(self, request): + # Abuse the script metadata to inline the script content so as to + # prepend "use strict". + path = self._get_filesystem_path(request) + try: + with open(path, encoding='utf-8') as f: + yield ('script', f.read()) + except OSError: + raise HTTPException(404) + + def _script_replacement(self, key, value): + return value + + class BaseWorkerHandler(WrapperHandler): headers = [("Content-Type", "text/javascript")] @@ -787,6 +882,13 @@ def add_mount_point(self, url_base, path): ("GET", "*.worker.html", WorkersHandler), ("GET", "*.worker-module.html", WorkerModulesHandler), ("GET", "*.window.html", WindowHandler), + ("GET", "*.test262.html", Test262WindowHandler), + ("GET", "*.test262-test.html", Test262WindowTestHandler), + ("GET", "*.test262-module.html", Test262WindowModuleHandler), + ("GET", "*.test262-module-test.html", Test262WindowModuleTestHandler), + ("GET", "*.test262.strict.html", Test262StrictWindowHandler), + ("GET", "*.test262-test.strict.html", Test262StrictWindowTestHandler), + ("GET", "*.test262.strict.js", Test262StrictHandler), ("GET", "*.extension.html", ExtensionHandler), ("GET", "*.any.html", AnyHtmlHandler), ("GET", "*.any.sharedworker.html", SharedWorkersHandler), diff --git a/tools/serve/test_serve.py b/tools/serve/test_serve.py index 9dcda584de2ea4..100b50f2b559e5 100644 --- a/tools/serve/test_serve.py +++ b/tools/serve/test_serve.py @@ -1,15 +1,30 @@ # mypy: allow-untyped-defs +import builtins +import io import logging import os import pickle import platform +from unittest.mock import MagicMock, patch +from typing import Generator, List, Tuple, Type import pytest import localpaths # type: ignore from . import serve -from .serve import ConfigBuilder, inject_script +from .serve import ( + ConfigBuilder, + WrapperHandler, + inject_script, + # Use 'T262' aliases to avoid naming collisions with the pytest collector + Test262WindowHandler as T262WindowHandler, + Test262WindowTestHandler as T262WindowTestHandler, + Test262WindowModuleHandler as T262WindowModuleHandler, + Test262WindowModuleTestHandler as T262WindowModuleTestHandler, + Test262StrictWindowHandler as T262StrictWindowHandler, + Test262StrictWindowTestHandler as T262StrictWindowTestHandler, + Test262StrictHandler as T262StrictHandler) logger = logging.getLogger() @@ -154,3 +169,181 @@ def test_inject_script_parse_error(): # On a parse error, the script should not be injected and the original content should be # returned. assert INJECT_SCRIPT_MARKER not in inject_script(html.replace(INJECT_SCRIPT_MARKER, b""), INJECT_SCRIPT_MARKER) + + +@pytest.fixture +def test262_handlers() -> Generator[Tuple[str, str], None, None]: + tests_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "tests", "testdata")) + url_base = "/" + + mock_file_contents = { + os.path.normpath(os.path.join(tests_root, "test262", "basic.js")): """/*---\ndescription: A basic test +includes: [assert.js, sta.js] +---*/ +assert.sameValue(1, 1); +""", + os.path.normpath(os.path.join(tests_root, "test262", "negative.js")): """/*---\ndescription: A negative test +negative: + phase: runtime + type: TypeError +---*/ +throw new TypeError(); +""", + os.path.normpath(os.path.join(tests_root, "test262", "module.js")): """/*---\ndescription: A module test +flags: [module] +---*/ +import {} from 'some-module'; +""", + os.path.normpath(os.path.join(tests_root, "test262", "teststrict.js")): """/*---\ndescription: A strict mode test +flags: [onlyStrict] +includes: [propertyHelper.js] +---*/ +console.log('hello'); +""" + } + + # Store original functions to be called if our mock doesn't handle the file + original_open = builtins.open + original_exists = os.path.exists + original_isdir = os.path.isdir + + def custom_open(file, mode='r', *args, **kwargs): + normalized_file = os.path.normpath(file) + if normalized_file in mock_file_contents: + if 'b' in mode: + return io.BytesIO(mock_file_contents[normalized_file].encode('utf-8')) + else: + return io.StringIO(mock_file_contents[normalized_file]) + return original_open(file, mode, *args, **kwargs) + + def custom_exists(path): + normalized_path = os.path.normpath(path) + return normalized_path in mock_file_contents or original_exists(path) + + def custom_isdir(path): + normalized_path = os.path.normpath(path) + expected_dir = os.path.normpath(os.path.join(tests_root, "test262")) + return normalized_path == expected_dir or original_isdir(path) + + with patch('builtins.open', side_effect=custom_open), \ + patch('os.path.exists', side_effect=custom_exists), \ + patch('os.path.isdir', side_effect=custom_isdir): + yield tests_root, url_base + + +def _create_mock_request(path: str) -> MagicMock: + mock_request = MagicMock() + mock_request.url_parts.path = path + mock_request.url_parts.query = "" + return mock_request + + +def _test_handler_path_replace(handler_cls: Type[WrapperHandler], + tests_root: str, + url_base: str, + expected: List[Tuple[str, str]]) -> None: + handler = handler_cls(base_path=tests_root, url_base=url_base) + assert handler.path_replace == expected + +def _test_handler_wrapper_content(handler_cls: Type[WrapperHandler], + tests_root: str, + url_base: str, + request_path: str, + expected_content: List[str]) -> None: + handler = handler_cls(base_path=tests_root, url_base=url_base) + mock_request = _create_mock_request(request_path) + mock_response = MagicMock() + handler.handle_request(mock_request, mock_response) # type: ignore[no-untyped-call] + content = mock_response.content + for item in expected_content: + assert item in content + +def _test_handler_get_metadata(handler_cls: Type[WrapperHandler], + tests_root: str, + url_base: str, + request_path: str, + expected_metadata: List[Tuple[str, str]]) -> None: + handler = handler_cls(tests_root, url_base) + mock_request = _create_mock_request(request_path) + metadata = list(handler._get_metadata(mock_request)) # type: ignore[no-untyped-call] + for item in expected_metadata: + assert item in metadata + assert len(expected_metadata) == len(metadata), f"{expected_metadata} != {metadata}" + + +@pytest.mark.parametrize("handler_cls, expected", [ + (T262WindowHandler, [(".test262.html", ".js", ".test262-test.html")]), + (T262WindowTestHandler, [(".test262-test.html", ".js")]), + (T262WindowModuleHandler, [(".test262-module.html", ".js", ".test262-module-test.html")]), + (T262WindowModuleTestHandler, [(".test262-module-test.html", ".js")]), + (T262StrictWindowHandler, [(".test262.strict.html", ".js", ".test262-test.strict.html")]), + (T262StrictWindowTestHandler, [(".test262-test.strict.html", ".js", ".test262.strict.js")]), +]) +def test_path_replace(test262_handlers, handler_cls, expected): + tests_root, url_base = test262_handlers + _test_handler_path_replace(handler_cls, tests_root, url_base, expected) + + +@pytest.mark.parametrize("handler_cls, request_path, expected_metadata", [ + ( + T262WindowTestHandler, + "/test262/basic.test262-test.html", + [('script', '/third_party/test262/harness/assert.js'), ('script', '/third_party/test262/harness/sta.js')] + ), + ( + T262WindowTestHandler, + "/test262/negative.test262-test.html", + [('negative', 'TypeError')] + ), + ( + T262StrictWindowTestHandler, + "/test262/teststrict.test262-test.strict.html", + [('script', '/third_party/test262/harness/propertyHelper.js')] + ), +]) +def test_get_metadata(test262_handlers, handler_cls, request_path, expected_metadata): + tests_root, url_base = test262_handlers + _test_handler_get_metadata(handler_cls, tests_root, url_base, request_path, expected_metadata) + + +@pytest.mark.parametrize("handler_cls, request_path, expected_substrings", [ + # T262WindowHandler: Should contain the iframe pointing to the test + ( + T262WindowHandler, + "/test262/basic.test262.html", + [''] + ), + # T262WindowTestHandler: Should contain script tags + ( + T262WindowTestHandler, + "/test262/basic.test262-test.html", + ['', '', ''] + ), + # T262WindowModuleTestHandler: Should contain module import + ( + T262WindowModuleTestHandler, + "/test262/module.test262-module-test.html", + ['"] + ), + # Strict HTML Case: points to the .strict.js variant + ( + T262StrictWindowTestHandler, + "/test262/teststrict.test262-test.strict.html", + ['src="/test262/teststrict.test262.strict.js"'] + ), + # Strict JS Case: The handler that serves the actual script + ( + T262StrictHandler, + "/test262/teststrict.test262.strict.js", + ['"use strict";', "console.log('hello');"] + ), +]) +def test_wrapper_content(test262_handlers, handler_cls, request_path, expected_substrings): + tests_root, url_base = test262_handlers + _test_handler_wrapper_content(handler_cls, tests_root, url_base, request_path, expected_substrings) From df146d59ea0d79a41df94ef455330637df8658b8 Mon Sep 17 00:00:00 2001 From: James Scott Date: Fri, 19 Dec 2025 17:55:58 +0000 Subject: [PATCH 2/3] do not use import alias for Test262 * in tests --- tools/serve/test_serve.py | 60 +++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/tools/serve/test_serve.py b/tools/serve/test_serve.py index 100b50f2b559e5..16f2c35ca9f90a 100644 --- a/tools/serve/test_serve.py +++ b/tools/serve/test_serve.py @@ -15,16 +15,15 @@ from . import serve from .serve import ( ConfigBuilder, + Test262WindowHandler, + Test262WindowTestHandler, + Test262WindowModuleHandler, + Test262WindowModuleTestHandler, + Test262StrictWindowHandler, + Test262StrictWindowTestHandler, + Test262StrictHandler, WrapperHandler, - inject_script, - # Use 'T262' aliases to avoid naming collisions with the pytest collector - Test262WindowHandler as T262WindowHandler, - Test262WindowTestHandler as T262WindowTestHandler, - Test262WindowModuleHandler as T262WindowModuleHandler, - Test262WindowModuleTestHandler as T262WindowModuleTestHandler, - Test262StrictWindowHandler as T262StrictWindowHandler, - Test262StrictWindowTestHandler as T262StrictWindowTestHandler, - Test262StrictHandler as T262StrictHandler) + inject_script) logger = logging.getLogger() @@ -270,14 +269,21 @@ def _test_handler_get_metadata(handler_cls: Type[WrapperHandler], assert item in metadata assert len(expected_metadata) == len(metadata), f"{expected_metadata} != {metadata}" +Test262WindowHandler.__test__ = False +Test262WindowTestHandler.__test__ = False +Test262WindowModuleHandler.__test__ = False +Test262WindowModuleTestHandler.__test__ = False +Test262StrictWindowHandler.__test__ = False +Test262StrictWindowTestHandler.__test__ = False +Test262StrictHandler.__test__ = False @pytest.mark.parametrize("handler_cls, expected", [ - (T262WindowHandler, [(".test262.html", ".js", ".test262-test.html")]), - (T262WindowTestHandler, [(".test262-test.html", ".js")]), - (T262WindowModuleHandler, [(".test262-module.html", ".js", ".test262-module-test.html")]), - (T262WindowModuleTestHandler, [(".test262-module-test.html", ".js")]), - (T262StrictWindowHandler, [(".test262.strict.html", ".js", ".test262-test.strict.html")]), - (T262StrictWindowTestHandler, [(".test262-test.strict.html", ".js", ".test262.strict.js")]), + (Test262WindowHandler, [(".test262.html", ".js", ".test262-test.html")]), + (Test262WindowTestHandler, [(".test262-test.html", ".js")]), + (Test262WindowModuleHandler, [(".test262-module.html", ".js", ".test262-module-test.html")]), + (Test262WindowModuleTestHandler, [(".test262-module-test.html", ".js")]), + (Test262StrictWindowHandler, [(".test262.strict.html", ".js", ".test262-test.strict.html")]), + (Test262StrictWindowTestHandler, [(".test262-test.strict.html", ".js", ".test262.strict.js")]), ]) def test_path_replace(test262_handlers, handler_cls, expected): tests_root, url_base = test262_handlers @@ -286,17 +292,17 @@ def test_path_replace(test262_handlers, handler_cls, expected): @pytest.mark.parametrize("handler_cls, request_path, expected_metadata", [ ( - T262WindowTestHandler, + Test262WindowTestHandler, "/test262/basic.test262-test.html", [('script', '/third_party/test262/harness/assert.js'), ('script', '/third_party/test262/harness/sta.js')] ), ( - T262WindowTestHandler, + Test262WindowTestHandler, "/test262/negative.test262-test.html", [('negative', 'TypeError')] ), ( - T262StrictWindowTestHandler, + Test262StrictWindowTestHandler, "/test262/teststrict.test262-test.strict.html", [('script', '/third_party/test262/harness/propertyHelper.js')] ), @@ -307,39 +313,39 @@ def test_get_metadata(test262_handlers, handler_cls, request_path, expected_meta @pytest.mark.parametrize("handler_cls, request_path, expected_substrings", [ - # T262WindowHandler: Should contain the iframe pointing to the test + # Test262WindowHandler: Should contain the iframe pointing to the test ( - T262WindowHandler, + Test262WindowHandler, "/test262/basic.test262.html", [''] ), - # T262WindowTestHandler: Should contain script tags + # Test262WindowTestHandler: Should contain script tags ( - T262WindowTestHandler, + Test262WindowTestHandler, "/test262/basic.test262-test.html", ['', '', ''] ), - # T262WindowModuleTestHandler: Should contain module import + # Test262WindowModuleTestHandler: Should contain module import ( - T262WindowModuleTestHandler, + Test262WindowModuleTestHandler, "/test262/module.test262-module-test.html", ['"] ), # Strict HTML Case: points to the .strict.js variant ( - T262StrictWindowTestHandler, + Test262StrictWindowTestHandler, "/test262/teststrict.test262-test.strict.html", ['src="/test262/teststrict.test262.strict.js"'] ), # Strict JS Case: The handler that serves the actual script ( - T262StrictHandler, + Test262StrictHandler, "/test262/teststrict.test262.strict.js", ['"use strict";', "console.log('hello');"] ), From f3cc5c74717793ff912d99aa5229d503b765344d Mon Sep 17 00:00:00 2001 From: James Scott Date: Wed, 14 Jan 2026 15:27:49 +0000 Subject: [PATCH 3/3] address feedback --- tools/serve/serve.py | 33 +++++++++++++++++++-------------- tools/serve/test_serve.py | 14 +++++++------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/tools/serve/serve.py b/tools/serve/serve.py index a02bdbea6574bf..30a9c6afbb4d05 100644 --- a/tools/serve/serve.py +++ b/tools/serve/serve.py @@ -19,7 +19,7 @@ from io import IOBase from itertools import chain, product from html5lib import html5parser -from typing import ClassVar, List, Optional, Set, Tuple, Union +from typing import ClassVar, List, Optional, Set, Tuple from localpaths import repo_root # type: ignore @@ -341,13 +341,15 @@ class Test262WindowHandler(HtmlWrapperHandler): """ -class Test262WindowTestHandler(HtmlWrapperHandler): +class Test262WindowTestBaseHandler(HtmlWrapperHandler): # For SHAB headers = [('Cross-Origin-Opener-Policy', 'same-origin'), ('Cross-Origin-Embedder-Policy', 'require-corp')] - path_replace: Union[List[Tuple[str, str]], List[Tuple[str, str, str]]] = [(".test262-test.html", ".js")] - + # Define a common HTML structure (testharness setup, etc.) that can be + # extended by subclasses. This avoids duplicating boilerplate. For example, + # Test262WindowModuleTestHandler reuses this `pre_wrapper` but appends a + # module script. pre_wrapper = """ Test @@ -378,12 +380,16 @@ def _meta_replacement(self, key: str, value: str) -> Optional[str]: return None +class Test262WindowTestHandler(Test262WindowTestBaseHandler): + path_replace = [(".test262-test.html", ".js")] + + class Test262WindowModuleHandler(Test262WindowHandler): path_replace = [(".test262-module.html", ".js", ".test262-module-test.html")] -class Test262WindowModuleTestHandler(Test262WindowTestHandler): +class Test262WindowModuleTestHandler(Test262WindowTestBaseHandler): path_replace = [(".test262-module-test.html", ".js")] - wrapper = Test262WindowTestHandler.pre_wrapper + """