mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 16:01:39 +00:00
837 lines
30 KiB
Python
837 lines
30 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
from moli_benchmark.chrome_dcl import (
|
|
CdpDclDumpResult,
|
|
CdpDclDumpTimeoutError,
|
|
DEFAULT_CHROME_DCL_USER_AGENT,
|
|
POST_DCL_SETTLE_MILLISECONDS,
|
|
_POST_DCL_OUTER_HTML_EXPRESSION,
|
|
_POST_DCL_SETTLE_EXPRESSION,
|
|
_binary_main_resource_mime_type_from_message,
|
|
_chrome_command,
|
|
_parse_navigation_result,
|
|
_recv_command_response,
|
|
_recv_until_dcl_or_binary_main_resource,
|
|
run_chrome_dcl_dump,
|
|
)
|
|
from moli_benchmark.public_web import TOP_SITES_CLASSIFIER, WILD_WEB_CLASSIFIER
|
|
from moli_benchmark.raw_cdp import RawCdpClient, RawCdpError, RawCdpTimeoutError
|
|
|
|
|
|
class _NonMatchingWebSocket:
|
|
async def send(self, _payload: str) -> None:
|
|
return None
|
|
|
|
async def recv(self) -> str:
|
|
return json.dumps({"method": "Runtime.consoleAPICalled"})
|
|
|
|
|
|
class _FakeTemporaryFile:
|
|
def __init__(self) -> None:
|
|
self.closed = False
|
|
|
|
def __enter__(self) -> "_FakeTemporaryFile":
|
|
return self
|
|
|
|
def __exit__(self, *_args: object) -> None:
|
|
self.closed = True
|
|
|
|
|
|
class _FakeProcess:
|
|
pid = 12345
|
|
|
|
def __init__(self) -> None:
|
|
self.returncode: int | None = None
|
|
|
|
def poll(self) -> int | None:
|
|
return self.returncode
|
|
|
|
|
|
class _RecordingCdpClient:
|
|
def __init__(self) -> None:
|
|
self.timeout: float | None = None
|
|
|
|
async def recv_until_id(self, _message_id: int, *, timeout: float) -> tuple[dict[str, object], list[dict[str, object]]]:
|
|
self.timeout = timeout
|
|
return {"id": 1, "result": {}}, []
|
|
|
|
|
|
class _LateCommandResponseClient:
|
|
def __init__(self, late_message: dict[str, object]) -> None:
|
|
self.late_message = late_message
|
|
|
|
async def recv_until_id(self, _message_id: int, *, timeout: float) -> tuple[dict[str, object], list[dict[str, object]]]:
|
|
del timeout
|
|
raise RawCdpTimeoutError("primary command deadline expired")
|
|
|
|
async def recv(self) -> dict[str, object]:
|
|
return self.late_message
|
|
|
|
|
|
class _QueuedMessageClient:
|
|
def __init__(self, messages: list[dict[str, object]]) -> None:
|
|
self.messages = messages
|
|
|
|
async def recv(self) -> dict[str, object]:
|
|
if not self.messages:
|
|
raise AssertionError("CDP receiver exhausted before matching navigation evidence")
|
|
return self.messages.pop(0)
|
|
|
|
|
|
def _document_response_message(
|
|
mime_type: str,
|
|
*,
|
|
resource_type: str | None = "Document",
|
|
request_id: str = "REQUEST-1",
|
|
status: int = 200,
|
|
loader_id: str = "LOADER-1",
|
|
url: str = "https://example.test/final",
|
|
) -> dict[str, object]:
|
|
return {
|
|
"sessionId": "SID-1",
|
|
"method": "Network.responseReceived",
|
|
"params": {
|
|
"type": resource_type,
|
|
"frameId": "FRAME-1",
|
|
"loaderId": loader_id,
|
|
"requestId": request_id,
|
|
"response": {
|
|
"mimeType": mime_type,
|
|
"status": status,
|
|
"url": url,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _document_request_message(
|
|
*,
|
|
request_id: str = "REQUEST-1",
|
|
loader_id: str = "LOADER-1",
|
|
url: str = "https://example.test/",
|
|
) -> dict[str, object]:
|
|
return {
|
|
"sessionId": "SID-1",
|
|
"method": "Network.requestWillBeSent",
|
|
"params": {
|
|
"type": "Document",
|
|
"frameId": "FRAME-1",
|
|
"loaderId": loader_id,
|
|
"requestId": request_id,
|
|
"request": {"url": url},
|
|
},
|
|
}
|
|
|
|
|
|
def _dcl_lifecycle_message(*, loader_id: str = "LOADER-1") -> dict[str, object]:
|
|
return {
|
|
"sessionId": "SID-1",
|
|
"method": "Page.lifecycleEvent",
|
|
"params": {
|
|
"frameId": "FRAME-1",
|
|
"loaderId": loader_id,
|
|
"name": "DOMContentLoaded",
|
|
},
|
|
}
|
|
|
|
|
|
class ChromeDclTests(unittest.TestCase):
|
|
def test_post_dcl_snapshot_uses_page_event_loop_settle(self) -> None:
|
|
self.assertEqual(POST_DCL_SETTLE_MILLISECONDS, 50)
|
|
self.assertIn("new Promise", _POST_DCL_SETTLE_EXPRESSION)
|
|
self.assertIn("setTimeout", _POST_DCL_SETTLE_EXPRESSION)
|
|
self.assertIn("50", _POST_DCL_SETTLE_EXPRESSION)
|
|
self.assertIn("document.documentElement.outerHTML", _POST_DCL_OUTER_HTML_EXPRESSION)
|
|
|
|
def test_chrome_command_uses_non_headless_desktop_user_agent(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
command = _chrome_command(Path("/bin/chromium"), 12345, Path(temp_dir))
|
|
|
|
user_agent_args = [arg for arg in command if arg.startswith("--user-agent=")]
|
|
self.assertEqual(user_agent_args, [f"--user-agent={DEFAULT_CHROME_DCL_USER_AGENT}"])
|
|
self.assertIn("Chrome/", user_agent_args[0])
|
|
self.assertNotIn("HeadlessChrome", user_agent_args[0])
|
|
|
|
def test_recv_until_id_deadline_raises_timeout_error(self) -> None:
|
|
async def run() -> None:
|
|
client = RawCdpClient(websocket=_NonMatchingWebSocket()) # type: ignore[arg-type]
|
|
with self.assertRaises(RawCdpTimeoutError) as raised:
|
|
await client.recv_until_id(1, timeout=0.001)
|
|
self.assertIsInstance(raised.exception, TimeoutError)
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_recv_command_response_uses_remaining_deadline_without_stage_cap(self) -> None:
|
|
async def run() -> None:
|
|
client = _RecordingCdpClient()
|
|
deadline = time.perf_counter() + 12.0
|
|
await _recv_command_response( # type: ignore[arg-type]
|
|
client,
|
|
1,
|
|
deadline=deadline,
|
|
stage="outerHTML",
|
|
)
|
|
self.assertIsNotNone(client.timeout)
|
|
self.assertGreater(client.timeout or 0.0, 10.0)
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_recv_command_response_surfaces_late_command_error_for_classification(self) -> None:
|
|
async def run() -> None:
|
|
client = _LateCommandResponseClient(
|
|
{
|
|
"id": 7,
|
|
"error": {
|
|
"code": -32000,
|
|
"message": "failed to fetch page `https://example.invalid/`: curl request failed",
|
|
},
|
|
}
|
|
)
|
|
|
|
with self.assertRaises(RawCdpError) as raised:
|
|
await _recv_command_response( # type: ignore[arg-type]
|
|
client,
|
|
7,
|
|
deadline=time.perf_counter(),
|
|
stage="Page.navigate",
|
|
late_error_grace_seconds=0.1,
|
|
)
|
|
|
|
self.assertIn("failed to fetch page", str(raised.exception))
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_recv_command_response_keeps_late_success_as_timeout(self) -> None:
|
|
async def run() -> None:
|
|
client = _LateCommandResponseClient({"id": 7, "result": {}})
|
|
|
|
with self.assertRaises(CdpDclDumpTimeoutError) as raised:
|
|
await _recv_command_response( # type: ignore[arg-type]
|
|
client,
|
|
7,
|
|
deadline=time.perf_counter(),
|
|
stage="Page.navigate",
|
|
late_error_grace_seconds=0.1,
|
|
)
|
|
|
|
self.assertEqual(raised.exception.stage, "Page.navigate")
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_navigation_result_surfaces_error_text_before_dcl_wait(self) -> None:
|
|
with self.assertRaises(RawCdpError) as raised:
|
|
_parse_navigation_result(
|
|
{
|
|
"result": {
|
|
"frameId": "FRAME-1",
|
|
"errorText": "net::ERR_NAME_NOT_RESOLVED",
|
|
}
|
|
},
|
|
"https://missing.example.test/",
|
|
)
|
|
|
|
self.assertEqual(
|
|
str(raised.exception),
|
|
"Page.navigate failed for `https://missing.example.test/`: "
|
|
"net::ERR_NAME_NOT_RESOLVED",
|
|
)
|
|
|
|
def test_navigation_result_exposes_frame_and_loader_identity(self) -> None:
|
|
navigation = _parse_navigation_result(
|
|
{"result": {"frameId": "FRAME-1", "loaderId": "LOADER-1"}},
|
|
"https://example.test/",
|
|
)
|
|
|
|
self.assertEqual(navigation.frame_id, "FRAME-1")
|
|
self.assertEqual(navigation.loader_id, "LOADER-1")
|
|
self.assertFalse(navigation.is_download)
|
|
|
|
def test_navigation_result_only_treats_literal_true_as_download(self) -> None:
|
|
navigation = _parse_navigation_result(
|
|
{
|
|
"result": {
|
|
"frameId": "FRAME-1",
|
|
"errorText": "net::ERR_ABORTED",
|
|
"isDownload": True,
|
|
}
|
|
},
|
|
"https://example.test/archive.zip",
|
|
)
|
|
self.assertTrue(navigation.is_download)
|
|
|
|
with self.assertRaises(RawCdpError):
|
|
_parse_navigation_result(
|
|
{
|
|
"result": {
|
|
"frameId": "FRAME-1",
|
|
"errorText": "net::ERR_ABORTED",
|
|
"isDownload": "true",
|
|
}
|
|
},
|
|
"https://example.test/no-content",
|
|
)
|
|
|
|
def test_download_navigation_does_not_require_error_text_or_loader_id(self) -> None:
|
|
navigation = _parse_navigation_result(
|
|
{"result": {"frameId": "FRAME-1", "isDownload": True}},
|
|
"https://example.test/archive.zip",
|
|
)
|
|
|
|
self.assertTrue(navigation.is_download)
|
|
self.assertIsNone(navigation.loader_id)
|
|
|
|
def test_download_navigation_keeps_abort_as_headers_only_evidence(self) -> None:
|
|
async def run() -> None:
|
|
navigation = _parse_navigation_result(
|
|
{
|
|
"result": {
|
|
"frameId": "FRAME-1",
|
|
"errorText": "net::ERR_ABORTED",
|
|
"isDownload": True,
|
|
}
|
|
},
|
|
"https://example.test/archive.zip",
|
|
)
|
|
|
|
observation = await _recv_until_dcl_or_binary_main_resource(
|
|
mock.Mock(),
|
|
session_id="SID-1",
|
|
frame_id=navigation.frame_id,
|
|
expected_loader_id=navigation.loader_id,
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[
|
|
_document_request_message(),
|
|
_document_response_message("application/zip"),
|
|
],
|
|
download_navigation=navigation.is_download,
|
|
)
|
|
|
|
self.assertTrue(observation.headers_only)
|
|
self.assertEqual(observation.response_status, 200)
|
|
self.assertEqual(observation.response_mime_type, "application/zip")
|
|
self.assertEqual(observation.final_url, "https://example.test/final")
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_download_navigation_is_authoritative_over_mime_and_status(self) -> None:
|
|
async def run() -> None:
|
|
observation = await _recv_until_dcl_or_binary_main_resource(
|
|
mock.Mock(),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id="LOADER-1",
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[
|
|
_document_response_message(
|
|
"text/html; charset=utf-8",
|
|
status=404,
|
|
)
|
|
],
|
|
download_navigation=True,
|
|
)
|
|
|
|
self.assertTrue(observation.headers_only)
|
|
self.assertEqual(observation.response_status, 404)
|
|
self.assertEqual(observation.response_mime_type, "text/html; charset=utf-8")
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_download_without_loader_ignores_stale_response_until_current_request(self) -> None:
|
|
async def run() -> None:
|
|
current_url = "https://example.test/current.zip"
|
|
client = _QueuedMessageClient(
|
|
[
|
|
_document_request_message(
|
|
request_id="CURRENT",
|
|
loader_id="LOADER-CURRENT",
|
|
url=current_url,
|
|
),
|
|
_document_response_message(
|
|
"application/zip",
|
|
request_id="CURRENT",
|
|
loader_id="LOADER-CURRENT",
|
|
url=current_url,
|
|
),
|
|
]
|
|
)
|
|
observation = await _recv_until_dcl_or_binary_main_resource( # type: ignore[arg-type]
|
|
client,
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id=None,
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[
|
|
_document_response_message(
|
|
"application/zip",
|
|
request_id="STALE",
|
|
loader_id="LOADER-STALE",
|
|
status=206,
|
|
url="https://example.test/stale.zip",
|
|
),
|
|
_dcl_lifecycle_message(loader_id="LOADER-STALE"),
|
|
],
|
|
download_navigation=True,
|
|
)
|
|
|
|
self.assertTrue(observation.headers_only)
|
|
self.assertEqual(observation.response_status, 200)
|
|
self.assertEqual(observation.final_url, current_url)
|
|
self.assertEqual(client.messages, [])
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_wait_timeout_names_download_and_document_stages(self) -> None:
|
|
async def run() -> None:
|
|
for is_download, expected in (
|
|
(True, "main-document response headers"),
|
|
(False, "DOMContentLoaded"),
|
|
):
|
|
with self.subTest(download=is_download):
|
|
with self.assertRaises(TimeoutError) as raised:
|
|
await _recv_until_dcl_or_binary_main_resource(
|
|
mock.Mock(),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id=None,
|
|
deadline=time.perf_counter() - 1.0,
|
|
seen=[],
|
|
download_navigation=is_download,
|
|
)
|
|
self.assertIn(expected, str(raised.exception))
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_binary_main_document_response_returns_mime_evidence(self) -> None:
|
|
mime_type = _binary_main_resource_mime_type_from_message(
|
|
_document_response_message("application/pdf"),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
)
|
|
|
|
self.assertEqual(mime_type, "application/pdf")
|
|
|
|
def test_binary_main_document_detection_ignores_html_and_subresources(self) -> None:
|
|
html_mime_type = _binary_main_resource_mime_type_from_message(
|
|
_document_response_message("text/html; charset=utf-8"),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
)
|
|
script_pdf_mime_type = _binary_main_resource_mime_type_from_message(
|
|
_document_response_message("application/pdf", resource_type="Script"),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
)
|
|
|
|
self.assertIsNone(html_mime_type)
|
|
self.assertIsNone(script_pdf_mime_type)
|
|
|
|
def test_binary_main_document_detection_ignores_error_status(self) -> None:
|
|
mime_type = _binary_main_resource_mime_type_from_message(
|
|
_document_response_message("application/pdf", status=404),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
)
|
|
|
|
self.assertIsNone(mime_type)
|
|
|
|
def test_recv_until_dcl_short_circuits_binary_document_seen_before_dcl(self) -> None:
|
|
async def run() -> None:
|
|
observation = await _recv_until_dcl_or_binary_main_resource(
|
|
mock.Mock(),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id="LOADER-1",
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[
|
|
_document_response_message("application/pdf"),
|
|
_dcl_lifecycle_message(),
|
|
],
|
|
)
|
|
self.assertTrue(observation.headers_only)
|
|
self.assertEqual(observation.response_status, 200)
|
|
self.assertEqual(observation.response_mime_type, "application/pdf")
|
|
self.assertEqual(observation.final_url, "https://example.test/final")
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_recv_until_dcl_accepts_main_frame_event_seen_before_command_response(self) -> None:
|
|
async def run() -> None:
|
|
observation = await _recv_until_dcl_or_binary_main_resource(
|
|
mock.Mock(),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id="LOADER-1",
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[_dcl_lifecycle_message()],
|
|
)
|
|
self.assertFalse(observation.headers_only)
|
|
self.assertIsNone(observation.response_status)
|
|
self.assertIsNone(observation.response_mime_type)
|
|
self.assertIsNone(observation.final_url)
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_recv_until_dcl_ignores_events_from_previous_loader(self) -> None:
|
|
async def run() -> None:
|
|
client = _QueuedMessageClient(
|
|
[
|
|
_document_response_message(
|
|
"text/html",
|
|
status=200,
|
|
loader_id="LOADER-NEW",
|
|
),
|
|
{
|
|
"sessionId": "SID-1",
|
|
"method": "Page.domContentEventFired",
|
|
"params": {},
|
|
},
|
|
_dcl_lifecycle_message(loader_id="LOADER-NEW"),
|
|
]
|
|
)
|
|
observation = await _recv_until_dcl_or_binary_main_resource( # type: ignore[arg-type]
|
|
client,
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id="LOADER-NEW",
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[
|
|
_document_response_message(
|
|
"text/html",
|
|
status=502,
|
|
loader_id="LOADER-OLD",
|
|
),
|
|
_dcl_lifecycle_message(loader_id="LOADER-OLD"),
|
|
],
|
|
)
|
|
|
|
self.assertFalse(observation.headers_only)
|
|
self.assertEqual(observation.response_status, 200)
|
|
self.assertEqual(observation.response_mime_type, "text/html")
|
|
self.assertEqual(observation.final_url, "https://example.test/final")
|
|
self.assertEqual(client.messages, [])
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_recv_until_dcl_retains_html_main_document_status(self) -> None:
|
|
async def run() -> None:
|
|
observation = await _recv_until_dcl_or_binary_main_resource(
|
|
mock.Mock(),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id="LOADER-1",
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[
|
|
_document_response_message("text/html", status=502),
|
|
_dcl_lifecycle_message(),
|
|
],
|
|
)
|
|
self.assertFalse(observation.headers_only)
|
|
self.assertEqual(observation.response_status, 502)
|
|
self.assertEqual(observation.response_mime_type, "text/html")
|
|
self.assertEqual(observation.final_url, "https://example.test/final")
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_recv_until_dcl_correlates_response_without_resource_type(self) -> None:
|
|
async def run() -> None:
|
|
observation = await _recv_until_dcl_or_binary_main_resource(
|
|
mock.Mock(),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id="LOADER-1",
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[
|
|
_document_request_message(),
|
|
_document_response_message(
|
|
"text/html",
|
|
resource_type=None,
|
|
status=400,
|
|
),
|
|
_dcl_lifecycle_message(),
|
|
],
|
|
)
|
|
self.assertFalse(observation.headers_only)
|
|
self.assertEqual(observation.response_status, 400)
|
|
self.assertEqual(observation.response_mime_type, "text/html")
|
|
self.assertEqual(observation.final_url, "https://example.test/final")
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_recv_until_dcl_ignores_untracked_response_without_resource_type(self) -> None:
|
|
async def run() -> None:
|
|
observation = await _recv_until_dcl_or_binary_main_resource(
|
|
mock.Mock(),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id="LOADER-1",
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[
|
|
_document_request_message(),
|
|
_document_response_message(
|
|
"application/json",
|
|
resource_type=None,
|
|
request_id="SUBRESOURCE-1",
|
|
status=503,
|
|
),
|
|
_dcl_lifecycle_message(),
|
|
],
|
|
)
|
|
self.assertFalse(observation.headers_only)
|
|
self.assertIsNone(observation.response_status)
|
|
self.assertIsNone(observation.response_mime_type)
|
|
self.assertIsNone(observation.final_url)
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_recv_until_dcl_keeps_status_for_latest_document_request(self) -> None:
|
|
async def run() -> None:
|
|
observation = await _recv_until_dcl_or_binary_main_resource(
|
|
mock.Mock(),
|
|
session_id="SID-1",
|
|
frame_id="FRAME-1",
|
|
expected_loader_id="LOADER-1",
|
|
deadline=time.perf_counter() + 1.0,
|
|
seen=[
|
|
_document_request_message(request_id="OLD"),
|
|
_document_request_message(request_id="NEW"),
|
|
_document_response_message(
|
|
"text/html",
|
|
request_id="OLD",
|
|
status=500,
|
|
),
|
|
_document_response_message(
|
|
"text/html",
|
|
resource_type=None,
|
|
request_id="NEW",
|
|
status=200,
|
|
),
|
|
_dcl_lifecycle_message(),
|
|
],
|
|
)
|
|
self.assertFalse(observation.headers_only)
|
|
self.assertEqual(observation.response_status, 200)
|
|
self.assertEqual(observation.response_mime_type, "text/html")
|
|
self.assertEqual(observation.final_url, "https://example.test/final")
|
|
|
|
asyncio.run(run())
|
|
|
|
def test_chrome_runner_exposes_main_document_status(self) -> None:
|
|
process = _FakeProcess()
|
|
|
|
def terminate(fake_process: _FakeProcess) -> None:
|
|
fake_process.returncode = -15
|
|
|
|
with (
|
|
mock.patch("moli_benchmark.chrome_dcl.subprocess.Popen", return_value=process),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._dump_dcl_html",
|
|
return_value=CdpDclDumpResult(
|
|
body="<html><body>gateway error</body></html>",
|
|
response_status=502,
|
|
response_mime_type="text/html",
|
|
main_document_body_capture="dom-snapshot",
|
|
final_url="https://example.test/final",
|
|
),
|
|
),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._terminate_process_group",
|
|
side_effect=terminate,
|
|
),
|
|
):
|
|
result = run_chrome_dcl_dump(
|
|
Path("/bin/chromium"),
|
|
"https://example.test/",
|
|
timeout_seconds=1.0,
|
|
sample_resources=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0)
|
|
self.assertEqual(result.response_status, 502)
|
|
self.assertEqual(result.response_mime_type, "text/html")
|
|
self.assertEqual(result.main_document_body_capture, "dom-snapshot")
|
|
self.assertEqual(result.final_url, "https://example.test/final")
|
|
self.assertIn(b"gateway error", result.stdout)
|
|
|
|
def test_chrome_runner_classifies_navigation_error_without_timeout(self) -> None:
|
|
process = _FakeProcess()
|
|
|
|
def terminate(fake_process: _FakeProcess) -> None:
|
|
fake_process.returncode = -15
|
|
|
|
with (
|
|
mock.patch("moli_benchmark.chrome_dcl.subprocess.Popen", return_value=process),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._dump_dcl_html",
|
|
side_effect=RawCdpError(
|
|
"Page.navigate failed for `https://missing.example.test/`: "
|
|
"net::ERR_NAME_NOT_RESOLVED"
|
|
),
|
|
),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._terminate_process_group",
|
|
side_effect=terminate,
|
|
),
|
|
):
|
|
result = run_chrome_dcl_dump(
|
|
Path("/bin/chromium"),
|
|
"https://missing.example.test/",
|
|
timeout_seconds=1.0,
|
|
sample_resources=False,
|
|
)
|
|
|
|
self.assertFalse(result.timed_out)
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn(b"net::ERR_NAME_NOT_RESOLVED", result.stderr)
|
|
for classifier in (TOP_SITES_CLASSIFIER, WILD_WEB_CLASSIFIER):
|
|
with self.subTest(policy=classifier.policy):
|
|
self.assertEqual(
|
|
classifier.classify_output(
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
returncode=result.returncode,
|
|
timed_out=result.timed_out,
|
|
),
|
|
"network-error",
|
|
)
|
|
|
|
def test_chrome_runner_does_not_fabricate_binary_response_body(self) -> None:
|
|
process = _FakeProcess()
|
|
|
|
def terminate(fake_process: _FakeProcess) -> None:
|
|
fake_process.returncode = -15
|
|
|
|
with (
|
|
mock.patch("moli_benchmark.chrome_dcl.subprocess.Popen", return_value=process),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._dump_dcl_html",
|
|
return_value=CdpDclDumpResult(
|
|
body="",
|
|
response_status=200,
|
|
response_mime_type="application/pdf",
|
|
main_document_body_capture="response-headers-only",
|
|
final_url="https://example.test/document.pdf",
|
|
),
|
|
),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._terminate_process_group",
|
|
side_effect=terminate,
|
|
),
|
|
):
|
|
result = run_chrome_dcl_dump(
|
|
Path("/bin/chromium"),
|
|
"https://example.test/document.pdf",
|
|
timeout_seconds=1.0,
|
|
sample_resources=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0)
|
|
self.assertEqual(result.stdout, b"")
|
|
self.assertEqual(result.response_mime_type, "application/pdf")
|
|
self.assertEqual(
|
|
result.main_document_body_capture,
|
|
"response-headers-only",
|
|
)
|
|
|
|
def test_chrome_runner_records_raw_cdp_deadline_as_timeout(self) -> None:
|
|
process = _FakeProcess()
|
|
|
|
def terminate(fake_process: _FakeProcess) -> None:
|
|
fake_process.returncode = -15
|
|
|
|
with (
|
|
mock.patch("moli_benchmark.chrome_dcl.subprocess.Popen", return_value=process),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._dump_dcl_html",
|
|
side_effect=CdpDclDumpTimeoutError(
|
|
"DCL",
|
|
RawCdpTimeoutError("timed out waiting for CDP response id=1"),
|
|
),
|
|
),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._terminate_process_group",
|
|
side_effect=terminate,
|
|
),
|
|
):
|
|
result = run_chrome_dcl_dump(
|
|
Path("/bin/chromium"),
|
|
"https://example.test/",
|
|
timeout_seconds=1.0,
|
|
sample_resources=False,
|
|
)
|
|
|
|
self.assertTrue(result.timed_out)
|
|
self.assertEqual(result.returncode, 124)
|
|
self.assertIn(b"chrome CDP DCL timeout", result.stderr)
|
|
|
|
def test_chrome_runner_distinguishes_outer_html_timeout(self) -> None:
|
|
process = _FakeProcess()
|
|
|
|
def terminate(fake_process: _FakeProcess) -> None:
|
|
fake_process.returncode = -15
|
|
|
|
with (
|
|
mock.patch("moli_benchmark.chrome_dcl.subprocess.Popen", return_value=process),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._dump_dcl_html",
|
|
side_effect=CdpDclDumpTimeoutError(
|
|
"outerHTML",
|
|
RawCdpTimeoutError("timed out waiting for CDP response id=9"),
|
|
),
|
|
),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl._terminate_process_group",
|
|
side_effect=terminate,
|
|
),
|
|
):
|
|
result = run_chrome_dcl_dump(
|
|
Path("/bin/chromium"),
|
|
"https://example.test/",
|
|
timeout_seconds=1.0,
|
|
sample_resources=False,
|
|
)
|
|
|
|
self.assertTrue(result.timed_out)
|
|
self.assertEqual(result.returncode, 124)
|
|
self.assertIn(b"chrome CDP outerHTML timeout", result.stderr)
|
|
self.assertNotIn(b"chrome CDP DCL timeout", result.stderr)
|
|
|
|
def test_chrome_runner_closes_tempfiles_when_popen_fails(self) -> None:
|
|
files: list[_FakeTemporaryFile] = []
|
|
|
|
def fake_temporary_file() -> _FakeTemporaryFile:
|
|
file = _FakeTemporaryFile()
|
|
files.append(file)
|
|
return file
|
|
|
|
with (
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl.tempfile.TemporaryFile",
|
|
side_effect=fake_temporary_file,
|
|
),
|
|
mock.patch(
|
|
"moli_benchmark.chrome_dcl.subprocess.Popen",
|
|
side_effect=OSError("boom"),
|
|
),
|
|
):
|
|
with self.assertRaises(OSError):
|
|
run_chrome_dcl_dump(
|
|
Path("/bin/chromium"),
|
|
"https://example.test/",
|
|
timeout_seconds=1.0,
|
|
)
|
|
|
|
self.assertEqual(len(files), 2)
|
|
self.assertTrue(all(file.closed for file in files))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|