Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
103 changes: 102 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 TestRecord # type: ignore
from wptserve import server as wptserve, handlers
from wptserve import stash
from wptserve import config
Expand Down Expand Up @@ -327,6 +328,74 @@ 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")]

pre_wrapper = """<!doctype html>
<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='ISO-8859-1') as f:
test_record = TestRecord.parse(f.read(), path)
yield from (('script', "/third_party/test262/harness/%s" % filename)
for filename in test_record.get("includes", []))
expected_error = test_record.get('negative', {}).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 +643,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='ISO-8859-1') 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")]

Expand Down Expand Up @@ -787,6 +881,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
195 changes: 194 additions & 1 deletion tools/serve/test_serve.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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('ISO-8859-1'))
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",
['<iframe id="test262-iframe" src="/test262/basic.test262-test.html"></iframe>']
),
# T262WindowTestHandler: Should contain script tags
(
T262WindowTestHandler,
"/test262/basic.test262-test.html",
['<script src="/test262/basic.js"></script>', '<script>test262Setup()</script>', '<script>test262Done()</script>']
),
# T262WindowModuleTestHandler: Should contain module import
(
T262WindowModuleTestHandler,
"/test262/module.test262-module-test.html",
['<script type="module">', 'import {} from "/test262/module.js";', 'test262Setup();', 'test262Done();']
),
# Verification of the 'negative' replacement in the HTML
(
T262WindowTestHandler,
"/test262/negative.test262-test.html",
["<script>test262Negative('TypeError')</script>"]
),
# 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)