fix(wpt): serve service worker update lifecycle fixtures

This commit is contained in:
ldm0
2026-09-23 00:14:07 +08:00
parent 8acf45f652
commit b66ea67b7b
3 changed files with 139 additions and 9 deletions
@@ -865,12 +865,16 @@ def _script_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str],
@lru_cache(maxsize=None)
def _service_worker_script_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], ...]:
references = []
for name in (
"redirect.py", "update-worker.py", "import-scripts-version.py",
"import-scripts-get.py", "import-scripts-echo.py",
"subdir/import-scripts-echo.py", "scope2/import-scripts-echo.py",
):
resource = "service-workers/service-worker/resources/" + name
resources = tuple(
"service-workers/service-worker/resources/" + name
for name in (
"redirect.py", "update-worker.py", "update-worker-from-file.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(
@@ -29,6 +29,8 @@ import http.client
import json
import math
import mimetypes
import os
import random
import re
import socket
import struct
@@ -147,6 +149,9 @@ FETCH_INSPECT_HEADERS_PATH = "/fetch/api/resources/inspect-headers.py"
SERVICE_WORKER_SCRIPT_RESOURCE_PATHS = {
"/service-workers/service-worker/resources/redirect.py",
"/service-workers/service-worker/resources/update-worker.py",
"/service-workers/service-worker/resources/update-worker-from-file.py",
"/service-workers/service-worker/resources/update-during-installation-worker.py",
"/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py",
"/service-workers/service-worker/resources/import-scripts-version.py",
"/service-workers/service-worker/resources/import-scripts-get.py",
"/service-workers/service-worker/resources/import-scripts-echo.py",
@@ -3603,7 +3608,33 @@ def _make_handler(
("Pragma", "no-cache"),
("Content-Type", "application/javascript"),
]
if path.endswith("/import-scripts-version.py"):
if path.endswith("/update-worker-from-file.py"):
count = fetch_stash.increment(params["Key"][0], path=parsed.path)
if count > 2:
self.send_error(500, "Unknown update worker state")
return
filename = os.fsdecode(params["First" if count == 1 else "Second"][0].encode("latin-1"))
source = ((wpt_root / path.lstrip("/")).parent / filename).resolve()
source.relative_to(wpt_root.resolve())
body = source.read_bytes()
elif path.endswith("/ServiceWorkerGlobalScope/resources/update-worker.py"):
headers = [
("Cache-Control", "max-age: 0"),
("Content-Type", "application/javascript"),
]
source = (wpt_root / path.lstrip("/")).with_suffix(".js")
script = source.read_text(encoding="utf-8")
body = f"// {time.time()}\n{script}".encode("utf-8")
elif path.endswith("/update-during-installation-worker.py"):
headers = [
("Content-Type", "application/javascript"),
("Cache-Control", "max-age=0"),
]
body = (
f"// {random.random()}\n"
"importScripts('update-during-installation-worker.js');"
).encode("ascii")
elif path.endswith("/import-scripts-version.py"):
# Match the upstream delay so update checks see new bytes.
if stopping.wait(0.1):
self.close_connection = True
@@ -3653,6 +3684,9 @@ def _make_handler(
if "\r" in value or "\n" in value:
raise ValueError("invalid response header")
value.encode("latin-1")
except OSError:
self.send_error(500)
return
except (KeyError, ValueError, UnicodeError):
self.send_error(400)
return
@@ -2,6 +2,7 @@ from __future__ import annotations
import re
import tempfile
import time
import unittest
import uuid
from concurrent.futures import ThreadPoolExecutor
@@ -31,12 +32,17 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase):
resources = self.root / DIRECTORY / "resources"
resources.mkdir(parents=True)
for name in (
"redirect.py", "update-worker.py", "import-scripts-version.py",
"redirect.py", "update-worker.py", "update-worker-from-file.py", "import-scripts-version.py",
"update-during-installation-worker.py",
"import-scripts-get.py", "import-scripts-echo.py",
"subdir/import-scripts-echo.py", "scope2/import-scripts-echo.py",
):
(resources / name).parent.mkdir(exist_ok=True)
(resources / name).write_text("# Python source must not be sent as a script")
global_resources = self.root / DIRECTORY / "ServiceWorkerGlobalScope/resources"
global_resources.mkdir(parents=True)
(global_resources / "update-worker.py").write_text("# Python source must not be sent as a script")
(global_resources / "update-worker.js").write_bytes(b"// caf\xc3\xa9\r\nself.ready = true;\r\n")
self.stack.enter_context(patch(
"moli_benchmark.wpt_cross.server._global_ipv6_address", return_value=None
))
@@ -49,7 +55,8 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase):
) -> tuple[int, list[tuple[str, str]], bytes]:
connection = HTTPConnection("127.0.0.1", port, timeout=5)
try:
connection.request(method, RESOURCES + resource + "?" + query)
path = resource if resource.startswith("/") else RESOURCES + resource
connection.request(method, path + "?" + query)
response = connection.getresponse()
return (
response.status,
@@ -181,6 +188,85 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase):
self.assertLess(versions[0], versions[1])
self.assertLess(versions[1], versions[2])
def test_update_from_file_preserves_bytes_and_shares_only_its_own_stash(self) -> None:
first, second = self.server(), self.server()
resources = self.root / DIRECTORY / "resources"
before, after = b"// before\r\n\xff\n", b"// after\x00\n"
(resources / "café.js").write_bytes(before)
(resources / "after.js").write_bytes(after)
key = str(uuid.uuid4())
query = urlencode([
("Key", key), ("First", "café.js"), ("First", "ignored.js"),
("Second", "after.js"),
])
for port, expected in (
(first.port, before),
(second.port, before),
(first.alternate_port, after),
):
status, headers, body = self.request(port, "update-worker-from-file.py", query)
self.assertEqual((status, body), (200, expected))
self.assertEqual(dict(headers)["content-type"], "application/javascript")
self.assertEqual(dict(headers)["cache-control"], "no-cache, must-revalidate")
self.assertEqual(dict(headers)["pragma"], "no-cache")
self.assertEqual(self.request(first.port, "update-worker-from-file.py", query)[0], 500)
self.assertEqual(
self.request(first.port, "update-worker.py", urlencode({"Key": key, "Mode": "normal"}))[2],
b"/* 1 */ ",
)
head_query = urlencode({"Key": str(uuid.uuid4()), "First": "café.js", "Second": "after.js"})
status, headers, body = self.request(first.port, "update-worker-from-file.py", head_query, method="HEAD")
self.assertEqual((status, dict(headers)["content-length"], body), (200, str(len(before)), b""))
self.assertEqual(self.request(first.port, "update-worker-from-file.py", head_query, method="POST")[2], after)
failure_query = urlencode({"Key": str(uuid.uuid4()), "First": "missing.js", "Second": "after.js"})
self.assertEqual(self.request(first.port, "update-worker-from-file.py", failure_query)[0], 500)
self.assertEqual(self.request(first.port, "update-worker-from-file.py", failure_query)[2], after)
def test_global_scope_update_script_uses_timestamp_and_original_text(self) -> None:
server = self.server()
resource = "/" + DIRECTORY + "/ServiceWorkerGlobalScope/resources/update-worker.py"
script = "// café\nself.ready = true;\n".encode("utf-8")
versions = []
for method in ("GET", "POST", "OPTIONS", "HEAD"):
before = time.time()
status, headers, body = self.request(server.port, resource, method=method)
after = time.time()
self.assertEqual(status, 200)
self.assertEqual(dict(headers)["cache-control"], "max-age: 0")
self.assertEqual(dict(headers)["content-type"], "application/javascript")
self.assertNotIn("pragma", dict(headers))
if method == "HEAD":
self.assertEqual(body, b"")
self.assertGreater(int(dict(headers)["content-length"]), len(script))
continue
stamp, source = body.split(b"\n", 1)
self.assertEqual(source, script)
self.assertTrue(stamp.startswith(b"// "))
versions.append(float(stamp[3:]))
self.assertLessEqual(before, versions[-1])
self.assertLessEqual(versions[-1], after)
self.assertEqual(versions, sorted(set(versions)))
def test_installation_update_script_changes_without_rewriting_its_import(self) -> None:
server = self.server()
versions = []
for method in ("GET", "POST", "OPTIONS", "HEAD"):
status, headers, body = self.request(server.port, "update-during-installation-worker.py", method=method)
self.assertEqual(status, 200)
self.assertEqual(dict(headers)["content-type"], "application/javascript")
self.assertEqual(dict(headers)["cache-control"], "max-age=0")
self.assertNotIn("pragma", dict(headers))
if method == "HEAD":
self.assertEqual(body, b"")
continue
version, script = body.split(b"\n", 1)
self.assertEqual(script, b"importScripts('update-during-installation-worker.js');")
self.assertTrue(version.startswith(b"// "))
versions.append(float(version[3:]))
self.assertGreaterEqual(versions[-1], 0)
self.assertLess(versions[-1], 1)
self.assertEqual(len(set(versions)), 3)
def test_imported_assignments_preserve_raw_parameters_and_directory(self) -> None:
server = self.server()
for resource, query, expected in (
@@ -212,6 +298,8 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase):
("update-worker.py", "Mode=normal"),
("update-worker.py", "Key=invalid&Mode=normal"),
("update-worker.py", "Key=" + str(uuid.uuid4())),
("update-worker-from-file.py", "First=x&Second=y"),
("update-worker-from-file.py", urlencode({"Key": str(uuid.uuid4()), "First": "../../../../outside.js"})),
("import-scripts-get.py", "output=x"),
("import-scripts-get.py", "msg=x"),
("import-scripts-echo.py", ""),
@@ -223,6 +311,10 @@ class ServiceWorkerScriptFixtureTests(unittest.TestCase):
cases = {
"redirect-relative.html": ("resources/redirect.py", True),
"update-absolute.html": (RESOURCES + "update-worker.py", True),
"update-from-file.html": ("resources/update-worker-from-file.py", True),
"update-installing.html": ("resources/update-during-installation-worker.py", True),
"ServiceWorkerGlobalScope/update.html": ("resources/update-worker.py", True),
"ServiceWorkerGlobalScope/update-wrong.html": ("resources/update-worker-from-file.py", False),
"sub/version-relative.html": ("../resources/import-scripts-version.py", True),
"sub/redirect-dot-relative.html": ("./../resources/redirect.py", True),
"sub/redirect-wrong-relative.html": ("resources/redirect.py", False),