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
386 lines
14 KiB
Python
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
|