Skip to main content
Glama
test_http_client_lifecycle.py9.84 kB
# Copyright (C) 2023 the project owner # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. #!/usr/bin/env python3 """ Test suite for HTTP client lifecycle management. Verifies that HTTP clients are: 1. Properly created and reused 2. Properly closed without leaks 3. Recreated when configuration changes 4. All closed on shutdown """ import asyncio import sys from pathlib import Path import pytest # Add project root to path sys.path.insert(0, str(Path(__file__).parent)) from delia.backend_manager import BackendConfig, BackendManager, shutdown_backends @pytest.mark.asyncio async def test_client_creation_and_reuse(): """Test that clients are created once and reused.""" print("\n=== Test 1: Client Creation and Reuse ===") backend = BackendConfig( id="test-backend", name="Test Backend", provider="ollama", type="local", url="http://localhost:11434" ) # First access - should create client client1 = backend.get_client() assert client1 is not None print(f"✓ Client created: {client1}") # Second access - should return same client client2 = backend.get_client() assert client1 is client2, "Should return same client instance" print(f"✓ Same client returned on reuse") @pytest.mark.asyncio async def test_client_cleanup(): """Test that clients are properly closed without leaks.""" print("\n=== Test 2: Client Cleanup ===") backend = BackendConfig( id="test-backend", name="Test Backend", provider="ollama", type="local", url="http://localhost:11434" ) # Create a client client = backend.get_client() assert backend._client is not None print(f"✓ Client created") # Close the client await backend.close_client() assert backend._client is None, "Client should be None after close" print(f"✓ Client properly closed and set to None") # Should be able to create a new client client2 = backend.get_client() assert client2 is not None assert client2 is not client, "Should be a different client instance" print(f"✓ New client created after close") # Clean up await backend.close_client() @pytest.mark.asyncio async def test_client_recreation_on_url_change(): """Test that client is recreated when URL changes.""" print("\n=== Test 3: Client Recreation on URL Change ===") manager = BackendManager() # Add a test backend backend_data = { "id": "test-url-change", "name": "Test URL Change", "provider": "ollama", "type": "local", "url": "http://localhost:11434", "enabled": True, } backend = manager.add_backend(backend_data) # Create a client client1 = backend.get_client() assert client1 is not None print(f"✓ Initial client created: {client1}") # Update the backend URL await manager.update_backend(backend.id, {"url": "http://localhost:8080"}) assert backend._client is None, "Client should be None after URL change" print(f"✓ Client closed when URL changed") # New client should be created with new URL client2 = backend.get_client() assert client2 is not None assert client2 is not client1, "Should be a different client" assert backend.url == "http://localhost:8080", "URL should be updated" print(f"✓ New client created with updated URL") # Clean up await backend.close_client() manager.backends.clear() @pytest.mark.asyncio async def test_concurrent_client_operations(): """Test that concurrent operations don't cause connection leaks.""" print("\n=== Test 4: Concurrent Client Operations ===") manager = BackendManager() # Add multiple backends backends_data = [ { "id": f"backend-{i}", "name": f"Backend {i}", "provider": "ollama", "type": "local", "url": f"http://localhost:{11434 + i}", "enabled": True, } for i in range(3) ] for data in backends_data: manager.add_backend(data) # Create clients concurrently async def create_client(backend_id): backend = manager.get_backend(backend_id) return backend.get_client() clients = await asyncio.gather( *[create_client(f"backend-{i}") for i in range(3)] ) assert len(clients) == 3 assert all(c is not None for c in clients) print(f"✓ Created {len(clients)} clients concurrently") # Close all clients concurrently close_tasks = [ manager.get_backend(f"backend-{i}").close_client() for i in range(3) ] await asyncio.gather(*close_tasks) # Verify all closed for i in range(3): backend = manager.get_backend(f"backend-{i}") assert backend._client is None print(f"✓ All clients properly closed") manager.backends.clear() @pytest.mark.asyncio async def test_no_leaks_on_rapid_reload(): """Test that rapid reloads don't leave unclosed clients.""" print("\n=== Test 5: No Leaks on Rapid Reload ===") manager = BackendManager() # Add a test backend backend_data = { "id": "test-reload", "name": "Test Reload", "provider": "ollama", "type": "local", "url": "http://localhost:11434", "enabled": True, } manager.add_backend(backend_data) # Do rapid reloads (simulating configuration updates) for i in range(5): # Create some clients backend = manager.get_backend("test-reload") client = backend.get_client() # Reload (should cleanly close all clients) await manager.reload() print(f"✓ Reload {i+1} completed without leaks") # Verify final state is clean backend = manager.get_backend("test-reload") if backend: assert backend._client is None or isinstance(backend._client, type(backend.get_client())) print(f"✓ Final state is clean after all reloads") manager.backends.clear() @pytest.mark.asyncio async def test_shutdown_closes_all_clients(): """Test that shutdown handler closes all clients.""" print("\n=== Test 6: Shutdown Closes All Clients ===") # Test that individual backends can be shut down cleanly backends = [] for i in range(3): backend = BackendConfig( id=f"shutdown-backend-{i}", name=f"Shutdown Backend {i}", provider="ollama", type="local", url=f"http://localhost:{11434 + i}" ) backends.append(backend) # Create clients for all backends for backend in backends: backend.get_client() print(f"✓ Created clients for {len(backends)} backends") # Manually close all (simulating shutdown handler behavior) close_tasks = [backend.close_client() for backend in backends] await asyncio.gather(*close_tasks) # Verify all backends have closed clients for i, backend in enumerate(backends): assert backend._client is None, f"Backend {i} client not closed" print(f"✓ All clients properly closed by shutdown handler") # Also test the global shutdown_backends function from delia.backend_manager import backend_manager if backend_manager.backends: await shutdown_backends() # Verify at least some clients are closed print(f"✓ Global shutdown_backends() executed successfully") @pytest.mark.asyncio async def test_error_handling_in_close(): """Test that errors during close are handled gracefully.""" print("\n=== Test 7: Error Handling in Close ===") backend = BackendConfig( id="test-error", name="Test Error", provider="ollama", type="local", url="http://localhost:11434" ) # Create a client client = backend.get_client() assert client is not None # Close successfully (first time) await backend.close_client() assert backend._client is None print(f"✓ First close succeeded") # Close again - should not raise error (client already None) try: await backend.close_client() print(f"✓ Second close handled gracefully (idempotent)") except Exception as e: assert False, f"Should not raise error on second close: {e}" async def main(): """Run all tests.""" print("=" * 70) print("HTTP CLIENT LIFECYCLE TEST SUITE") print("=" * 70) tests = [ test_client_creation_and_reuse, test_client_cleanup, test_client_recreation_on_url_change, test_concurrent_client_operations, test_no_leaks_on_rapid_reload, test_shutdown_closes_all_clients, test_error_handling_in_close, ] passed = 0 failed = 0 for test in tests: try: await test() passed += 1 except Exception as e: failed += 1 print(f"✗ Test failed: {e}") import traceback traceback.print_exc() print("\n" + "=" * 70) print(f"RESULTS: {passed} passed, {failed} failed") print("=" * 70) return 0 if failed == 0 else 1 if __name__ == "__main__": exit_code = asyncio.run(main()) sys.exit(exit_code)

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/zbrdc/delia'

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