Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion tools/serve/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -327,6 +328,75 @@ class ExtensionHandler(HtmlWrapperHandler):
"""


class Test262WindowHandler(HtmlWrapperHandler):
path_replace = [(".test262.html", ".js", ".test262-test.html")]
wrapper = """<!doctype html>
<meta charset=utf-8>
<title>Test</title>
<script src="/resources/test262/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
%(meta)s
%(script)s
<div id=log></div>
<iframe id="test262-iframe" src="%(path)s"></iframe>"""


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")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have a type annotation here but nowhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! Initially, Test262WindowTestHandler defined path_replace with 2-element tuples, but its subclass, Test262StrictWindowTestHandler, needed 3-element tuples. This caused a mypy error because the subclass was essentially trying to override a parent attribute with a different type signature.

My first thought was to use a Union type hint on the parent's path_replace to tell mypy it could handle both 2-tuple and 3-tuple lists.

Now, I will create a new base class, Test262WindowTestBaseHandler. This new base class now holds all the shared logic, and both Test262WindowTestHandler and Test262StrictWindowTestHandler define their own path_replace attributes independently.


pre_wrapper = """<!doctype html>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is split into multiple variables so we can reuse this part in subclasses? Would be good to mention that in a comment, otherwise it looks rather strange.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

<meta charset=utf-8>
<title>Test</title>
<script src="/resources/test262/testharness-client.js"></script>
<script src="/third_party/test262/harness/assert.js"></script>
<script src="/third_party/test262/harness/sta.js"></script>
<script src="/resources/test262/harness-adapter.js"></script>
%(meta)s
%(script)s"""
wrapper = pre_wrapper + """<script>test262Setup()</script>
<script src="%(path)s"></script>
<script>test262Done()</script>"""

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 """<script>test262Negative('%s')</script>""" % 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 + """<script type="module">
test262Setup();
import {} from "%(path)s";
test262Done();
</script>"""


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")]
Expand Down Expand Up @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this override actually necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I think a better idea would be to override _get_script itself (so that it doesn't call _script_replacement).

return value


class BaseWorkerHandler(WrapperHandler):
headers = [("Content-Type", "text/javascript")]

Expand Down Expand Up @@ -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),
Expand Down
201 changes: 200 additions & 1 deletion tools/serve/test_serve.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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
Test262WindowTestHandler.__test__ = False
Test262WindowModuleHandler.__test__ = False
Test262WindowModuleTestHandler.__test__ = False
Test262StrictWindowHandler.__test__ = False
Test262StrictWindowTestHandler.__test__ = False
Test262StrictHandler.__test__ = False

@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",
['<iframe id="test262-iframe" src="/test262/basic.test262-test.html"></iframe>']
),
# Test262WindowTestHandler: Should contain script tags
(
Test262WindowTestHandler,
"/test262/basic.test262-test.html",
['<script src="/test262/basic.js"></script>', '<script>test262Setup()</script>', '<script>test262Done()</script>']
),
# Test262WindowModuleTestHandler: Should contain module import
(
Test262WindowModuleTestHandler,
"/test262/module.test262-module-test.html",
['<script type="module">', 'import {} from "/test262/module.js";', 'test262Setup();', 'test262Done();']
),
# Verification of the 'negative' replacement in the HTML
(
Test262WindowTestHandler,
"/test262/negative.test262-test.html",
["<script>test262Negative('TypeError')</script>"]
),
# 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)