Merge pull request #93 from chris22333-dot/fix/windows-chrome-driver

fix: support ChromeDriver setup on Windows
This commit is contained in:
WECENG
2026-05-14 18:25:02 +08:00
committed by GitHub
4 changed files with 264 additions and 71 deletions

3
.gitignore vendored
View File

@@ -162,6 +162,7 @@ chromedriver.log
config.json config.json
credentials.json credentials.json
*.cookie *.cookie
*.pkl
*.session *.session
# Temporary files # Temporary files
@@ -177,4 +178,4 @@ credentials.json
# Lock files (except poetry.lock) # Lock files (except poetry.lock)
*.lock *.lock
!poetry.lock !poetry.lock

View File

@@ -7,6 +7,7 @@
import os import os
import re import re
import shutil
import subprocess import subprocess
import sys import sys
@@ -23,16 +24,147 @@ def _run_command_get_version(command):
result = subprocess.run( result = subprocess.run(
command, command,
capture_output=True, capture_output=True,
errors="replace",
text=True, text=True,
timeout=5 timeout=5
) )
if result.returncode == 0: if result.returncode == 0:
return result.stdout.strip() return (result.stdout or result.stderr).strip()
except Exception: except Exception:
pass pass
return None return None
def _unique_paths(paths):
"""保持顺序去重,并忽略空路径。"""
unique = []
seen = set()
for path in paths:
if not path or path in seen:
continue
seen.add(path)
unique.append(path)
return unique
def _get_chrome_paths():
"""返回常见平台上的 Chrome 可执行文件候选路径。"""
program_files = os.environ.get("PROGRAMFILES", r"C:\Program Files")
program_files_x86 = os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)")
local_appdata = os.environ.get("LOCALAPPDATA")
paths = [
os.environ.get("CHROME_PATH"),
os.environ.get("GOOGLE_CHROME_BIN"),
os.path.join(program_files, "Google", "Chrome", "Application", "chrome.exe"),
os.path.join(program_files_x86, "Google", "Chrome", "Application", "chrome.exe"),
os.path.join(local_appdata, "Google", "Chrome", "Application", "chrome.exe") if local_appdata else None,
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
]
for command in [
"chrome.exe",
"chrome",
"google-chrome",
"google-chrome-stable",
"chromium",
"chromium-browser",
]:
paths.append(shutil.which(command))
return _unique_paths(paths)
def _get_chromedriver_paths():
"""返回常见平台上的 ChromeDriver 候选路径。"""
paths = [
os.environ.get("CHROMEDRIVER_PATH"),
os.environ.get("SELENIUM_DRIVER_PATH"),
shutil.which("chromedriver.exe"),
shutil.which("chromedriver"),
os.path.join(os.getcwd(), "chromedriver.exe"),
os.path.join(os.getcwd(), "chromedriver"),
"/opt/homebrew/bin/chromedriver",
"/usr/local/bin/chromedriver",
"/opt/homebrew/Caskroom/chromedriver",
]
try:
import chromedriver_autoinstaller
package_dir = os.path.dirname(chromedriver_autoinstaller.__file__)
for name in os.listdir(package_dir):
driver_dir = os.path.join(package_dir, name)
if os.path.isdir(driver_dir):
paths.append(os.path.join(driver_dir, "chromedriver.exe"))
paths.append(os.path.join(driver_dir, "chromedriver"))
except (AttributeError, ImportError, OSError):
pass
return _unique_paths(paths)
def _version_sort_key(version):
return tuple(int(part) for part in version.split("."))
def _get_windows_chrome_version(chrome_path):
"""Windows 上 chrome.exe --version 可能被已有会话吞掉,改从安装目录读取版本。"""
app_dir = os.path.dirname(chrome_path)
try:
version_dirs = [
name for name in os.listdir(app_dir)
if re.fullmatch(r"\d+(?:\.\d+){1,3}", name)
and os.path.isdir(os.path.join(app_dir, name))
]
except OSError:
version_dirs = []
if version_dirs:
return sorted(version_dirs, key=_version_sort_key)[-1]
try:
import winreg
except ImportError:
return None
registry_locations = [
(winreg.HKEY_CURRENT_USER, r"Software\Google\Chrome\BLBeacon"),
(winreg.HKEY_LOCAL_MACHINE, r"Software\Google\Chrome\BLBeacon"),
(winreg.HKEY_LOCAL_MACHINE, r"Software\WOW6432Node\Google\Chrome\BLBeacon"),
]
for hive, key_path in registry_locations:
try:
with winreg.OpenKey(hive, key_path) as key:
version, _ = winreg.QueryValueEx(key, "version")
if version:
return version
except OSError:
continue
return None
def _find_chrome():
"""查找 Chrome返回 (path, version_str, major_version)。"""
for chrome_path in _get_chrome_paths():
if os.path.exists(chrome_path):
version_str = _run_command_get_version([chrome_path, "--version"])
if version_str:
chrome_version = _get_version_from_output(version_str)
if chrome_version:
return chrome_path, version_str, chrome_version
windows_version = _get_windows_chrome_version(chrome_path)
if windows_version:
return chrome_path, f"Google Chrome {windows_version}", _get_version_from_output(windows_version)
return None, None, None
def check_python_version(): def check_python_version():
"""检查 Python 版本""" """检查 Python 版本"""
print("Python 版本检查...") print("Python 版本检查...")
@@ -73,23 +205,13 @@ def check_dependencies():
def check_chrome(): def check_chrome():
"""检查 Chrome 浏览器""" """检查 Chrome 浏览器"""
print("Chrome 浏览器检查...") print("Chrome 浏览器检查...")
chrome_paths = [ chrome_path, version_str, chrome_version = _find_chrome()
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", if chrome_path:
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", print(f" ✓ Chrome 浏览器: {version_str}")
"/usr/bin/google-chrome", print(f" ✓ 主版本号: {chrome_version}")
"/usr/bin/chromium-browser", print(f" ✓ 路径: {chrome_path}")
] print()
return True
for chrome_path in chrome_paths:
if os.path.exists(chrome_path):
version_str = _run_command_get_version([chrome_path, "--version"])
if version_str:
chrome_version = _get_version_from_output(version_str)
if chrome_version:
print(f" ✓ Chrome 浏览器: {version_str}")
print(f" ✓ 主版本号: {chrome_version}")
print()
return True
print(" ✗ 未找到 Chrome 浏览器") print(" ✗ 未找到 Chrome 浏览器")
print(" 请安装 Chrome: https://www.google.com/chrome/") print(" 请安装 Chrome: https://www.google.com/chrome/")
@@ -100,13 +222,7 @@ def check_chrome():
def check_chromedriver(): def check_chromedriver():
"""检查 ChromeDriver""" """检查 ChromeDriver"""
print("ChromeDriver 检查...") print("ChromeDriver 检查...")
chromedriver_paths = [ for driver_path in _get_chromedriver_paths():
"/opt/homebrew/bin/chromedriver",
"/usr/local/bin/chromedriver",
"/opt/homebrew/Caskroom/chromedriver",
]
for driver_path in chromedriver_paths:
if os.path.exists(driver_path) or os.path.islink(driver_path): if os.path.exists(driver_path) or os.path.islink(driver_path):
version_str = _run_command_get_version([driver_path, "--version"]) version_str = _run_command_get_version([driver_path, "--version"])
if version_str: if version_str:
@@ -121,7 +237,8 @@ def check_chromedriver():
print(" ⚠ 未找到 ChromeDriver") print(" ⚠ 未找到 ChromeDriver")
print(" 安装方法:") print(" 安装方法:")
print(" - macOS: brew install --cask chromedriver") print(" - macOS: brew install --cask chromedriver")
print(" - 使用脚本自动安装: chromedriver-autoinstaller") print(" - Windows/macOS/Linux: 使用脚本自动安装 chromedriver-autoinstaller")
print(" - 或手动下载: https://googlechromelabs.github.io/chrome-for-testing/")
print() print()
return False return False
@@ -130,19 +247,15 @@ def check_version_match():
"""检查 Chrome 和 ChromeDriver 版本是否匹配""" """检查 Chrome 和 ChromeDriver 版本是否匹配"""
print("版本匹配检查...") print("版本匹配检查...")
chrome_version = None _, _, chrome_version = _find_chrome()
chrome_path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
if os.path.exists(chrome_path):
version_str = _run_command_get_version([chrome_path, "--version"])
if version_str:
chrome_version = _get_version_from_output(version_str)
driver_version = None driver_version = None
driver_path = "/opt/homebrew/bin/chromedriver" for driver_path in _get_chromedriver_paths():
if os.path.exists(driver_path) or os.path.islink(driver_path): if os.path.exists(driver_path) or os.path.islink(driver_path):
version_str = _run_command_get_version([driver_path, "--version"]) version_str = _run_command_get_version([driver_path, "--version"])
if version_str: if version_str:
driver_version = _get_version_from_output(version_str) driver_version = _get_version_from_output(version_str)
break
if not chrome_version or not driver_version: if not chrome_version or not driver_version:
print(" ⚠ 无法获取版本信息") print(" ⚠ 无法获取版本信息")
@@ -171,45 +284,30 @@ def check_version_match():
def get_chromedriver_path(): def get_chromedriver_path():
""" """
获取 ChromeDriver 路径,如果不存在则自动安装 获取 ChromeDriver 路径,如果不存在则自动安装
供其他脚本导入使用 供其他脚本导入使用
:return: ChromeDriver 可执行文件路径 :return: ChromeDriver 可执行文件路径
""" """
chrome_path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" _, _, chrome_version = _find_chrome()
if not os.path.exists(chrome_path): if chrome_version:
raise RuntimeError("未找到 Chrome 浏览器,请先安装 Chrome") for driver_path in _get_chromedriver_paths():
if os.path.exists(driver_path) or os.path.islink(driver_path):
driver_version_str = _run_command_get_version([driver_path, "--version"])
if driver_version_str:
driver_version = _get_version_from_output(driver_version_str)
if driver_version == chrome_version:
return driver_path
# 获取 Chrome 版本 print(f" Chrome 版本: {chrome_version}")
chrome_version_str = _run_command_get_version([chrome_path, "--version"]) else:
chrome_version = _get_version_from_output(chrome_version_str) if chrome_version_str else None print(" ⚠ 未通过常见路径检测到 Chrome尝试使用 chromedriver-autoinstaller 自动检测...")
if not chrome_version:
raise RuntimeError("无法获取 Chrome 版本")
# 检查已安装的 ChromeDriver 是否匹配
chromedriver_paths = [
"/opt/homebrew/bin/chromedriver",
"/usr/local/bin/chromedriver",
]
for driver_path in chromedriver_paths:
if os.path.exists(driver_path) or os.path.islink(driver_path):
driver_version_str = _run_command_get_version([driver_path, "--version"])
if driver_version_str:
driver_version = _get_version_from_output(driver_version_str)
if driver_version == chrome_version:
# 版本匹配,直接使用
return driver_path
# 版本不匹配或不存在,使用自动安装器
print(f" Chrome 版本: {chrome_version}")
print(" 正在自动安装匹配的 ChromeDriver...") print(" 正在自动安装匹配的 ChromeDriver...")
try: try:
import chromedriver_autoinstaller import chromedriver_autoinstaller
chromedriver_path = chromedriver_autoinstaller.install() chromedriver_path = chromedriver_autoinstaller.install()
# 验证安装的版本
result = _run_command_get_version([chromedriver_path, "--version"]) result = _run_command_get_version([chromedriver_path, "--version"])
if not result: if not result:
raise RuntimeError("ChromeDriver 无法执行") raise RuntimeError("ChromeDriver 无法执行")

View File

@@ -64,9 +64,7 @@ def mock_appium_driver():
mock_driver.swipe = Mock() mock_driver.swipe = Mock()
mock_driver.quit = Mock() mock_driver.quit = Mock()
# Mock the Remote class yield mock_driver
with patch.object(mock_driver, "__class__.__name__", "Remote"):
yield mock_driver
@pytest.fixture @pytest.fixture
@@ -153,4 +151,4 @@ def pytest_collection_modifyitems(config, items):
if "unit" in str(item.fspath): if "unit" in str(item.fspath):
item.add_marker(pytest.mark.unit) item.add_marker(pytest.mark.unit)
elif "integration" in str(item.fspath): elif "integration" in str(item.fspath):
item.add_marker(pytest.mark.integration) item.add_marker(pytest.mark.integration)

View File

@@ -0,0 +1,96 @@
import sys
from pathlib import Path
from types import SimpleNamespace
from damai import check_environment
WINDOWS_CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
WINDOWS_CHROMEDRIVER = r"C:\Tools\chromedriver.exe"
def test_check_chrome_detects_windows_program_files_install(monkeypatch):
"""Chrome installed in Program Files should be detected on Windows."""
def fake_exists(path):
return path == WINDOWS_CHROME
def fake_version(command):
if command[0] == WINDOWS_CHROME:
return "Google Chrome 136.0.7103.114"
return None
monkeypatch.setattr(check_environment.os.path, "exists", fake_exists)
monkeypatch.setattr(check_environment, "_run_command_get_version", fake_version)
assert check_environment.check_chrome() is True
def test_check_version_match_uses_windows_chrome_and_driver_paths(monkeypatch):
"""Version matching should work for Windows Chrome and chromedriver.exe."""
def fake_exists(path):
return path in {WINDOWS_CHROME, WINDOWS_CHROMEDRIVER}
def fake_islink(path):
return False
def fake_version(command):
if command[0] == WINDOWS_CHROME:
return "Google Chrome 136.0.7103.114"
if command[0] == WINDOWS_CHROMEDRIVER:
return "ChromeDriver 136.0.7103.114"
return None
monkeypatch.setenv("CHROMEDRIVER_PATH", WINDOWS_CHROMEDRIVER)
monkeypatch.setattr(check_environment.os.path, "exists", fake_exists)
monkeypatch.setattr(check_environment.os.path, "islink", fake_islink)
monkeypatch.setattr(check_environment, "_run_command_get_version", fake_version)
assert check_environment.check_version_match() is True
def test_find_chrome_reads_windows_install_version_directory(monkeypatch, tmp_path):
"""
On Windows, chrome.exe --version can open an existing browser session instead
of printing a version, so fall back to the versioned install directory.
"""
chrome_path = str(tmp_path / "Google" / "Chrome" / "Application" / "chrome.exe")
version_dir = Path(chrome_path).parent / "136.0.7103.114"
def fake_exists(path):
return path == chrome_path
def fake_listdir(path):
if path == str(Path(chrome_path).parent):
return [version_dir.name]
return []
def fake_isdir(path):
return path == str(version_dir)
monkeypatch.setattr(check_environment, "_get_chrome_paths", lambda: [chrome_path], raising=False)
monkeypatch.setattr(check_environment.os.path, "exists", fake_exists)
monkeypatch.setattr(check_environment.os.path, "isdir", fake_isdir)
monkeypatch.setattr(check_environment.os, "listdir", fake_listdir)
monkeypatch.setattr(check_environment, "_run_command_get_version", lambda command: "正在现有的浏览器会话中打开。")
assert check_environment._find_chrome() == (
chrome_path,
"Google Chrome 136.0.7103.114",
"136",
)
def test_get_chromedriver_paths_includes_autoinstaller_cache(monkeypatch, tmp_path):
"""Environment checks should find drivers downloaded by chromedriver-autoinstaller."""
package_dir = tmp_path / "chromedriver_autoinstaller"
driver_dir = package_dir / "136"
driver_dir.mkdir(parents=True)
driver_path = driver_dir / "chromedriver.exe"
driver_path.write_text("")
fake_autoinstaller = SimpleNamespace(__file__=str(package_dir / "__init__.py"))
monkeypatch.setitem(sys.modules, "chromedriver_autoinstaller", fake_autoinstaller)
assert str(driver_path) in check_environment._get_chromedriver_paths()