Skip to content

Commit 400a056

Browse files
lordjabezclaude
andcommitted
Stabilize CI browser tests on macOS and Windows runners
- SE_SKIP_DRIVER_IN_PATH: runner images ship a chromedriver on PATH that lags the installed Chrome, so Selenium Manager picked a mismatched driver ('ChromeDriver only supports Chrome version 152') and macOS Chrome tests failed or were silently skipped - Pass --no-sandbox/--disable-dev-shm-usage to every Requestium-launched Chrome in tests via own_chrome_session(), as the conftest drivers already did. On Windows runners, sandboxed Chrome for Testing (selected by selenium 4.32's Selenium Manager) silently drops navigations: measured 21/30 OK sandboxed vs 30/30 with --no-sandbox - Widen test_session_default_timeout_is_used's margin; it missed by 20ms on a slow Ubuntu runner Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01446x2ZH4bs3pKQ6mV84K1a
1 parent 584e054 commit 400a056

8 files changed

Lines changed: 40 additions & 26 deletions

File tree

‎.github/workflows/python.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ jobs:
6666
- run: pytest -r A
6767
env:
6868
SE_AUTO_UPDATE: true # opt-in to selenium 5 behavior
69+
# Runner images put a chromedriver on PATH that can lag the installed Chrome ("This version of
70+
# ChromeDriver only supports Chrome version N"); let Selenium Manager resolve a matching driver instead.
71+
SE_SKIP_DRIVER_IN_PATH: true
6972
COVERAGE_FILE: .coverage.${{ matrix.os }}.${{ matrix.python-version }}.${{ matrix.resolution }}
7073

7174
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f

‎tests/conftest.py‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from collections.abc import Generator, Sequence
88
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
99
from pathlib import Path
10-
from typing import TYPE_CHECKING, cast
10+
from typing import TYPE_CHECKING, Any, cast
1111
from urllib.parse import parse_qs, urlsplit
1212

1313
import pytest
@@ -46,6 +46,11 @@
4646
DISABLE_HTTPS_UPGRADES = "--disable-features=HttpsUpgrades,HttpsFirstBalancedModeAutoEnable"
4747

4848

49+
# Chrome for Testing with its sandbox enabled silently dropped navigations on GitHub's Windows runners (driver.get returned but
50+
# the page stayed on data:, in 9 of 30 launches). The library must not disable the sandbox itself, so tests pass these.
51+
CHROME_CI_ARGUMENTS = ("--no-sandbox", "--disable-dev-shm-usage")
52+
53+
4954
class _Handler(BaseHTTPRequestHandler):
5055
"""
5156
Serves pages and small JSON endpoints, and doubles as an HTTP forward proxy.
@@ -206,13 +211,20 @@ def chrome_options(server: LocalServer, *, headless: bool, arguments: Sequence[s
206211
options.add_argument(DISABLE_HTTPS_UPGRADES)
207212
for argument in arguments:
208213
options.add_argument(argument)
209-
options.add_argument("--no-sandbox")
210-
options.add_argument("--disable-dev-shm-usage")
214+
for argument in CHROME_CI_ARGUMENTS:
215+
options.add_argument(argument)
211216
if headless:
212217
options.add_argument("--headless=new")
213218
return options
214219

215220

221+
def own_chrome_session(*, arguments: Sequence[str] = (), **kwargs: Any) -> requestium.Session: # noqa: ANN401
222+
"""Create a Session that starts its own Chrome with the CI-safe arguments, followed by any in webdriver_options and then arguments."""
223+
webdriver_options = dict(kwargs.pop("webdriver_options", None) or {})
224+
webdriver_options["arguments"] = [*CHROME_CI_ARGUMENTS, *webdriver_options.get("arguments", ()), *arguments]
225+
return requestium.Session(webdriver_options=webdriver_options, **kwargs)
226+
227+
216228
def _create_chrome_driver(server: LocalServer, *, headless: bool) -> webdriver.Chrome:
217229
try:
218230
driver = webdriver.Chrome(options=chrome_options(server, headless=headless))

‎tests/test_add_cookie.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import requestium.requestium
1515
from requestium.requestium_mixin import DriverMixin
1616

17-
from .conftest import LocalServer, assert_first_cookie_matches, chrome_options, cookies_sent_by_driver
17+
from .conftest import LocalServer, assert_first_cookie_matches, chrome_options, cookies_sent_by_driver, own_chrome_session
1818

1919

2020
def test_ensure_add_cookie(clean_session: requestium.Session, cookie_data: dict[str, str]) -> None:
@@ -63,12 +63,10 @@ def chrome(server: LocalServer) -> Generator[DriverMixin, None, None]:
6363
def secure_chrome(https_server: LocalServer) -> Generator[DriverMixin, None, None]:
6464
"""Start Chrome with *.secure.com mapped to the TLS listener (a forward proxy can't tunnel to it) and its certificate trusted."""
6565
arguments = [
66-
"--no-sandbox",
67-
"--disable-dev-shm-usage",
6866
f"--host-resolver-rules=MAP *.secure.com 127.0.0.1:{https_server.port}",
6967
"--ignore-certificate-errors",
7068
]
71-
session = requestium.Session(headless=True, webdriver_options={"arguments": arguments})
69+
session = own_chrome_session(headless=True, arguments=arguments)
7270
yield session.driver
7371
session.close()
7472

‎tests/test_cookies.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import requestium.requestium
88

9-
from .conftest import LocalServer, assert_first_cookie_matches, cookies_sent_by_driver
9+
from .conftest import LocalServer, assert_first_cookie_matches, cookies_sent_by_driver, own_chrome_session
1010

1111
# Cookie domains in these tests are fake public hostnames such as example.com. The browsers and requests are pointed at
1212
# the local server as an HTTP proxy (see LocalServer), so nothing leaves the machine, yet cookie and domain rules are
@@ -107,7 +107,7 @@ def test_transfer_session_cookies_to_driver_no_domain_error(clean_session: reque
107107
def localhost_session(server: LocalServer) -> Generator[requestium.Session, None, None]:
108108
"""Start a Chrome session where http://localhost:<port> reaches the test server and counts as a secure context for Secure cookies."""
109109
arguments = ["--host-resolver-rules=MAP localhost 127.0.0.1", f"--unsafely-treat-insecure-origin-as-secure=http://localhost:{server.port}"]
110-
session = requestium.Session(headless=True, webdriver_options={"arguments": ["--no-sandbox", "--disable-dev-shm-usage", *arguments]})
110+
session = own_chrome_session(headless=True, arguments=arguments)
111111
yield session
112112
session.close()
113113

‎tests/test_ensure_element.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,12 @@ def test_session_default_timeout_is_used(session: requestium.Session) -> None:
144144
short_session = requestium.Session(driver=session.driver, default_timeout=0.6)
145145
assert short_session.driver.default_timeout == 0.6
146146

147-
run_later(short_session, "document.body.appendChild(Object.assign(document.createElement('div'), {id: 'slow'}));", delay_ms=1500)
147+
run_later(short_session, "document.body.appendChild(Object.assign(document.createElement('div'), {id: 'slow'}));", delay_ms=3000)
148148
start = time.monotonic()
149149
with pytest.raises(TimeoutException):
150150
short_session.driver.ensure_element(By.ID, "slow")
151-
assert 0.6 <= time.monotonic() - start < 1.4
151+
# Well under the 3s it takes the element to appear, with headroom for slow CI runners
152+
assert 0.6 <= time.monotonic() - start < 2.5
152153

153154
# An explicit timeout overrides the default
154155
assert short_session.driver.ensure_element(By.ID, "slow", timeout=5) is not None

‎tests/test_proxy.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
import requestium
88

9-
from .conftest import DISABLE_HTTPS_UPGRADES, LocalServer
9+
from .conftest import DISABLE_HTTPS_UPGRADES, LocalServer, own_chrome_session
1010

1111
PROXIED_HOST = "requestium-proxy-test.invalid"
1212

1313

1414
def test_proxy_flag_reaches_chrome(server: LocalServer) -> None:
15-
session = requestium.Session(headless=True, webdriver_options={"arguments": [DISABLE_HTTPS_UPGRADES]})
15+
session = own_chrome_session(headless=True, arguments=[DISABLE_HTTPS_UPGRADES])
1616
try:
1717
# Set after construction but before the first driver access
1818
session.proxies = {"http": server.url}
@@ -35,7 +35,7 @@ def test_proxy_flag_format() -> None:
3535

3636

3737
def test_credentialed_proxy_warns_and_is_skipped() -> None:
38-
session = requestium.Session(headless=True)
38+
session = own_chrome_session(headless=True)
3939
session.proxies = {"http": "http://user:secret@proxy.example:8080"}
4040
try:
4141
with pytest.warns(UserWarning, match="credentials") as record:
@@ -46,7 +46,7 @@ def test_credentialed_proxy_warns_and_is_skipped() -> None:
4646

4747

4848
def test_no_warnings_without_credentials() -> None:
49-
session = requestium.Session(headless=True)
49+
session = own_chrome_session(headless=True)
5050
session.proxies = {"http": "http://proxy.example:8080"}
5151
with warnings.catch_warnings():
5252
warnings.simplefilter("error")

‎tests/test_session.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from requestium.requestium import DriverMixin, RequestiumResponse
1414
from requestium.requestium_session import _mixin_class
1515

16-
from .conftest import LocalServer, chrome_options, validate_session
16+
from .conftest import LocalServer, chrome_options, own_chrome_session, validate_session
1717

1818
SessionFactory = Callable[..., requestium.Session]
1919

@@ -24,7 +24,7 @@ def make_session() -> Generator[SessionFactory, None, None]:
2424
sessions: list[requestium.Session] = []
2525

2626
def _make_session(**kwargs: Any) -> requestium.Session: # noqa: ANN401
27-
session = requestium.Session(**kwargs)
27+
session = own_chrome_session(**kwargs)
2828
sessions.append(session)
2929
return session
3030

@@ -90,7 +90,7 @@ def driver_is_dead(driver: WebDriver) -> bool:
9090

9191

9292
def test_close_quits_lazily_started_chrome() -> None:
93-
session = requestium.Session(headless=True)
93+
session = own_chrome_session(headless=True)
9494
driver = session.driver
9595
assert not driver_is_dead(driver)
9696
session.close()
@@ -99,7 +99,7 @@ def test_close_quits_lazily_started_chrome() -> None:
9999

100100

101101
def test_close_then_driver_access_starts_fresh_chrome() -> None:
102-
session = requestium.Session(headless=True)
102+
session = own_chrome_session(headless=True)
103103
first = session.driver
104104
session.close()
105105
second = session.driver
@@ -111,20 +111,20 @@ def test_close_then_driver_access_starts_fresh_chrome() -> None:
111111

112112

113113
def test_context_manager_quits_driver() -> None:
114-
with requestium.Session(headless=True) as session:
114+
with own_chrome_session(headless=True) as session:
115115
driver = session.driver
116116
assert not driver_is_dead(driver)
117117
assert driver_is_dead(driver)
118118

119119

120120
def test_close_without_driver_does_not_start_one() -> None:
121-
session = requestium.Session(headless=True)
121+
session = own_chrome_session(headless=True)
122122
session.close()
123123
assert session._driver is None
124124

125125

126126
def test_close_twice_is_harmless() -> None:
127-
session = requestium.Session(headless=True)
127+
session = own_chrome_session(headless=True)
128128
driver = session.driver
129129
driver.quit()
130130
session.close()

‎tests/test_user_agent.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import requestium.requestium
66

7-
from .conftest import LocalServer
7+
from .conftest import LocalServer, own_chrome_session
88

99

1010
@pytest.fixture
@@ -55,7 +55,7 @@ def test_transfer_driver_cookies_to_session_can_skip_user_agent(reset_session_he
5555

5656

5757
def test_custom_user_agent_reaches_chrome() -> None:
58-
session = requestium.Session(headless=True)
58+
session = own_chrome_session(headless=True)
5959
session.headers["User-Agent"] = "RequestiumTest/1.0"
6060
try:
6161
assert session.driver.execute_script("return navigator.userAgent;") == "RequestiumTest/1.0"
@@ -64,7 +64,7 @@ def test_custom_user_agent_reaches_chrome() -> None:
6464

6565

6666
def test_default_user_agent_is_not_forced() -> None:
67-
session = requestium.Session(headless=True)
67+
session = own_chrome_session(headless=True)
6868
try:
6969
user_agent = session.driver.execute_script("return navigator.userAgent;")
7070
assert "python-requests" not in user_agent
@@ -73,7 +73,7 @@ def test_default_user_agent_is_not_forced() -> None:
7373

7474

7575
def test_user_supplied_user_agent_argument_wins() -> None:
76-
session = requestium.Session(headless=True, webdriver_options={"arguments": ["--user-agent=FromOptions/2.0"]})
76+
session = own_chrome_session(headless=True, arguments=["--user-agent=FromOptions/2.0"])
7777
session.headers["User-Agent"] = "FromHeaders/1.0"
7878
try:
7979
assert session.driver.execute_script("return navigator.userAgent;") == "FromOptions/2.0"

0 commit comments

Comments
 (0)