From 2150d79898bc91051ddd67415d7e8ff2c06d0a7a Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 23:37:27 +0000 Subject: [PATCH 1/4] ci(acceptance): experiment: poll waits every 0.1s, capture debug info only on failure Co-Authored-By: Joshua Li <14209781+joshuarli@users.noreply.github.com> --- src/sentry/testutils/pytest/selenium.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/sentry/testutils/pytest/selenium.py b/src/sentry/testutils/pytest/selenium.py index ebbde71a3e83..86fba494209f 100644 --- a/src/sentry/testutils/pytest/selenium.py +++ b/src/sentry/testutils/pytest/selenium.py @@ -31,6 +31,11 @@ logger = logging.getLogger("sentry.testutils") +# Selenium's default of 0.5s adds up to half a second to every wait that +# isn't already satisfied, and acceptance tests wait many times each. +WAIT_POLL_SECONDS = 0.1 + + class Browser: def __init__(self, driver, live_server): self.driver = driver @@ -214,7 +219,7 @@ def wait_until_clickable(self, selector=None, xpath=None, timeout=10): Waits until ``selector`` is visible and enabled to be clicked, or until ``timeout`` is hit, whichever happens first. """ - wait = WebDriverWait(self.driver, timeout) + wait = WebDriverWait(self.driver, timeout, poll_frequency=WAIT_POLL_SECONDS) if selector: wait.until(expected_conditions.element_to_be_clickable((By.CSS_SELECTOR, selector))) elif xpath: @@ -229,7 +234,7 @@ def wait_until(self, selector=None, xpath=None, title=None, timeout=10): Waits until ``selector`` is found in the browser, or until ``timeout`` is hit, whichever happens first. """ - wait = WebDriverWait(self.driver, timeout) + wait = WebDriverWait(self.driver, timeout, poll_frequency=WAIT_POLL_SECONDS) if selector: wait.until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector))) elif xpath: @@ -249,7 +254,7 @@ def wait_until_not(self, selector=None, xpath=None, title=None, timeout=10): Waits until ``selector`` is NOT found in the browser, or until ``timeout`` is hit, whichever happens first. """ - wait = WebDriverWait(self.driver, timeout) + wait = WebDriverWait(self.driver, timeout, poll_frequency=WAIT_POLL_SECONDS) if selector: wait.until_not( expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector)) @@ -268,7 +273,7 @@ def wait_until_script_execution(self, script, timeout=10): Waits until ``script`` executes and evaluates truthy, or until ``timeout`` is hit, whichever happens first. """ - wait = WebDriverWait(self.driver, timeout) + wait = WebDriverWait(self.driver, timeout, poll_frequency=WAIT_POLL_SECONDS) wait.until(lambda driver: driver.execute_script(script)) return self @@ -564,7 +569,9 @@ def pytest_runtest_makereport(item, call): summary: MutableSequence[str] = [] extra = getattr(report, "extra", []) driver = getattr(item, "_driver", None) - if driver is not None: + # Each phase of every test used to take a screenshot and dump the page + # source. Only a failure needs them. + if driver is not None and report.failed: _gather_url(item, report, driver, summary, extra) _gather_screenshot(item, report, driver, summary, extra) _gather_html(item, report, driver, summary, extra) From cf172876dd6080f67f41b2818d86958f630c971b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 23:38:16 +0000 Subject: [PATCH 2/4] ci(acceptance): experiment: reuse one Chrome per xdist worker Co-Authored-By: Joshua Li <14209781+joshuarli@users.noreply.github.com> --- src/sentry/testutils/pytest/selenium.py | 49 ++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/sentry/testutils/pytest/selenium.py b/src/sentry/testutils/pytest/selenium.py index 86fba494209f..0656d1732df2 100644 --- a/src/sentry/testutils/pytest/selenium.py +++ b/src/sentry/testutils/pytest/selenium.py @@ -13,6 +13,7 @@ import pytest from selenium import webdriver from selenium.common.exceptions import ( + NoAlertPresentException, NoSuchElementException, SessionNotCreatedException, WebDriverException, @@ -504,12 +505,43 @@ def _take_prelaunched_chrome() -> webdriver.Chrome | None: def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # A worker whose tests never use the browser still has to close the # prelaunched Chrome. - driver = _take_prelaunched_chrome() - if driver is not None: + for driver in (_take_prelaunched_chrome(), _reusable_chrome.pop("driver", None)): + if driver is not None: + try: + driver.quit() + except Exception: + pass + + +# Starting Chrome for every test costs the launch, and each test then loads the +# app bundle into a cold cache. One Chrome per worker, reset between tests, +# keeps the cache warm. +_reusable_chrome: dict[str, WebDriver] = {} + + +def _reset_chrome(driver: WebDriver, origin: str) -> bool: + """Clears what a test can leave behind. Returns False if Chrome is unusable.""" + try: try: - driver.quit() - except Exception: + driver.switch_to.alert.dismiss() + except NoAlertPresentException: pass + handles = driver.window_handles + for handle in handles[1:]: + driver.switch_to.window(handle) + driver.close() + driver.switch_to.window(handles[0]) + driver.get("about:blank") + driver.execute_cdp_cmd("Network.clearBrowserCookies", {}) + driver.execute_cdp_cmd( + "Storage.clearDataForOrigin", {"origin": origin, "storageTypes": "all"} + ) + driver.execute_cdp_cmd("Emulation.setTouchEmulationEnabled", {"enabled": False}) + driver.execute_cdp_cmd("Emulation.clearDeviceMetricsOverride", {}) + except Exception: + logger.warning("selenium.reset-chrome.failed", exc_info=True) + return False + return True @pytest.fixture(scope="function") @@ -520,7 +552,11 @@ def browser(request, live_server): driver_type = request.config.getoption("selenium_driver") driver: WebDriver if driver_type == "chrome": - driver = _take_prelaunched_chrome() or _launch_chrome(request.config) + driver = ( + _reusable_chrome.pop("driver", None) + or _take_prelaunched_chrome() + or _launch_chrome(request.config) + ) elif driver_type == "firefox": driver = webdriver.Firefox() else: @@ -555,6 +591,9 @@ def browser(request, live_server): sys.stderr.write("[browser console] ") sys.stderr.write(repr(entry)) sys.stderr.write("\n") + if driver_type == "chrome" and _reset_chrome(driver, live_server.url): + _reusable_chrome["driver"] = driver + return # Teardown Selenium. try: driver.quit() From 5e8d52ec614e74807c04d048a59a146e48df58c3 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 23:51:55 +0000 Subject: [PATCH 3/4] ci(acceptance): fix typing of reused Chrome driver execute_cdp_cmd exists only on the Chrome driver, so the reuse cache and reset helper take webdriver.Chrome. Popping the cache into its own local avoids the WebDriver type context rejecting the None default. --- src/sentry/testutils/pytest/selenium.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/sentry/testutils/pytest/selenium.py b/src/sentry/testutils/pytest/selenium.py index 0656d1732df2..e0fee3f8e5e1 100644 --- a/src/sentry/testutils/pytest/selenium.py +++ b/src/sentry/testutils/pytest/selenium.py @@ -516,10 +516,10 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # Starting Chrome for every test costs the launch, and each test then loads the # app bundle into a cold cache. One Chrome per worker, reset between tests, # keeps the cache warm. -_reusable_chrome: dict[str, WebDriver] = {} +_reusable_chrome: dict[str, webdriver.Chrome] = {} -def _reset_chrome(driver: WebDriver, origin: str) -> bool: +def _reset_chrome(driver: webdriver.Chrome, origin: str) -> bool: """Clears what a test can leave behind. Returns False if Chrome is unusable.""" try: try: @@ -552,11 +552,8 @@ def browser(request, live_server): driver_type = request.config.getoption("selenium_driver") driver: WebDriver if driver_type == "chrome": - driver = ( - _reusable_chrome.pop("driver", None) - or _take_prelaunched_chrome() - or _launch_chrome(request.config) - ) + reused = _reusable_chrome.pop("driver", None) + driver = reused or _take_prelaunched_chrome() or _launch_chrome(request.config) elif driver_type == "firefox": driver = webdriver.Firefox() else: @@ -591,7 +588,7 @@ def browser(request, live_server): sys.stderr.write("[browser console] ") sys.stderr.write(repr(entry)) sys.stderr.write("\n") - if driver_type == "chrome" and _reset_chrome(driver, live_server.url): + if isinstance(driver, webdriver.Chrome) and _reset_chrome(driver, live_server.url): _reusable_chrome["driver"] = driver return # Teardown Selenium. From dbfa71d4f62b2ddbf23070de8ea8043dc60c8021 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 23:38:16 +0000 Subject: [PATCH 4/4] ci(acceptance): experiment: 3 xdist workers with Chrome reuse --- .github/workflows/acceptance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index a687e8b65ada..580c05b5b5f0 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -69,7 +69,7 @@ jobs: TEST_GROUP_STRATEGY: roundrobin PYTHONHASHSEED: '0' XDIST_PER_WORKER_SNUBA: '1' - XDIST_WORKERS: '2' + XDIST_WORKERS: '3' steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7