diff --git a/tools/serve/serve.py b/tools/serve/serve.py index e8c42129702ae1..30a9c6afbb4d05 100644 --- a/tools/serve/serve.py +++ b/tools/serve/serve.py @@ -24,6 +24,7 @@ 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,81 @@ class ExtensionHandler(HtmlWrapperHandler): """ +class Test262WindowHandler(HtmlWrapperHandler): + path_replace = [(".test262.html", ".js", ".test262-test.html")] + wrapper = """ + +Test + + +%(meta)s +%(script)s +
+""" + + +class Test262WindowTestBaseHandler(HtmlWrapperHandler): + # For SHAB + headers = [('Cross-Origin-Opener-Policy', 'same-origin'), + ('Cross-Origin-Embedder-Policy', 'require-corp')] + + # 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 + + + + +%(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 Test262WindowTestHandler(Test262WindowTestBaseHandler): + path_replace = [(".test262-test.html", ".js")] + + +class Test262WindowModuleHandler(Test262WindowHandler): + path_replace = [(".test262-module.html", ".js", ".test262-module-test.html")] + +class Test262WindowModuleTestHandler(Test262WindowTestBaseHandler): + path_replace = [(".test262-module-test.html", ".js")] + wrapper = Test262WindowTestBaseHandler.pre_wrapper + """""" + + +class Test262StrictWindowHandler(Test262WindowHandler): + path_replace = [(".test262.strict.html", ".js", ".test262-test.strict.html")] + +class Test262StrictWindowTestHandler(Test262WindowTestBaseHandler): + 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 +650,30 @@ 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_script(self, request): + """ + Reads the entire content of the associated JavaScript file to be + prepended with "use strict". + """ + path = self._get_filesystem_path(request) + try: + with open(path, encoding='utf-8') as f: + yield f.read() + except OSError: + raise HTTPException(404) + + class BaseWorkerHandler(WrapperHandler): headers = [("Content-Type", "text/javascript")] @@ -787,6 +887,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..dd47429a97ed24 100644 --- a/tools/serve/test_serve.py +++ b/tools/serve/test_serve.py @@ -1,15 +1,29 @@ # 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, + Test262WindowHandler, + Test262WindowTestHandler, + Test262WindowModuleHandler, + Test262WindowModuleTestHandler, + Test262StrictWindowHandler, + Test262StrictWindowTestHandler, + Test262StrictHandler, + WrapperHandler, + inject_script) logger = logging.getLogger() @@ -154,3 +168,188 @@ 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}" + +Test262WindowHandler.__test__ = False # type: ignore[attr-defined] +Test262WindowTestHandler.__test__ = False # type: ignore[attr-defined] +Test262WindowModuleHandler.__test__ = False # type: ignore[attr-defined] +Test262WindowModuleTestHandler.__test__ = False # type: ignore[attr-defined] +Test262StrictWindowHandler.__test__ = False # type: ignore[attr-defined] +Test262StrictWindowTestHandler.__test__ = False # type: ignore[attr-defined] +Test262StrictHandler.__test__ = False # type: ignore[attr-defined] + +@pytest.mark.parametrize("handler_cls, expected", [ + (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 + _test_handler_path_replace(handler_cls, tests_root, url_base, expected) + + +@pytest.mark.parametrize("handler_cls, request_path, expected_metadata", [ + ( + Test262WindowTestHandler, + "/test262/basic.test262-test.html", + [('script', '/third_party/test262/harness/assert.js'), ('script', '/third_party/test262/harness/sta.js')] + ), + ( + Test262WindowTestHandler, + "/test262/negative.test262-test.html", + [('negative', 'TypeError')] + ), + ( + Test262StrictWindowTestHandler, + "/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", [ + # Test262WindowHandler: Should contain the iframe pointing to the test + ( + Test262WindowHandler, + "/test262/basic.test262.html", + [''] + ), + # Test262WindowTestHandler: Should contain script tags + ( + Test262WindowTestHandler, + "/test262/basic.test262-test.html", + ['', '', ''] + ), + # Test262WindowModuleTestHandler: Should contain module import + ( + Test262WindowModuleTestHandler, + "/test262/module.test262-module-test.html", + ['"] + ), + # Strict HTML Case: points to the .strict.js variant + ( + Test262StrictWindowTestHandler, + "/test262/teststrict.test262-test.strict.html", + ['src="/test262/teststrict.test262.strict.js"'] + ), + # Strict JS Case: The handler that serves the actual script + ( + Test262StrictHandler, + "/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)