Skip to main content
Glama
test_config_watcher.py6.72 kB
# # Copyright (C) 2024 Billy Bryant # Portions copyright (C) 2024 Sergey Parfenyuk (original MIT-licensed author) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. # # MIT License attribution: Portions of this file were originally licensed # under the MIT License by Sergey Parfenyuk (2024). # """Tests for the configuration file watcher module.""" import asyncio import json import tempfile from collections.abc import Generator from pathlib import Path from unittest.mock import AsyncMock import pytest from mcp_foxxy_bridge.utils.config_watcher import ConfigWatcher @pytest.fixture def temp_config_file() -> Generator[Path, None, None]: """Create a temporary config file for testing.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: config = { "servers": { "test_server": { "command": "echo", "args": ["test"], "enabled": True, } } } json.dump(config, f) f.flush() yield Path(f.name) # Cleanup Path(f.name).unlink(missing_ok=True) @pytest.fixture def mock_callback() -> AsyncMock: """Mock callback function for config changes.""" return AsyncMock() class TestConfigWatcher: """Test cases for ConfigWatcher class.""" def test_init(self, temp_config_file: Path, mock_callback: AsyncMock) -> None: """Test ConfigWatcher initialization.""" watcher = ConfigWatcher(str(temp_config_file), mock_callback) assert watcher.config_path.resolve() == temp_config_file.resolve() assert watcher.reload_callback == mock_callback assert watcher._observer is None assert not watcher.is_running() @pytest.mark.asyncio async def test_start_and_stop(self, temp_config_file: Path, mock_callback: AsyncMock) -> None: """Test starting and stopping the config watcher.""" watcher = ConfigWatcher(str(temp_config_file), mock_callback) # Start the watcher await watcher.start() assert watcher.is_running() assert watcher._observer is not None # Stop the watcher await watcher.stop() assert not watcher.is_running() assert watcher._observer is None @pytest.mark.asyncio async def test_start_already_running(self, temp_config_file: Path, mock_callback: AsyncMock) -> None: """Test starting watcher when already running.""" watcher = ConfigWatcher(str(temp_config_file), mock_callback) await watcher.start() # Try to start again - should be a no-op await watcher.start() assert watcher.is_running() await watcher.stop() @pytest.mark.asyncio async def test_stop_not_running(self, temp_config_file: Path, mock_callback: AsyncMock) -> None: """Test stopping watcher when not running.""" watcher = ConfigWatcher(str(temp_config_file), mock_callback) # Stop without starting - should be a no-op await watcher.stop() assert not watcher.is_running() @pytest.mark.asyncio @pytest.mark.skip(reason="Integration test - requires complex async/threading setup") async def test_file_modification_triggers_callback(self, temp_config_file: Path, mock_callback: AsyncMock) -> None: """Test that file modification triggers the callback.""" # This test requires complex setup to handle asyncio tasks from watchdog threads # Skipped for now - functionality works in practice but hard to test in unit tests @pytest.mark.asyncio async def test_nonexistent_file(self, mock_callback: AsyncMock) -> None: """Test behavior with nonexistent config file.""" with tempfile.TemporaryDirectory() as tmpdir: nonexistent_path = f"{tmpdir}/nonexistent_config.json" watcher = ConfigWatcher(nonexistent_path, mock_callback) # Should not raise an exception await watcher.start() await watcher.stop() @pytest.mark.asyncio @pytest.mark.skip(reason="Integration test - requires complex async/threading setup") async def test_callback_exception_handling(self, temp_config_file: Path) -> None: """Test that callback exceptions are handled gracefully.""" # This test requires complex setup to handle asyncio tasks from watchdog threads # Skipped for now - functionality works in practice but hard to test in unit tests @pytest.mark.asyncio @pytest.mark.skip(reason="Integration test - requires complex async/threading setup") async def test_multiple_rapid_changes(self, temp_config_file: Path, mock_callback: AsyncMock) -> None: """Test handling of multiple rapid file changes.""" # This test requires complex setup to handle asyncio tasks from watchdog threads # Skipped for now - functionality works in practice but hard to test in unit tests @pytest.mark.asyncio async def test_directory_watching(self, temp_config_file: Path, mock_callback: AsyncMock) -> None: """Test that watcher monitors the correct directory.""" watcher = ConfigWatcher(str(temp_config_file), mock_callback) await watcher.start() # Verify the watcher is monitoring the parent directory assert watcher._observer is not None # The observer should have at least one watch for the directory watches = watcher._observer._watches assert len(watches) > 0 await watcher.stop() def test_context_manager(self, temp_config_file: Path, mock_callback: AsyncMock) -> None: """Test ConfigWatcher as an async context manager.""" async def test_context() -> None: async with ConfigWatcher(str(temp_config_file), mock_callback) as watcher: assert watcher.is_running() assert watcher._observer is not None # After exiting context, should be stopped assert not watcher.is_running() assert watcher._observer is None asyncio.run(test_context())

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/billyjbryant/mcp-foxxy-bridge'

If you have feedback or need assistance with the MCP directory API, please join our Discord server