Skip to content

Commit 16aa645

Browse files
committed
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: web-platform-tests/rfcs#229 This commit is the second in a series of smaller PRs split from the larger, original implementation in #55997.
1 parent ad5fa6b commit 16aa645

File tree

2 files changed

+296
-2
lines changed

2 files changed

+296
-2
lines changed

tools/serve/serve.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919
from io import IOBase
2020
from itertools import chain, product
2121
from html5lib import html5parser
22-
from typing import ClassVar, List, Optional, Set, Tuple
22+
from typing import ClassVar, List, Optional, Set, Tuple, Union
2323

2424
from localpaths import repo_root # type: ignore
2525

2626
from manifest.sourcefile import read_script_metadata, js_meta_re, parse_variants # type: ignore
27+
from manifest.test262 import TestRecord # type: ignore
2728
from wptserve import server as wptserve, handlers
2829
from wptserve import stash
2930
from wptserve import config
@@ -327,6 +328,74 @@ class ExtensionHandler(HtmlWrapperHandler):
327328
"""
328329

329330

331+
class Test262WindowHandler(HtmlWrapperHandler):
332+
path_replace = [(".test262.html", ".js", ".test262-test.html")]
333+
wrapper = """<!doctype html>
334+
<meta charset=utf-8>
335+
<title>Test</title>
336+
<script src="/resources/test262/testharness.js"></script>
337+
<script src="/resources/testharnessreport.js"></script>
338+
%(meta)s
339+
%(script)s
340+
<div id=log></div>
341+
<iframe id="test262-iframe" src="%(path)s"></iframe>"""
342+
343+
344+
class Test262WindowTestHandler(HtmlWrapperHandler):
345+
# For SHAB
346+
headers = [('Cross-Origin-Opener-Policy', 'same-origin'),
347+
('Cross-Origin-Embedder-Policy', 'require-corp')]
348+
349+
path_replace: Union[List[Tuple[str, str]], List[Tuple[str, str, str]]] = [(".test262-test.html", ".js")]
350+
351+
pre_wrapper = """<!doctype html>
352+
<meta charset=utf-8>
353+
<title>Test</title>
354+
<script src="/resources/test262/testharness-client.js"></script>
355+
<script src="/third_party/test262/harness/assert.js"></script>
356+
<script src="/third_party/test262/harness/sta.js"></script>
357+
<script src="/resources/test262/harness-adapter.js"></script>
358+
%(meta)s
359+
%(script)s"""
360+
wrapper = pre_wrapper + """<script>test262Setup()</script>
361+
<script src="%(path)s"></script>
362+
<script>test262Done()</script>"""
363+
364+
def _get_metadata(self, request):
365+
path = self._get_filesystem_path(request)
366+
with open(path, encoding='ISO-8859-1') as f:
367+
test_record = TestRecord.parse(f.read(), path)
368+
yield from (('script', "/third_party/test262/harness/%s" % filename)
369+
for filename in test_record.get("includes", []))
370+
expected_error = test_record.get('negative', {}).get('type', None)
371+
if expected_error is not None:
372+
yield ('negative', expected_error)
373+
374+
def _meta_replacement(self, key: str, value: str) -> Optional[str]:
375+
if key == 'negative':
376+
return """<script>test262Negative('%s')</script>""" % value
377+
return None
378+
379+
380+
class Test262WindowModuleHandler(Test262WindowHandler):
381+
path_replace = [(".test262-module.html", ".js", ".test262-module-test.html")]
382+
383+
class Test262WindowModuleTestHandler(Test262WindowTestHandler):
384+
path_replace = [(".test262-module-test.html", ".js")]
385+
wrapper = Test262WindowTestHandler.pre_wrapper + """<script type="module">
386+
test262Setup();
387+
import {} from "%(path)s";
388+
test262Done();
389+
</script>"""
390+
391+
392+
class Test262StrictWindowHandler(Test262WindowHandler):
393+
path_replace = [(".test262.strict.html", ".js", ".test262-test.strict.html")]
394+
395+
class Test262StrictWindowTestHandler(Test262WindowTestHandler):
396+
path_replace = [(".test262-test.strict.html", ".js", ".test262.strict.js")]
397+
398+
330399
class WindowModulesHandler(HtmlWrapperHandler):
331400
global_type = "window-module"
332401
path_replace = [(".any.window-module.html", ".any.js")]
@@ -574,6 +643,31 @@ class ShadowRealmInAudioWorkletHandler(HtmlWrapperHandler):
574643
"""
575644

576645

646+
class Test262StrictHandler(WrapperHandler):
647+
path_replace = [(".test262.strict.js", ".js")]
648+
headers = [('Content-Type', 'text/javascript')]
649+
wrapper = """
650+
"use strict";
651+
%(script)s
652+
"""
653+
654+
def _meta_replacement(self, key, value):
655+
return None
656+
657+
def _get_metadata(self, request):
658+
# Abuse the script metadata to inline the script content so as to
659+
# prepend "use strict".
660+
path = self._get_filesystem_path(request)
661+
try:
662+
with open(path, encoding='ISO-8859-1') as f:
663+
yield ('script', f.read())
664+
except OSError:
665+
raise HTTPException(404)
666+
667+
def _script_replacement(self, key, value):
668+
return value
669+
670+
577671
class BaseWorkerHandler(WrapperHandler):
578672
headers = [("Content-Type", "text/javascript")]
579673

@@ -787,6 +881,13 @@ def add_mount_point(self, url_base, path):
787881
("GET", "*.worker.html", WorkersHandler),
788882
("GET", "*.worker-module.html", WorkerModulesHandler),
789883
("GET", "*.window.html", WindowHandler),
884+
("GET", "*.test262.html", Test262WindowHandler),
885+
("GET", "*.test262-test.html", Test262WindowTestHandler),
886+
("GET", "*.test262-module.html", Test262WindowModuleHandler),
887+
("GET", "*.test262-module-test.html", Test262WindowModuleTestHandler),
888+
("GET", "*.test262.strict.html", Test262StrictWindowHandler),
889+
("GET", "*.test262-test.strict.html", Test262StrictWindowTestHandler),
890+
("GET", "*.test262.strict.js", Test262StrictHandler),
790891
("GET", "*.extension.html", ExtensionHandler),
791892
("GET", "*.any.html", AnyHtmlHandler),
792893
("GET", "*.any.sharedworker.html", SharedWorkersHandler),

tools/serve/test_serve.py

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
# mypy: allow-untyped-defs
22

3+
import builtins
4+
import io
35
import logging
46
import os
57
import pickle
68
import platform
9+
from unittest.mock import MagicMock, patch
10+
from typing import Generator, List, Tuple, Type
711

812
import pytest
913

1014
import localpaths # type: ignore
1115
from . import serve
12-
from .serve import ConfigBuilder, inject_script
16+
from .serve import (
17+
ConfigBuilder,
18+
WrapperHandler,
19+
inject_script,
20+
# Use 'T262' aliases to avoid naming collisions with the pytest collector
21+
Test262WindowHandler as T262WindowHandler,
22+
Test262WindowTestHandler as T262WindowTestHandler,
23+
Test262WindowModuleHandler as T262WindowModuleHandler,
24+
Test262WindowModuleTestHandler as T262WindowModuleTestHandler,
25+
Test262StrictWindowHandler as T262StrictWindowHandler,
26+
Test262StrictWindowTestHandler as T262StrictWindowTestHandler,
27+
Test262StrictHandler as T262StrictHandler)
1328

1429

1530
logger = logging.getLogger()
@@ -154,3 +169,181 @@ def test_inject_script_parse_error():
154169
# On a parse error, the script should not be injected and the original content should be
155170
# returned.
156171
assert INJECT_SCRIPT_MARKER not in inject_script(html.replace(INJECT_SCRIPT_MARKER, b""), INJECT_SCRIPT_MARKER)
172+
173+
174+
@pytest.fixture
175+
def test262_handlers() -> Generator[Tuple[str, str], None, None]:
176+
tests_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "tests", "testdata"))
177+
url_base = "/"
178+
179+
mock_file_contents = {
180+
os.path.normpath(os.path.join(tests_root, "test262", "basic.js")): """/*---\ndescription: A basic test
181+
includes: [assert.js, sta.js]
182+
---*/
183+
assert.sameValue(1, 1);
184+
""",
185+
os.path.normpath(os.path.join(tests_root, "test262", "negative.js")): """/*---\ndescription: A negative test
186+
negative:
187+
phase: runtime
188+
type: TypeError
189+
---*/
190+
throw new TypeError();
191+
""",
192+
os.path.normpath(os.path.join(tests_root, "test262", "module.js")): """/*---\ndescription: A module test
193+
flags: [module]
194+
---*/
195+
import {} from 'some-module';
196+
""",
197+
os.path.normpath(os.path.join(tests_root, "test262", "teststrict.js")): """/*---\ndescription: A strict mode test
198+
flags: [onlyStrict]
199+
includes: [propertyHelper.js]
200+
---*/
201+
console.log('hello');
202+
"""
203+
}
204+
205+
# Store original functions to be called if our mock doesn't handle the file
206+
original_open = builtins.open
207+
original_exists = os.path.exists
208+
original_isdir = os.path.isdir
209+
210+
def custom_open(file, mode='r', *args, **kwargs):
211+
normalized_file = os.path.normpath(file)
212+
if normalized_file in mock_file_contents:
213+
if 'b' in mode:
214+
return io.BytesIO(mock_file_contents[normalized_file].encode('ISO-8859-1'))
215+
else:
216+
return io.StringIO(mock_file_contents[normalized_file])
217+
return original_open(file, mode, *args, **kwargs)
218+
219+
def custom_exists(path):
220+
normalized_path = os.path.normpath(path)
221+
return normalized_path in mock_file_contents or original_exists(path)
222+
223+
def custom_isdir(path):
224+
normalized_path = os.path.normpath(path)
225+
expected_dir = os.path.normpath(os.path.join(tests_root, "test262"))
226+
return normalized_path == expected_dir or original_isdir(path)
227+
228+
with patch('builtins.open', side_effect=custom_open), \
229+
patch('os.path.exists', side_effect=custom_exists), \
230+
patch('os.path.isdir', side_effect=custom_isdir):
231+
yield tests_root, url_base
232+
233+
234+
def _create_mock_request(path: str) -> MagicMock:
235+
mock_request = MagicMock()
236+
mock_request.url_parts.path = path
237+
mock_request.url_parts.query = ""
238+
return mock_request
239+
240+
241+
def _test_handler_path_replace(handler_cls: Type[WrapperHandler],
242+
tests_root: str,
243+
url_base: str,
244+
expected: List[Tuple[str, str]]) -> None:
245+
handler = handler_cls(base_path=tests_root, url_base=url_base)
246+
assert handler.path_replace == expected
247+
248+
def _test_handler_wrapper_content(handler_cls: Type[WrapperHandler],
249+
tests_root: str,
250+
url_base: str,
251+
request_path: str,
252+
expected_content: List[str]) -> None:
253+
handler = handler_cls(base_path=tests_root, url_base=url_base)
254+
mock_request = _create_mock_request(request_path)
255+
mock_response = MagicMock()
256+
handler.handle_request(mock_request, mock_response) # type: ignore[no-untyped-call]
257+
content = mock_response.content
258+
for item in expected_content:
259+
assert item in content
260+
261+
def _test_handler_get_metadata(handler_cls: Type[WrapperHandler],
262+
tests_root: str,
263+
url_base: str,
264+
request_path: str,
265+
expected_metadata: List[Tuple[str, str]]) -> None:
266+
handler = handler_cls(tests_root, url_base)
267+
mock_request = _create_mock_request(request_path)
268+
metadata = list(handler._get_metadata(mock_request)) # type: ignore[no-untyped-call]
269+
for item in expected_metadata:
270+
assert item in metadata
271+
assert len(expected_metadata) == len(metadata), f"{expected_metadata} != {metadata}"
272+
273+
274+
@pytest.mark.parametrize("handler_cls, expected", [
275+
(T262WindowHandler, [(".test262.html", ".js", ".test262-test.html")]),
276+
(T262WindowTestHandler, [(".test262-test.html", ".js")]),
277+
(T262WindowModuleHandler, [(".test262-module.html", ".js", ".test262-module-test.html")]),
278+
(T262WindowModuleTestHandler, [(".test262-module-test.html", ".js")]),
279+
(T262StrictWindowHandler, [(".test262.strict.html", ".js", ".test262-test.strict.html")]),
280+
(T262StrictWindowTestHandler, [(".test262-test.strict.html", ".js", ".test262.strict.js")]),
281+
])
282+
def test_path_replace(test262_handlers, handler_cls, expected):
283+
tests_root, url_base = test262_handlers
284+
_test_handler_path_replace(handler_cls, tests_root, url_base, expected)
285+
286+
287+
@pytest.mark.parametrize("handler_cls, request_path, expected_metadata", [
288+
(
289+
T262WindowTestHandler,
290+
"/test262/basic.test262-test.html",
291+
[('script', '/third_party/test262/harness/assert.js'), ('script', '/third_party/test262/harness/sta.js')]
292+
),
293+
(
294+
T262WindowTestHandler,
295+
"/test262/negative.test262-test.html",
296+
[('negative', 'TypeError')]
297+
),
298+
(
299+
T262StrictWindowTestHandler,
300+
"/test262/teststrict.test262-test.strict.html",
301+
[('script', '/third_party/test262/harness/propertyHelper.js')]
302+
),
303+
])
304+
def test_get_metadata(test262_handlers, handler_cls, request_path, expected_metadata):
305+
tests_root, url_base = test262_handlers
306+
_test_handler_get_metadata(handler_cls, tests_root, url_base, request_path, expected_metadata)
307+
308+
309+
@pytest.mark.parametrize("handler_cls, request_path, expected_substrings", [
310+
# T262WindowHandler: Should contain the iframe pointing to the test
311+
(
312+
T262WindowHandler,
313+
"/test262/basic.test262.html",
314+
['<iframe id="test262-iframe" src="/test262/basic.test262-test.html"></iframe>']
315+
),
316+
# T262WindowTestHandler: Should contain script tags
317+
(
318+
T262WindowTestHandler,
319+
"/test262/basic.test262-test.html",
320+
['<script src="/test262/basic.js"></script>', '<script>test262Setup()</script>', '<script>test262Done()</script>']
321+
),
322+
# T262WindowModuleTestHandler: Should contain module import
323+
(
324+
T262WindowModuleTestHandler,
325+
"/test262/module.test262-module-test.html",
326+
['<script type="module">', 'import {} from "/test262/module.js";', 'test262Setup();', 'test262Done();']
327+
),
328+
# Verification of the 'negative' replacement in the HTML
329+
(
330+
T262WindowTestHandler,
331+
"/test262/negative.test262-test.html",
332+
["<script>test262Negative('TypeError')</script>"]
333+
),
334+
# Strict HTML Case: points to the .strict.js variant
335+
(
336+
T262StrictWindowTestHandler,
337+
"/test262/teststrict.test262-test.strict.html",
338+
['src="/test262/teststrict.test262.strict.js"']
339+
),
340+
# Strict JS Case: The handler that serves the actual script
341+
(
342+
T262StrictHandler,
343+
"/test262/teststrict.test262.strict.js",
344+
['"use strict";', "console.log('hello');"]
345+
),
346+
])
347+
def test_wrapper_content(test262_handlers, handler_cls, request_path, expected_substrings):
348+
tests_root, url_base = test262_handlers
349+
_test_handler_wrapper_content(handler_cls, tests_root, url_base, request_path, expected_substrings)

0 commit comments

Comments
 (0)