diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a69381c..138f69bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,6 +74,15 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Install Chromium on Linux + if: runner.os == 'Linux' + id: setup-chromium + uses: browser-actions/setup-chrome@v2 + with: + # "latest" is resolved from Chromium Snapshots, not stable Google Chrome. + chrome-version: latest + install-dependencies: true + - name: Install browsers on Linux if: runner.os == 'Linux' run: | @@ -92,7 +101,8 @@ jobs: sudo apt-get update sudo apt-get -y --no-install-recommends install opera-stable - sudo apt-get install chromium-browser + sudo ln -sf "${{ steps.setup-chromium.outputs.chrome-path }}" /usr/local/bin/chromium + chromium --version sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main"|sudo tee /etc/apt/sources.list.d/brave-browser-release.list @@ -112,7 +122,17 @@ jobs: if: runner.os == 'Windows' shell: powershell run: | - choco install chromium opera brave googlechrome --no-progress -y --force + choco install chromium opera brave --no-progress -y --force + + $chromePaths = @( + "$env:PROGRAMFILES\Google\Chrome\Application\chrome.exe", + "${env:PROGRAMFILES(X86)}\Google\Chrome\Application\chrome.exe", + "$env:LOCALAPPDATA\Google\Chrome\Application\chrome.exe" + ) + $chromePath = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1 + if (-not $chromePath) { + choco install googlechrome --no-progress -y --force + } - name: Install browsers on MacOS if: startsWith(runner.os, 'macOS') diff --git a/CHANGELOG.md b/CHANGELOG.md index ef08bbeb..a2274cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ --- +## 4.1.2 + +### Fixes + +- ChromeDriver: restored legacy ChromeDriver storage URL handling for Chrome/ChromeDriver 114 and older, fixing invalid Chrome for Testing download URLs for older versions such as ChromeDriver `102.0.5005.61` on Windows. (#736) +- ChromeDriver on 64-bit Windows: use the legacy `win32` archive name for ChromeDriver 114 and older while preserving `win64` Chrome for Testing downloads for ChromeDriver 115 and newer. (#736) +- Firefox/GeckoDriver: report a readable error when a GeckoDriver release does not contain an asset matching the requested OS type instead of failing with an ambiguous missing-list entry. + +### Tests + +- Added regression coverage for ChromeDriver `102.0.5005.61` URL construction, legacy latest-release lookup, and the `ChromeDriverManager.install()` download-manager path without live network calls. (#736) +- Pinned GeckoDriver cache coverage to a known release and added coverage for missing GeckoDriver release assets. + +### CI + +- Windows: install Google Chrome only when it is missing, while continuing to install Chromium, Opera, and Brave through Chocolatey. +- Linux: install Chromium through `browser-actions/setup-chrome` and expose it as `chromium`, avoiding the unavailable/unstable `apt` Chromium package path on GitHub Actions. + +--- + ## 4.1.1 ### Packaging diff --git a/pyproject.toml b/pyproject.toml index 0103ffa7..6ea800b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "webdriver-manager" -version = "4.1.1" +version = "4.1.2" description = "Library provides the way to automatically manage drivers for different browsers" readme = "README.md" requires-python = ">=3.7" diff --git a/setup.cfg b/setup.cfg index 89844653..1eaa5314 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.1.1 +current_version = 4.1.2 commit = True tag = True diff --git a/tests/helper.py b/tests/helper.py index 4a8cbfe7..d4975b48 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,7 +1,11 @@ import json from webdriver_manager.core.os_manager import ChromeType -from webdriver_manager.drivers.chrome import ChromeDriver +from webdriver_manager.drivers.chrome import ( + CHROME_FOR_TESTING_DOWNLOAD_URL, + CHROME_FOR_TESTING_LATEST_RELEASE_URL, + ChromeDriver, +) class ResponseMock: @@ -33,15 +37,20 @@ def get_browser_version_from_os(self, browser_type=None): return self.browser_version -def chrome_driver_for(browser_version, responses, chrome_type=ChromeType.CHROMIUM): +def chrome_driver_for( + browser_version, + responses, + chrome_type=ChromeType.CHROMIUM, + driver_version=None, + url=CHROME_FOR_TESTING_DOWNLOAD_URL, + latest_release_url=CHROME_FOR_TESTING_LATEST_RELEASE_URL, +): http_client = HttpClientMock(responses) driver = ChromeDriver( name="chromedriver", - driver_version=None, - url="https://storage.googleapis.com/chrome-for-testing-public/", - latest_release_url=( - "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE" - ), + driver_version=driver_version, + url=url, + latest_release_url=latest_release_url, http_client=http_client, os_system_manager=OperationSystemManagerMock(browser_version), chrome_type=chrome_type, diff --git a/tests/test_chrome_driver.py b/tests/test_chrome_driver.py index 5f1ba620..3f214589 100644 --- a/tests/test_chrome_driver.py +++ b/tests/test_chrome_driver.py @@ -14,8 +14,11 @@ from webdriver_manager.core.driver_cache import DriverCacheManager from webdriver_manager.core.os_manager import OperationSystemManager, ChromeType -from webdriver_manager.drivers.chrome import CHROME_FOR_TESTING_LATEST_PATCH_VERSIONS_PER_BUILD_URL, \ - CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL +from webdriver_manager.drivers.chrome import ( + CHROMEDRIVER_STORAGE_LATEST_RELEASE_URL, + CHROME_FOR_TESTING_LATEST_PATCH_VERSIONS_PER_BUILD_URL, + CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL, +) os.environ.setdefault("WDM_LOCAL", "false") @@ -156,6 +159,130 @@ def test_chrome_118_resolves_cft_driver_version_and_download_url(): assert CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL in http_client.requested_urls +def test_chrome_102_uses_legacy_storage_url_and_win32_archive_for_win64(): + driver, http_client = chrome_driver_for( + browser_version="102.0.5005.63", + driver_version="102.0.5005.61", + chrome_type=ChromeType.GOOGLE, + responses={}, + ) + + assert driver.get_driver_download_url("win64") == ( + "https://chromedriver.storage.googleapis.com/" + "102.0.5005.61/chromedriver_win32.zip" + ) + assert http_client.requested_urls == [] + + +def test_chrome_102_detected_version_uses_legacy_latest_release_url(): + expected_url = f"{CHROMEDRIVER_STORAGE_LATEST_RELEASE_URL}_102.0.5005" + driver, http_client = chrome_driver_for( + browser_version="102.0.5005.63", + chrome_type=ChromeType.GOOGLE, + responses={ + expected_url: "102.0.5005.61", + }, + ) + + assert driver.get_latest_release_version() == "102.0.5005.61" + assert http_client.requested_urls == [expected_url] + + +def test_chrome_download_url_boundary_switches_from_legacy_to_cft(): + legacy_driver, legacy_http_client = chrome_driver_for( + browser_version="114.0.5735.199", + driver_version="114.0.5735.90", + chrome_type=ChromeType.GOOGLE, + responses={}, + ) + cft_url = ( + "https://storage.googleapis.com/chrome-for-testing-public/" + "115.0.5790.170/win64/chromedriver-win64.zip" + ) + cft_driver, cft_http_client = chrome_driver_for( + browser_version="115.0.5790.99", + driver_version="115.0.5790.170", + chrome_type=ChromeType.GOOGLE, + responses={ + CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL: { + "versions": [ + { + "version": "115.0.5790.170", + "downloads": { + "chromedriver": [ + {"platform": "win64", "url": cft_url}, + ], + }, + }, + ], + }, + }, + ) + + assert legacy_driver.get_driver_download_url("win64") == ( + "https://chromedriver.storage.googleapis.com/" + "114.0.5735.90/chromedriver_win32.zip" + ) + assert legacy_http_client.requested_urls == [] + assert cft_driver.get_driver_download_url("win64") == cft_url + assert cft_http_client.requested_urls == [ + CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL, + ] + + +def test_chrome_manager_downloads_legacy_chrome_102_url_for_win64(tmp_path): + class CacheManagerMock: + def find_driver(self, _driver): + return None + + def get_driver_lock_path(self, _driver_name, _os_type): + return str(tmp_path / ".wdm-lock") + + def save_file_to_cache(self, _driver, _file): + driver_path = tmp_path / "chromedriver.exe" + driver_path.write_text("") + return str(driver_path) + + class DownloadManagerMock: + http_client = None + + def __init__(self): + self.requested_urls = [] + + def download_file(self, url): + self.requested_urls.append(url) + return object() + + class Windows64OSManagerMock: + def get_os_type(self): + return "win64" + + def get_os_architecture(self): + return 64 + + def is_mac_os(self, _os_type): + return False + + def get_browser_version_from_os(self, _browser_type=None): + return "102.0.5005.63" + + download_manager = DownloadManagerMock() + manager = ChromeDriverManager( + driver_version="102.0.5005.61", + download_manager=download_manager, + cache_manager=CacheManagerMock(), + os_system_manager=Windows64OSManagerMock(), + ) + + driver_path = manager.install() + + assert os.path.exists(driver_path) + assert download_manager.requested_urls == [ + "https://chromedriver.storage.googleapis.com/" + "102.0.5005.61/chromedriver_win32.zip" + ] + + def test_chrome_115_plus_prefers_win64_download_when_available(): expected_url = ( "https://storage.googleapis.com/chrome-for-testing-public/" diff --git a/tests/test_firefox_manager.py b/tests/test_firefox_manager.py index 8e192c87..900b69c9 100644 --- a/tests/test_firefox_manager.py +++ b/tests/test_firefox_manager.py @@ -76,6 +76,13 @@ def _cache_os_types_for_current_platform(): @pytest.mark.parametrize('os_type', _cache_os_types_for_current_platform()) @requires_gh_token def test_can_get_driver_from_cache(os_type): - GeckoDriverManager(os_system_manager=OperationSystemManager(os_type)).install() - driver_path = GeckoDriverManager(os_system_manager=OperationSystemManager(os_type)).install() + driver_version = "v0.36.0" + GeckoDriverManager( + version=driver_version, + os_system_manager=OperationSystemManager(os_type), + ).install() + driver_path = GeckoDriverManager( + version=driver_version, + os_system_manager=OperationSystemManager(os_type), + ).install() assert os.path.exists(driver_path) diff --git a/uv.lock b/uv.lock index 054ee188..d2197545 100644 --- a/uv.lock +++ b/uv.lock @@ -1143,7 +1143,7 @@ socks = [ [[package]] name = "webdriver-manager" -version = "4.1.1" +version = "4.1.2" source = { editable = "." } dependencies = [ { name = "packaging", version = "24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, diff --git a/webdriver_manager/__init__.py b/webdriver_manager/__init__.py index 72aa7583..13ffcf42 100644 --- a/webdriver_manager/__init__.py +++ b/webdriver_manager/__init__.py @@ -1 +1 @@ -__version__ = "4.1.1" +__version__ = "4.1.2" diff --git a/webdriver_manager/chrome.py b/webdriver_manager/chrome.py index 1feb1d6a..2d7ee9c9 100644 --- a/webdriver_manager/chrome.py +++ b/webdriver_manager/chrome.py @@ -5,7 +5,11 @@ from webdriver_manager.core.driver_cache import DriverCacheManager from webdriver_manager.core.manager import DriverManager from webdriver_manager.core.os_manager import OperationSystemManager, ChromeType -from webdriver_manager.drivers.chrome import ChromeDriver +from webdriver_manager.drivers.chrome import ( + CHROME_FOR_TESTING_DOWNLOAD_URL, + CHROME_FOR_TESTING_LATEST_RELEASE_URL, + ChromeDriver, +) class ChromeDriverManager(DriverManager): @@ -13,8 +17,8 @@ def __init__( self, driver_version: Optional[str] = None, name: str = "chromedriver", - url: str = "https://storage.googleapis.com/chrome-for-testing-public/", - latest_release_url: str = "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE", + url: str = CHROME_FOR_TESTING_DOWNLOAD_URL, + latest_release_url: str = CHROME_FOR_TESTING_LATEST_RELEASE_URL, chrome_type: str = ChromeType.GOOGLE, download_manager: Optional[DownloadManager] = None, cache_manager: Optional[DriverCacheManager] = None, diff --git a/webdriver_manager/drivers/chrome.py b/webdriver_manager/drivers/chrome.py index dee94ed8..895fcd6f 100644 --- a/webdriver_manager/drivers/chrome.py +++ b/webdriver_manager/drivers/chrome.py @@ -17,6 +17,14 @@ CHROME_FOR_TESTING_KNOWN_GOOD_VERSIONS_URL = ( "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json" ) +CHROME_FOR_TESTING_DOWNLOAD_URL = "https://storage.googleapis.com/chrome-for-testing-public/" +CHROME_FOR_TESTING_LATEST_RELEASE_URL = ( + "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE" +) +CHROMEDRIVER_STORAGE_URL = "https://chromedriver.storage.googleapis.com" +CHROMEDRIVER_STORAGE_LATEST_RELEASE_URL = ( + f"{CHROMEDRIVER_STORAGE_URL}/LATEST_RELEASE" +) class ChromeDriver(Driver): @@ -59,7 +67,13 @@ def get_driver_download_url(self, os_type): log(f"Modern chrome version {modern_version_url}") return modern_version_url - return f"{self._url}/{driver_version_to_download}/{self.get_name()}_{os_type}.zip" + if os_type == "win64": + os_type = "win32" + + return ( + f"{self._legacy_url()}/{driver_version_to_download}/" + f"{self.get_name()}_{os_type}.zip" + ) def get_browser_type(self): return self._browser_type @@ -74,13 +88,25 @@ def get_latest_release_version(self): elif determined_browser_version is not None: # Remove the build version (the last segment) from determined_browser_version for version < 115 determined_browser_version = ".".join(determined_browser_version.split(".")[:3]) - latest_release_url = f"{self._latest_release_url}_{determined_browser_version}" + latest_release_url = ( + f"{self._legacy_latest_release_url()}_{determined_browser_version}" + ) else: latest_release_url = self._latest_release_url resp = self._http_client.get(url=latest_release_url) return resp.text.rstrip() + def _legacy_url(self): + if self._url.rstrip("/") == CHROME_FOR_TESTING_DOWNLOAD_URL.rstrip("/"): + return CHROMEDRIVER_STORAGE_URL + return self._url.rstrip("/") + + def _legacy_latest_release_url(self): + if self._latest_release_url == CHROME_FOR_TESTING_LATEST_RELEASE_URL: + return CHROMEDRIVER_STORAGE_LATEST_RELEASE_URL + return self._latest_release_url + def _latest_cft_version_for_browser_version(self, browser_version): browser_build_version = ".".join(browser_version.split(".")[:3]) browser_milestone = browser_version.split(".")[0] diff --git a/webdriver_manager/drivers/firefox.py b/webdriver_manager/drivers/firefox.py index 7d7dc293..98ee75f6 100644 --- a/webdriver_manager/drivers/firefox.py +++ b/webdriver_manager/drivers/firefox.py @@ -44,6 +44,11 @@ def get_driver_download_url(self, os_type): name = f"{self.get_name()}-{driver_version_to_download}-{os_type}." output_dict = [ asset for asset in assets if asset["name"].startswith(name)] + if not output_dict: + available_assets = ", ".join(asset.get("name", "") for asset in assets) + raise ValueError( + f"Could not find GeckoDriver asset for '{name}'. Available assets: {available_assets}" + ) return output_dict[0]["browser_download_url"] @property