from pathlib import Path from unittest.mock import MagicMock from watchfiles import Change from app.monitor import DirectoryChangeEvent, LocalDirectoryWatcher, Monitor class CallbackRecorder: """ 测试用目录监控回调记录器。 """ def __init__(self): """ 初始化事件记录列表。 """ self.events = [] def event_handler(self, event, text: str, event_path: str, file_size: int = None): """ 记录目录监控分发出来的事件。 :param event: 目录监控事件 :param text: 事件描述 :param event_path: 事件路径 :param file_size: 文件大小 """ self.events.append((event, text, event_path, file_size)) def test_handle_changes_dispatches_added_and_modified_files(tmp_path): """ 新增和修改文件应转换成目录监控整理回调。 """ added_file = tmp_path / "a_added.mkv" modified_file = tmp_path / "b_modified.mkv" skipped_dir = tmp_path / "c_dir" added_file.write_bytes(b"added") modified_file.write_bytes(b"modified") skipped_dir.mkdir() callback = CallbackRecorder() watcher = LocalDirectoryWatcher(tmp_path, callback=callback, force_polling=True) watcher._handle_changes({ (Change.added, added_file.as_posix()), (Change.modified, modified_file.as_posix()), (Change.deleted, added_file.as_posix()), (Change.added, skipped_dir.as_posix()), }) assert len(callback.events) == 2 assert ( Change.added, "新增", added_file.as_posix(), 5, ) == ( callback.events[0][0].change_type, callback.events[0][1], callback.events[0][2], callback.events[0][3], ) assert ( Change.modified, "修改", modified_file.as_posix(), 8, ) == ( callback.events[1][0].change_type, callback.events[1][1], callback.events[1][2], callback.events[1][3], ) def test_handle_changes_skips_missing_paths(tmp_path): """ 事件到达时已经消失的路径不应触发整理。 """ missing_file = tmp_path / "missing.mkv" callback = CallbackRecorder() watcher = LocalDirectoryWatcher(tmp_path, callback=callback, force_polling=True) watcher._handle_changes({(Change.added, missing_file.as_posix())}) assert callback.events == [] def test_event_handler_routes_file_events_to_transfer_handler(): """ 文件事件应继续按 local 存储交给整理流程。 """ monitor = object.__new__(Monitor) monitor.all_exts = [".mkv"] handle_file = MagicMock() setattr(monitor, "_Monitor__handle_file", handle_file) event_path = Path("/downloads/movie.mkv") event = DirectoryChangeEvent( change_type=Change.added, src_path=event_path.as_posix(), is_directory=False ) monitor.event_handler( event=event, text="新增", event_path=event_path.as_posix(), file_size=1024 ) handle_file.assert_called_once_with( storage="local", event_path=event_path, file_size=1024 ) def test_event_handler_ignores_directory_events(): """ 目录事件不应进入文件整理流程。 """ monitor = object.__new__(Monitor) monitor.all_exts = [".mkv"] handle_file = MagicMock() setattr(monitor, "_Monitor__handle_file", handle_file) event_path = Path("/downloads/folder") event = DirectoryChangeEvent( change_type=Change.added, src_path=event_path.as_posix(), is_directory=True ) monitor.event_handler( event=event, text="新增", event_path=event_path.as_posix() ) handle_file.assert_not_called() def test_event_handler_ignores_download_temp_files(): """ 下载器临时文件不应进入整理流程。 """ monitor = object.__new__(Monitor) monitor.all_exts = [".mkv"] handle_file = MagicMock() setattr(monitor, "_Monitor__handle_file", handle_file) event_path = Path("/downloads/movie.mkv.!qB") event = DirectoryChangeEvent( change_type=Change.modified, src_path=event_path.as_posix(), is_directory=False ) monitor.event_handler( event=event, text="修改", event_path=event_path.as_posix(), file_size=1024 ) handle_file.assert_not_called() def test_event_handler_ignores_non_transferable_files(): """ 非可整理后缀文件不应进入整理流程。 """ monitor = object.__new__(Monitor) monitor.all_exts = [".mkv"] handle_file = MagicMock() setattr(monitor, "_Monitor__handle_file", handle_file) event_path = Path("/downloads/movie.nfo") event = DirectoryChangeEvent( change_type=Change.added, src_path=event_path.as_posix(), is_directory=False ) monitor.event_handler( event=event, text="新增", event_path=event_path.as_posix(), file_size=1024 ) handle_file.assert_not_called() def test_handle_file_skips_transfer_when_history_exists(monkeypatch): """ 已有整理记录的源文件不应再次进入整理链。 """ monitor = object.__new__(Monitor) monitor.all_exts = [".mkv"] monitor._cache = {} event_path = Path("/downloads/movie.mkv") lookups = [] class FakeTransferHistoryOper: """ 测试用整理历史查询。 """ def get_by_src(self, src: str, storage: str = None): """ 记录查询参数并返回已有记录。 """ lookups.append((src, storage)) return object() transfer_chain = MagicMock() logger_info = MagicMock() logger_debug = MagicMock() monkeypatch.setattr("app.monitor.TransferHistoryOper", FakeTransferHistoryOper) monkeypatch.setattr("app.monitor.TransferChain", transfer_chain) monkeypatch.setattr("app.monitor.logger.info", logger_info) monkeypatch.setattr("app.monitor.logger.debug", logger_debug) handled = monitor._Monitor__handle_file( storage="local", event_path=event_path, file_size=1024, ) assert not handled assert lookups == [(event_path.as_posix(), "local")] transfer_chain.assert_not_called() logger_info.assert_not_called() logger_debug.assert_not_called() def test_handle_file_invokes_transfer_when_history_missing(monkeypatch): """ 没有整理记录的源文件应继续进入整理链。 """ monitor = object.__new__(Monitor) monitor.all_exts = [".mkv"] monitor._cache = {} event_path = Path("/downloads/movie.mkv") class FakeTransferHistoryOper: """ 测试用空整理历史查询。 """ def get_by_src(self, src: str, storage: str = None): """ 返回空整理记录。 """ return None transfer_chain_instance = MagicMock() transfer_chain = MagicMock(return_value=transfer_chain_instance) monkeypatch.setattr("app.monitor.TransferHistoryOper", FakeTransferHistoryOper) monkeypatch.setattr("app.monitor.TransferChain", transfer_chain) handled = monitor._Monitor__handle_file( storage="local", event_path=event_path, file_size=1024, ) assert handled transfer_chain_instance.do_transfer.assert_called_once() fileitem = transfer_chain_instance.do_transfer.call_args.kwargs["fileitem"] assert fileitem.storage == "local" assert fileitem.path == event_path.as_posix() assert fileitem.size == 1024