"""Tests specifically for the synchronization functions in the CLI module.""" import json import logging import socket import tempfile import time from unittest.mock import MagicMock, patch import pytest from jimaku_dl.cli import run_background_sync, sync_subtitles_thread class TestSyncSubtitlesThread: """Test the sync_subtitles_thread function.""" def test_successful_sync_and_socket_communication(self): """Test the full sync process with successful socket communication.""" # Mock subprocess to simulate successful ffsubsync run mock_subprocess = MagicMock() mock_subprocess.return_value.returncode = 0 mock_subprocess.return_value.stderr = "" # Mock socket functions mock_socket = MagicMock() mock_socket.recv.side_effect = [ # Response for track-list query json.dumps( { "data": [ {"type": "video", "id": 1}, {"type": "audio", "id": 1}, {"type": "sub", "id": 1}, ] } ).encode("utf-8"), # Additional responses for subsequent commands b"{}", b"{}", b"{}", b"{}", b"{}", b"{}", ] # Create a temp file path for socket with tempfile.NamedTemporaryFile() as temp: socket_path = temp.name with patch("jimaku_dl.cli.subprocess_run", mock_subprocess), patch( "jimaku_dl.cli.path.exists", return_value=True ), patch("socket.socket", return_value=mock_socket), patch( "builtins.print" ) as mock_print, patch( "jimaku_dl.cli.time.sleep" ), patch( "logging.FileHandler", MagicMock() ), patch( "logging.getLogger", MagicMock() ): # Run the function sync_subtitles_thread( "/path/to/video.mkv", "/path/to/subtitle.srt", "/path/to/output.srt", socket_path, ) # Check subprocess call mock_subprocess.assert_called_once() assert mock_subprocess.call_args[0][0][0] == "ffsubsync" # Check socket connectivity mock_socket.connect.assert_called_once_with(socket_path) # Verify socket commands were sent assert mock_socket.send.call_count >= 3 # Verify success message mock_print.assert_any_call("Synchronization successful!") mock_print.assert_any_call("Updated MPV with synchronized subtitle") def test_ffsubsync_failure(self): """Test handling of ffsubsync failure.""" # Mock subprocess to simulate failed ffsubsync run mock_subprocess = MagicMock() mock_subprocess.return_value.returncode = 1 mock_subprocess.return_value.stderr = "Error: Failed to sync" with patch("jimaku_dl.cli.subprocess_run", mock_subprocess), patch( "builtins.print" ) as mock_print, patch("logging.FileHandler", MagicMock()), patch( "logging.getLogger", MagicMock() ): # Run the function sync_subtitles_thread( "/path/to/video.mkv", "/path/to/subtitle.srt", "/path/to/output.srt", "/tmp/mpv.sock", ) # Check error message mock_print.assert_any_call("Sync failed: Error: Failed to sync") # Verify we don't proceed to socket communication assert mock_subprocess.called assert mock_print.call_count == 1 def test_socket_not_found(self): """Test handling of socket not found.""" # Mock subprocess to simulate successful ffsubsync run mock_subprocess = MagicMock() mock_subprocess.return_value.returncode = 0 mock_subprocess.return_value.stderr = "" # Set up logger mock mock_logger_instance = MagicMock() mock_logger = MagicMock(return_value=mock_logger_instance) # This is the key fix - patch time.time() to break out of the wait loop # by simulating enough time has passed mock_time = MagicMock() mock_time.side_effect = [ 0, 100, ] # First call returns 0, second returns 100 (exceeding max_wait) # Also need to mock path.exists to control behavior for different paths: # - First call should return True for the output file # - Second call should return False for the socket path_exists_results = { "/path/to/output.srt": True, # Output file exists (to ensure the sync message is printed) "/tmp/mpv.sock": False, # Socket does NOT exist } def mock_path_exists(path): # Use the mock dictionary but default to True for any other paths return path_exists_results.get(path, True) with patch("jimaku_dl.cli.subprocess_run", mock_subprocess), patch( "jimaku_dl.cli.path.exists", side_effect=mock_path_exists ), patch("jimaku_dl.cli.time.sleep"), patch( "jimaku_dl.cli.time.time", mock_time ), patch( "builtins.print" ) as mock_print, patch( "logging.FileHandler", MagicMock() ), patch( "logging.getLogger", mock_logger ): # Run the function sync_subtitles_thread( "/path/to/video.mkv", "/path/to/subtitle.srt", "/path/to/output.srt", "/tmp/mpv.sock", ) # Now the test should pass because we're ensuring the output file exists mock_print.assert_any_call("Synchronization successful!") mock_logger_instance.error.assert_called_with( "Socket not found after waiting: /tmp/mpv.sock" ) def test_socket_connection_error(self): """Test handling of socket connection error.""" # Mock subprocess to simulate successful ffsubsync run mock_subprocess = MagicMock() mock_subprocess.return_value.returncode = 0 mock_subprocess.return_value.stderr = "" # Mock socket to raise connection error mock_socket = MagicMock() mock_socket.connect.side_effect = socket.error("Connection refused") with patch("jimaku_dl.cli.subprocess_run", mock_subprocess), patch( "jimaku_dl.cli.path.exists", return_value=True ), patch("socket.socket", return_value=mock_socket), patch( "builtins.print" ) as mock_print, patch( "logging.FileHandler", MagicMock() ), patch( "logging.getLogger" ) as mock_logger: # Setup mock logger mock_logger_instance = MagicMock() mock_logger.return_value = mock_logger_instance # Run the function sync_subtitles_thread( "/path/to/video.mkv", "/path/to/subtitle.srt", "/path/to/output.srt", "/tmp/mpv.sock", ) # Check success message but log socket error mock_print.assert_any_call("Synchronization successful!") mock_logger_instance.error.assert_called_with( "Socket connection error: Connection refused" ) def test_socket_send_error(self): """Test handling of socket send error.""" # Mock subprocess for successful ffsubsync run mock_subprocess = MagicMock() mock_subprocess.return_value.returncode = 0 mock_subprocess.return_value.stderr = "" # Create mock socket but make socket behavior more robust mock_socket = MagicMock() # Set up recv to handle multiple calls including empty response at shutdown recv_responses = [b""] * 10 # Multiple empty responses for the cleanup loop mock_socket.recv.side_effect = recv_responses # Make send raise an error on the first real command send_called = [False] def mock_send(data): if b"get_property" in data or b"sub-reload" in data: send_called[0] = True raise socket.error("Send failed") return None mock_socket.send.side_effect = mock_send # Set up all the patches needed with patch("jimaku_dl.cli.subprocess_run", mock_subprocess), patch( "jimaku_dl.cli.path.exists", return_value=True ), patch("socket.socket", return_value=mock_socket), patch( "builtins.print" ) as mock_print, patch( "jimaku_dl.cli.time.sleep" ), patch( "logging.FileHandler", MagicMock() ), patch( "logging.getLogger" ) as mock_logger: # Set up the logger mock mock_logger_instance = MagicMock() mock_logger.return_value = mock_logger_instance # Patch socket.shutdown to avoid another hang point with patch.object(mock_socket, "shutdown"): # Run the function under test sync_subtitles_thread( "/path/to/video.mkv", "/path/to/subtitle.srt", "/path/to/output.srt", "/tmp/mpv.sock", ) # Verify sync message printed but not MPV update message mock_print.assert_any_call("Synchronization successful!") # Check for debug message about socket error debug_calls = [ call[0][0] for call in mock_logger_instance.debug.call_args_list if call[0] and isinstance(call[0][0], str) ] socket_error_logged = any( "Socket send error: Send failed" in msg for msg in debug_calls ) assert socket_error_logged, "Socket error message not logged" # Verify "Updated MPV" message was not printed update_messages = [ call[0][0] for call in mock_print.call_args_list if call[0] and isinstance(call[0][0], str) and "Updated MPV" in call[0][0] ] assert not update_messages, "MPV update message should not be printed" def test_socket_recv_error(self): """Test handling of socket receive error.""" # Mock subprocess mock_subprocess = MagicMock() mock_subprocess.return_value.returncode = 0 mock_subprocess.return_value.stderr = "" # Mock socket with robust receive error behavior mock_socket = MagicMock() # Make recv raise timeout explicitly mock_socket.recv.side_effect = socket.timeout("Receive timeout") with patch("jimaku_dl.cli.subprocess_run", mock_subprocess), patch( "jimaku_dl.cli.path.exists", return_value=True ), patch("socket.socket", return_value=mock_socket), patch( "builtins.print" ) as mock_print, patch( "jimaku_dl.cli.time.sleep" ), patch( "logging.FileHandler", MagicMock() ), patch( "logging.getLogger" ) as mock_logger: # Setup mock logger mock_logger_instance = MagicMock() mock_logger.return_value = mock_logger_instance # Patch socket.shutdown to avoid another hang point with patch.object( mock_socket, "shutdown", side_effect=socket.error ), patch.object(mock_socket, "close"): # Run the function sync_subtitles_thread( "/path/to/video.mkv", "/path/to/subtitle.srt", "/path/to/output.srt", "/tmp/mpv.sock", ) # Check success message happened mock_print.assert_any_call("Synchronization successful!") # We need to check that the socket.timeout exception happened # This should create a debug message containing the word "timeout" # The best way to check this is to examine the mock_socket.recv calls mock_socket.recv.assert_called()