From 2a4829fa4e9dfd1674d67ca53fcc0f7b02c6ab04 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 10 Sep 2026 17:57:53 +0800 Subject: [PATCH] fix(wpt): stream the Fetch trickle fixture response Model trickle.py by draining the request body, delaying headers and body chunks, and writing TEST_TRICKLE lines according to ms/count. Preserve the optional Content-Type omission and close-delimited response framing. Exercise supported methods, defaults, duplicate/invalid parameters, progressive delivery, and both fixed-length and chunked uploads. Validation: 221 Python tests passed. With the same Moli binary, 16 WPT cases add two passing cases and eight passing subtests, with no regressions. No Rust source or build metadata changed. --- .../moli_benchmark/wpt_cross/server.py | 38 ++++++ moli-benchmark/tests/test_wpt_cross.py | 109 ++++++++++++++++++ .../wpt-cross-current/passed-cases.txt | 2 + 3 files changed, 149 insertions(+) diff --git a/moli-benchmark/moli_benchmark/wpt_cross/server.py b/moli-benchmark/moli_benchmark/wpt_cross/server.py index 602e2a2fc..f376f87ce 100644 --- a/moli-benchmark/moli_benchmark/wpt_cross/server.py +++ b/moli-benchmark/moli_benchmark/wpt_cross/server.py @@ -2345,6 +2345,9 @@ def _make_handler( 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 == "/fetch/api/resources/inspect-headers.py": self._send_bytes( "text/plain", @@ -3363,6 +3366,41 @@ def _make_handler( except (BrokenPipeError, ConnectionResetError): return + def _serve_fetch_trickle(self, query: str, *, emit_body: bool) -> None: + params = parse_qs(query, keep_blank_values=True) + delay = _wpt_delay_seconds(query) + try: + count = int(params.get("count", ["50"])[0]) + except ValueError: + self.send_error(500) + return + if delay is None: + self.send_error(500) + return + # Upstream reads the upload before delaying the response headers. + if not self._consume_request_body(): + return + self.close_connection = True + try: + time.sleep(delay) + self.send_response(200) + if "notype" not in params: + self.send_header("Content-Type", "text/plain") + # Like wptserve's explicit writer, delimit the body by EOF. + # Flush each chunk so readers can consume it before EOF. + self.send_header("Connection", "close") + self.end_headers() + self.wfile.flush() + if not emit_body: + return + time.sleep(delay) + for _ in range(count): + self.wfile.write(b"TEST_TRICKLE\n") + self.wfile.flush() + time.sleep(delay) + except (BrokenPipeError, ConnectionResetError, OSError): + return + def _serve_fetch_status(self, query: str, *, emit_body: bool) -> None: try: status, text, content_type, body = _fetch_status_response(query) diff --git a/moli-benchmark/tests/test_wpt_cross.py b/moli-benchmark/tests/test_wpt_cross.py index 2591faa7f..1e3589493 100644 --- a/moli-benchmark/tests/test_wpt_cross.py +++ b/moli-benchmark/tests/test_wpt_cross.py @@ -4391,6 +4391,115 @@ test(() => {}, "ok"); finally: connection.close() + def test_fixture_server_models_fetch_trickle_parameters_and_methods(self) -> None: + with tempfile.TemporaryDirectory() as root: + root_path = Path(root) + (root_path / "resources").mkdir() + (root_path / "resources" / "testharness.js").write_text("// testharness") + with WptFixtureServer(root_path) as server, patch( + "moli_benchmark.wpt_cross.server.time.sleep" + ) as sleep: + cases = [ + ("", 50, "text/plain", 0.5), + ("count=2&count=9&ms=1.5&ms=900", 2, "text/plain", 0.0015), + ("count=0&ms=0¬ype", 0, None, 0.0), + ("count=-1&ms=0¬ype=false", 0, None, 0.0), + ] + for method in ["GET", "HEAD", "POST", "OPTIONS", "PUT", "PATCH", "DELETE", "YO"]: + for query, count, mime, delay in cases: + with self.subTest(method=method, query=query): + sleep.reset_mock() + connection = HTTPConnection("127.0.0.1", server.port, timeout=2) + try: + connection.request( + method, "/fetch/api/resources/trickle.py?" + query, + body=b"upload" if method not in {"GET", "HEAD"} else None, + ) + response = connection.getresponse() + self.assertEqual(response.status, 200) + self.assertEqual(response.headers.get("Content-Type"), mime) + self.assertIsNone(response.headers.get("Content-Length")) + self.assertIsNone(response.headers.get("Transfer-Encoding")) + self.assertEqual(response.read(), b"" if method == "HEAD" else b"TEST_TRICKLE\n" * count) + expected_sleeps = 1 if method == "HEAD" else count + 2 + self.assertEqual([args.args[0] for args in sleep.call_args_list], [delay] * expected_sleeps) + finally: + connection.close() + for query in ["count=invalid", "count=", "ms=invalid", "ms=-1", "ms=nan", "ms=inf"]: + with self.subTest(query=query): + connection = HTTPConnection("127.0.0.1", server.port, timeout=2) + try: + connection.request("GET", "/fetch/api/resources/trickle.py?" + query) + response = connection.getresponse() + self.assertEqual(response.status, 500) + response.read() + finally: + connection.close() + + def test_fixture_server_delivers_fetch_trickle_headers_and_chunks_before_eof(self) -> None: + with tempfile.TemporaryDirectory() as root: + root_path = Path(root) + (root_path / "resources").mkdir() + (root_path / "resources" / "testharness.js").write_text("// testharness") + headers_ready, chunk_ready = threading.Event(), threading.Event() + release_first, release_second = threading.Event(), threading.Event() + sleeps = [] + + def pause(delay: float) -> None: + sleeps.append(delay) + if len(sleeps) == 2: + headers_ready.set() + self.assertTrue(release_first.wait(2)) + elif len(sleeps) == 3: + chunk_ready.set() + self.assertTrue(release_second.wait(2)) + + with WptFixtureServer(root_path) as server, patch( + "moli_benchmark.wpt_cross.server.time.sleep", side_effect=pause + ): + connection = HTTPConnection("127.0.0.1", server.port, timeout=2) + try: + connection.request("GET", "/fetch/api/resources/trickle.py?count=2¬ype") + response = connection.getresponse() + self.assertTrue(headers_ready.wait(2)) + self.assertIsNone(response.headers.get("Content-Type")) + release_first.set() + self.assertEqual(response.read(13), b"TEST_TRICKLE\n") + self.assertTrue(chunk_ready.wait(2)) + release_second.set() + self.assertEqual(response.read(), b"TEST_TRICKLE\n") + self.assertEqual(sleeps, [0.5] * 4) + finally: + release_first.set() + release_second.set() + connection.close() + + def test_fixture_server_reads_upload_before_starting_fetch_trickle(self) -> None: + with tempfile.TemporaryDirectory() as root: + root_path = Path(root) + (root_path / "resources").mkdir() + (root_path / "resources" / "testharness.js").write_text("// testharness") + started = threading.Event() + with WptFixtureServer(root_path) as server, patch( + "moli_benchmark.wpt_cross.server.time.sleep", + side_effect=lambda _: started.set(), + ): + for framing, first, last in [ + (b"Content-Length: 6", b"abc", b"def"), + (b"Transfer-Encoding: chunked", b"3\r\nabc\r\n", b"3\r\ndef\r\n0\r\n\r\n"), + ]: + with self.subTest(framing=framing), socket.create_connection(("127.0.0.1", server.port), timeout=2) as connection: + started.clear() + connection.sendall( + b"POST /fetch/api/resources/trickle.py?count=1&ms=0 HTTP/1.1\r\n" + b"Host: localhost\r\n" + framing + b"\r\n\r\n" + first + ) + self.assertFalse(started.wait(0.05), "response started before upload completed") + connection.sendall(last) + self.assertTrue(started.wait(2)) + response = connection.makefile("rb").read() + self.assertEqual(response.split(b"\r\n\r\n", 1)[1], b"TEST_TRICKLE\n") + def test_fixture_server_models_fetch_inspect_headers_handler(self) -> None: self.assertEqual( _inspect_headers_response_headers( diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 6ccd526f9..879884f32 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -5527,6 +5527,8 @@ event-timing/supported-types-consistent-with-self.html fetch/api/abort/destroyed-context.html fetch/api/basic/response-null-body.any.js?moli-wpt-any=dedicatedworker fetch/api/basic/response-null-body.any.js?moli-wpt-any=window +fetch/api/basic/stream-response.any.js?moli-wpt-any=dedicatedworker +fetch/api/basic/stream-response.any.js?moli-wpt-any=window fetch/api/basic/text-utf8.any.js?moli-wpt-any=dedicatedworker fetch/api/basic/text-utf8.any.js?moli-wpt-any=window fetch/api/cors/data-url-iframe.html