#!/usr/bin/env python3
"""Tests for scitex_writer._utils._watch."""
import subprocess
from unittest.mock import MagicMock, patch
import pytest
from scitex_writer._utils._watch import watch_manuscript
class TestWatchManuscriptCompileScript:
"""Tests for watch_manuscript compile script handling."""
def test_returns_early_when_compile_script_missing(self, tmp_path):
"""Verify returns immediately when compile script doesn't exist."""
with patch("subprocess.Popen") as mock_popen:
watch_manuscript(tmp_path)
mock_popen.assert_not_called()
def test_creates_correct_command(self, tmp_path):
"""Verify correct command is built for watch mode."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash\necho 'compiling'")
mock_process = MagicMock()
mock_process.stdout.readline.return_value = "" # Stop iteration immediately
with patch("subprocess.Popen", return_value=mock_process) as mock_popen:
watch_manuscript(tmp_path)
expected_cmd = [str(compile_script), "-m", "-w"]
mock_popen.assert_called_once()
actual_cmd = mock_popen.call_args[0][0]
assert actual_cmd == expected_cmd
class TestWatchManuscriptProcess:
"""Tests for watch_manuscript process handling."""
def test_runs_process_with_correct_settings(self, tmp_path):
"""Verify Popen is called with correct settings."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash\necho 'compiling'")
mock_process = MagicMock()
mock_process.stdout.readline.return_value = ""
with patch("subprocess.Popen", return_value=mock_process) as mock_popen:
watch_manuscript(tmp_path)
call_kwargs = mock_popen.call_args[1]
assert call_kwargs["cwd"] == tmp_path
assert call_kwargs["stdout"] == subprocess.PIPE
assert call_kwargs["stderr"] == subprocess.STDOUT
assert call_kwargs["text"] is True
assert call_kwargs["bufsize"] == 1
def test_waits_for_process_completion(self, tmp_path):
"""Verify process.wait is called with timeout."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash\necho 'compiling'")
mock_process = MagicMock()
mock_process.stdout.readline.return_value = ""
with patch("subprocess.Popen", return_value=mock_process):
watch_manuscript(tmp_path, timeout=30)
mock_process.wait.assert_called_once_with(timeout=30)
class TestWatchManuscriptCallback:
"""Tests for watch_manuscript callback handling."""
def test_calls_callback_on_compilation_event(self, tmp_path):
"""Verify callback is called when 'Compilation' appears in output."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash\necho 'compiling'")
mock_process = MagicMock()
mock_process.stdout.readline.side_effect = [
"Starting...\n",
"Compilation complete\n",
"", # End iteration
]
callback = MagicMock()
with patch("subprocess.Popen", return_value=mock_process):
watch_manuscript(tmp_path, on_compile=callback)
callback.assert_called_once()
def test_callback_not_called_without_compilation_keyword(self, tmp_path):
"""Verify callback is not called for non-compilation output."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash\necho 'compiling'")
mock_process = MagicMock()
mock_process.stdout.readline.side_effect = [
"Starting...\n",
"Processing files...\n",
"",
]
callback = MagicMock()
with patch("subprocess.Popen", return_value=mock_process):
watch_manuscript(tmp_path, on_compile=callback)
callback.assert_not_called()
def test_callback_error_does_not_stop_watch(self, tmp_path):
"""Verify callback errors are caught and logged."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash\necho 'compiling'")
mock_process = MagicMock()
mock_process.stdout.readline.side_effect = [
"Compilation complete\n",
"",
]
callback = MagicMock(side_effect=Exception("Callback failed"))
with patch("subprocess.Popen", return_value=mock_process):
# Should not raise
watch_manuscript(tmp_path, on_compile=callback)
class TestWatchManuscriptExceptions:
"""Tests for watch_manuscript exception handling."""
def test_keyboard_interrupt_terminates_process(self, tmp_path):
"""Verify KeyboardInterrupt terminates the process."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash\necho 'compiling'")
mock_process = MagicMock()
mock_process.stdout.readline.side_effect = KeyboardInterrupt()
with patch("subprocess.Popen", return_value=mock_process):
watch_manuscript(tmp_path)
mock_process.terminate.assert_called_once()
def test_generic_exception_terminates_process(self, tmp_path):
"""Verify generic exceptions terminate the process."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash\necho 'compiling'")
mock_process = MagicMock()
mock_process.stdout.readline.side_effect = RuntimeError("Connection lost")
with patch("subprocess.Popen", return_value=mock_process):
watch_manuscript(tmp_path)
mock_process.terminate.assert_called_once()
class TestWatchManuscriptParameters:
"""Tests for watch_manuscript parameter defaults."""
def test_interval_default(self, tmp_path):
"""Verify interval parameter has default value."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash")
mock_process = MagicMock()
mock_process.stdout.readline.return_value = ""
with patch("subprocess.Popen", return_value=mock_process):
# Should work without interval parameter
watch_manuscript(tmp_path)
def test_timeout_none_by_default(self, tmp_path):
"""Verify timeout defaults to None."""
compile_script = tmp_path / "compile"
compile_script.write_text("#!/bin/bash")
mock_process = MagicMock()
mock_process.stdout.readline.return_value = ""
with patch("subprocess.Popen", return_value=mock_process):
watch_manuscript(tmp_path)
mock_process.wait.assert_called_once_with(timeout=None)
if __name__ == "__main__":
import os
import pytest
pytest.main([os.path.abspath(__file__)])