diff --git a/moli-benchmark/moli_benchmark/wpt_cross/case_set.py b/moli-benchmark/moli_benchmark/wpt_cross/case_set.py index 4445a3e768..a47d7975a9 100644 --- a/moli-benchmark/moli_benchmark/wpt_cross/case_set.py +++ b/moli-benchmark/moli_benchmark/wpt_cross/case_set.py @@ -751,6 +751,37 @@ def _common_echo_handler_reference_patterns(directory: str) -> tuple[re.Pattern[ ) +@lru_cache(maxsize=None) +def _remote_context_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], ...]: + references = [] + for resource in ( + "common/dispatcher/dispatcher.py", + "html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.py", + ): + relative = posixpath.relpath(resource, directory) + references.extend(("/" + resource, relative, "./" + relative)) + return tuple( + re.compile( + rf"(? tuple[re.Pattern[str], ...]: + resource = "html/semantics/embedded-content/the-iframe-element/stash.py" + relative = posixpath.relpath(resource, directory) + return tuple( + re.compile( + rf"(? tuple[re.Pattern[str], ...]: resource = "html/semantics/scripting-1/the-script-element/serve-with-content-type.py" @@ -764,6 +795,68 @@ def _script_content_type_handler_reference_patterns(directory: str) -> tuple[re. ) +@lru_cache(maxsize=None) +def _document_charset_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], ...]: + references = [] + for name in ("bogus-charset-http.py", "bogus-charset-http-valid-meta.py"): + resource = f"html/syntax/charset/resources/{name}" + relative = posixpath.relpath(resource, directory) + references.extend(("/" + resource, relative, "./" + relative)) + return tuple( + re.compile( + rf"(? tuple[re.Pattern[str], ...]: + resource = "fetch/content-type/resources/content-type.py" + relative = posixpath.relpath(resource, directory) + return tuple( + re.compile( + rf"(? tuple[re.Pattern[str], ...]: + names = ( + "preflight.py", "clean-stash.py", "inspect-headers.py", "redirect.py", + "redirect-empty-location.py", + ) + references = [] + for name in names: + resource = "fetch/api/resources/" + name + relative = posixpath.relpath(resource, directory) + references.extend(("/" + resource, relative, "./" + relative)) + patterns = [ + re.compile( + rf"(? tuple[re.Pattern[str], ...]: resource = ( @@ -829,28 +922,17 @@ def _empty_location_handler_reference_patterns(directory: str) -> tuple[re.Patte @lru_cache(maxsize=None) -def _remote_context_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], ...]: +def _xhr_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], ...]: references = [] - for resource in ( - "common/dispatcher/dispatcher.py", - "html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.py", + for name in ( + "requri.py", "redirect.py", "headers.py", "inspect-headers.py", "echo-headers.py", "content.py", + "echo-content-type.py", "corsenabled.py", "win-1252-xml.py", "win-1252-html.py", + "access-control-basic-put-allow.py", + "access-control-preflight-request-allow-headers-returns-star.py", + "invalid-utf8-html.py", "shift-jis-html.py", "img-utf8-html.py", + "empty-div-utf8-html.py", "status.py", "last-modified.py", + "bad-chunk-encoding.py", "infinite-redirects.py", ): - relative = posixpath.relpath(resource, directory) - references.extend(("/" + resource, relative, "./" + relative)) - return tuple( - re.compile( - rf"(? tuple[re.Pattern[str], ...]: - references = [] - for name in ("requri.py", "redirect.py"): resource = f"xhr/resources/{name}" relative = posixpath.relpath(resource, directory) references.extend(("/" + resource, relative, "./" + relative)) @@ -863,38 +945,82 @@ def _xhr_url_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str] ) +@lru_cache(maxsize=None) +def _script_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], ...]: + references = [] + for name in ( + "serve-with-content-type.py", "resources/load-error-events.py", + "json-module/load-error-events.py", "serve-json-then-js.py", + ): + resource = "html/semantics/scripting-1/the-script-element/" + name + relative = posixpath.relpath(resource, directory) + references.extend(("/" + resource, relative, "./" + relative)) + return tuple( + re.compile( + rf"(? tuple[re.Pattern[str], ...]: + references = [] + resources = tuple( + "service-workers/service-worker/resources/" + name + for name in ( + "redirect.py", "update-worker.py", "update-worker-from-file.py", + "mime-type-worker.py", "import-mime-type-worker.py", "malformed-worker.py", + "invalid-chunked-encoding.py", "invalid-chunked-encoding-with-flush.py", + "update-during-installation-worker.py", + "import-scripts-version.py", "import-scripts-get.py", "import-scripts-echo.py", + "subdir/import-scripts-echo.py", "scope2/import-scripts-echo.py", + ) + ) + ("service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py",) + for resource in resources: + relative = posixpath.relpath(resource, directory) + references.extend(("/" + resource, relative, "./" + relative)) + return tuple( + re.compile( + rf"(? tuple[re.Pattern[str], ...]: supported: tuple[re.Pattern[str], ...] = () - if rel is not None: - supported += _service_worker_script_handler_reference_patterns(posixpath.dirname(rel) or ".") - supported += _fetch_resource_handler_reference_patterns(posixpath.dirname(rel) or ".") if rel is not None and rel.startswith("fetch/api/"): supported += _empty_location_handler_reference_patterns(posixpath.dirname(rel)) if rel is not None: supported += _common_echo_handler_reference_patterns(posixpath.dirname(rel) or ".") - supported += _remote_context_handler_reference_patterns(posixpath.dirname(rel) or ".") supported += _fetch_content_type_handler_reference_patterns(posixpath.dirname(rel) or ".") supported += _document_charset_handler_reference_patterns(posixpath.dirname(rel) or ".") supported += _iframe_stash_handler_reference_patterns(posixpath.dirname(rel) or ".") + supported += _remote_context_handler_reference_patterns(posixpath.dirname(rel) or ".") supported += _preload_count_handler_reference_patterns(posixpath.dirname(rel) or ".") supported += _navigation_handler_reference_patterns(posixpath.dirname(rel) or ".") supported += _json_module_handler_reference_patterns(posixpath.dirname(rel) or ".") if rel is not None: - supported += _script_content_type_handler_reference_patterns(posixpath.dirname(rel) or ".") + supported += _script_handler_reference_patterns(posixpath.dirname(rel) or ".") + supported += _service_worker_script_handler_reference_patterns(posixpath.dirname(rel) or ".") if rel is not None and rel.rsplit("/", 1)[0] == "fetch/api/abort": supported += SUPPORTED_FETCH_ABORT_WPTSERVE_HANDLER_PATTERNS if rel is not None and rel.rsplit("/", 1)[0] == "fetch/range": supported += SUPPORTED_FETCH_RANGE_WPTSERVE_HANDLER_PATTERNS + if rel is not None and rel.startswith("fetch/api/"): + supported += _fetch_resource_handler_reference_patterns(posixpath.dirname(rel)) if rel is not None and rel.startswith("wasm/webapi/"): supported += SUPPORTED_WASM_WEBAPI_WPTSERVE_HANDLER_PATTERNS if rel is not None: supported += _script_load_error_handler_reference_patterns(posixpath.dirname(rel) or ".") if rel is not None and rel.startswith("xhr/"): supported += SUPPORTED_XHR_DELAY_WPTSERVE_HANDLER_PATTERNS - supported += _xhr_url_handler_reference_patterns(rel.rsplit("/", 1)[0]) + supported += _xhr_handler_reference_patterns(rel.rsplit("/", 1)[0]) if rel is not None and rel.startswith( "html/semantics/scripting-1/the-script-element/module/" ): @@ -911,9 +1037,7 @@ def _xhr_response_handler_reference_patterns(directory: str) -> tuple[re.Pattern "inspect-headers.py", "echo-headers.py", "content.py", "echo-content-type.py", "win-1252-xml.py", "win-1252-html.py", "invalid-utf8-html.py", "shift-jis-html.py", "img-utf8-html.py", - "empty-div-utf8-html.py", "headers.py", "bad-chunk-encoding.py", "infinite-redirects.py", - "access-control-basic-put-allow.py", - "access-control-preflight-request-allow-headers-returns-star.py"): + "empty-div-utf8-html.py"): resource = f"xhr/resources/{name}" relative = posixpath.relpath(resource, directory) references.extend(("/" + resource, relative, "./" + relative)) @@ -1670,109 +1794,3 @@ def explicit_case(wpt_root: Path, case_path: str) -> WptCase: timeout_multiplier=timeout_multiplier, test_type="crashtest" if _is_crashtest_path(source_case_path) else "testharness", ) - - -@lru_cache(maxsize=None) -def _service_worker_script_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], ...]: - references = [] - resources = tuple( - "service-workers/service-worker/resources/" + name - for name in ( - "redirect.py", "update-worker.py", "update-worker-from-file.py", - "mime-type-worker.py", "import-mime-type-worker.py", "malformed-worker.py", - "invalid-chunked-encoding.py", "invalid-chunked-encoding-with-flush.py", - "update-during-installation-worker.py", - "import-scripts-version.py", "import-scripts-get.py", "import-scripts-echo.py", - "subdir/import-scripts-echo.py", "scope2/import-scripts-echo.py", - ) - ) + ("service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py",) - for resource in resources: - relative = posixpath.relpath(resource, directory) - references.extend(("/" + resource, relative, "./" + relative)) - return tuple( - re.compile( - rf"(? tuple[re.Pattern[str], ...]: - names = ( - "preflight.py", "clean-stash.py", "inspect-headers.py", "redirect.py", - "redirect-empty-location.py", - ) - references = [] - for name in names: - resource = "fetch/api/resources/" + name - relative = posixpath.relpath(resource, directory) - references.extend(("/" + resource, relative, "./" + relative)) - patterns = [ - re.compile( - rf"(? tuple[re.Pattern[str], ...]: - resource = "fetch/content-type/resources/content-type.py" - relative = posixpath.relpath(resource, directory) - return tuple( - re.compile( - rf"(? tuple[re.Pattern[str], ...]: - references = [] - for name in ("bogus-charset-http.py", "bogus-charset-http-valid-meta.py"): - resource = f"html/syntax/charset/resources/{name}" - relative = posixpath.relpath(resource, directory) - references.extend(("/" + resource, relative, "./" + relative)) - return tuple( - re.compile( - rf"(? tuple[re.Pattern[str], ...]: - resource = "html/semantics/embedded-content/the-iframe-element/stash.py" - relative = posixpath.relpath(resource, directory) - return tuple( - re.compile( - rf"(?\xa2\n", +} XHR_DOCUMENT_FIXTURES = { "/xhr/resources/win-1252-xml.py": ("application/xml;charset=windows-1252", b"<\xff/>"), # The upstream handler returns a Unicode string, encoded by wptserve as UTF-8. @@ -95,48 +106,22 @@ XHR_DOCUMENT_FIXTURES = { "/xhr/resources/img-utf8-html.py": ("text/html;charset=utf-8", b"foo"), "/xhr/resources/empty-div-utf8-html.py": ("text/html;charset=utf-8", b"
"), } -XHR_BODY_RESOURCE_PATHS = { - "/xhr/resources/content.py", - "/xhr/resources/echo-content-type.py", - *XHR_DOCUMENT_FIXTURES, -} -XHR_RESPONSE_RESOURCE_PATHS = { - *XHR_BODY_RESOURCE_PATHS, - "/xhr/resources/headers.py", - "/xhr/resources/inspect-headers.py", - "/xhr/resources/echo-headers.py", - "/xhr/resources/access-control-basic-put-allow.py", - "/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py", - "/xhr/resources/corsenabled.py", - - "/xhr/resources/bad-chunk-encoding.py", - "/xhr/resources/infinite-redirects.py", - "/xhr/resources/status.py", - "/xhr/resources/last-modified.py", -} -DISPATCHER_PATH = "/common/dispatcher/dispatcher.py" - -REMOTE_CONTEXT_EXECUTOR_PATH = ( - "/html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.py" -) - -REMOTE_CONTEXT_RESOURCE_PATHS = {DISPATCHER_PATH, REMOTE_CONTEXT_EXECUTOR_PATH} -FETCH_CONTENT_TYPE_PATH = "/fetch/content-type/resources/content-type.py" -DOCUMENT_CHARSET_FIXTURES = { - "/html/syntax/charset/resources/bogus-charset-http.py": b"\xa2\n", - "/html/syntax/charset/resources/bogus-charset-http-valid-meta.py": - b"\xa2\n", -} -IFRAME_STASH_PATH = "/html/semantics/embedded-content/the-iframe-element/stash.py" - -COMMON_ECHO_PATH = "/common/echo.py" -COMMON_REDIRECT_PATH = "/common/redirect.py" -FETCH_INSPECT_HEADERS_PATH = "/fetch/api/resources/inspect-headers.py" -FETCH_REDIRECT_RESOURCE_PATH = "/fetch/api/resources/redirect.py" -FETCH_EMPTY_LOCATION_PATH = "/fetch/api/resources/redirect-empty-location.py" -XHR_URL_RESOURCE_PATHS = { +XHR_RESOURCE_PATHS = { "/xhr/resources/requri.py", "/xhr/resources/redirect.py", + "/xhr/resources/inspect-headers.py", + "/xhr/resources/headers.py", + "/xhr/resources/echo-headers.py", + "/xhr/resources/content.py", + "/xhr/resources/corsenabled.py", + "/xhr/resources/access-control-basic-put-allow.py", + "/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py", + "/xhr/resources/echo-content-type.py", + "/xhr/resources/status.py", + "/xhr/resources/last-modified.py", + "/xhr/resources/bad-chunk-encoding.py", + "/xhr/resources/infinite-redirects.py", + *XHR_DOCUMENT_FIXTURES, } FETCH_ABORT_RESOURCE_PATHS = { "/fetch/api/resources/stash-put.py", @@ -147,18 +132,16 @@ FETCH_RANGE_RESOURCE_PATHS = { "/fetch/range/resources/long-wav.py", "/fetch/range/resources/stash-take.py", } -LINK_STYLESHEET_COUNTER_PATH = ( - "/html/semantics/document-metadata/the-link-element/stylesheet.py" -) -JSON_THEN_JS_PATH = "/html/semantics/scripting-1/the-script-element/serve-json-then-js.py" -JSON_LOAD_ERROR_PATH = ( - "/html/semantics/scripting-1/the-script-element/json-module/load-error-events.py" -) - -NAVIGATION_SECOND_VISIT_PATH = ( - "/navigation-api/navigation-methods/return-value/resources/" - "204-205-download-on-second-visit.py" -) +FETCH_PREFLIGHT_RESOURCE_PATHS = { + "/fetch/api/resources/preflight.py", + "/fetch/api/resources/clean-stash.py", +} +FETCH_REDIRECT_RESOURCE_PATHS = { + "/fetch/api/resources/redirect.py", + "/fetch/api/resources/redirect-empty-location.py", +} +FETCH_INSPECT_HEADERS_PATH = "/fetch/api/resources/inspect-headers.py" +FETCH_CONTENT_TYPE_PATH = "/fetch/content-type/resources/content-type.py" SERVICE_WORKER_SCRIPT_RESOURCE_PATHS = { "/service-workers/service-worker/resources/mime-type-worker.py", "/service-workers/service-worker/resources/import-mime-type-worker.py", @@ -188,17 +171,22 @@ SERVICE_WORKER_MALFORMED_SCRIPTS = { "instantiation-error-and-top-level-await": 'import nonexistent from "./imported-module-script.js"; await Promise.resolve(1);', } -FETCH_PREFLIGHT_RESOURCE_PATHS = { - "/fetch/api/resources/preflight.py", - "/fetch/api/resources/clean-stash.py", -} - +LINK_STYLESHEET_COUNTER_PATH = ( + "/html/semantics/document-metadata/the-link-element/stylesheet.py" +) +JSON_THEN_JS_PATH = "/html/semantics/scripting-1/the-script-element/serve-json-then-js.py" +JSON_LOAD_ERROR_PATH = ( + "/html/semantics/scripting-1/the-script-element/json-module/load-error-events.py" +) +NAVIGATION_SECOND_VISIT_PATH = ( + "/navigation-api/navigation-methods/return-value/resources/" + "204-205-download-on-second-visit.py" +) +BENCH_TIMEOUT_MULTIPLIER_QUERY = "__moli_bench_timeout_multiplier" FORM_ECHO_PATH = "/html/semantics/forms/form-submission-0/form-echo.py" FORM_SUBMISSION_PATH = ( "/html/semantics/forms/form-submission-0/resources/form-submission.py" ) - -BENCH_TIMEOUT_MULTIPLIER_QUERY = "__moli_bench_timeout_multiplier" BENCH_REPORT_BRIDGE_SRC_RE = re.compile( rb"(?P\bsrc\s*=\s*)(?P['\"])" rb"/resources/testharnessreport\.js(?P=quote)", @@ -1157,6 +1145,17 @@ def _response_content_type_and_extra_headers( return merged_content_type, merged_extra_headers +def _fetch_status_response(query: str) -> tuple[int, str, str, bytes]: + """Model the identical Fetch/XHR status.py handlers without decoding payloads.""" + params = parse_qs(query, keep_blank_values=True, encoding="latin-1") + return ( + int(params.get("code", ["200"])[0]), + params.get("text", ["OMG"])[0], + params.get("type", [""])[0], + params.get("content", [""])[0].encode("latin-1"), + ) + + def _wasm_webapi_status_code(query: str) -> int | None: """Return the status for WPT's wasm/webapi/status.py fixture. @@ -1224,6 +1223,23 @@ def _xhr_inspect_headers_fixture_response( return headers, b"".join(parts) +def _workers_url_encoding_response(query: str) -> bytes: + """Model workers/semantics/encodings/003-1.py's UTF-8 query check.""" + value = next( + ( + value + for name, value in parse_qsl(query, keep_blank_values=True) + if name == "x" + ), + None, + ) + return b"PASS" if value == "å" else b"FAIL" + + +def _form_echo_response(body: bytes) -> bytes: + return b" ".join(f"{byte:02x}".encode("ascii") for byte in body) + + def _form_submission_response( query: str, content_type: str | None, body: bytes ) -> bytes: @@ -1423,6 +1439,24 @@ def _inspect_headers_response_headers( return response_headers +def _nosniff_javascript_response(query: str) -> tuple[str | None, bytes]: + """Model fetch/nosniff/resources/js.py's MIME-controlled script body.""" + + params = parse_qsl(query, keep_blank_values=True) + outcome = next( + (value for name, value in params if name == "outcome"), + "f", + ) + content_type = next( + (value for name, value in params if name == "type"), + None, + ) + type_label = content_type if content_type is not None else "Content-Type missing" + result_call = "log('FAIL: " + type_label + "')" if outcome == "f" else "p()" + body = ("// nothing to see here\n" + result_call).encode() + return content_type, body + + def _url_host_literal(hostname: str) -> str: if hostname.startswith("[") and hostname.endswith("]"): return hostname @@ -1515,6 +1549,8 @@ def _substitute_wpt_template_variables( alternate_port = port if remote_port is None: remote_port = alternate_port + # Named hosts belong to the configured server, not the requesting subdomain. + # External IPv6 endpoints retain the existing loopback alias fallback. template_primary_hostname = primary_hostname or "localhost" if ":" in template_primary_hostname: template_primary_hostname = "localhost" @@ -1608,6 +1644,7 @@ def _substitute_wpt_template_variables( b"ws://" + primary_url_host_bytes + b":" + str(alternate_port).encode("ascii") ), b"{{host}}": primary_hostname_bytes, + b"{{domains[]}}": primary_hostname_bytes, b"{{location[scheme]}}": b"http", b"{{location[server]}}": current_origin, b"{{location[host]}}": request_host_bytes, @@ -1824,6 +1861,56 @@ class _Ipv6ThreadingHTTPServer(_FixtureThreadingHTTPServer): address_family = socket.AF_INET6 +class FetchStash: + """Write-once, read-once stash with independent resource-path namespaces. + + Every origin of one fixture server shares these namespaces. UUID + normalization matches wptserve, including equivalent key spellings. + Counter and queue updates compose take/put while holding the same lock. + """ + + def __init__(self) -> None: + self._lock = threading.Lock() + self._values: dict[tuple[str, uuid.UUID], object] = {} + + def put( + self, key: str, value: object, *, overwrite: bool = False, + path: str = "/fetch/api/resources/", + ) -> None: + parsed_key = (path, uuid.UUID(key)) + if value is None: + raise ValueError("Shared stash values cannot be None") + with self._lock: + if not overwrite and parsed_key in self._values: + raise ValueError("Tried to overwrite existing shared stash value") + self._values[parsed_key] = value + + def take(self, key: str, *, path: str = "/fetch/api/resources/") -> object: + parsed_key = (path, uuid.UUID(key)) + with self._lock: + return self._values.pop(parsed_key, None) + + def increment(self, key: str, *, path: str) -> int: + parsed_key = (path, uuid.UUID(key)) + with self._lock: + value = int(self._values.get(parsed_key, 0)) + 1 + self._values[parsed_key] = value + return value + + def exchange_queue(self, key: str, value: bytes | None = None, *, path: str) -> bytes | None: + """Atomically append a message or take the oldest message in a stash queue.""" + parsed_key = (path, uuid.UUID(key)) + with self._lock: + queue = self._values.pop(parsed_key, None) or [] + if value is not None: + queue.append(value) + result = None + else: + result = queue.pop(0) if queue else None + self._values[parsed_key] = queue + return result + + class CspReportStore: """Thread-safe subset of WPT reporting stash used by CSP report checks.""" @@ -1831,6 +1918,7 @@ class CspReportStore: self._lock = threading.Lock() self._cv = threading.Condition(self._lock) self._reports: dict[str, list[dict]] = {} + self._counts: dict[str, int] = {} def append_reports(self, report_id: str, reports: list[dict]) -> None: with self._cv: @@ -1873,67 +1961,6 @@ class CspReportStore: self._cv.notify_all() -def _fetch_status_response(query: str) -> tuple[int, str, str, bytes]: - """Model fetch/api/resources/status.py without decoding its byte payload.""" - params = parse_qs(query, keep_blank_values=True, encoding="latin-1") - return ( - int(params.get("code", ["200"])[0]), - params.get("text", ["OMG"])[0], - params.get("type", [""])[0], - params.get("content", [""])[0].encode("latin-1"), - ) - - -class FetchStash: - """Write-once, read-once stash with independent resource-path namespaces. - - Every origin of one fixture server shares these namespaces. UUID - normalization matches wptserve, including equivalent key spellings. - """ - - def __init__(self) -> None: - self._lock = threading.Lock() - self._values: dict[tuple[str, uuid.UUID], object] = {} - - def put( - self, key: str, value: object, *, overwrite: bool = False, - path: str = "/fetch/api/resources/", - ) -> None: - parsed_key = (path, uuid.UUID(key)) - if value is None: - raise ValueError("Shared stash values cannot be None") - with self._lock: - if not overwrite and parsed_key in self._values: - raise ValueError("Tried to overwrite existing shared stash value") - self._values[parsed_key] = value - - def increment(self, key: str, *, path: str) -> int: - parsed_key = (path, uuid.UUID(key)) - with self._lock: - value = int(self._values.get(parsed_key, 0)) + 1 - self._values[parsed_key] = value - return value - - - def take(self, key: str, *, path: str = "/fetch/api/resources/") -> object: - parsed_key = (path, uuid.UUID(key)) - with self._lock: - return self._values.pop(parsed_key, None) - - def exchange_queue(self, key: str, value: bytes | None = None, *, path: str) -> bytes | None: - """Atomically append a message or take the oldest message in a stash queue.""" - parsed_key = (path, uuid.UUID(key)) - with self._lock: - queue = self._values.pop(parsed_key, None) or [] - if value is not None: - queue.append(value) - result = None - else: - result = queue.pop(0) if queue else None - self._values[parsed_key] = queue - return result - - def _directory_listing_body(directory: Path, path: str) -> bytes: items = [] if path == "/" else ['
  • ..
  • '] for entry in sorted(directory.iterdir(), key=lambda entry: entry.name): @@ -1947,41 +1974,6 @@ def _directory_listing_body(directory: Path, path: str) -> bytes: ).encode("utf-8") -def _workers_url_encoding_response(query: str) -> bytes: - """Model workers/semantics/encodings/003-1.py's UTF-8 query check.""" - value = next( - ( - value - for name, value in parse_qsl(query, keep_blank_values=True) - if name == "x" - ), - None, - ) - return b"PASS" if value == "å" else b"FAIL" - - -def _form_echo_response(body: bytes) -> bytes: - return b" ".join(f"{byte:02x}".encode("ascii") for byte in body) - - -def _nosniff_javascript_response(query: str) -> tuple[str | None, bytes]: - """Model fetch/nosniff/resources/js.py's MIME-controlled script body.""" - - params = parse_qsl(query, keep_blank_values=True) - outcome = next( - (value for name, value in params if name == "outcome"), - "f", - ) - content_type = next( - (value for name, value in params if name == "type"), - None, - ) - type_label = content_type if content_type is not None else "Content-Type missing" - result_call = "log('FAIL: " + type_label + "')" if outcome == "f" else "p()" - body = ("// nothing to see here\n" + result_call).encode() - return content_type, body - - def _make_handler( wpt_root: Path, results_store: "ResultsStore", @@ -1989,9 +1981,6 @@ def _make_handler( fetch_stash: FetchStash, stopping: threading.Event, ) -> type[BaseHTTPRequestHandler]: - link_stylesheet_stash = FetchStash() - range_stash = FetchStash() - class WptHandler(BaseHTTPRequestHandler): def _serve_remote_context_resource(self) -> bool: parsed = urlsplit(self.path) @@ -2056,17 +2045,9 @@ def _make_handler( for value in values: js_value = json.dumps(value.encode("latin-1").decode("utf-8")) initialize_headers += f"window.__requestHeaders.append({js_name}, {js_value});\n" - request_url = self.path - if not request_url.startswith("http://"): - authority = self.headers.get("Host") - if authority is None: - authority = _url_host_literal(str(self.server.server_address[0])) - if urlsplit("//" + authority).port is None: - authority += ":" + str(self.server.server_address[1]) - request_url = f"http://{authority}{self.path}" body = f""" - + @@ -2116,7 +2097,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); self._send_python_handler_response(IFRAME_STASH_PATH, parsed.query, body) return True - def _send_python_handler_response( self, path: str, query: str, body: bytes | None, *, headers: list[tuple[str, str]] | None = None, @@ -2204,10 +2184,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); ) return True - - def _serve_xhr_url_method(self) -> None: - self._serve_xhr_url_resource(emit_body=self.command != "HEAD") - def do_GET(self) -> None: # noqa: N802 (BaseHTTPRequestHandler API) if self.headers.get("Upgrade", "").lower() == "websocket": self._serve_websocket() @@ -2218,21 +2194,11 @@ requestExecutor("{executor_uuid}", {start_on_js}); self._serve(emit_body=False) def do_OPTIONS(self) -> None: # noqa: N802 - if unquote(urlsplit(self.path).path) == FETCH_INSPECT_HEADERS_PATH: - self._serve_fetch_inspect_headers( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) - return - if unquote(urlsplit(self.path).path) == FETCH_REDIRECT_RESOURCE_PATH: - self._serve_fetch_redirect_resource( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) - return - if self._serve_remote_context_resource(): + if self._serve_document_charset_resource(): return if self._serve_content_type_resource(): return - if self._serve_document_charset_resource(): + if self._serve_remote_context_resource(): return if self._serve_iframe_stash_resource(): return @@ -2244,27 +2210,19 @@ requestExecutor("{executor_uuid}", {start_on_js}); return if self._serve_empty_location_resource(emit_body=self.command != "HEAD"): return - if unquote(urlsplit(self.path).path) in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: - self._serve_service_worker_script_resource() - return - if unquote(urlsplit(self.path).path) in FETCH_PREFLIGHT_RESOURCE_PATHS: - parsed = urlsplit(self.path) - self._serve_fetch_preflight_resource( - unquote(parsed.path), parsed.query, emit_body=self.command != "HEAD", - ) - return - if ( - self._serve_xhr_response_resource() - or self._serve_xhr_url_resource(emit_body=True) - ): + if self._serve_xhr_resource(emit_body=True): return parsed = urlparse(self.path) path = unquote(parsed.path) if path == NAVIGATION_SECOND_VISIT_PATH: self._serve_navigation_second_visit() return - if path in FETCH_ABORT_RESOURCE_PATHS | FETCH_RANGE_RESOURCE_PATHS | { - "/fetch/api/resources/status.py", "/fetch/api/resources/trickle.py" + if path in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: + self._serve_service_worker_script_resource() + return + if path in FETCH_ABORT_RESOURCE_PATHS | FETCH_PREFLIGHT_RESOURCE_PATHS | FETCH_RANGE_RESOURCE_PATHS | { + "/fetch/api/resources/status.py", "/fetch/api/resources/trickle.py", + FETCH_INSPECT_HEADERS_PATH, *FETCH_REDIRECT_RESOURCE_PATHS }: self._serve_fetch_resource_method() return @@ -2286,26 +2244,11 @@ requestExecutor("{executor_uuid}", {start_on_js}); self.send_error(404) def do_POST(self) -> None: # noqa: N802 - if unquote(urlsplit(self.path).path) == FORM_ECHO_PATH: - raw = self._read_content_length_request_body() - if raw is not None: - self._send_bytes("text/plain", _form_echo_response(raw), emit_body=True) - return - if unquote(urlsplit(self.path).path) == FETCH_INSPECT_HEADERS_PATH: - self._serve_fetch_inspect_headers( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) - return - if unquote(urlsplit(self.path).path) == FETCH_REDIRECT_RESOURCE_PATH: - self._serve_fetch_redirect_resource( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) - return - if self._serve_remote_context_resource(): + if self._serve_document_charset_resource(): return if self._serve_content_type_resource(): return - if self._serve_document_charset_resource(): + if self._serve_remote_context_resource(): return if self._serve_iframe_stash_resource(): return @@ -2317,27 +2260,19 @@ requestExecutor("{executor_uuid}", {start_on_js}); return if self._serve_empty_location_resource(emit_body=self.command != "HEAD"): return - if unquote(urlsplit(self.path).path) in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: - self._serve_service_worker_script_resource() - return - if unquote(urlsplit(self.path).path) in FETCH_PREFLIGHT_RESOURCE_PATHS: - parsed = urlsplit(self.path) - self._serve_fetch_preflight_resource( - unquote(parsed.path), parsed.query, emit_body=self.command != "HEAD", - ) - return - if ( - self._serve_xhr_response_resource() - or self._serve_xhr_url_resource(emit_body=True) - ): + if self._serve_xhr_resource(emit_body=True): return parsed = urlparse(self.path) path = unquote(parsed.path) if path == NAVIGATION_SECOND_VISIT_PATH: self._serve_navigation_second_visit() return - if path in FETCH_ABORT_RESOURCE_PATHS | FETCH_RANGE_RESOURCE_PATHS | { - "/fetch/api/resources/status.py", "/fetch/api/resources/trickle.py" + if path in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: + self._serve_service_worker_script_resource() + return + if path in FETCH_ABORT_RESOURCE_PATHS | FETCH_PREFLIGHT_RESOURCE_PATHS | FETCH_RANGE_RESOURCE_PATHS | { + "/fetch/api/resources/status.py", "/fetch/api/resources/trickle.py", + FETCH_INSPECT_HEADERS_PATH, *FETCH_REDIRECT_RESOURCE_PATHS }: self._serve_fetch_resource_method() return @@ -2352,6 +2287,15 @@ requestExecutor("{executor_uuid}", {start_on_js}); emit_body=True, ) return + if path == FORM_ECHO_PATH: + raw = self._read_content_length_request_body() + if raw is not None: + self._send_bytes( + "text/plain", + _form_echo_response(raw), + emit_body=True, + ) + return if path == "/xhr/resources/delay.py": if self._consume_request_body(): self._serve_xhr_delay(parsed.query, emit_body=True) @@ -2391,21 +2335,11 @@ requestExecutor("{executor_uuid}", {start_on_js}); self.end_headers() def _serve_fetch_resource_method(self) -> None: - if unquote(urlsplit(self.path).path) == FETCH_INSPECT_HEADERS_PATH: - self._serve_fetch_inspect_headers( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) - return - if unquote(urlsplit(self.path).path) == FETCH_REDIRECT_RESOURCE_PATH: - self._serve_fetch_redirect_resource( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) - return - if self._serve_remote_context_resource(): + if self._serve_document_charset_resource(): return if self._serve_content_type_resource(): return - if self._serve_document_charset_resource(): + if self._serve_remote_context_resource(): return if self._serve_iframe_stash_resource(): return @@ -2417,20 +2351,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); return if self._serve_empty_location_resource(emit_body=self.command != "HEAD"): return - if unquote(urlsplit(self.path).path) in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: - self._serve_service_worker_script_resource() - return - if unquote(urlsplit(self.path).path) in FETCH_PREFLIGHT_RESOURCE_PATHS: - parsed = urlsplit(self.path) - self._serve_fetch_preflight_resource( - unquote(parsed.path), parsed.query, emit_body=self.command != "HEAD", - ) - return - if ( - self._serve_xhr_response_resource() - or self._serve_xhr_url_resource(emit_body=True) - ): - return parsed = urlparse(self.path) path = unquote(parsed.path) if path == NAVIGATION_SECOND_VISIT_PATH: @@ -2439,6 +2359,22 @@ requestExecutor("{executor_uuid}", {start_on_js}); if path in FETCH_RANGE_RESOURCE_PATHS: self._serve_fetch_range_resource(path, parsed.query, emit_body=self.command != "HEAD") return + if self._serve_xhr_resource(emit_body=True): + return + if unquote(parsed.path) in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: + self._serve_service_worker_script_resource() + return + if unquote(parsed.path) in FETCH_REDIRECT_RESOURCE_PATHS: + self._serve_fetch_redirect_resource(parsed.query, emit_body=self.command != "HEAD") + return + if unquote(parsed.path) == FETCH_INSPECT_HEADERS_PATH: + self._serve_fetch_inspect_headers(parsed.query, emit_body=self.command != "HEAD") + return + if unquote(parsed.path) in FETCH_PREFLIGHT_RESOURCE_PATHS: + self._serve_fetch_preflight_resource( + unquote(parsed.path), parsed.query, emit_body=self.command != "HEAD" + ) + return if unquote(parsed.path) in FETCH_ABORT_RESOURCE_PATHS: self._serve_fetch_abort_resource( unquote(parsed.path), parsed.query, emit_body=True @@ -2454,25 +2390,17 @@ requestExecutor("{executor_uuid}", {start_on_js}); self._serve_fetch_status(parsed.query, emit_body=True) do_PUT = _serve_fetch_resource_method + do_PATCH = _serve_fetch_resource_method + do_DELETE = _serve_fetch_resource_method def do_YO(self) -> None: # noqa: N802 (WPT custom method) - if unquote(urlsplit(self.path).path) == FETCH_INSPECT_HEADERS_PATH: - self._serve_fetch_inspect_headers( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) - return - if unquote(urlsplit(self.path).path) == FETCH_REDIRECT_RESOURCE_PATH: - self._serve_fetch_redirect_resource( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) - return - if self._serve_remote_context_resource(): + if self._serve_document_charset_resource(): return if self._serve_content_type_resource(): return - if self._serve_document_charset_resource(): + if self._serve_remote_context_resource(): return if self._serve_iframe_stash_resource(): return @@ -2484,30 +2412,18 @@ requestExecutor("{executor_uuid}", {start_on_js}); return if self._serve_empty_location_resource(emit_body=self.command != "HEAD"): return - if unquote(urlparse(self.path).path) in { - "/xhr/resources/inspect-headers.py", "/xhr/resources/echo-headers.py", - "/xhr/resources/access-control-basic-put-allow.py", - "/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py", - } | XHR_BODY_RESOURCE_PATHS: - self._serve_xhr_response_resource() - return - if unquote(urlsplit(self.path).path) in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: - self._serve_service_worker_script_resource() - return - if unquote(urlsplit(self.path).path) in FETCH_PREFLIGHT_RESOURCE_PATHS: - parsed = urlsplit(self.path) - self._serve_fetch_preflight_resource( - unquote(parsed.path), parsed.query, emit_body=self.command != "HEAD", - ) - return - if self._serve_xhr_url_resource(emit_body=True): + if self._serve_xhr_resource(emit_body=True): return parsed = urlparse(self.path) if unquote(parsed.path) == NAVIGATION_SECOND_VISIT_PATH: self._serve_navigation_second_visit() return - if unquote(parsed.path) in FETCH_ABORT_RESOURCE_PATHS | FETCH_RANGE_RESOURCE_PATHS | { - "/fetch/api/resources/status.py", "/fetch/api/resources/trickle.py" + if unquote(parsed.path) in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: + self._serve_service_worker_script_resource() + return + if unquote(parsed.path) in FETCH_ABORT_RESOURCE_PATHS | FETCH_RANGE_RESOURCE_PATHS | FETCH_PREFLIGHT_RESOURCE_PATHS | { + "/fetch/api/resources/status.py", "/fetch/api/resources/trickle.py", + FETCH_INSPECT_HEADERS_PATH, *FETCH_REDIRECT_RESOURCE_PATHS }: self._serve_fetch_resource_method() return @@ -2701,13 +2617,9 @@ requestExecutor("{executor_uuid}", {start_on_js}); return True def _serve(self, *, emit_body: bool) -> None: - if self._serve_remote_context_resource(): - return - if self._serve_content_type_resource(): - return if self._serve_document_charset_resource(): return - if self._serve_iframe_stash_resource(): + if self._serve_content_type_resource(): return try: self._serve_response(emit_body=emit_body) @@ -2715,28 +2627,9 @@ requestExecutor("{executor_uuid}", {start_on_js}); self.send_error(500, "Invalid WPT template or pipe") def _serve_response(self, *, emit_body: bool) -> None: - if unquote(urlsplit(self.path).path) == "/fetch/nosniff/resources/js.py": - self._serve_nosniff_javascript(urlsplit(self.path).query, emit_body=emit_body) + if self._serve_remote_context_resource(): return - if unquote(urlsplit(self.path).path) == FORM_ECHO_PATH: - self._send_bytes("text/plain", _form_echo_response(b""), emit_body=emit_body) - return - if unquote(urlsplit(self.path).path) == "/workers/semantics/encodings/003-1.py": - self._send_bytes( - "text/plain; charset=utf-8", - _workers_url_encoding_response(urlsplit(self.path).query), - emit_body=emit_body, - ) - return - if unquote(urlsplit(self.path).path) == FETCH_INSPECT_HEADERS_PATH: - self._serve_fetch_inspect_headers( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) - return - if unquote(urlsplit(self.path).path) == FETCH_REDIRECT_RESOURCE_PATH: - self._serve_fetch_redirect_resource( - urlsplit(self.path).query, emit_body=self.command != "HEAD", - ) + if self._serve_iframe_stash_resource(): return if self._serve_common_echo_resource(): return @@ -2746,19 +2639,7 @@ requestExecutor("{executor_uuid}", {start_on_js}); return if self._serve_empty_location_resource(emit_body=emit_body): return - if unquote(urlsplit(self.path).path) in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: - self._serve_service_worker_script_resource() - return - if unquote(urlsplit(self.path).path) in FETCH_PREFLIGHT_RESOURCE_PATHS: - parsed = urlsplit(self.path) - self._serve_fetch_preflight_resource( - unquote(parsed.path), parsed.query, emit_body=self.command != "HEAD", - ) - return - if ( - self._serve_xhr_response_resource(emit_body=emit_body) - or self._serve_xhr_url_resource(emit_body=emit_body) - ): + if self._serve_xhr_resource(emit_body=emit_body): return parsed = urlparse(self.path) path = unquote(parsed.path) @@ -2768,15 +2649,24 @@ requestExecutor("{executor_uuid}", {start_on_js}); if path in FETCH_RANGE_RESOURCE_PATHS: self._serve_fetch_range_resource(path, parsed.query, emit_body=emit_body) return + if path in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: + self._serve_service_worker_script_resource() + return + if path == LINK_STYLESHEET_COUNTER_PATH: + self._serve_link_stylesheet_counter(parsed.query, emit_body=emit_body) + return + if path in FETCH_REDIRECT_RESOURCE_PATHS: + self._serve_fetch_redirect_resource(parsed.query, emit_body=emit_body) + return + if path in FETCH_PREFLIGHT_RESOURCE_PATHS: + self._serve_fetch_preflight_resource(path, parsed.query, emit_body=emit_body) + return if path == ( "/html/semantics/scripting-1/the-script-element/" "serve-with-content-type.py" ): self._serve_script_with_content_type(parsed.query, emit_body=emit_body) return - if path == LINK_STYLESHEET_COUNTER_PATH: - self._serve_link_stylesheet_counter(parsed.query, emit_body=emit_body) - return if path == "/fetch/api/resources/status.py": self._serve_fetch_status(parsed.query, emit_body=emit_body) return @@ -2803,13 +2693,8 @@ requestExecutor("{executor_uuid}", {start_on_js}); if path == "/resources/testdriver-vendor.js": self._send_bytes("application/javascript; charset=utf-8", BENCH_TESTDRIVER_VENDOR_BRIDGE, emit_body=emit_body) return - if path == JSON_LOAD_ERROR_PATH: - self._serve_script_load_error_events( - parsed.query, emit_body=emit_body, json_module=True, - ) - return - if path == JSON_THEN_JS_PATH: - self._serve_json_then_js(parsed.query, emit_body=emit_body) + if path == FORM_ECHO_PATH: + self._send_bytes("text/plain", b"", emit_body=emit_body) return if path == FORM_SUBMISSION_PATH: self._send_bytes( @@ -2823,6 +2708,28 @@ requestExecutor("{executor_uuid}", {start_on_js}); if path == "/xhr/resources/delay.py": self._serve_xhr_delay(parsed.query, emit_body=emit_body) return + if path == "/workers/semantics/encodings/003-1.py": + self._send_bytes( + "text/plain; charset=utf-8", + _workers_url_encoding_response(parsed.query), + emit_body=emit_body, + ) + return + if path == "/fetch/api/resources/status.py": + self._serve_fetch_status(parsed.query, emit_body=emit_body) + return + if path == "/fetch/api/resources/trickle.py": + self._serve_fetch_trickle(parsed.query, emit_body=emit_body) + return + if path in FETCH_ABORT_RESOURCE_PATHS: + self._serve_fetch_abort_resource(path, parsed.query, emit_body=emit_body) + return + if path == FETCH_INSPECT_HEADERS_PATH: + self._serve_fetch_inspect_headers(parsed.query, emit_body=emit_body) + return + if path == "/fetch/nosniff/resources/js.py": + self._serve_nosniff_javascript(parsed.query, emit_body=emit_body) + return if path == ( "/html/semantics/scripting-1/the-script-element/module/" "resources/delayed-modulescript.py" @@ -2835,6 +2742,14 @@ requestExecutor("{executor_uuid}", {start_on_js}); ): self._serve_script_load_error_events(parsed.query, emit_body=emit_body) return + if path == JSON_LOAD_ERROR_PATH: + self._serve_script_load_error_events( + parsed.query, emit_body=emit_body, json_module=True, + ) + return + if path == JSON_THEN_JS_PATH: + self._serve_json_then_js(parsed.query, emit_body=emit_body) + return if path in {"/wasm/webapi/status.py", "/wasm/webapi/webapi/status.py"}: status_code = _wasm_webapi_status_code(parsed.query) if status_code is None: @@ -2848,9 +2763,30 @@ requestExecutor("{executor_uuid}", {start_on_js}); ) return - if path in { - "/common/redirect-opt-in.py", - }: + if path == ( + "/html/semantics/scripting-1/the-script-element/module/" + "dynamic-import/beta/redirect.py" + ): + params = parse_qs(parsed.query, keep_blank_values=True) + try: + status_code = int(params.get("status", ["302"])[0]) + except ValueError: + status_code = 302 + location = params.get("location", [""])[0] + if not 100 <= status_code <= 599 or any( + character in location for character in "\r\n" + ): + self.send_error(400) + return + self._send_bytes( + "text/plain; charset=utf-8", + b"", + emit_body=emit_body, + extra_headers=[("Location", location)], + status_code=status_code, + ) + return + if path == "/common/redirect-opt-in.py": redirect = _redirect_fixture_response(parsed.query) if redirect is None: self.send_error(400) @@ -2860,11 +2796,8 @@ requestExecutor("{executor_uuid}", {start_on_js}); ("Cache-Control", "no-cache"), ("Pragma", "no-cache"), ("Location", location), + ("Timing-Allow-Origin", "*"), ] - if path == "/common/redirect-opt-in.py": - redirect_headers.append(("Timing-Allow-Origin", "*")) - else: - redirect_headers.append(("Access-Control-Allow-Origin", "*")) self._send_bytes( "text/plain; charset=utf-8", b"", @@ -3232,7 +3165,7 @@ requestExecutor("{executor_uuid}", {start_on_js}); def _serve_link_stylesheet_counter(self, query: str, *, emit_body: bool) -> None: params = parse_qs(query, keep_blank_values=True) try: - count = int(link_stylesheet_stash.take(params["id"][0])) + count = int(fetch_stash.take(params["id"][0], path=LINK_STYLESHEET_COUNTER_PATH)) except (KeyError, TypeError, ValueError): count = 0 if "count" in params: @@ -3242,8 +3175,8 @@ requestExecutor("{executor_uuid}", {start_on_js}); ) return try: - link_stylesheet_stash.put( - params["id"][0], str(count + 1), + fetch_stash.put( + params["id"][0], str(count + 1), path=LINK_STYLESHEET_COUNTER_PATH, ) except (KeyError, ValueError): self.send_error(500) @@ -3275,7 +3208,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); except OSError: return - def _serve_service_worker_script_resource(self) -> None: parsed = urlsplit(self.path) path = unquote(parsed.path) @@ -3413,7 +3345,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); extra_headers=headers, status_code=status, cache_control=None, ) - def _serve_fetch_preflight_resource( self, path: str, query: str, *, emit_body: bool ) -> None: @@ -3495,53 +3426,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); except (KeyError, ValueError, TypeError): self.send_error(500) - - def _serve_xhr_url_resource(self, *, emit_body: bool) -> bool: - parsed = urlsplit(self.path) - path = unquote(parsed.path) - if path not in XHR_URL_RESOURCE_PATHS: - return False - try: - if path == "/xhr/resources/requri.py": - params = parse_qs(parsed.query, keep_blank_values=True) - uri = self.path - if "full" in params and not uri.startswith("http://"): - authority = self.headers.get("Host") - if authority is None: - authority = _url_host_literal( - str(self.server.server_address[0]) - ) - if urlsplit("//" + authority).port is None: - authority += ":" + str(self.server.server_address[1]) - uri = f"http://{authority}{uri}" - status, reason, headers, body = 200, None, [], uri.encode("utf-8") - else: - status, reason, headers, body, delay = _xhr_redirect_fixture_response( - parsed.path, parsed.query - ) - if delay is not None: - time.sleep(delay) - except (ValueError, KeyError, OverflowError): - self.send_error(500) - return True - # These handlers reply without reading uploads. Close connections - # with unread bodies so a redirect can arrive before upload finishes. - if ( - self.headers.get("Transfer-Encoding") is not None - or self.headers.get("Content-Length", "0").strip() not in {"", "0"} - ): - self.close_connection = True - headers.append(("Connection", "close")) - self._send_bytes( - None, - body, - emit_body=emit_body, - extra_headers=headers, - status_code=status, - status_text=reason, - ) - return True - def _serve_fetch_redirect_resource(self, query: str, *, emit_body: bool) -> None: connection_headers = [] if (self.headers.get("Transfer-Encoding") is not None @@ -3622,7 +3506,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); except (KeyError, ValueError, TypeError, OverflowError): self.send_error(500) - def _serve_fetch_inspect_headers(self, query: str, *, emit_body: bool) -> None: try: status_code = _pipe_response_status(query) or 200 @@ -3641,7 +3524,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); status_code=status_code, ) - def _serve_nosniff_javascript(self, query: str, *, emit_body: bool) -> None: content_type, body = _nosniff_javascript_response(query) self.send_response(200) @@ -3656,7 +3538,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); except (BrokenPipeError, ConnectionResetError): return - def _serve_xhr_delay(self, query: str, *, emit_body: bool) -> None: delay_seconds = _wpt_delay_seconds(query) if delay_seconds is None: @@ -3686,7 +3567,8 @@ requestExecutor("{executor_uuid}", {start_on_js}); self.send_error(400, "Not enough parameters or file not found") return self._send_bytes( - content_type, body, emit_body=emit_body, + None, body, emit_body=emit_body, + extra_headers=[("Content-Type", content_type)], ) def _serve_delayed_module_script(self, query: str, *, emit_body: bool) -> None: @@ -3701,40 +3583,35 @@ requestExecutor("{executor_uuid}", {start_on_js}); emit_body=emit_body, ) - def __getattr__(self, name: str): - if name.startswith("do_") and unquote(urlsplit(self.path).path) == FETCH_INSPECT_HEADERS_PATH: - return self._serve_fetch_resource_method - if name.startswith("do_") and unquote(urlsplit(self.path).path) == PRELOAD_COUNT_PATH: - return self._serve_preload_count_resource - if name.startswith("do_") and unquote(urlsplit(self.path).path) == FETCH_REDIRECT_RESOURCE_PATH: - return self._serve_fetch_resource_method - if name.startswith("do_") and unquote(urlsplit(self.path).path) in REMOTE_CONTEXT_RESOURCE_PATHS: - return self._serve_remote_context_resource - if name.startswith("do_") and unquote(urlsplit(self.path).path) == FETCH_CONTENT_TYPE_PATH: - return self._serve_content_type_resource - if name.startswith("do_") and unquote(urlsplit(self.path).path) in DOCUMENT_CHARSET_FIXTURES: - return self._serve_document_charset_resource - if name.startswith("do_") and unquote(urlsplit(self.path).path) == IFRAME_STASH_PATH: - return self._serve_iframe_stash_resource - - if name.startswith("do_") and unquote(urlsplit(self.path).path) in XHR_URL_RESOURCE_PATHS: - return self._serve_xhr_url_method - if name.startswith("do_") and unquote(urlsplit(self.path).path) == COMMON_ECHO_PATH: - return self._serve_common_echo_resource - if name.startswith("do_") and unquote(urlsplit(self.path).path) == COMMON_REDIRECT_PATH: - return self._serve_common_redirect_resource - if name.startswith("do_") and unquote(urlparse(self.path).path) == NAVIGATION_SECOND_VISIT_PATH: - return self._serve_navigation_second_visit - if name.startswith("do_") and unquote(urlparse(self.path).path) == FETCH_EMPTY_LOCATION_PATH: - return self._serve_empty_location_resource - if name.startswith("do_") and unquote(urlsplit(self.path).path) in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: - return self._serve_service_worker_script_resource - if name.startswith("do_") and unquote(urlsplit(self.path).path) in FETCH_PREFLIGHT_RESOURCE_PATHS: - return self._serve_fetch_resource_method - if name.startswith("do_") and unquote(urlparse(self.path).path) in XHR_RESPONSE_RESOURCE_PATHS: - return self._serve_xhr_response_resource - if name.startswith("do_") and unquote(urlparse(self.path).path) in FETCH_RANGE_RESOURCE_PATHS: - return self._serve_fetch_resource_method + def __getattr__(self, name: str) -> Callable[[], None]: + if name.startswith("do_"): + path = unquote(urlsplit(getattr(self, "path", "")).path) + if path == COMMON_ECHO_PATH: + return self._serve_common_echo_resource + if path == COMMON_REDIRECT_PATH: + return self._serve_common_redirect_resource + if path == IFRAME_STASH_PATH: + return self._serve_iframe_stash_resource + if path in REMOTE_CONTEXT_RESOURCE_PATHS: + return self._serve_remote_context_resource + if path == PRELOAD_COUNT_PATH: + return self._serve_preload_count_resource + if path == FETCH_EMPTY_LOCATION_PATH: + return self._serve_empty_location_resource + if path == FETCH_CONTENT_TYPE_PATH: + return self._serve_content_type_resource + if path in DOCUMENT_CHARSET_FIXTURES: + return self._serve_document_charset_resource + if path == NAVIGATION_SECOND_VISIT_PATH: + return self._serve_navigation_second_visit + if path in SERVICE_WORKER_SCRIPT_RESOURCE_PATHS: + return self._serve_service_worker_script_resource + if path in XHR_RESOURCE_PATHS: + return self._serve_xhr_method + if path in FETCH_PREFLIGHT_RESOURCE_PATHS | FETCH_REDIRECT_RESOURCE_PATHS | FETCH_RANGE_RESOURCE_PATHS | { + FETCH_INSPECT_HEADERS_PATH + }: + return self._serve_fetch_resource_method raise AttributeError(name) def _read_content_length_request_body(self, *, ignore_transfer_encoding: bool = False) -> bytes | None: @@ -3765,39 +3642,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); return None return raw - def _serve_xhr_cors_echo(self, parsed, *, emit_body: bool) -> None: - try: - params = parse_qs(parsed.query, keep_blank_values=True, encoding="latin-1") - if "delay" in params: - time.sleep(int(params["delay"][0])) - request_body = self._read_content_length_request_body() - if request_body is None: - return - status, reason, body = 200, None, b"Test" - headers = [ - ("Access-Control-Allow-Origin", "*"), - ("Access-Control-Allow-Credentials", "true"), - ("Access-Control-Allow-Methods", "GET, POST, PUT, FOO"), - ("Access-Control-Allow-Headers", "x-test, x-foo"), - ("Access-Control-Expose-Headers", - "x-request-method, x-request-content-type, x-request-query, " - "x-request-content-length, x-request-data"), - ] - if "safelist_content_type" in params: - headers.append(("Access-Control-Allow-Headers", "content-type")) - headers.extend([ - ("X-Request-Method", self.command), - ("X-Request-Query", parsed.query or "NO"), - ("X-Request-Content-Length", self.headers.get("Content-Length", "NO")), - ("X-Request-Content-Type", self.headers.get("Content-Type", "NO")), - ("X-Request-Data", request_body.decode("latin-1")), - ]) - except (ValueError, OSError, OverflowError): - self.send_error(500) - return - self._send_bytes(None, body, emit_body=emit_body, extra_headers=headers, - status_code=status, status_text=reason) - def _serve_common_redirect_resource(self) -> bool: parsed = urlsplit(self.path) if unquote(parsed.path) != COMMON_REDIRECT_PATH: @@ -3840,53 +3684,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); ) return True - def _serve_xhr_body_resource(self, parsed, *, emit_body: bool) -> None: - path = unquote(parsed.path) - upload_consumed = False - try: - if path == "/xhr/resources/content.py": - params = parse_qs(parsed.query, keep_blank_values=True, encoding="latin-1") - if "content" in params: - body = params["content"][0].encode("latin-1") - else: - body = self._read_content_length_request_body() - if body is None: - return - upload_consumed = True - content_type = "text/plain" - if "response_charset_label" in params: - content_type += ";charset=" + params["response_charset_label"][0] - headers = [ - ("Content-Type", content_type), - ("X-Request-Method", self.command), - ("X-Request-Query", parsed.query or "NO"), - ("X-Request-Content-Length", self.headers.get("Content-Length", "NO")), - ("X-Request-Content-Type", self.headers.get("Content-Type", "NO")), - ] - elif path == "/xhr/resources/echo-content-type.py": - headers = [("Content-Type", "text/plain")] - body = self.headers.get("Content-Type", "").encode("latin-1") - else: - content_type, body = XHR_DOCUMENT_FIXTURES[path] - headers = [("Content-Type", content_type)] - except (ValueError, KeyError, OverflowError, OSError): - self.send_error(500) - return - # Unused uploads must not delay an early response or be parsed as - # another request. Consumed uploads can finish normally. - if path == "/xhr/resources/echo-content-type.py" or not upload_consumed and ( - self.headers.get("Transfer-Encoding") is not None - or self.headers.get("Content-Length", "0").strip() not in {"", "0"} - ): - self.close_connection = True - headers.append(("Connection", "close")) - self._send_bytes( - None, body, emit_body=emit_body, extra_headers=headers, cache_control=None, - auto_content_length=( - path != "/xhr/resources/echo-content-type.py" or "Content-Type" in self.headers - ), - ) - def _xhr_request_url(self) -> str: if self.path.startswith("http://"): return self.path @@ -3925,151 +3722,16 @@ requestExecutor("{executor_uuid}", {start_on_js}); # Clients may abort once they receive a partial response. return - def _serve_xhr_infinite_redirects(self, parsed, *, emit_body: bool) -> None: - try: - params = parse_qs(parsed.query, keep_blank_values=True, encoding="latin-1") - page = "default" if params.get("page", [None])[0] == "alternate" else "alternate" - redirect_type = 301 if params.get("type", [None])[0] == "301" else 302 - mix = int(params.get("mix", [None])[0] == "1") - if mix: - redirect_type = 302 if redirect_type == 301 else 301 - request_url = urlsplit(self._xhr_request_url()) - location = urlunsplit(( - request_url.scheme, request_url.netloc, request_url.path, - f"page={page}&type={redirect_type}&mix={mix}", "", - )) - # Upstream returns 301 regardless of the next URL's `type`. - status, reason = 301, None - headers = [("Pragma", "no-cache"), ("Location", location)] - cache_control = "no-cache" - body = ("Hello guest. You have been redirected to " + location).encode("utf-8") - except (ValueError, OSError, OverflowError): - self.send_error(500) - return - self.close_connection = True - headers.append(("Connection", "close")) - self._send_bytes(None, body, emit_body=emit_body, extra_headers=headers, - status_code=status, status_text=reason, cache_control=cache_control) - - def _serve_xhr_preflight_fixture(self, parsed, *, emit_body: bool) -> None: - status, headers, body = 200, [], b"" - try: - if unquote(parsed.path) == "/xhr/resources/access-control-basic-put-allow.py": - headers = [("Content-Type", "text/plain")] - if self.command in {"OPTIONS", "PUT"}: - origin = self.headers.get("Origin") - if origin is None: - raise ValueError("upstream handler requires Origin") - headers.extend([ - ("Access-Control-Allow-Credentials", "true"), - ("Access-Control-Allow-Origin", origin), - ]) - if self.command == "OPTIONS": - headers.append(("Access-Control-Allow-Methods", "PUT")) - else: - request_body = self._read_content_length_request_body() - if request_body is None: - return - body = b"PASS: Cross-domain access allowed.\n" + request_body - else: - body = b"Wrong method: " + self.command.encode("latin-1") - elif self.command == "OPTIONS": - headers = [ - ("Access-Control-Allow-Origin", "*"), - ("Access-Control-Allow-Headers", "*"), - ] - elif self.command == "GET": - headers = [("Access-Control-Allow-Origin", "*")] - if self.headers.get("X-Test"): - headers.append(("Content-Type", "text/plain")) - body = b"PASS" - else: - status = 400 - except ValueError: - self.send_error(500) - return - self.close_connection = True - headers.append(("Connection", "close")) - self._send_bytes(None, body, emit_body=emit_body, extra_headers=headers, - status_code=status, cache_control=None) - - def _serve_xhr_response_resource(self, *, emit_body: bool = True) -> bool: - parsed = urlparse(self.path) - path = unquote(parsed.path) - if path not in XHR_RESPONSE_RESOURCE_PATHS: - return False - if path in XHR_BODY_RESOURCE_PATHS: - self._serve_xhr_body_resource(parsed, emit_body=emit_body) - return True - if path in { - "/xhr/resources/access-control-basic-put-allow.py", - "/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py", - }: - self._serve_xhr_preflight_fixture(parsed, emit_body=emit_body) - return True - if path == "/xhr/resources/corsenabled.py": - self._serve_xhr_cors_echo(parsed, emit_body=emit_body) - return True - if path == "/xhr/resources/bad-chunk-encoding.py": - self._serve_xhr_bad_chunk_encoding() - return True - if path == "/xhr/resources/infinite-redirects.py": - self._serve_xhr_infinite_redirects(parsed, emit_body=emit_body) - return True - try: - if path == "/xhr/resources/inspect-headers.py": - status, reason, content_type = 200, None, None - headers, body = _xhr_inspect_headers_fixture_response( - parsed.query, list(self.headers.raw_items()), - ) - elif path == "/xhr/resources/headers.py": - status, reason, content_type, body = 200, None, "text/plain", b"TEST" - headers = [ - ("X-Custom-Header", "test"), - ("Set-Cookie", "test"), - ("Set-Cookie2", "test"), - ("X-Custom-Header-Empty", ""), - ("X-Custom-Header-Comma", "1"), - ("X-Custom-Header-Comma", "2"), - # send_header encodes strings as Latin-1; preserve the - # UTF-8 ellipsis bytes written by the upstream fixture. - ("X-Custom-Header-Bytes", "\u00e2\u0080\u00a6"), - ] - elif path == "/xhr/resources/echo-headers.py": - status, reason, content_type = 200, None, "text/plain" - headers = [] - body = str(self.headers).encode("utf-8") - elif path == "/xhr/resources/status.py": - status, reason, content_type, body = _fetch_status_response(parsed.query) - headers = [("X-Request-Method", self.command)] - else: - source = wpt_root / "xhr/resources/well-formed.xml" - modified = formatdate(source.stat().st_mtime, usegmt=True) - body = source.read_text(encoding="utf-8").encode("utf-8") - status, reason, content_type = 200, None, "application/xml" - headers = [("Last-Modified", modified)] - except (ValueError, OSError, OverflowError): - self.send_error(500) - return True - # These upstream handlers can respond without consuming the upload. - # Close the connection so unread bytes cannot become another request. - self.close_connection = True - headers.append(("Connection", "close")) - self._send_bytes( - content_type, body, emit_body=emit_body, extra_headers=headers, - status_code=status, status_text=reason, - ) - return True - def _serve_fetch_range_resource( self, path: str, query: str, *, emit_body: bool ) -> None: if not self._consume_request_body(): return params = parse_qs(query, keep_blank_values=True, encoding="latin-1") + stash_path = "/fetch/range/" try: if path.endswith("/stash-take.py"): - body = json.dumps(range_stash.take(params["key"][0])).encode("ascii") + body = json.dumps(fetch_stash.take(params["key"][0], path=stash_path)).encode("ascii") self._send_bytes("application/json", body, emit_body=emit_body, cache_control=None) return if self.command == "OPTIONS": @@ -4081,10 +3743,10 @@ requestExecutor("{executor_uuid}", {start_on_js}); range_key = params.get("range-received-key", [""])[0] encoding_key = params.get("accept-encoding-key", [""])[0] if range_key and range_header: - range_stash.put(range_key, "range-header-received", overwrite=True) + fetch_stash.put(range_key, "range-header-received", path=stash_path, overwrite=True) if encoding_key: - range_stash.put(encoding_key, ", ".join(self.headers.get_all("Accept-Encoding", [])), - overwrite=True) + fetch_stash.put(encoding_key, ", ".join(self.headers.get_all("Accept-Encoding", [])), + path=stash_path, overwrite=True) # Match long-wav.py's 8 kHz, 8-bit mono, five-minute response, # including its header-inclusive Content-Length convention. @@ -4139,7 +3801,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); except OSError: pass # Upstream ends its stream when a write reports disconnect. - def _serve_navigation_second_visit(self) -> None: if not self._consume_request_body(): return @@ -4304,7 +3965,6 @@ requestExecutor("{executor_uuid}", {start_on_js}); content_type, body = "application/javascript", b"export default 'hello';" self._send_bytes(content_type, body, emit_body=emit_body, cache_control=None) - def _serve_script_load_error_events( self, query: str, *, emit_body: bool, json_module: bool = False, ) -> None: @@ -4499,6 +4159,208 @@ requestExecutor("{executor_uuid}", {start_on_js}); getattr(self.server, "wpt_harness_timeout_multiplier", 1.0) ) + def _serve_xhr_method(self) -> None: + self._serve_xhr_resource(emit_body=self.command != "HEAD") + + def _serve_xhr_resource(self, *, emit_body: bool) -> bool: + parsed = urlsplit(self.path) + path = unquote(parsed.path) + if path not in XHR_RESOURCE_PATHS: + return False + if path == "/xhr/resources/bad-chunk-encoding.py": + self._serve_xhr_bad_chunk_encoding() + return True + upload_consumed = False + cache_control = "no-store" + try: + if path == "/xhr/resources/requri.py": + params = parse_qs(parsed.query, keep_blank_values=True) + uri = self._xhr_request_url() if "full" in params else self.path + status, reason, headers, body = 200, None, [], uri.encode("utf-8") + elif path == "/xhr/resources/infinite-redirects.py": + params = parse_qs(parsed.query, keep_blank_values=True, encoding="latin-1") + page = "default" if params.get("page", [None])[0] == "alternate" else "alternate" + redirect_type = 301 if params.get("type", [None])[0] == "301" else 302 + mix = int(params.get("mix", [None])[0] == "1") + if mix: + redirect_type = 302 if redirect_type == 301 else 301 + request_url = urlsplit(self._xhr_request_url()) + location = urlunsplit(( + request_url.scheme, request_url.netloc, request_url.path, + f"page={page}&type={redirect_type}&mix={mix}", "", + )) + # Upstream returns 301 regardless of the next URL's `type`. + status, reason = 301, None + headers = [("Pragma", "no-cache"), ("Location", location)] + cache_control = "no-cache" + body = ("Hello guest. You have been redirected to " + location).encode("utf-8") + elif path == "/xhr/resources/headers.py": + status, reason, body = 200, None, b"TEST" + headers = [ + ("Content-Type", "text/plain"), + ("X-Custom-Header", "test"), + ("Set-Cookie", "test"), + ("Set-Cookie2", "test"), + ("X-Custom-Header-Empty", ""), + ("X-Custom-Header-Comma", "1"), + ("X-Custom-Header-Comma", "2"), + # Upstream writes the UTF-8 bytes for an ellipsis. + # send_header encodes its string argument as Latin-1. + ("X-Custom-Header-Bytes", "\u00e2\u0080\u00a6"), + ] + elif path == "/xhr/resources/inspect-headers.py": + status, reason = 200, None + headers, body = _xhr_inspect_headers_fixture_response( + parsed.query, list(self.headers.raw_items()) + ) + elif path == "/xhr/resources/echo-headers.py": + status, reason = 200, None + headers = [("Content-Type", "text/plain")] + # wptserve exposes the same HTTPMessage as raw_headers. + body = str(self.headers).encode("utf-8") + elif path == "/xhr/resources/corsenabled.py": + params = parse_qs(parsed.query, keep_blank_values=True, encoding="latin-1") + if "delay" in params: + time.sleep(int(params["delay"][0])) + request_body = self._read_content_length_request_body() + if request_body is None: + return True + upload_consumed = True + status, reason, body = 200, None, b"Test" + headers = [ + ("Access-Control-Allow-Origin", "*"), + ("Access-Control-Allow-Credentials", "true"), + ("Access-Control-Allow-Methods", "GET, POST, PUT, FOO"), + ("Access-Control-Allow-Headers", "x-test, x-foo"), + ("Access-Control-Expose-Headers", + "x-request-method, x-request-content-type, x-request-query, " + "x-request-content-length, x-request-data"), + ] + if "safelist_content_type" in params: + headers.append(("Access-Control-Allow-Headers", "content-type")) + headers.extend([ + ("X-Request-Method", self.command), + ("X-Request-Query", parsed.query or "NO"), + ("X-Request-Content-Length", self.headers.get("Content-Length", "NO")), + ("X-Request-Content-Type", self.headers.get("Content-Type", "NO")), + ("X-Request-Data", request_body.decode("latin-1")), + ]) + elif path == "/xhr/resources/access-control-basic-put-allow.py": + status, reason, body = 200, None, b"" + headers = [("Content-Type", "text/plain")] + cache_control = None + if self.command in {"OPTIONS", "PUT"}: + origin = self.headers.get("Origin") + if origin is None: + raise ValueError("upstream handler requires Origin") + headers.extend([ + ("Access-Control-Allow-Credentials", "true"), + ("Access-Control-Allow-Origin", origin), + ]) + if self.command == "OPTIONS": + headers.append(("Access-Control-Allow-Methods", "PUT")) + else: + request_body = self._read_content_length_request_body() + if request_body is None: + return True + upload_consumed = True + body = b"PASS: Cross-domain access allowed.\n" + request_body + else: + body = b"Wrong method: " + self.command.encode("latin-1") + elif path == "/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py": + status, reason, headers, body = 200, None, [], b"" + cache_control = None + if self.command == "OPTIONS": + headers = [ + ("Access-Control-Allow-Origin", "*"), + ("Access-Control-Allow-Headers", "*"), + ] + elif self.command == "GET": + headers = [("Access-Control-Allow-Origin", "*")] + if self.headers.get("X-Test"): + headers.append(("Content-Type", "text/plain")) + body = b"PASS" + else: + status = 400 + elif path == "/xhr/resources/content.py": + cache_control = None + params = parse_qs(parsed.query, keep_blank_values=True, encoding="latin-1") + if "content" in params: + body = params["content"][0].encode("latin-1") + else: + body = self._read_content_length_request_body() + if body is None: + return True + upload_consumed = True + content_type = "text/plain" + if "response_charset_label" in params: + content_type += ";charset=" + params["response_charset_label"][0] + status, reason = 200, None + headers = [ + ("Content-Type", content_type), + ("X-Request-Method", self.command), + ("X-Request-Query", parsed.query or "NO"), + ("X-Request-Content-Length", self.headers.get("Content-Length", "NO")), + ("X-Request-Content-Type", self.headers.get("Content-Type", "NO")), + ] + elif path == "/xhr/resources/echo-content-type.py": + cache_control = None + status, reason = 200, None + headers = [("Content-Type", "text/plain")] + body = self.headers.get("Content-Type", "").encode("latin-1") + elif path == "/xhr/resources/status.py": + status, reason, content_type, body = _fetch_status_response(parsed.query) + headers = [ + ("Content-Type", content_type), + ("X-Request-Method", self.command), + ] + elif path == "/xhr/resources/last-modified.py": + source = wpt_root / "xhr/resources/well-formed.xml" + modified = formatdate(source.stat().st_mtime, usegmt=True) + body = source.read_text(encoding="utf-8").encode("utf-8") + status, reason = 200, None + headers = [ + ("Content-Type", "application/xml"), + ("Last-Modified", modified), + ] + elif path in XHR_DOCUMENT_FIXTURES: + cache_control = None + content_type, body = XHR_DOCUMENT_FIXTURES[path] + status, reason = 200, None + headers = [("Content-Type", content_type)] + else: + status, reason, headers, body, delay = _xhr_redirect_fixture_response( + parsed.path, parsed.query + ) + if delay is not None: + time.sleep(delay) + except (ValueError, KeyError, OverflowError, OSError): + self.send_error(500) + return True + # Close connections with unread uploads so early responses and + # redirects do not wait for the request body to finish. + if path in { + "/xhr/resources/echo-content-type.py", "/xhr/resources/echo-headers.py" + } or not upload_consumed and ( + self.headers.get("Transfer-Encoding") is not None + or self.headers.get("Content-Length", "0").strip() not in {"", "0"} + ): + self.close_connection = True + headers.append(("Connection", "close")) + self._send_bytes( + None, + body, + emit_body=emit_body, + extra_headers=headers, + status_code=status, + status_text=reason, + cache_control=cache_control, + auto_content_length=( + path != "/xhr/resources/echo-content-type.py" or "Content-Type" in self.headers + ), + ) + return True + return WptHandler diff --git a/moli-benchmark/tests/test_wpt_cross_fixture_resources.py b/moli-benchmark/tests/test_wpt_cross_fixture_resources.py index 13ec260830..eb198c71c9 100644 --- a/moli-benchmark/tests/test_wpt_cross_fixture_resources.py +++ b/moli-benchmark/tests/test_wpt_cross_fixture_resources.py @@ -40,6 +40,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): reports[0]["metadata"]["content_type"], "application/csp-report", ) + def test_fixture_server_accepts_standard_websocket_echo_endpoint(self) -> None: with tempfile.TemporaryDirectory() as root: root_path = Path(root) @@ -80,6 +81,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): while len(echoed) < 6: echoed += connection.recv(6 - len(echoed)) self.assertEqual(echoed, b"\x81\x04ping") + def test_fixture_server_serves_raw_asis_header_bytes(self) -> None: with tempfile.TemporaryDirectory() as root: root_path = Path(root) @@ -101,6 +103,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): header = response.headers["X-Custom-Header-Bytes"] self.assertEqual(header.encode("latin-1"), b"\xe2\x80\xa6") + def test_fixture_server_serves_raw_sidecar_header_bytes(self) -> None: with tempfile.TemporaryDirectory() as root: root_path = Path(root) @@ -122,6 +125,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): header = response.headers["Content-Type"] self.assertEqual(header.encode("latin-1"), b"\xc3\x97\xc2\xba invalid") + def test_fixture_server_uses_explicit_content_type_header_as_override(self) -> None: self.assertEqual( _static_response_header_block( @@ -134,6 +138,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): _static_response_header_block("application/javascript", [("X-Test", "ok")]), [("Content-Type", "application/javascript"), ("X-Test", "ok")], ) + def test_fixture_server_detects_explicit_content_length_header(self) -> None: header_block = _static_response_header_block( "text/html", @@ -141,6 +146,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): ) self.assertTrue(_headers_include(header_block, "content-length")) + def test_fixture_server_preserves_explicit_content_length_body_boundary(self) -> None: with tempfile.TemporaryDirectory() as root: root_path = Path(root) @@ -172,12 +178,14 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): self.assertEqual(served, body) self.assertNotIn(BENCH_TIMEOUT_MULTIPLIER_QUERY.encode("ascii"), served) + def test_fixture_server_maps_legacy_webidl_parser_resource(self) -> None: self.assertEqual( _legacy_wpt_resource_alias("/resources/WebIDLParser.js"), "resources/webidl2/lib/webidl2.js", ) self.assertIsNone(_legacy_wpt_resource_alias("/resources/testharness.js")) + def test_fixture_server_builds_any_js_window_wrapper(self) -> None: body = _any_js_window_wrapper( "/WebCryptoAPI/sign_verify/hmac.https.any.html", @@ -197,6 +205,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): self.assertIn(b'', body) self.assertIn(b'
    ', body) self.assertIn(b'', body) + def test_fixture_server_builds_window_js_wrapper_without_any_global(self) -> None: body = _window_js_window_wrapper( "/WebCryptoAPI/algorithm-discards-context.https.window.html", @@ -211,6 +220,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): self.assertIn(b'', body) self.assertIn(b'', body) self.assertNotIn(b"self.GLOBAL", body) + def test_fixture_server_builds_wpt_any_js_query_wrapper(self) -> None: html = _wpt_any_window_wrapper_html( "wasm/jsapi/feature.any.js", @@ -227,6 +237,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): html, ) self.assertNotIn("moli-wpt-any", html) + def test_fixture_server_builds_any_js_dedicated_worker_wrapper(self) -> None: html = _wpt_any_dedicated_worker_wrapper_html( "wasm/jsapi/feature.any.js", @@ -240,6 +251,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): html, ) self.assertNotIn("moli-wpt-any", html) + def test_fixture_server_builds_any_js_dedicated_worker_script(self) -> None: js = _wpt_any_dedicated_worker_wrapper_js( "wasm/jsapi/feature.any.js", @@ -255,6 +267,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): self.assertIn('importScripts("/wasm/jsapi/feature.any.js?variant=1");', js) self.assertTrue(js.rstrip().endswith("done();")) self.assertNotIn("moli-wpt-any", js) + def test_fixture_server_builds_window_js_wrapper(self) -> None: html = _wpt_window_js_wrapper_html( "wasm/serialization/transfer.window.js", @@ -271,6 +284,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): html, ) self.assertNotIn("moli-wpt-script", html) + def test_fixture_server_builds_worker_js_wrapper(self) -> None: html = _wpt_dedicated_worker_js_wrapper_html( "wasm/create_multiple_memory.worker.js", @@ -284,6 +298,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): html, ) self.assertNotIn("moli-wpt-script", html) + def test_any_js_worker_script_path_round_trips_to_source_path(self) -> None: worker_path = any_js_worker_script_path( "wasm/jsapi/feature.any.js?variant=1" @@ -297,6 +312,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): any_js_source_script_path(worker_path), "wasm/jsapi/feature.any.js?variant=1", ) + def test_any_js_case_path_for_global_replaces_existing_wrapper_query(self) -> None: self.assertEqual( any_js_case_path_for_global( @@ -305,6 +321,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): ), "wasm/jsapi/feature.any.js?variant=1&moli-wpt-any=dedicatedworker", ) + def test_fixture_server_resolves_any_js_meta_scripts_within_wpt_root(self) -> None: self.assertEqual( _resolve_wpt_static_script_url( @@ -326,6 +343,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): "../../../escape.js", ) ) + def test_fixture_server_substitutes_core_sub_template_variables(self) -> None: body = ( b"http://{{domains[www2]}}:{{ports[http][0]}}/" @@ -372,6 +390,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): b" ws-url=ws://example.test:12345/socket" b" wss-url=ws://example.test:12345/socket", ) + def test_fixture_server_substitutes_idna_domain_aliases(self) -> None: cases = [ ("{{domains[天気の良い日]}}", b"xn--n8j6ds53lwwkrqhv28a.localhost"), @@ -455,6 +474,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): "", ) ) + def test_fixture_server_maps_external_ipv6_domain_location_port_to_remote_port( self, ) -> None: @@ -480,6 +500,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): b" http://[2001:db8::1]:34567/d" b" http://[2001:db8::1]:23456/e", ) + def test_fixture_server_substitutes_get_query_template_variables(self) -> None: body = ( b"var expected_logs = {{GET[logs]}};" @@ -497,6 +518,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): b' var timeout = "2";' b' var missing = "";', ) + def test_fixture_server_substitutes_template_variables_in_sidecar_headers(self) -> None: with tempfile.TemporaryDirectory() as root: fixture = Path(root) / "frame-ancestors.sub.html" @@ -521,6 +543,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): ) ], ) + def test_fixture_server_substitution_preserves_non_utf8_bytes(self) -> None: body = b"\xff{{host}}\xfe{{ports[http][0]}}" @@ -528,6 +551,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): _substitute_wpt_template_variables(body, port=12345), b"\xfflocalhost\xfe12345", ) + def test_fixture_server_distinguishes_primary_host_from_request_hostname( self, ) -> None: @@ -547,10 +571,12 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): b"host=localhost domain=localhost location=www1.localhost " b"HTTP_ORIGIN: 'http://' + ORIGINAL_HOST + HTTP_PORT_ELIDED,", ) + def test_host_header_hostname_preserves_ipv6_brackets(self) -> None: self.assertEqual(_host_header_hostname("[2001:db8::1]:1234"), "[2001:db8::1]") self.assertEqual(_host_header_hostname("localhost:1234"), "localhost") self.assertEqual(_host_header_hostname("example.test"), "example.test") + def test_fixture_server_substitutes_ipv6_websocket_template_urls(self) -> None: body = ( b"ws=ws://{{host}}:{{ports[ws][0]}}/echo" @@ -566,6 +592,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): b"ws=ws://[2001:db8::1]:12345/echo" b" wss=ws://[2001:db8::1]:12345/echo", ) + def test_fixture_server_wasm_status_handler_normalizes_status_codes(self) -> None: self.assertEqual(_wasm_webapi_status_code("status=404"), 404) self.assertEqual(_wasm_webapi_status_code("status=300"), 300) @@ -573,6 +600,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): self.assertEqual(_wasm_webapi_status_code("status=700"), 599) self.assertEqual(_wasm_webapi_status_code("status=not-a-number"), 400) self.assertEqual(_wasm_webapi_status_code(""), 200) + def test_fixture_server_redirect_handler_models_wasm_origin_probe(self) -> None: self.assertEqual( _redirect_fixture_response( @@ -589,17 +617,20 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): _redirect_fixture_response("status=307&location=/target"), (307, "/target"), ) + def test_fixture_server_models_csp_resource_py(self) -> None: body, headers = _content_security_policy_resource_response() self.assertIn(b"success", body) self.assertIn(("Access-Control-Allow-Origin", "*"), headers) + def test_fixture_server_models_workers_modules_export_on_load_script_py(self) -> None: body, headers = _workers_modules_export_on_load_script_response() self.assertIn(b"export const importedModules", body) self.assertIn(("Content-Type", "text/javascript"), headers) self.assertIn(("Access-Control-Allow-Origin", "*"), headers) + def test_fixture_server_preserves_raw_sidecar_header_bytes(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) @@ -614,6 +645,7 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): self.assertEqual(headers, [("Content-Type", "\xc3\x97\xc2\xba invalid")]) for _name, value in headers: value.encode("latin-1") + def test_fixture_server_substitutes_get_host_info_remote_host_for_loopback(self) -> None: body = ( b"var REMOTE_HOST = (ORIGINAL_HOST === 'localhost') ? " @@ -724,4 +756,21 @@ class WptCrossFixtureResourcesTests(WptCrossTestCase): ], ) + def test_fixture_server_substitutes_request_header_or_default(self) -> None: + body = ( + b"present={{header_or_default(Referer, missing)}}" + b" absent={{header_or_default(X-Absent, )}}" + b" empty={{header_or_default(X-Empty, )}}" + ) + self.assertEqual( + _substitute_wpt_template_variables( + body, + port=12345, + request_headers={ + "referer": "https://example.test/path?a=1&b=2", + }, + ), + b"present=https://example.test/path?a=1&b=2" + b" absent=<missing> empty=", + ) diff --git a/moli-benchmark/tests/test_wpt_cross_fixture_responses.py b/moli-benchmark/tests/test_wpt_cross_fixture_responses.py index ed84d5f2e0..74e5ac423a 100644 --- a/moli-benchmark/tests/test_wpt_cross_fixture_responses.py +++ b/moli-benchmark/tests/test_wpt_cross_fixture_responses.py @@ -13,6 +13,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): ) self.assertEqual(_pipe_trickle_delay_seconds("pipe=trickle(d999)"), 10.0) self.assertEqual(_pipe_trickle_delay_seconds("notpipe=trickle(d1)"), 0.0) + def test_fixture_server_parses_delay_handler_duration(self) -> None: self.assertEqual(_wpt_delay_seconds("ms=3000"), 3.0) self.assertEqual(_wpt_delay_seconds("ms=2.5"), 0.0025) @@ -21,6 +22,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): self.assertIsNone(_wpt_delay_seconds("ms=invalid")) self.assertIsNone(_wpt_delay_seconds("ms=-1")) self.assertIsNone(_wpt_delay_seconds("ms=nan")) + def test_fixture_server_models_xhr_delay_py_methods(self) -> None: with tempfile.TemporaryDirectory() as root: root_path = Path(root) @@ -66,6 +68,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): ("YO", 200, b"TEST_DELAY", "text/plain", "*", "YO"), ], ) + def test_fixture_server_models_delayed_module_script_handler(self) -> None: with tempfile.TemporaryDirectory() as root: root_path = Path(root) @@ -111,6 +114,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): ("HEAD", 200, b"", "text/javascript"), ], ) + def test_fixture_server_models_common_redirect_opt_in_handler(self) -> None: with tempfile.TemporaryDirectory() as root: root_path = Path(root) @@ -129,6 +133,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): self.assertEqual(response.headers.get("Location"), "/target") self.assertEqual(response.headers.get("Timing-Allow-Origin"), "*") connection.close() + def test_fixture_server_drains_xhr_delay_yo_body_before_next_request( self, ) -> None: @@ -163,6 +168,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): self.assertIs(connection.sock, first_socket) finally: connection.close() + def test_fixture_server_drains_chunked_xhr_delay_body_before_next_request( self, ) -> None: @@ -202,6 +208,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): self.assertIs(connection.sock, first_socket) finally: connection.close() + def test_fixture_server_parses_wpt_header_pipe(self) -> None: def applied_pipe_headers(query: str) -> list[tuple[str, str]]: return _apply_header_operations( @@ -237,6 +244,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): [("X-Test", "bad Injected: x")], ) self.assertEqual(applied_pipe_headers("notpipe=header(X,Y)"), []) + def test_fixture_server_parses_wpt_status_pipe(self) -> None: self.assertEqual(_pipe_response_status("pipe=status(204)&cachebust=1"), 204) self.assertEqual( @@ -250,6 +258,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): self.assertIsNone(_pipe_response_status("pipe=status(099)")) self.assertIsNone(_pipe_response_status("pipe=status(600)")) self.assertIsNone(_pipe_response_status("notpipe=status(204)")) + def test_fixture_server_parses_wpt_headers_sidecar(self) -> None: with tempfile.TemporaryDirectory() as root: fixture = Path(root) / "case.html" @@ -265,6 +274,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): _sidecar_response_headers(fixture), [("Referrer-Policy", "no-referrer"), ("X-Test", "ok")], ) + def test_fixture_server_parses_sub_headers_sidecar_for_sub_files(self) -> None: with tempfile.TemporaryDirectory() as root: fixture = Path(root) / "worker.sub.js" @@ -278,6 +288,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): _sidecar_response_headers(fixture), [("Content-Security-Policy", "connect-src 'none'")], ) + def test_fixture_server_combines_immediate_directory_and_file_headers(self) -> None: with tempfile.TemporaryDirectory() as root: root_path = Path(root) @@ -306,6 +317,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): ("X-File", "exact"), ], ) + def test_fixture_server_prefers_file_sub_headers_when_both_exist(self) -> None: with tempfile.TemporaryDirectory() as root: fixture = Path(root) / "case.sub.html" @@ -323,6 +335,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): _sidecar_response_headers(fixture), [("X-Source", "substituted")], ) + def test_fixture_server_prefers_directory_sub_headers_when_both_exist(self) -> None: with tempfile.TemporaryDirectory() as root: root_path = Path(root) @@ -341,6 +354,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): _sidecar_response_headers(fixture), [("X-Source", "substituted")], ) + def test_fixture_server_content_type_sidecar_overrides_static_guess(self) -> None: content_type, headers = _response_content_type_and_extra_headers( "application/octet-stream", @@ -352,6 +366,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): self.assertEqual(content_type, "text/javascript; charset=utf-8") self.assertEqual(headers, [("X-Test", "ok")]) + def test_fixture_server_combines_sidecar_and_pipe_headers_for_static_responses( self, ) -> None: @@ -390,6 +405,7 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): ), ], ) + def test_fixture_server_reads_sub_headers_for_plain_static_resource(self) -> None: with tempfile.TemporaryDirectory() as root: fixture = Path(root) / "policy.html" @@ -692,3 +708,44 @@ class WptCrossFixtureResponsesTests(WptCrossTestCase): ("text/javascript", "nosniff", b"// nothing to see here\np()"), ], ) + + def test_fixture_server_models_dynamic_import_redirect_without_cors(self) -> None: + with tempfile.TemporaryDirectory() as root: + root_path = Path(root) + (root_path / "resources").mkdir() + (root_path / "resources" / "testharness.js").write_text( + "// testharness", encoding="utf-8" + ) + with WptFixtureServer(root_path) as server: + for method in ("GET", "HEAD"): + for query, status, location in ( + ("location=%2Ftarget", 302, "/target"), + ( + "status=307&location=https%3A%2F%2Fexample.test%2Fx", + 307, + "https://example.test/x", + ), + ("status=invalid&location=%2Ftarget", 302, "/target"), + ( + "status=302&status=307&location=%2Fa&location=%2Fb", + 302, + "/a", + ), + ): + with self.subTest(method=method, query=query): + connection = HTTPConnection("127.0.0.1", server.port, timeout=2) + try: + connection.request( + method, + "/html/semantics/scripting-1/the-script-element/module/" + f"dynamic-import/beta/redirect.py?{query}", + ) + response = connection.getresponse() + self.assertEqual(response.status, status) + self.assertEqual(response.headers.get("Location"), location) + self.assertIsNone( + response.headers.get("Access-Control-Allow-Origin") + ) + self.assertEqual(response.read(), b"") + finally: + connection.close() diff --git a/moli-benchmark/tests/test_wpt_fetch_preflight_server.py b/moli-benchmark/tests/test_wpt_fetch_preflight_server.py index a4974b0894..ec874e14f8 100644 --- a/moli-benchmark/tests/test_wpt_fetch_preflight_server.py +++ b/moli-benchmark/tests/test_wpt_fetch_preflight_server.py @@ -231,7 +231,7 @@ class FetchPreflightFixtureTests(unittest.TestCase): } utils = self.root / "fetch/api/resources/utils.js" utils.parent.mkdir(parents=True) - utils.write_text('var RESOURCES_DIR = "../resources/";') + utils.write_text('var RESOURCES_DIR = "../resources/"; fetch(RESOURCES_DIR + "inspect-headers.py");') for name, source in cases.items(): path = self.root / f"fetch/api/cors/{name}.any.js" path.parent.mkdir(parents=True, exist_ok=True) diff --git a/moli-benchmark/tests/test_wpt_pipes_server.py b/moli-benchmark/tests/test_wpt_pipes_server.py index 5ce8f2508b..4858be17ab 100644 --- a/moli-benchmark/tests/test_wpt_pipes_server.py +++ b/moli-benchmark/tests/test_wpt_pipes_server.py @@ -119,6 +119,29 @@ class WptPipeFixtureTests(unittest.TestCase): self.assertEqual(status, 200) self.assertEqual(headers.get_all("X-Test"), expected) + def test_explicit_cache_control_is_not_overridden_by_the_fixture_default(self) -> None: + for method in ("GET", "HEAD"): + with self.subTest(method=method, source="default"): + status, headers, _ = self.request("", method=method) + self.assertEqual(status, 200) + self.assertEqual(headers.get_all("Cache-Control"), ["no-store"]) + (self.root / "module.json.headers").write_text("cAcHe-CoNtRoL: max-age=600\n") + for pipe, expected in ( + ("", ["max-age=600"]), + ("header(Cache-Control,max-age=30)", ["max-age=30"]), + ("header(cache-control,public,true)", ["max-age=600", "public"]), + ("header(Cache-Control,)", [""]), + ): + for method in ("GET", "HEAD"): + with self.subTest(method=method, pipe=pipe): + status, headers, body = self.request(urlencode({"pipe": pipe}), method=method) + self.assertEqual(status, 200) + self.assertEqual(headers.get_all("Cache-Control"), expected) + self.assertEqual(body, b"" if method == "HEAD" else b'{"value": 1}') + (self.root / "module.json.headers").unlink() + _, headers, _ = self.request(urlencode({"pipe": "header(Cache-Control,max-age=60)"})) + self.assertEqual(headers.get_all("Cache-Control"), ["max-age=60"]) + def test_only_last_nonempty_pipe_parameter_is_applied(self) -> None: cases = ( (["unknown", "header(X-Test,last)"], 200, "last"), @@ -156,6 +179,22 @@ class WptPipeFixtureTests(unittest.TestCase): self.assertEqual(headers["X-Test"], "before Injected: value") self.assertIsNone(headers["Injected"]) + def test_status_conversion_and_errors_also_work_for_inspection_methods(self) -> None: + for method in ("GET", "HEAD", "POST", "OPTIONS", "CUSTOM"): + for command, expected_status in ( + ("status(00201)", 201), + ("status( +201 )", 201), + ("status(invalid)", 500), + ("status(201)|unknown", 500), + ): + with self.subTest(method=method, command=command): + status, _, body = self.request( + urlencode({"pipe": command}), method=method, + path="/fetch/api/resources/inspect-headers.py", + ) + self.assertEqual(status, expected_status) + if method == "HEAD": + self.assertEqual(body, b"") if __name__ == "__main__": diff --git a/moli-benchmark/tests/test_wpt_xhr_cors_fixture.py b/moli-benchmark/tests/test_wpt_xhr_cors_fixture.py index 193a098348..15ced0353b 100644 --- a/moli-benchmark/tests/test_wpt_xhr_cors_fixture.py +++ b/moli-benchmark/tests/test_wpt_xhr_cors_fixture.py @@ -12,11 +12,10 @@ from moli_benchmark.wpt_cross.server import WptFixtureServer RESOURCE = "/xhr/resources/corsenabled.py" - - PUT_RESOURCE = "/xhr/resources/access-control-basic-put-allow.py" STAR_RESOURCE = "/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py" + class XhrCorsFixtureTests(unittest.TestCase): def setUp(self) -> None: self.stack = ExitStack() @@ -100,24 +99,6 @@ class XhrCorsFixtureTests(unittest.TestCase): status, _, _ = self.request(server, resource=resource) self.assertEqual(status, 404) - def test_case_selection_recognizes_relative_and_absolute_references(self) -> None: - sources = { - "xhr/absolute.html": f"fetch('{RESOURCE}');", - "xhr/relative.html": "fetch('resources/corsenabled.py');", - "xhr/nested/relative.html": "fetch('../resources/corsenabled.py?delay=0');", - "xhr/unknown.html": "fetch('resources/corsenabled.py'); fetch('unknown.py');", - "xhr/wrong-relative.html": "fetch('corsenabled.py');", - } - for name, script in sources.items(): - path = self.root / name - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text('') - selected = enumerate_cases(self.root, dir_prefixes=("xhr",)) - self.assertEqual([case.case_path for case in selected], [ - "xhr/absolute.html", "xhr/nested/relative.html", "xhr/relative.html", - ]) - def test_put_preflight_and_upload_preserve_origin_and_bytes(self) -> None: server = self.stack.enter_context(WptFixtureServer(self.root)) for origin in ("https://caller.test", "null", ""): @@ -143,7 +124,6 @@ class XhrCorsFixtureTests(unittest.TestCase): self.assertEqual(headers["Access-Control-Allow-Credentials"], "true") self.assertIsNone(headers["Access-Control-Allow-Methods"]) - def test_put_fixture_missing_origin_matches_upstream_failure(self) -> None: server = self.stack.enter_context(WptFixtureServer(self.root)) for method in ("OPTIONS", "PUT"): @@ -152,7 +132,6 @@ class XhrCorsFixtureTests(unittest.TestCase): self.assertEqual(status, 500) self.assertIsNone(headers["Access-Control-Allow-Origin"]) - def test_put_fixture_reports_other_methods_without_cors_headers(self) -> None: server = self.stack.enter_context(WptFixtureServer(self.root)) for method in ("GET", "HEAD", "POST", "DELETE", "FOO"): @@ -164,7 +143,6 @@ class XhrCorsFixtureTests(unittest.TestCase): self.assertEqual(headers["Content-Length"], str(len(expected))) self.assertIsNone(headers["Access-Control-Allow-Origin"]) - def test_wildcard_preflight_and_actual_request_have_distinct_headers(self) -> None: server = self.stack.enter_context(WptFixtureServer(self.root)) status, headers, body = self.request(server, "OPTIONS", resource=STAR_RESOURCE) @@ -184,7 +162,6 @@ class XhrCorsFixtureTests(unittest.TestCase): self.assertEqual(headers["Content-Type"], "text/plain") self.assertIsNone(headers["Access-Control-Allow-Headers"]) - def test_wildcard_fixture_rejects_missing_or_empty_test_header(self) -> None: server = self.stack.enter_context(WptFixtureServer(self.root)) for request_headers in ({}, {"X-Test": ""}): @@ -196,7 +173,6 @@ class XhrCorsFixtureTests(unittest.TestCase): self.assertEqual(headers["Access-Control-Allow-Origin"], "*") self.assertIsNone(headers["Content-Type"]) - def test_wildcard_fixture_other_methods_have_no_cors_grants(self) -> None: server = self.stack.enter_context(WptFixtureServer(self.root)) for method in ("HEAD", "POST", "PUT", "DELETE", "FOO"): @@ -208,7 +184,6 @@ class XhrCorsFixtureTests(unittest.TestCase): self.assertIsNone(headers["Access-Control-Allow-Origin"]) self.assertIsNone(headers["Content-Type"]) - def test_preflight_fixtures_require_exact_resource_paths(self) -> None: server = self.stack.enter_context(WptFixtureServer(self.root)) for resource in (PUT_RESOURCE, STAR_RESOURCE): @@ -217,7 +192,6 @@ class XhrCorsFixtureTests(unittest.TestCase): status, _, _ = self.request(server, resource=wrong) self.assertEqual(status, 404) - def test_preflight_case_selection_rejects_unsupported_dependencies(self) -> None: for resource in (PUT_RESOURCE, STAR_RESOURCE): name = Path(resource).name @@ -240,3 +214,21 @@ class XhrCorsFixtureTests(unittest.TestCase): for prefix in ("absolute-", "relative-", "nested/relative-") ) self.assertEqual([case.case_path for case in selected], expected) + + def test_case_selection_recognizes_relative_and_absolute_references(self) -> None: + sources = { + "xhr/absolute.html": f"fetch('{RESOURCE}');", + "xhr/relative.html": "fetch('resources/corsenabled.py');", + "xhr/nested/relative.html": "fetch('../resources/corsenabled.py?delay=0');", + "xhr/unknown.html": "fetch('resources/corsenabled.py'); fetch('unknown.py');", + "xhr/wrong-relative.html": "fetch('corsenabled.py');", + } + for name, script in sources.items(): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text('') + selected = enumerate_cases(self.root, dir_prefixes=("xhr",)) + self.assertEqual([case.case_path for case in selected], [ + "xhr/absolute.html", "xhr/nested/relative.html", "xhr/relative.html", + ]) diff --git a/moli-benchmark/tests/wpt_cross_test_support.py b/moli-benchmark/tests/wpt_cross_test_support.py index 227652158b..8902f62e2f 100644 --- a/moli-benchmark/tests/wpt_cross_test_support.py +++ b/moli-benchmark/tests/wpt_cross_test_support.py @@ -110,9 +110,6 @@ from moli_benchmark.wpt_cross.scheduler import ( build_run_schedule, ) from moli_benchmark.wpt_cross.server import ( - _nosniff_javascript_response, - _workers_url_encoding_response, - _fetch_status_response, BENCH_REPORT_BRIDGE, BENCH_TESTDRIVER_VENDOR_BRIDGE, BENCH_TIMEOUT_MULTIPLIER_QUERY, @@ -126,8 +123,10 @@ from moli_benchmark.wpt_cross.server import ( _inject_bench_report_bridge_config, _host_header_hostname, _headers_include, - _normalize_harness_case_key, _inspect_headers_response_headers, + _fetch_status_response, + _normalize_harness_case_key, + _nosniff_javascript_response, _needs_wpt_template_substitution, _legacy_wpt_resource_alias, _pipe_response_header_operations, @@ -142,6 +141,7 @@ from moli_benchmark.wpt_cross.server import ( _substitute_wpt_template_variables, _window_js_window_wrapper, _wasm_webapi_status_code, + _workers_url_encoding_response, _wpt_delay_seconds, _wpt_dedicated_worker_js_wrapper_html, _wpt_any_dedicated_worker_wrapper_html, @@ -156,8 +156,6 @@ from moli_benchmark.wpt_cross.server import ( clear_current_proxy_env() - - class WptCrossTestCase(unittest.TestCase): def _run_wpt_cross_with_fake_moli( self,