Files
sousa-gecko/testing/web-platform/tests/tools/serve/test_serve.py
T
James C Scott III 438b64ffa8 Bug 2006815 [wpt PR 56842] - feat(runner): Integrate Test262 execution with wptrunner and add smoke tests [pt 3/5], a=testonly
Automatic update from web-platform-tests
feat(runner): Integrate Test262 execution with wptrunner and add smoke tests [pt 3/5] (#56842)

* feat(runner): Integrate Test262 execution with wptrunner and add smoke tests

This commit completes the core integration of Test262 into the WPT
runner (`wptrunner`), enabling the execution of Test262 JavaScript
conformance tests within the browser environment. This work builds upon
the manifest tooling (PR 1 #56840) and server-side handling (PR 2 #56841) to provide
a full end-to-end testing solution for Test262.

Key components introduced and integrated:
- **Client-Side Harness:** New JavaScript files (`harness-adapter.js`,
  `testharness-client.js`, `testharness.js`) under `resources/test262/`
  provide the necessary environment for Test262 tests to execute in an
  `iframe` and report results back to the main WPT testharness.
- **Test262 Assertion Libraries (Vendored):** For the purpose of enabling these
  initial smoke tests, the standard Test262 assertion libraries (`assert.js`
  and `sta.js`) are directly placed into `third_party/test262/harness/`.
  This provides the immediate dependencies required by the smoke tests.
  An autoamted vendoring solution, including version tracking via
  `vendored.toml`, will be implemented in a subsequent PR focused on
  automated updates, as described in the RFC.
- **Wptrunner Integration:** Modifications to `tools/wptrunner/wpttest.py`
  and `tools/wptrunner/browsers/*.py` enable `wptrunner` to recognize the
  `test262` test type and execute it using existing testharness executors.
- **Smoke Tests and Metadata:** A suite of basic Test262 smoke tests
  (`infrastructure/test262/`) and their corresponding expected results
  (`infrastructure/metadata/infrastructure/test262/`) are added to verify
  the correct functioning of the entire integration.
- **Linting Exemption:** `lint.ignore` is updated to exclude the vendored
  Test262 harness files from linting, respecting their upstream style.

This integration allows browser vendors to run Test262 directly with `wptrunner`,
streamlining conformance testing efforts as specified in the RFC:
https://github.com/web-platform-tests/rfcs/pull/229

This commit is the third in a series of smaller PRs split from the larger,
original implementation in https://github.com/web-platform-tests/wpt/pull/55997.

* address feedback

* address more feedback

* address feedback

* address feedback

* Apply suggestion from @jcscottiii

* fix lint

* move need third_party lint because other doesn't work

* fix lint

* Refactor Test262 integration to remove custom custom harness bridge
This commit addresses reviewer feedback (jgraham) regarding the Test262
integration "reimplementing a subset of testharness.js". The architecture
has been drastically simplified by eliminating the custom `postMessage`
protocol between the Test262 iframe and the parent WPT test wrapper.
Changes include:
- Deleted `test262-harness-bridge.js` completely.
- Updated the wrapper `<script>` generated by `tools/serve/serve.py` to
  expose a native `window.test262HarnessDone(status, error)` hook directly
  in the parent's `testharness.js` execution environment.
- Refactored `test262-reporter.js` to natively dispatch test completions
  and harness errors to this synchronous hook instead of manually wrapping
  results in `{type: "complete"}` message payloads.
- Ensured strict "Realm Isolation" is maintained for the `test262` tests,
  as required by `INTERPRETING.md`, by keeping all WPT globals cleanly
  isolated in the parent window.
- Updated `tools/serve/test_serve.py` assertions to reflect the removed
  bridge dependency.
- Leveraged `setTimeout` to safely elevate test harness crashes into the
  parent's macro-task queue, ensuring infrastructure `.ini` expectations
  (like timeouts and parsing ERRORs) correctly fail the WPT harness.

* Update tools/serve/serve.py

* update to docs from last commits

* touch ups

* nit

* start to address feedback

* finish feedback

* tidy up

* doc touchups

* add missing support for phases

* final touchups
--

wpt-commits: 0a0fd8c16c2a51e2c9b915780962500cd641eda7
wpt-pr: 56842
2026-04-23 10:14:03 +00:00

386 lines
14 KiB
Python

# 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, Tuple
import pytest
import localpaths # type: ignore
from . import serve
from .serve import (
ConfigBuilder,
Test262WindowHandler,
Test262WindowTestHandler,
Test262WindowModuleHandler,
Test262WindowModuleTestHandler,
Test262StrictWindowHandler,
Test262StrictWindowTestHandler,
Test262StrictHandler,
inject_script)
logger = logging.getLogger()
@pytest.mark.skipif(platform.uname()[0] == "Windows",
reason="Expected contents are platform-dependent")
def test_make_hosts_file_nix():
with ConfigBuilder(logger,
ports={"http": [8000]},
browser_host="foo.bar",
alternate_hosts={"alt": "foo2.bar"},
subdomains={"a", "b"},
not_subdomains={"x, y"}) as c:
hosts = serve.make_hosts_file(c, "192.168.42.42")
lines = hosts.split("\n")
assert lines == [
"# Start web-platform-tests hosts",
"192.168.42.42\tfoo.bar",
"192.168.42.42\ta.foo.bar",
"192.168.42.42\tb.foo.bar",
"192.168.42.42\tfoo2.bar",
"192.168.42.42\ta.foo2.bar",
"192.168.42.42\tb.foo2.bar",
"# End web-platform-tests hosts",
"",
]
@pytest.mark.skipif(platform.uname()[0] != "Windows",
reason="Expected contents are platform-dependent")
def test_make_hosts_file_windows():
with ConfigBuilder(logger,
ports={"http": [8000]},
browser_host="foo.bar",
alternate_hosts={"alt": "foo2.bar"},
subdomains={"a", "b"},
not_subdomains={"x", "y"}) as c:
hosts = serve.make_hosts_file(c, "192.168.42.42")
lines = hosts.split("\n")
assert lines == [
"# Start web-platform-tests hosts",
"192.168.42.42\tfoo.bar",
"192.168.42.42\ta.foo.bar",
"192.168.42.42\tb.foo.bar",
"192.168.42.42\tfoo2.bar",
"192.168.42.42\ta.foo2.bar",
"192.168.42.42\tb.foo2.bar",
"0.0.0.0\tx.foo.bar",
"0.0.0.0\ty.foo.bar",
"0.0.0.0\tx.foo2.bar",
"0.0.0.0\ty.foo2.bar",
"# End web-platform-tests hosts",
"",
]
def test_ws_doc_root_default():
with ConfigBuilder(logger) as c:
assert c.doc_root == localpaths.repo_root
assert c.ws_doc_root == os.path.join(localpaths.repo_root, "websockets", "handlers")
assert c.paths["ws_doc_root"] == c.ws_doc_root
def test_init_ws_doc_root():
with ConfigBuilder(logger, ws_doc_root="/") as c:
assert c.doc_root == localpaths.repo_root # check this hasn't changed
assert c.ws_doc_root == "/"
assert c.paths["ws_doc_root"] == c.ws_doc_root
def test_set_ws_doc_root():
cb = ConfigBuilder(logger)
cb.ws_doc_root = "/"
with cb as c:
assert c.doc_root == localpaths.repo_root # check this hasn't changed
assert c.ws_doc_root == "/"
assert c.paths["ws_doc_root"] == c.ws_doc_root
def test_pickle():
# Ensure that the config object can be pickled
with ConfigBuilder(logger) as c:
pickle.dumps(c)
def test_alternate_host_unspecified():
ConfigBuilder(logger, browser_host="web-platform.test")
@pytest.mark.parametrize("primary, alternate", [
("web-platform.test", "web-platform.test"),
("a.web-platform.test", "web-platform.test"),
("web-platform.test", "a.web-platform.test"),
("a.web-platform.test", "a.web-platform.test"),
])
def test_alternate_host_invalid(primary, alternate):
with pytest.raises(ValueError):
ConfigBuilder(logger, browser_host=primary, alternate_hosts={"alt": alternate})
@pytest.mark.parametrize("primary, alternate", [
("web-platform.test", "not-web-platform.test"),
("a.web-platform.test", "b.web-platform.test"),
("web-platform-tests.dev", "web-platform-tests.live"),
])
def test_alternate_host_valid(primary, alternate):
ConfigBuilder(logger, browser_host=primary, alternate_hosts={"alt": alternate})
# A token marking the location of expected script injection.
INJECT_SCRIPT_MARKER = b"<!-- inject here -->"
def test_inject_script_after_head():
html = b"""<!DOCTYPE html>
<html>
<head>
<!-- inject here --><script src="test.js"></script>
</head>
<body>
</body>
</html>"""
assert INJECT_SCRIPT_MARKER in html
assert inject_script(html.replace(INJECT_SCRIPT_MARKER, b""), INJECT_SCRIPT_MARKER) == html
def test_inject_script_no_html_head():
html = b"""<!DOCTYPE html>
<!-- inject here --><div></div>"""
assert INJECT_SCRIPT_MARKER in html
assert inject_script(html.replace(INJECT_SCRIPT_MARKER, b""), INJECT_SCRIPT_MARKER) == html
def test_inject_script_no_doctype():
html = b"""<!-- inject here --><div></div>"""
assert INJECT_SCRIPT_MARKER in html
assert inject_script(html.replace(INJECT_SCRIPT_MARKER, b""), INJECT_SCRIPT_MARKER) == html
def test_inject_script_parse_error():
html = b"""<!--<!-- inject here --><div></div>"""
assert INJECT_SCRIPT_MARKER in html
# 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 handler_setup() -> Generator[Tuple[str, str], None, None]:
"""Provides a mocked filesystem environment for testing any handler class."""
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", "async.js")): """/*---
description: An async test
flags: [async]
---*/
print('Test262:AsyncTestComplete');
""",
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
# Ensure pytest doesn't consider Test-prefixed classes to be testcases
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(handler_setup, handler_cls, expected):
"""Verifies that handlers correctly map request paths to internal resources."""
root, url_base = handler_setup
handler = handler_cls(base_path=root, url_base=url_base)
assert handler.path_replace == expected
@pytest.mark.parametrize("handler_cls, request_path, expected_tags", [
(
Test262WindowTestHandler,
"/test262/basic.test262-test.html",
[
'<script src="/third_party/test262/harness/assert.js"></script>',
'<script src="/third_party/test262/harness/sta.js"></script>',
]
),
(
Test262WindowTestHandler,
"/test262/negative.test262-test.html",
[
"<script>test262Negative('TypeError', 'runtime')</script>",
]
),
(
Test262WindowTestHandler,
"/test262/async.test262-test.html",
[
"<script>test262IsAsync(true)</script>",
'<script src="/third_party/test262/harness/doneprintHandle.js"></script>',
]
),
(
Test262StrictWindowTestHandler,
"/test262/teststrict.test262-test.strict.html",
[
'<script src="/third_party/test262/harness/propertyHelper.js"></script>',
]
),
])
def test_get_meta_and_script(handler_setup, handler_cls, request_path, expected_tags):
"""Verifies script and meta injection logic for Test262 handlers."""
root, url_base = handler_setup
handler = handler_cls(root, url_base)
mock_request = _create_mock_request(request_path)
# Combine output of _get_meta and _get_script
output = list(handler._get_meta(mock_request)) + list(handler._get_script(mock_request))
for item in expected_tags:
assert item in output
assert len(expected_tags) == len(output)
@pytest.mark.parametrize("handler_cls, request_path, expected_substrings", [
# Test262WindowHandler: Should contain the iframe pointing to the test
(
Test262WindowHandler,
"/test262/basic.test262.html",
[
'<script src="/resources/testharness.js"></script>',
'<script src="/resources/testharnessreport.js"></script>',
'window.test262HarnessDone = t.step_func_done(function(status, message)',
'<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" onerror="test262ScriptError()"></script>',
'<script>test262Setup()</script>',
]
),
# Test262WindowModuleTestHandler: Should contain module import and new catch logic
(
Test262WindowModuleTestHandler,
"/test262/module.test262-module-test.html",
[
'<script type="module">',
'test262Setup();',
'import("/test262/module.js")',
'setTimeout(() => { throw error; });',
]
),
# Verification of the 'negative' replacement in the HTML
(
Test262WindowTestHandler,
"/test262/negative.test262-test.html",
[
"<script>test262Negative('TypeError', 'runtime')</script>",
]
),
# Verification of the 'async' replacement in the HTML
(
Test262WindowTestHandler,
"/test262/async.test262-test.html",
[
"<script>test262IsAsync(true)</script>",
'<script src="/third_party/test262/harness/doneprintHandle.js">'
]
),
# 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(handler_setup, handler_cls, request_path, expected_substrings):
"""Verifies generated HTML/JS content for any handler."""
root, url_base = handler_setup
handler = handler_cls(base_path=root, url_base=url_base)
mock_request = _create_mock_request(request_path)
mock_response = MagicMock()
handler.handle_request(mock_request, mock_response)
content = mock_response.content
for item in expected_substrings:
assert item in content