Skip to content

Commit 75784e0

Browse files
authored
Merge branch 'main' into andrew/update_last_known_good_chrome
2 parents df25f84 + c8b22f9 commit 75784e0

File tree

7 files changed

+233
-48
lines changed

7 files changed

+233
-48
lines changed

CHANGELOG.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
- Alter `get_chrome` verbose to print whole JSON
55
- Change chrome download path to use XDG cache dir
66
- Don't download chrome if we already have that version: add force argument
7+
- Remove unused system inspection code
8+
- Add a set of helper functions to await for tab loading and send javascript
79
v1.2.1
810
- Use custom threadpool for functions that could be running during shutdown:
911
Python's stdlib threadpool isn't available during interpreter shutdown, nor

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,9 @@ asyncio_default_fixture_loop_scope = "function"
110110
log_cli = false
111111
addopts = "--import-mode=append"
112112

113+
# tell poe to use the env we give it, otherwise it detects uv and overrides flags
113114
[tool.poe]
114-
executor.type = "virtualenv"
115+
executor.type = "simple"
115116

116117
[tool.poe.tasks]
117118
test_proc = "pytest --log-level=1 -W error -n auto -v -rfE --capture=fd tests/test_process.py"

src/choreographer/browser_async.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from choreographer import protocol
1515

1616
from ._brokers import Broker
17-
from .browsers import BrowserClosedError, BrowserDepsError, BrowserFailedError, Chromium
17+
from .browsers import BrowserClosedError, BrowserFailedError, Chromium
1818
from .channels import ChannelClosedError, Pipe
1919
from .protocol.devtools_async import Session, Target
2020
from .utils import TmpDirWarning, _manual_thread_pool
@@ -175,11 +175,6 @@ def run() -> subprocess.Popen[bytes] | subprocess.Popen[str]: # depends on args
175175
if counter == MAX_POPULATE_LOOPS:
176176
break
177177
except (BrowserClosedError, BrowserFailedError, asyncio.CancelledError) as e:
178-
if (
179-
hasattr(self._browser_impl, "missing_libs")
180-
and self._browser_impl.missing_libs # type: ignore[reportAttributeAccessIssue]
181-
):
182-
raise BrowserDepsError from e
183178
raise BrowserFailedError(
184179
"The browser seemed to close immediately after starting.",
185180
"You can set the `logging.Logger` level lower to see more output.",

src/choreographer/browsers/_errors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class BrowserFailedError(RuntimeError):
66
"""An error for when the browser fails to launch."""
77

88

9+
# not currently used but keeping copy + not breaking API
910
class BrowserDepsError(BrowserFailedError):
1011
"""An error for when the browser is closed because of missing libs."""
1112

src/choreographer/browsers/chromium.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -115,46 +115,6 @@ def logger_parser(
115115

116116
return True
117117

118-
def _libs_ok(self) -> bool:
119-
"""Return true if libs ok."""
120-
if self.skip_local:
121-
_logger.debug(
122-
"If we HAVE to skip local.",
123-
)
124-
return True
125-
_logger.debug("Checking for libs needed.")
126-
if platform.system() != "Linux":
127-
_logger.debug("We're not in linux, so no need for check.")
128-
return True
129-
p = None
130-
try:
131-
_logger.debug(f"Trying ldd {self.path}")
132-
p = subprocess.run( # noqa: S603, validating run with variables
133-
[ # noqa: S607 path is all we have
134-
"ldd",
135-
str(self.path),
136-
],
137-
capture_output=True,
138-
timeout=5,
139-
check=True,
140-
)
141-
except Exception as e: # noqa: BLE001
142-
msg = "ldd failed."
143-
stderr = p.stderr.decode() if p and p.stderr else None
144-
# Log failure as INFO rather than WARNING so that it's hidden by default,
145-
# since browser may succeed even if ldd fails
146-
_logger.info(
147-
msg # noqa: G003 + in log
148-
+ f" e: {e}, stderr: {stderr}",
149-
)
150-
return False
151-
if b"not found" in p.stdout:
152-
msg = "Found deps missing in chrome"
153-
_logger.debug2(msg + f" {p.stdout.decode()}")
154-
return False
155-
_logger.debug("No problems found with dependencies")
156-
return True
157-
158118
def __init__(
159119
self,
160120
channel: ChannelInterface,
@@ -220,7 +180,6 @@ def pre_open(self) -> None:
220180
path=self._tmp_dir_path,
221181
sneak=self._is_isolated,
222182
)
223-
self.missing_libs = not self._libs_ok()
224183
_logger.info(f"Temporary directory at: {self.tmp_dir.path}")
225184

226185
def is_isolated(self) -> bool:
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Async helper functions for common Chrome DevTools Protocol patterns."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
from typing import TYPE_CHECKING
7+
8+
if TYPE_CHECKING:
9+
from choreographer import Browser, Tab
10+
11+
from . import BrowserResponse
12+
13+
14+
async def create_and_wait(
15+
browser: Browser,
16+
url: str = "",
17+
*,
18+
timeout: float = 30.0,
19+
) -> Tab:
20+
"""
21+
Create a new tab and wait for it to load.
22+
23+
Args:
24+
browser: Browser instance
25+
url: URL to navigate to (default: blank page)
26+
timeout: Seconds to wait for page load (default: 30.0)
27+
28+
Returns:
29+
The created Tab
30+
31+
"""
32+
tab = await browser.create_tab(url)
33+
temp_session = await tab.create_session()
34+
35+
try:
36+
load_future = temp_session.subscribe_once("Page.loadEventFired")
37+
await temp_session.send_command("Page.enable")
38+
await temp_session.send_command("Runtime.enable")
39+
40+
if url:
41+
try:
42+
await asyncio.wait_for(load_future, timeout=timeout)
43+
except (asyncio.TimeoutError, asyncio.CancelledError, TimeoutError):
44+
# Stop the page load when timeout occurs
45+
await temp_session.send_command("Page.stopLoading")
46+
raise
47+
finally:
48+
await tab.close_session(temp_session.session_id)
49+
50+
return tab
51+
52+
53+
async def navigate_and_wait(
54+
tab: Tab,
55+
url: str,
56+
*,
57+
timeout: float = 30.0,
58+
) -> Tab:
59+
"""
60+
Navigate an existing tab to a URL and wait for it to load.
61+
62+
Args:
63+
tab: Tab to navigate
64+
url: URL to navigate to
65+
timeout: Seconds to wait for page load (default: 30.0)
66+
67+
Returns:
68+
The Tab after navigation completes
69+
70+
"""
71+
temp_session = await tab.create_session()
72+
73+
try:
74+
await temp_session.send_command("Page.enable")
75+
await temp_session.send_command("Runtime.enable")
76+
load_future = temp_session.subscribe_once("Page.loadEventFired")
77+
try:
78+
79+
async def _freezers():
80+
# If no resolve, will freeze
81+
await temp_session.send_command("Page.navigate", params={"url": url})
82+
# Can freeze if resolve bad
83+
await load_future
84+
85+
await asyncio.wait_for(_freezers(), timeout=timeout)
86+
except (asyncio.TimeoutError, asyncio.CancelledError, TimeoutError):
87+
# Stop the navigation when timeout occurs
88+
await temp_session.send_command("Page.stopLoading")
89+
raise
90+
finally:
91+
await tab.close_session(temp_session.session_id)
92+
93+
return tab
94+
95+
96+
async def execute_js_and_wait(
97+
tab: Tab,
98+
expression: str,
99+
*,
100+
timeout: float = 30.0,
101+
) -> BrowserResponse:
102+
"""
103+
Execute JavaScript in a tab and return the result.
104+
105+
Args:
106+
tab: Tab to execute JavaScript in
107+
expression: JavaScript expression to evaluate
108+
timeout: Seconds to wait for execution (default: 30.0)
109+
110+
Returns:
111+
Response dict from Runtime.evaluate with 'result' and optional
112+
'exceptionDetails'
113+
114+
"""
115+
temp_session = await tab.create_session()
116+
117+
try:
118+
await temp_session.send_command("Page.enable")
119+
await temp_session.send_command("Runtime.enable")
120+
121+
response = await asyncio.wait_for(
122+
temp_session.send_command(
123+
"Runtime.evaluate",
124+
params={
125+
"expression": expression,
126+
"awaitPromise": True,
127+
"returnByValue": True,
128+
},
129+
),
130+
timeout=timeout,
131+
)
132+
133+
return response
134+
finally:
135+
await tab.close_session(temp_session.session_id)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import asyncio
2+
3+
import logistro
4+
import pytest
5+
6+
from choreographer.protocol.devtools_async_helpers import (
7+
create_and_wait,
8+
execute_js_and_wait,
9+
navigate_and_wait,
10+
)
11+
12+
pytestmark = pytest.mark.asyncio(loop_scope="function")
13+
14+
_logger = logistro.getLogger(__name__)
15+
16+
17+
# Errata: don't use data urls, whether or not they load is variable
18+
# depends on how long chrome has been open for, how they were entered,
19+
# etc
20+
21+
22+
@pytest.mark.asyncio
23+
async def test_create_and_wait(browser):
24+
"""Test create_and_wait with both valid data URL and blank URL."""
25+
_logger.info("testing create_and_wait...")
26+
27+
# Count tabs before
28+
initial_tab_count = len(browser.tabs)
29+
30+
# Create a simple HTML page as a data URL
31+
data_url = "chrome://version"
32+
33+
# Test 1: Create tab with data URL - should succeed
34+
tab1 = await create_and_wait(browser, url=data_url, timeout=5.0)
35+
assert tab1 is not None
36+
37+
# Verify the page loaded correctly using execute_js_and_wait
38+
result = await execute_js_and_wait(tab1, "window.location.href", timeout=5.0)
39+
location = result["result"]["result"]["value"]
40+
assert location.startswith(data_url)
41+
42+
# Test 2: Create tab without URL - should succeed (blank page)
43+
tab2 = await create_and_wait(browser, url="", timeout=5.0)
44+
assert tab2 is not None
45+
46+
# Verify we have 2 more tabs
47+
final_tab_count = len(browser.tabs)
48+
assert final_tab_count == initial_tab_count + 2
49+
50+
# Test 3: Create tab with bad URL that won't load - should timeout
51+
with pytest.raises(asyncio.TimeoutError):
52+
await create_and_wait(browser, url="http://192.0.2.1:9999", timeout=0.5)
53+
54+
55+
@pytest.mark.asyncio
56+
async def test_navigate_and_wait(browser):
57+
"""Test navigate_and_wait with both valid data URL and bad URL."""
58+
_logger.info("testing navigate_and_wait...")
59+
# Create two blank tabs first
60+
tab = await browser.create_tab("")
61+
62+
# navigating to dataurls seems to be fine right now,
63+
# but if one day you have an error here,
64+
# change to the strategy above
65+
66+
# Create a data URL with identifiable content
67+
html_content1 = "<html><body><h1>Navigation Test 1</h1></body></html>"
68+
data_url1 = f"data:text/html,{html_content1}"
69+
70+
html_content2 = "<html><body><h1>Navigation Test 2</h1></body></html>"
71+
data_url2 = f"data:text/html,{html_content2}"
72+
73+
# Test 1: Navigate first tab to valid data URL - should succeed
74+
result_tab1 = await navigate_and_wait(tab, url=data_url1, timeout=5.0)
75+
assert result_tab1 is tab
76+
77+
# Verify the navigation succeeded using execute_js_and_wait
78+
result = await execute_js_and_wait(tab, "window.location.href", timeout=5.0)
79+
location = result["result"]["result"]["value"]
80+
assert location.startswith("data:text/html")
81+
82+
# Test 2: Navigate second tab to another valid data URL - should succeed
83+
result_tab2 = await navigate_and_wait(tab, url=data_url2, timeout=5.0)
84+
assert result_tab2 is tab
85+
86+
# Verify the navigation succeeded
87+
result = await execute_js_and_wait(tab, "window.location.href", timeout=5.0)
88+
location = result["result"]["result"]["value"]
89+
assert location.startswith("data:text/html")
90+
# Test 3: Navigate to bad URL that won't load - should timeout
91+
with pytest.raises(asyncio.TimeoutError):
92+
await navigate_and_wait(tab, url="http://192.0.2.1:9999", timeout=0.5)

0 commit comments

Comments
 (0)