Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:

- uses: install-pinned/uv@9f8c76e6929eb07a49a7c681bf82edb9936cbdad

- run: uv pip install --system pytest-xvfb
if: ${{ matrix.os == 'ubuntu-latest' }}

- run: uv pip install --system --resolution ${{ matrix.resolution }} -e .[dev]

- id: cache-pytest
Expand All @@ -60,11 +63,10 @@ jobs:
path: .pytest_cache
key: ${{ matrix.os }}-pytest-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}

- uses: GabrielBB/xvfb-action@5bcda06da84ba084708898801da79736b88e00a9
- run: pytest -r A
env:
SE_AUTO_UPDATE: true # opt-in to selenium 5 behavior
COVERAGE_FILE: .coverage.${{ matrix.os }}.${{ matrix.python-version }}.${{ matrix.resolution }}
with:
run: pytest

- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
Expand Down Expand Up @@ -144,8 +146,7 @@ jobs:
key: ${{ runner.os }}-ruff-3.13-${{ hashFiles('pyproject.toml') }}

- id: run-ruff-sarif
run: |
ruff check --output-format=sarif -o results.sarif .
run: ruff check --output-format=sarif -o results.sarif .

- uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89
if: ( success() || failure() ) && contains('["success", "failure"]', steps.run-ruff-sarif.outcome)
Expand All @@ -154,8 +155,7 @@ jobs:

- id: run-ruff
if: failure() && contains('["failure"]', steps.run-ruff-sarif.outcome)
run: |
ruff check --output-format=github .
run: ruff check --output-format=github .

mypy:
name: Mypy type checking
Expand Down Expand Up @@ -191,8 +191,7 @@ jobs:
key: ${{ runner.os }}-mypy-3.13-${{ hashFiles('pyproject.toml') }}

- id: run-mypy
run: |
mypy .
run: mypy .

bandit:
name: Bandit security
Expand Down Expand Up @@ -235,8 +234,7 @@ jobs:

- id: run-bandit
if: failure() && contains('["failure"]', steps.run-bandit-sarif.outcome)
run: |
bandit --confidence-level 'medium' --recursive 'requestium'
run: bandit --confidence-level 'medium' --recursive 'requestium'

coverage:
runs-on: ubuntu-latest
Expand Down
104 changes: 63 additions & 41 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
from typing import TYPE_CHECKING, cast

import pytest
import urllib3.exceptions
from _pytest.fixtures import FixtureRequest
from selenium import webdriver
from selenium.common import WebDriverException
from selenium.webdriver.support import expected_conditions as EC # noqa: N812
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

import requestium

if TYPE_CHECKING:
from requestium.requestium_mixin import DriverMixin

# ruff: noqa FBT003


@pytest.fixture(scope="module")
def example_html() -> str:
Expand All @@ -35,28 +34,58 @@ def example_html() -> str:
"""


def _create_chrome_driver(headless: bool) -> webdriver.Chrome:
def _create_chrome_driver(*, headless: bool) -> webdriver.Chrome:
options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
if headless:
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
WebDriverWait(driver, 5).until(EC.number_of_windows_to_be(1))
return driver

try:
driver = webdriver.Chrome(options=options)
WebDriverWait(driver, 5).until(EC.number_of_windows_to_be(1))
return driver
except (urllib3.exceptions.ReadTimeoutError, TimeoutError, WebDriverException) as e:
error_msg = f"Chrome driver initialization failed: {e}"
raise RuntimeError(error_msg) from e


def _create_firefox_driver(headless: bool) -> webdriver.Firefox:
def _create_firefox_driver(*, headless: bool) -> webdriver.Firefox:
options = webdriver.FirefoxOptions()
options.set_preference("browser.cache.disk.enable", False)
options.set_preference("browser.cache.memory.enable", False)
options.set_preference("browser.cache.offline.enable", False)
options.set_preference("network.http.use-cache", False)
options.set_preference("browser.cache.disk.enable", value=False)
options.set_preference("browser.cache.memory.enable", value=False)
options.set_preference("browser.cache.offline.enable", value=False)
options.set_preference("network.http.use-cache", value=False)
if headless:
options.add_argument("--headless")
driver = webdriver.Firefox(options=options)
WebDriverWait(driver, 5).until(EC.number_of_windows_to_be(1))
return driver

try:
driver = webdriver.Firefox(options=options)
WebDriverWait(driver, 5).until(EC.number_of_windows_to_be(1))
return driver
except (urllib3.exceptions.ReadTimeoutError, TimeoutError, WebDriverException) as e:
error_msg = f"Firefox driver initialization failed: {e}"
raise RuntimeError(error_msg) from e


def validate_session(session: requestium.Session) -> None:
"""
Check basic validity of requestium Session object.

If browser context is missing, try recovering.
If attempted recovery raises WebDriverException, skip test.
"""
try:
_ = session.driver.current_url
_ = session.driver.window_handles
except WebDriverException as e:
if "Browsing context has been discarded" not in str(e):
raise

try:
session.driver.switch_to.new_window("tab")
except WebDriverException as e:
pytest.skip(f"Browser context discarded and cannot be recovered: {e!s}")


@pytest.fixture(
Expand All @@ -68,37 +97,30 @@ def session(request: FixtureRequest) -> Generator[requestium.Session, None, None
browser, _, mode = driver_type.partition("-")
headless = mode == "headless"

driver: webdriver.Chrome | webdriver.Firefox
if browser == "chrome":
driver = _create_chrome_driver(headless)
elif browser == "firefox":
driver = _create_firefox_driver(headless)
else:
msg = f"Unknown driver type: {browser}"
raise ValueError(msg)
driver: webdriver.Chrome | webdriver.Firefox | None = None

try:
if browser == "chrome":
driver = _create_chrome_driver(headless=headless)
elif browser == "firefox":
driver = _create_firefox_driver(headless=headless)
else:
msg = f"Unknown driver type: {browser}"
raise ValueError(msg)

assert driver.name in browser
session = requestium.Session(driver=cast("DriverMixin", driver))
assert session.driver.name in browser

yield session
finally:
with contextlib.suppress(WebDriverException, OSError):
driver.quit()

validate_session(session)

@pytest.fixture(autouse=True)
def ensure_valid_session(session: requestium.Session) -> None:
"""Skip test if browser context is discarded."""
try:
_ = session.driver.current_url
_ = session.driver.window_handles
except WebDriverException as e:
if "Browsing context has been discarded" not in str(e):
raise
yield session

try:
session.driver.switch_to.new_window("tab")
except WebDriverException:
pytest.skip("Browser context discarded and cannot be recovered")
except RuntimeError as e:
# Driver creation failed - skip all tests using this session
pytest.skip(str(e))

finally:
if driver:
with contextlib.suppress(WebDriverException, OSError, Exception):
driver.quit()
7 changes: 7 additions & 0 deletions tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import requestium.requestium

from .conftest import validate_session


@pytest.mark.parametrize(
"headless",
Expand All @@ -19,6 +21,7 @@
)
def test_initialize_session_without_explicit_driver(example_html: str, headless: bool) -> None: # noqa: FBT001
session = requestium.Session(headless=headless)
validate_session(session)
session.driver.get(f"data:text/html,{example_html}")
session.driver.ensure_element(By.TAG_NAME, "h1")

Expand All @@ -30,6 +33,7 @@ def test_initialize_session_without_explicit_driver(example_html: str, headless:

def test_initialize_session_with_webdriver_options(example_html: str) -> None:
session = requestium.Session(webdriver_options={"arguments": ["headless=new"]})
validate_session(session)
session.driver.get(f"data:text/html,{example_html}")
session.driver.ensure_element(By.TAG_NAME, "h1")

Expand All @@ -41,6 +45,7 @@ def test_initialize_session_with_webdriver_options(example_html: str) -> None:

def test_initialize_session_with_experimental_options(example_html: str) -> None:
session = requestium.Session(webdriver_options={"experimental_options": {"useAutomationExtension": False}})
validate_session(session)
session.driver.get(f"data:text/html,{example_html}")
session.driver.ensure_element(By.TAG_NAME, "h1")

Expand All @@ -52,6 +57,7 @@ def test_initialize_session_with_experimental_options(example_html: str) -> None

def test_initialize_session_with_webdriver_prefs(example_html: str) -> None:
session = requestium.Session(webdriver_options={"prefs": {"plugins.always_open_pdf_externally": True}})
validate_session(session)
session.driver.get(f"data:text/html,{example_html}")
session.driver.ensure_element(By.TAG_NAME, "h1")

Expand All @@ -64,6 +70,7 @@ def test_initialize_session_with_webdriver_prefs(example_html: str) -> None:
def test_initialize_session_with_extension(example_html: str) -> None:
test_extension_path = Path(__file__).parent / "resources/test_extension.crx"
session = requestium.Session(webdriver_options={"extensions": [str(test_extension_path)]})
validate_session(session)
session.driver.get(f"data:text/html,{example_html}")
session.driver.ensure_element(By.TAG_NAME, "h1")

Expand Down
Loading