import threading import time from unittest.mock import MagicMock, patch import typer import watchdog.events from rendercv.cli.render_command import watcher from rendercv.cli.render_command.watcher import EventHandler class TestRunFunctionIfFilesChange: def test_runs_function_immediately_on_start(self, tmp_path): file_path = tmp_path / "test.yaml" file_path.touch() mock_function = MagicMock() mock_observer_class = MagicMock() # First join() raises KeyboardInterrupt; second join() (after stop) succeeds: mock_observer_class.return_value.join.side_effect = [KeyboardInterrupt, None] with patch.object(watcher.watchdog.observers, "Observer", mock_observer_class): watcher.run_function_if_files_change([file_path], mock_function) mock_function.assert_called_once() def test_reruns_function_when_file_is_modified(self, tmp_path): watched_file = tmp_path / "test.yaml" watched_file.write_text("initial", encoding="utf-8") call_count = 0 def tracked_function(): nonlocal call_count call_count += 1 watcher_thread = threading.Thread( target=watcher.run_function_if_files_change, args=([watched_file], tracked_function), daemon=True, ) watcher_thread.start() time.sleep(0.2) initial_count = call_count watched_file.write_text("first edit", encoding="utf-8") time.sleep(0.2) assert call_count > initial_count def test_reruns_function_when_secondary_file_is_modified(self, tmp_path): main_file = tmp_path / "cv.yaml" main_file.write_text("main content", encoding="utf-8") design_file = tmp_path / "design.yaml" design_file.write_text("initial design", encoding="utf-8") call_count = 0 def tracked_function(): nonlocal call_count call_count += 1 watcher_thread = threading.Thread( target=watcher.run_function_if_files_change, args=([main_file, design_file], tracked_function), daemon=True, ) watcher_thread.start() time.sleep(0.2) count_before_edit = call_count # Edit the design file (not the main file) design_file.write_text("updated design", encoding="utf-8") time.sleep(0.2) assert call_count > count_before_edit def test_continues_watching_after_typer_exit(self, tmp_path): watched_file = tmp_path / "test.yaml" watched_file.write_text("initial", encoding="utf-8") call_count = 0 should_raise = False def tracked_function(): nonlocal call_count call_count += 1 if should_raise: raise typer.Exit(code=1) watcher_thread = threading.Thread( target=watcher.run_function_if_files_change, args=([watched_file], tracked_function), daemon=True, ) watcher_thread.start() time.sleep(0.2) should_raise = True count_before_exit = call_count watched_file.write_text("edit that raises exit", encoding="utf-8") time.sleep(0.2) assert call_count > count_before_exit should_raise = False count_after_exit = call_count watched_file.write_text("edit after exit", encoding="utf-8") time.sleep(0.2) assert call_count > count_after_exit class TestEventHandler: def test_ignores_events_for_unwatched_files(self): mock_fn = MagicMock() handler = EventHandler(mock_fn, watched_files={"/watched/file.yaml"}) event = watchdog.events.FileModifiedEvent("/other/file.yaml") handler.on_modified(event) mock_fn.assert_not_called() def test_calls_function_for_watched_file(self): mock_fn = MagicMock() handler = EventHandler(mock_fn, watched_files={"/watched/file.yaml"}) event = watchdog.events.FileModifiedEvent("/watched/file.yaml") handler.on_modified(event) mock_fn.assert_called_once() def test_suppresses_typer_exit(self): mock_fn = MagicMock(side_effect=typer.Exit(code=1)) handler = EventHandler(mock_fn, watched_files={"/watched/file.yaml"}) event = watchdog.events.FileModifiedEvent("/watched/file.yaml") handler.on_modified(event) mock_fn.assert_called_once()