blob: f7aa452a7cb9a569ebd4c1a8b6dae39c53820d92 [file]
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import annotations
import json
import logging
import urllib.request
from typing import TYPE_CHECKING, Any, Optional, Sequence, cast
from typing_extensions import override
from crossbench import exception
from crossbench.browsers.attributes import BrowserAttributes
from crossbench.browsers.browser import Browser
from crossbench.browsers.shell.d8.shell import D8Shell
from crossbench.browsers.shell.d8.version import D8Version
from crossbench.browsers.shell.url_mapper import D8URLMapper, DummyURLMapper
from crossbench.browsers.viewport import Viewport
from crossbench.flags.chrome import ChromeFlags
from crossbench.network.local_file_server import LocalFileNetwork
if TYPE_CHECKING:
import datetime as dt
import crossbench.path as pth
from crossbench.browsers.settings import Settings
from crossbench.flags.base import FlagsData
from crossbench.flags.js_flags import JSFlags
from crossbench.runner.groups.session import BrowserSessionRunGroup
class D8(Browser):
@classmethod
@override
def type_name(cls) -> str:
return "d8"
@classmethod
@override
def attributes(cls) -> BrowserAttributes:
return BrowserAttributes.D8 | BrowserAttributes.CHROMIUM_BASED
@override
def __init__(self,
label: str,
path: pth.AnyPath | None = None,
settings: Settings | None = None) -> None:
super().__init__(label, path, settings)
if not self.network.is_local_file_server:
raise RuntimeError("D8 wrapper only works with --local-file-server"
f"but got {self.network} network.")
self._d8_shell: D8Shell | None = None
self._url_mapper: D8URLMapper = DummyURLMapper(self)
@property
def network(self) -> LocalFileNetwork:
return cast(LocalFileNetwork, super().network)
@property
def d8_path(self) -> pth.LocalPath:
return self.platform.local_path(self.app_path)
@override
def _setup_cache_dir(self) -> Optional[pth.AnyPath]:
pass
@override
def _extract_version(self) -> D8Version:
# Some d8 versions don't support --version
shell = D8Shell(self.platform, self.d8_path)
version: str = shell.version
shell.quit()
return D8Version.parse(version)
@property
def viewport(self) -> Viewport:
return Viewport.HEADLESS
@viewport.setter
def viewport(self, value: Viewport) -> None:
raise NotImplementedError("Cannot set viewport")
def user_agent(self) -> str:
return "V8"
@classmethod
@override
def default_flags(cls,
initial_data: FlagsData = None,
milestone: int = 0) -> ChromeFlags:
del milestone
return ChromeFlags(initial_data)
@property
@override
def js_flags(self) -> JSFlags:
return cast(ChromeFlags, self._flags).js_flags
def start(self, session: BrowserSessionRunGroup) -> None:
super().start(session)
js_flags_copy = self.js_flags.copy()
js_flags_copy.update(session.extra_js_flags)
self._log_browser_start(tuple(js_flags_copy))
self._url_mapper = D8URLMapper.create(self, session)
self._d8_shell = D8Shell(
self.platform,
self.d8_path,
flags=list(js_flags_copy),
cwd=self.network.path)
self._pid = self._d8_shell.pid
self._is_running = True
self._install_d8_mocks()
def _install_d8_mocks(self) -> None:
with exception.annotate("D8 setup"):
if shell := self._d8_shell:
if setup_file := self._url_mapper.setup_file:
shell.load(setup_file)
def force_quit(self) -> None:
if not self._is_running:
return
logging.info("Browser.force_quit()")
if d8_shell := self._d8_shell:
d8_shell.quit()
self._is_running = False
@override
def js(
self,
script: str,
timeout: Optional[dt.timedelta] = None,
arguments: Sequence[object] = ()
) -> Any:
logging.debug("JS: %s", script)
args_str: str = json.dumps(arguments)
script = """JSON.stringify((function exceptionWrapper(){
try {
return [
(function(...arguments){
%(script)s
}).apply(globalThis, %(args_str)s),
true];
} catch(e) {
return [e + "", false];
}
})());""" % {
"script": script,
"args_str": args_str
}
result, is_success = "Not started", False
if d8_shell := self._d8_shell:
json_result: str = d8_shell.execute(script, eval=True, timeout=timeout)
logging.debug("D8 Result: %s", json_result)
# D8 always adds double quotes for every string
if json_result[0] != '"' or json_result[-1] != '"':
logging.debug("D8: JSON results has no double quotes")
return None
unquoted = json_result.strip()[1:-1]
json_decoded = json.loads(unquoted)
assert len(json_decoded) == 2, (
f"Expectedtuple[Any, Any], got {type(json_decoded)}: {json_decoded}")
result, is_success = json_decoded
if not is_success:
raise RuntimeError(f"D8 JS Exception: {result}")
return result
@override
def show_url(self, url: str, target: Optional[str] = None) -> None:
if url.startswith("data:text/html;"):
self._print_data_url(url)
return
if file_path := self._url_mapper.lookup(url):
if d8_shell := self._d8_shell:
result = d8_shell.load(file_path)
logging.debug("D8 Result: %s", result)
return
raise RuntimeError(f"D8 unsupported URL: {url}")
def _print_data_url(self, data_url: str) -> None:
logging.debug("D8: SKIPPING data url")
with urllib.request.urlopen(data_url) as response: # noqa: S310
info = response.info()
charset = info.get_content_charset() or "utf-8"
data_bytes = response.read()
data_content = data_bytes.decode(charset)
logging.info(data_content)