tests/test_folder_picker.py
Rama master · réplica de lectura del repositorio original.
"""`peach.folder_picker`:由运行 Peach 的这台电脑弹系统文件夹对话框,替浏览器拿到绝对路径。
对话框本身没法在测试里弹,这里钉住的是命令行的形状、取消与失败的区分,以及一次只开一个。
"""
from __future__ import annotations
import base64
import ntpath
import os
import subprocess
import unittest
from unittest import mock
from peach import folder_picker
class FakeRun:
def __init__(self, stdout: bytes = b"", returncode: int = 0, stderr: bytes = b""):
self.stdout, self.returncode, self.stderr = stdout, returncode, stderr
self.calls: list[tuple[list[str], dict]] = []
def __call__(self, argv, **kwargs):
self.calls.append((list(argv), kwargs))
return subprocess.CompletedProcess(argv, self.returncode, self.stdout, self.stderr)
class CommandTests(unittest.TestCase):
def test_windows_uses_the_modern_folder_picker_through_the_built_in_powershell(self):
"""`powershell.exe` 每台 Windows 都有,pwsh 7 是另装的;脚本以 -EncodedCommand 传,不碰引号转义。
对话框是 Shell 的 `IFileOpenDialog`(带地址栏),不是 Windows Forms 那块树形面板。
"""
argv, env = folder_picker.command(r"D:\Media", platform="win32")
self.assertEqual(ntpath.basename(argv[0]).casefold(), "powershell.exe")
self.assertIn("-STA", argv, "COM 对话框要求单线程套间")
self.assertEqual(argv[-2], "-EncodedCommand")
script = base64.b64decode(argv[-1]).decode("utf-16-le")
self.assertLess(script.index('SetThreadDpiAwarenessContext(new IntPtr(-4))'),
script.index('new FileOpenDialogRCW()'))
self.assertIn('SetThreadDpiAwarenessContext(previousDpi)', script)
self.assertIn('Guid("42f85136-db7e-439c-85f1-e4075d135fc8")', script, "IFileOpenDialog 的接口 ID")
self.assertIn("dialog.SetOptions(0x8 | 0x20 | 0x40)", script, "FOS_NOCHANGEDIR、FOS_PICKFOLDERS、FOS_FORCEFILESYSTEM")
self.assertNotIn("FolderBrowserDialog", script)
self.assertIn("$env:PEACH_PICK_INITIAL", script)
self.assertIn("dialog.Show(IntPtr.Zero)", script, "不挂所有者窗口:透明置顶的 Form 会把对话框一起藏起来")
self.assertIn("SetWindowPos(window, new IntPtr(-1)", script, "后台服务进程没有前台权,对话框要自己置顶")
self.assertNotIn("System.Windows.Forms", script)
self.assertIn(f"Pick('{folder_picker.PROMPT}', $env:PEACH_PICK_INITIAL)", script)
self.assertEqual(env, {"PEACH_PICK_INITIAL": r"D:\Media"})
def test_windows_powershell_is_found_under_the_system_root_not_through_path(self):
"""服务进程的 PATH 可能被安装器或用户改坏;`powershell.exe` 固定装在系统目录里。"""
with mock.patch.dict(os.environ, {"SystemRoot": r"C:\Windows"}):
argv, _ = folder_picker.command(None, platform="win32")
self.assertEqual(argv[0], r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe")
with mock.patch.dict(os.environ):
os.environ.pop("SystemRoot", None)
argv, _ = folder_picker.command(None, platform="win32")
self.assertEqual(argv[0], "powershell.exe", "取不到系统目录时退回按名字查找")
def test_macos_uses_applescript_choose_folder_and_escapes_the_initial_path(self):
with mock.patch("os.path.isdir", return_value=True):
argv, env = folder_picker.command('/Volumes/My "Disk"', platform="darwin")
self.assertEqual(argv[:2], ["osascript", "-e"])
self.assertTrue(argv[2].startswith("POSIX path of (choose folder with prompt"))
self.assertIn('default location POSIX file "/Volumes/My \\"Disk\\""', argv[2])
self.assertEqual(env, {})
def test_a_missing_initial_folder_is_not_passed_to_applescript(self):
"""`choose folder` 收到不存在的 default location 会直接报错,还不如不给。"""
with mock.patch("os.path.isdir", return_value=False):
argv, _ = folder_picker.command("/gone", platform="darwin")
self.assertNotIn("default location", argv[2])
def test_other_platforms_say_so(self):
with self.assertRaises(folder_picker.PickerUnavailable):
folder_picker.command(None, platform="linux")
class PickTests(unittest.TestCase):
def test_the_chosen_path_comes_back_stripped_and_the_initial_path_travels_by_environment(self):
run = FakeRun(b"E:\\Movies\r\n")
chosen = folder_picker.pick_folder(r"D:\Media", platform="win32", run=run)
self.assertEqual(chosen, r"E:\Movies")
(argv, kwargs), = run.calls
self.assertEqual(ntpath.basename(argv[0]).casefold(), "powershell.exe")
self.assertEqual(kwargs["env"]["PEACH_PICK_INITIAL"], r"D:\Media")
self.assertTrue(kwargs["capture_output"])
def test_cancelling_yields_none_on_both_platforms(self):
self.assertIsNone(folder_picker.pick_folder(platform="win32", run=FakeRun(b"")))
cancelled = FakeRun(b"", returncode=1, stderr=b"execution error: User canceled. (-128)\n")
self.assertIsNone(folder_picker.pick_folder(platform="darwin", run=cancelled))
def test_cancelling_on_a_localized_macos_is_still_a_cancel(self):
"""osascript 的消息跟随系统语言,中文系统写「用户已取消。」;错误码 -128 不变。"""
cancelled = FakeRun(b"", returncode=1,
stderr="0:120: execution error: 用户已取消。 (-128)\n".encode("utf-8"))
self.assertIsNone(folder_picker.pick_folder(platform="darwin", run=cancelled))
def test_macos_without_interaction_or_automation_permission_has_no_picker(self):
"""-1713 不允许用户交互、-1743 未获自动化授权:这台机器此刻不提供对话框,不是服务故障。"""
for code in (b"-1713", b"-1743"):
denied = FakeRun(b"", returncode=1, stderr=b"execution error: Not authorized. (" + code + b")\n")
with self.subTest(code=code), self.assertRaises(folder_picker.PickerUnsupported):
folder_picker.pick_folder(platform="darwin", run=denied)
def test_macos_paths_lose_the_trailing_slash(self):
self.assertEqual(folder_picker.pick_folder(platform="darwin", run=FakeRun(b"/Volumes/Media/\n")),
"/Volumes/Media")
self.assertEqual(folder_picker.pick_folder(platform="darwin", run=FakeRun(b"/\n")), "/")
def test_a_dialog_that_fails_to_open_is_reported_not_swallowed(self):
with self.assertRaises(folder_picker.PickerUnavailable):
folder_picker.pick_folder(platform="win32", run=FakeRun(b"", returncode=1, stderr=b"boom"))
def test_a_dialog_program_that_cannot_start_means_no_picker_here(self):
"""找不到或起不来对话框程序是这台机器不提供,不是服务故障;锁照样放开。"""
def missing(argv, **kwargs):
raise FileNotFoundError(2, "No such file", argv[0])
with self.assertRaises(folder_picker.PickerUnsupported):
folder_picker.pick_folder(platform="win32", run=missing)
self.assertFalse(folder_picker._LOCK.locked())
def test_a_forgotten_dialog_does_not_hold_the_worker_forever(self):
def hang(argv, **kwargs):
raise subprocess.TimeoutExpired(argv, kwargs["timeout"])
self.assertIsNone(folder_picker.pick_folder(platform="win32", run=hang))
def test_only_one_dialog_opens_at_a_time(self):
"""系统对话框是模态的,第二个只会藏在第一个后面。"""
with folder_picker._LOCK:
with self.assertRaises(folder_picker.PickerBusy):
folder_picker.pick_folder(platform="win32", run=FakeRun(b""))
if __name__ == "__main__":
unittest.main()