diff --git a/.gitignore b/.gitignore index dd56363..abfd439 100644 --- a/.gitignore +++ b/.gitignore @@ -162,6 +162,7 @@ chromedriver.log config.json credentials.json *.cookie +*.pkl *.session # Temporary files @@ -177,4 +178,4 @@ credentials.json # Lock files (except poetry.lock) *.lock -!poetry.lock \ No newline at end of file +!poetry.lock diff --git a/damai/check_environment.py b/damai/check_environment.py index 7cb3b56..23005af 100755 --- a/damai/check_environment.py +++ b/damai/check_environment.py @@ -7,6 +7,7 @@ import os import re +import shutil import subprocess import sys @@ -23,16 +24,147 @@ def _run_command_get_version(command): result = subprocess.run( command, capture_output=True, + errors="replace", text=True, timeout=5 ) if result.returncode == 0: - return result.stdout.strip() + return (result.stdout or result.stderr).strip() except Exception: pass 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(): """检查 Python 版本""" print("Python 版本检查...") @@ -73,23 +205,13 @@ def check_dependencies(): def check_chrome(): """检查 Chrome 浏览器""" print("Chrome 浏览器检查...") - chrome_paths = [ - "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", - "/usr/bin/google-chrome", - "/usr/bin/chromium-browser", - ] - - 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 + chrome_path, version_str, chrome_version = _find_chrome() + if chrome_path: + print(f" ✓ Chrome 浏览器: {version_str}") + print(f" ✓ 主版本号: {chrome_version}") + print(f" ✓ 路径: {chrome_path}") + print() + return True print(" ✗ 未找到 Chrome 浏览器") print(" 请安装 Chrome: https://www.google.com/chrome/") @@ -100,13 +222,7 @@ def check_chrome(): def check_chromedriver(): """检查 ChromeDriver""" print("ChromeDriver 检查...") - chromedriver_paths = [ - "/opt/homebrew/bin/chromedriver", - "/usr/local/bin/chromedriver", - "/opt/homebrew/Caskroom/chromedriver", - ] - - for driver_path in chromedriver_paths: + for driver_path in _get_chromedriver_paths(): if os.path.exists(driver_path) or os.path.islink(driver_path): version_str = _run_command_get_version([driver_path, "--version"]) if version_str: @@ -121,7 +237,8 @@ def check_chromedriver(): print(" ⚠ 未找到 ChromeDriver") print(" 安装方法:") print(" - macOS: brew install --cask chromedriver") - print(" - 或使用脚本自动安装: chromedriver-autoinstaller") + print(" - Windows/macOS/Linux: 使用脚本自动安装 chromedriver-autoinstaller") + print(" - 或手动下载: https://googlechromelabs.github.io/chrome-for-testing/") print() return False @@ -130,19 +247,15 @@ def check_version_match(): """检查 Chrome 和 ChromeDriver 版本是否匹配""" print("版本匹配检查...") - chrome_version = None - 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) + _, _, chrome_version = _find_chrome() driver_version = None - driver_path = "/opt/homebrew/bin/chromedriver" - if os.path.exists(driver_path) or os.path.islink(driver_path): - version_str = _run_command_get_version([driver_path, "--version"]) - if version_str: - driver_version = _get_version_from_output(version_str) + for driver_path in _get_chromedriver_paths(): + if os.path.exists(driver_path) or os.path.islink(driver_path): + version_str = _run_command_get_version([driver_path, "--version"]) + if version_str: + driver_version = _get_version_from_output(version_str) + break if not chrome_version or not driver_version: print(" ⚠ 无法获取版本信息") @@ -171,45 +284,30 @@ def check_version_match(): def get_chromedriver_path(): """ - 获取 ChromeDriver 路径,如果不存在则自动安装 - 供其他脚本导入使用 + 获取 ChromeDriver 路径,如果不存在则自动安装。 + 供其他脚本导入使用。 :return: ChromeDriver 可执行文件路径 """ - chrome_path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + _, _, chrome_version = _find_chrome() - if not os.path.exists(chrome_path): - raise RuntimeError("未找到 Chrome 浏览器,请先安装 Chrome") + if chrome_version: + 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 版本 - chrome_version_str = _run_command_get_version([chrome_path, "--version"]) - chrome_version = _get_version_from_output(chrome_version_str) if chrome_version_str else None + print(f" Chrome 版本: {chrome_version}") + else: + 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...") try: import chromedriver_autoinstaller chromedriver_path = chromedriver_autoinstaller.install() - # 验证安装的版本 result = _run_command_get_version([chromedriver_path, "--version"]) if not result: raise RuntimeError("ChromeDriver 无法执行") diff --git a/tests/conftest.py b/tests/conftest.py index e0984c3..141250a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -64,9 +64,7 @@ def mock_appium_driver(): mock_driver.swipe = Mock() mock_driver.quit = Mock() - # Mock the Remote class - with patch.object(mock_driver, "__class__.__name__", "Remote"): - yield mock_driver + yield mock_driver @pytest.fixture @@ -153,4 +151,4 @@ def pytest_collection_modifyitems(config, items): if "unit" in str(item.fspath): item.add_marker(pytest.mark.unit) elif "integration" in str(item.fspath): - item.add_marker(pytest.mark.integration) \ No newline at end of file + item.add_marker(pytest.mark.integration) diff --git a/tests/unit/test_check_environment_windows.py b/tests/unit/test_check_environment_windows.py new file mode 100644 index 0000000..5f30d77 --- /dev/null +++ b/tests/unit/test_check_environment_windows.py @@ -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()