tests/test_platform.py
Rama master · réplica de lectura del repositorio original.
import os
import tempfile
import threading
import time
import unittest
from dataclasses import replace
from pathlib import Path
from unittest.mock import patch
from peach import settings_file
from peach.media import FilesystemBackend, MediaOffline, MediaUnavailable
from peach.platform import (
MOUNTS_ENV,
UNMAPPED_ROOT,
is_unmapped,
is_windows_path,
location_mounts,
location_roots,
resolve_location,
resolve_root,
root_online,
root_status,
system_volume,
translate_ledger_path,
translate_roots,
within_root,
)
from peach.repository import MediaAsset
POSIX_ONLY = unittest.skipIf(os.name == "nt", "路径翻译只在非 Windows 上发生")
#: 测试显式声明数据库路径前缀,不依赖本机 config.toml。
LOCATIONS = {"local": (r"R:\media",), "115": ("B:/",), "pikpak": ("A:/",)}
def _roots(table):
"""测试可传单个字符串;设置层使用与数据库路径前缀按序对应的元组。"""
return {key: ((value,) if value else ()) if isinstance(value, str) else tuple(value)
for key, value in table.items()}
def with_media(mounts, locations=None):
"""把设置层的 `[media.locations]` / `[media.mounts]` 钉死成给定值。"""
config = settings_file.load_config(environ={}, strict=False)
fixed = replace(
config, mounts=_roots(mounts),
locations=_roots(LOCATIONS if locations is None else locations),
)
return patch.object(settings_file, "active", lambda: fixed)
class LocationResolutionTests(unittest.TestCase):
"""按数据库路径前缀判定来源,两个平台的行为一致。"""
def test_windows_path_shape_is_recognised(self):
self.assertTrue(is_windows_path(r"R:\Media\one.mp4"))
self.assertTrue(is_windows_path("B:/one.mp4"))
self.assertFalse(is_windows_path("/Volumes/Media/media/one.mp4"))
self.assertFalse(is_windows_path("relative/one.mp4"))
def test_path_under_a_declared_root_resolves_to_that_location(self):
with with_media({}):
self.assertEqual(
resolve_location(r"R:\media\创作者\one.mp4"),
("local", ("创作者", "one.mp4")),
)
self.assertEqual(resolve_location("B:/x/one.mp4"), ("115", ("x", "one.mp4")))
def test_declared_root_matching_ignores_case(self):
"""数据库中 `R:\\Media` 与配置中的 `R:\\media` 对应同一目录,匹配时忽略大小写。"""
with with_media({}):
self.assertEqual(resolve_location(r"R:\MEDIA\one.mp4")[0], "local")
def test_the_longest_declared_root_wins(self):
with with_media({}, {"whole": "R:/", "local": r"R:\media"}):
self.assertEqual(resolve_location(r"R:\media\a.mp4"), ("local", ("a.mp4",)))
self.assertEqual(
resolve_location(r"R:\Resources\a.mp4"), ("whole", ("Resources", "a.mp4")))
def test_path_outside_every_declared_root_has_no_location(self):
with with_media({}):
self.assertEqual(resolve_location(r"Z:\secret\one.mp4"), (None, ()))
# 同一盘符下仍按目录区分:`R:\Resources` 不在 `R:\media` 中。
self.assertIsNone(resolve_location(r"R:\Resources\Intake\one.mp4")[0])
def test_declared_roots_are_read_from_the_settings_layer(self):
with with_media({}):
self.assertEqual(location_roots().get("local"), (r"R:\media",))
self.assertNotIn("online", location_roots())
def test_a_source_with_two_roots_resolves_each_to_its_own_index(self):
roots = {"local": (r"R:\media", r"S:\more"), "115": ("B:/",)}
self.assertEqual(resolve_root(r"R:\media\a.mp4", roots), ("local", 0, ("a.mp4",)))
self.assertEqual(resolve_root(r"S:\more\x\b.mp4", roots), ("local", 1, ("x", "b.mp4")))
self.assertEqual(resolve_root("B:/c.mp4", roots), ("115", 0, ("c.mp4",)))
self.assertEqual(resolve_root(r"Z:\nope", roots), (None, -1, ()))
@POSIX_ONLY
def test_each_root_translates_to_the_mount_at_the_same_position(self):
with with_media({"local": ["/Volumes/A", "/Volumes/B"]},
{"local": [r"R:\media", r"R:\media2"]}):
self.assertEqual(translate_ledger_path(r"R:\media\one.mp4"), Path("/Volumes/A/one.mp4"))
self.assertEqual(translate_ledger_path(r"R:\media2\two.mp4"), Path("/Volumes/B/two.mp4"))
# 部分前缀没有对应的本机目录时按无法访问处理,不转换到其他目录。
with with_media({"local": ["/Volumes/A"]}, {"local": [r"R:\media", r"R:\media2"]}):
self.assertTrue(is_unmapped(translate_ledger_path(r"R:\media2\two.mp4")))
class MountSourceTests(unittest.TestCase):
def test_mixed_mount_mapping_keeps_each_roots_position(self):
declared = {'local': ('R:\\one', 'S:\\two')}
with tempfile.TemporaryDirectory() as temporary:
mounted = Path(temporary).resolve()
with with_media({'local': ('', str(mounted))}, declared), patch.dict(os.environ, {MOUNTS_ENV: ''}):
self.assertEqual(location_mounts()['local'][1], mounted)
first = translate_ledger_path('R:\\one\\a.mp4')
if os.name == 'nt':
self.assertEqual(first, Path('R:\\one\\a.mp4'))
else:
self.assertTrue(is_unmapped(first))
self.assertEqual(translate_ledger_path('S:\\two\\b.mp4'), mounted / 'b.mp4')
"""挂载表按来源 ID 取自设置文件;`PEACH_MEDIA_MOUNTS` 仍然压过它。"""
def test_empty_source_uses_the_settings_parser_shape(self):
with with_media({"local": "/mnt/res", "115": "", "pikpak": ""}):
with patch.dict("os.environ", {MOUNTS_ENV: ""}):
self.assertEqual(settings_file.active().mounts["115"], ())
self.assertEqual(location_mounts(), {"local": (Path("/mnt/res"),)})
def test_settings_file_supplies_the_base_mapping(self):
with with_media({"local": "/mnt/res"}):
with patch.dict("os.environ", {MOUNTS_ENV: ""}):
self.assertEqual(location_mounts(), {"local": (Path("/mnt/res"),)})
def test_environment_still_wins_over_the_settings_file(self):
with with_media({"local": "/mnt/res", "115": "/mnt/115"}):
with patch.dict("os.environ", {MOUNTS_ENV: "local=/mnt/other"}):
self.assertEqual(
location_mounts(),
{"local": (Path("/mnt/other"),), "115": (Path("/mnt/115"),)})
def test_a_fresh_machine_has_no_mounts_at_all(self):
"""内建默认为空:没挂的来源按脱盘处理,绝不猜一个本机路径。"""
with with_media({}):
with patch.dict("os.environ", {MOUNTS_ENV: ""}):
self.assertEqual(location_mounts(), {})
class TranslationTests(unittest.TestCase):
@POSIX_ONLY
def test_declared_root_is_rebased_onto_the_local_mount(self):
with with_media({"local": "/mnt/res/media", "115": "/mnt/115"}):
with patch.dict("os.environ", {MOUNTS_ENV: ""}):
self.assertEqual(
translate_ledger_path(r"R:\media\创作者\one.mp4"),
Path("/mnt/res/media/创作者/one.mp4"),
)
self.assertEqual(
translate_ledger_path("B:/x/one.mp4"), Path("/mnt/115/x/one.mp4"))
@POSIX_ONLY
def test_unmapped_source_never_becomes_a_relative_path(self):
"""没有本机挂载目录的来源转换到 UNMAPPED_ROOT,避免读取当前目录下的同名文件。"""
with with_media({"local": "/mnt/res/media"}):
with patch.dict("os.environ", {MOUNTS_ENV: ""}):
path = translate_ledger_path(r"Z:\secret\one.mp4")
self.assertTrue(path.is_absolute())
self.assertTrue(is_unmapped(path))
self.assertTrue(str(path).startswith(str(UNMAPPED_ROOT)))
@POSIX_ONLY
def test_declared_but_unmounted_source_is_unmapped_too(self):
"""已配置路径前缀但没有本机挂载目录时,仍标为无法访问,不返回相对路径。"""
with with_media({}):
with patch.dict("os.environ", {MOUNTS_ENV: ""}):
self.assertTrue(is_unmapped(translate_ledger_path(r"R:\media\one.mp4")))
@POSIX_ONLY
def test_unmapped_roots_are_dropped_from_authorisation(self):
with with_media({"local": "/mnt/res/media", "pikpak": "", "115": ""}):
with patch.dict("os.environ", {MOUNTS_ENV: ""}):
roots = translate_roots((r"R:\media", "B:/", "A:/"))
self.assertEqual(roots, (Path("/mnt/res/media"),))
def test_posix_path_passes_through_untouched(self):
self.assertEqual(translate_ledger_path("/tmp/one.mp4"), Path("/tmp/one.mp4"))
def test_system_volume_is_the_disk_gate_target(self):
volume = system_volume()
self.assertTrue(volume.is_absolute())
self.assertTrue(volume.is_dir())
self.assertEqual(volume, Path(volume.anchor))
class SystemVolumeTests(unittest.TestCase):
@unittest.skipUnless(os.name == 'nt', 'Windows 系统目录 API')
def test_system_directory_can_be_on_another_drive(self):
import ctypes
def windows_directory(buffer, size):
buffer.value = 'Z:\\Windows'
return len(buffer.value)
with patch.object(ctypes.windll.kernel32, 'GetWindowsDirectoryW', side_effect=windows_directory):
self.assertEqual(system_volume(), Path('Z:/'))
@unittest.skipUnless(os.name == 'nt', 'Windows 系统目录 API')
def test_missing_system_directory_is_reported(self):
import ctypes
with patch.object(ctypes.windll.kernel32, 'GetWindowsDirectoryW', return_value=0):
with self.assertRaises(OSError):
system_volume()
class RootBoundaryTests(unittest.TestCase):
def test_within_root_accepts_exact_and_nested(self):
root = Path("/mnt/res/media")
self.assertTrue(within_root(root, root))
self.assertTrue(within_root(root / "a" / "b.mp4", root))
def test_within_root_rejects_sibling_and_shorter_paths(self):
root = Path("/mnt/res/media")
self.assertFalse(within_root(Path("/mnt/res/other/b.mp4"), root))
self.assertFalse(within_root(Path("/mnt"), root))
def test_within_root_survives_case_only_differences_on_insensitive_mounts(self):
"""账本写 `R:\\Media`,授权根是 `.../media`;大小写不敏感的挂载必须仍然放行。"""
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve() / "media"
root.mkdir()
shouting = Path(tmp).resolve() / "MEDIA" / "one.mp4"
insensitive = (Path(tmp).resolve() / "MEDIA").is_dir()
self.assertEqual(within_root(shouting, root), insensitive)
@unittest.skipUnless(os.name == "nt", "盘符只在 Windows 上有意义")
def test_windows_roots_answer_from_the_path_text_without_touching_the_drives(self):
"""网盘盘符每次 stat 都是一趟网络往返:大小写不同和跨盘符都不该去问盘。"""
with patch.object(Path, "samefile", side_effect=AssertionError("不该 stat 盘符")):
self.assertTrue(within_root(Path("B:/Media/a.mp4"), Path("b:/media")))
self.assertFalse(within_root(Path("A:/media/a.mp4"), Path("B:/")))
self.assertFalse(within_root(Path("B:/other/a.mp4"), Path("C:/media")))
def test_root_online_needs_a_readable_mount_not_just_a_directory(self):
with tempfile.TemporaryDirectory() as tmp:
self.assertTrue(root_online(Path(tmp)))
self.assertFalse(root_online(Path(tmp)))
self.assertFalse(root_online(UNMAPPED_ROOT))
class OfflineModeTests(unittest.TestCase):
"""脱盘是来源级判定:一个来源不在,不影响其他来源上的资产。"""
def _backend(self, tmp: Path) -> FilesystemBackend:
return FilesystemBackend([tmp], tmp / "snapshots")
@POSIX_ONLY
def test_asset_on_an_unmapped_source_reports_offline(self):
with tempfile.TemporaryDirectory() as tmp:
backend = self._backend(Path(tmp).resolve())
with with_media({"local": "/mnt/res/media"}):
with self.assertRaises(MediaOffline) as caught:
backend.file_for(MediaAsset(1, r"Z:\video\one.mp4", None))
self.assertEqual(caught.exception.source, "Z:")
self.assertEqual(caught.exception.asset_id, 1)
@POSIX_ONLY
def test_asset_on_a_detached_but_mapped_source_reports_offline(self):
with tempfile.TemporaryDirectory() as tmp:
detached = Path(tmp).resolve() / "gone"
backend = FilesystemBackend([detached], Path(tmp).resolve() / "snapshots")
with with_media({"local": str(detached)}):
with self.assertRaises(MediaOffline):
backend.file_for(MediaAsset(2, r"R:\media\video\one.mp4", None))
@POSIX_ONLY
def test_missing_file_on_a_live_source_is_not_offline(self):
"""单个文件丢失和整个来源不在必须分开报,否则前端会误显示脱盘模式。"""
with tempfile.TemporaryDirectory() as tmp:
live = Path(tmp).resolve()
backend = FilesystemBackend([live], live / "snapshots")
with with_media({"local": str(live)}):
with self.assertRaises(MediaUnavailable) as caught:
backend.file_for(MediaAsset(3, r"R:\media\nope.mp4", None))
self.assertNotIsInstance(caught.exception, MediaOffline)
def test_source_status_reports_each_root_separately(self):
with tempfile.TemporaryDirectory() as tmp:
live = Path(tmp).resolve()
dead = live / "gone"
status = FilesystemBackend([live, dead], live / "snapshots").source_status()
self.assertEqual([row["online"] for row in status], [True, False])
class MountReachabilityTests(unittest.TestCase):
def test_an_offline_root_does_not_disable_other_roots_of_the_same_source(self):
from peach.mount_reachability import MountReachability, MountRoot
monitor = MountReachability([
MountRoot('local', '本地磁盘', 'D:/JAV', self.root, 'root-one', 'library-one'),
MountRoot('local', '本地磁盘', 'E:/JAV', None, 'root-two', 'library-one'),
], probe=lambda _path: 'ok', retry_delay=0)
self.addCleanup(monitor.stop)
monitor._states[self.root].state = 'ok'
row = monitor.sources()[0]
self.assertTrue(row['online'])
self.assertEqual([(root['root_id'], root['online']) for root in row['roots']],
[('root-one', True), ('root-two', False)])
self.assertEqual(monitor.summary()['state'], 'degraded')
def setUp(self):
from peach.mount_reachability import MountReachability, MountRoot
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name).resolve()
self.now = 0.0
self.calls = []
self.answer = "ok"
def probe(path):
self.calls.append(path)
return self.answer
self.monitor = MountReachability([MountRoot("local", "本地磁盘", "R:/media", self.root)],
probe=probe, clock=lambda: self.now, retry_delay=0)
self.addCleanup(self.monitor.stop)
def wait_state(self, expected):
deadline = time.monotonic() + 2
while time.monotonic() < deadline:
self.monitor.tick()
if self.monitor.summary()["sources"][0]["state"] == expected:
return
time.sleep(0.005)
self.fail(f"没有取得状态 {expected}")
def test_directory_probe_distinguishes_empty_missing_file_and_permission(self):
self.assertEqual(root_status(self.root), "ok")
self.assertEqual(root_status(self.root / "missing"), "missing")
file = self.root / "file"
file.touch()
self.assertEqual(root_status(file), "not_directory")
for error, expected in ((PermissionError(), "permission_denied"), (OSError(), "unavailable")):
with patch("peach.platform.os.scandir", side_effect=error):
self.assertEqual(root_status(self.root), expected)
def test_disconnected_network_mount_is_unreachable_not_missing_or_denied(self):
# Windows 把网络路径断开(53/67)映射成 FileNotFoundError,设备未就绪(21)映射成 PermissionError。
for error_type, winerror in ((FileNotFoundError, 53), (FileNotFoundError, 67), (PermissionError, 21)):
error = error_type(2, "network")
error.winerror = winerror
with self.subTest(winerror=winerror), patch("peach.platform.os.scandir", side_effect=error):
self.assertEqual(root_status(self.root), "unavailable")
def test_failure_while_reading_the_first_entry_is_not_online(self):
class Entries:
def __enter__(self): return self
def __exit__(self, *args): pass
def __iter__(self): return self
def __next__(self): raise PermissionError
with patch("peach.platform.os.scandir", return_value=Entries()):
self.assertEqual(root_status(self.root), "permission_denied")
self.assertFalse(root_online(self.root))
def test_healthy_and_failed_probes_use_different_intervals_and_recover(self):
self.wait_state("ok")
self.assertEqual(len(self.calls), 1)
self.now = 599
self.monitor.tick()
self.assertEqual(len(self.calls), 1)
self.answer = "permission_denied"
self.now = 600
self.wait_state("permission_denied")
self.assertEqual(len(self.calls), 3)
self.assertEqual(self.monitor.summary()["warnings"], ["本地磁盘:没有权限读取"])
self.now = 659
self.monitor.tick()
self.assertEqual(len(self.calls), 3)
self.answer = "ok"
self.now = 660
self.wait_state("ok")
self.assertEqual(len(self.calls), 4)
self.assertEqual(self.monitor.summary()["warnings"], [])
def test_a_successful_retry_does_not_report_a_failure(self):
answers = iter(["missing", "ok"])
self.monitor.probe = lambda path: next(answers)
self.wait_state("ok")
self.assertEqual(self.monitor.summary()["warnings"], [])
def test_first_probe_does_not_depend_on_the_clock_origin(self):
self.now = -10000
self.wait_state("ok")
self.assertEqual(len(self.calls), 1)
def test_pending_probe_is_unknown_and_timeout_keeps_one_worker(self):
entered, release = threading.Event(), threading.Event()
self.addCleanup(release.set)
def blocked(path):
self.calls.append(path)
entered.set()
release.wait(2)
return "ok"
self.monitor.probe = blocked
self.monitor.tick()
self.assertTrue(entered.wait(1))
self.assertIsNone(self.monitor.sources()[0]["online"])
self.now = 6
self.monitor.tick()
self.assertEqual(self.monitor.summary()["sources"][0]["state"], "timeout")
for self.now in (60, 600, 6000):
self.monitor.tick()
self.assertEqual(len(self.calls), 1)
self.assertEqual(self.monitor.summary()["warnings"], [])
release.set()
self.wait_state("ok")
def test_slow_roots_have_a_global_limit_and_stop_does_not_wait_for_them(self):
from peach.mount_reachability import MountReachability, MountRoot, MAX_PENDING
release = threading.Event()
self.addCleanup(release.set)
def blocked(path):
self.calls.append(path)
release.wait(2)
return "ok"
self.monitor = MountReachability([
MountRoot("local", "本地磁盘", str(at), self.root / str(at)) for at in range(12)
], probe=blocked)
self.addCleanup(self.monitor.stop)
self.monitor.tick()
self.monitor.tick()
deadline = time.monotonic() + 1
while len(self.calls) < MAX_PENDING and time.monotonic() < deadline:
time.sleep(0.005)
self.assertEqual(len(self.calls), MAX_PENDING)
self.monitor.stop()
self.assertFalse(release.is_set())
self.monitor.tick()
self.assertEqual(len(self.calls), MAX_PENDING)
def test_summary_hides_paths_and_all_roots_contribute_to_source_health(self):
from peach.mount_reachability import MountReachability, MountRoot
self.monitor = MountReachability([
MountRoot("local", "本地磁盘", "R:/private", self.root),
MountRoot("local", "本地磁盘", "S:/private", None),
])
self.assertFalse(self.monitor.sources()[0]["online"])
row = self.monitor.summary()["sources"][0]
self.assertEqual(row["state"], "unmapped")
self.assertNotIn("roots", row)
self.assertNotIn("declared", row)
self.assertNotIn("resolved", row)
self.assertEqual(row["message"], "本地磁盘:未配置挂载点")
if __name__ == "__main__":
unittest.main()