Download tests/error_paths/conftest.py from techprotrade/annator-command-center: direct link, hf CLI and curl.
- Browser
- Download file 20.6 kB
-
https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/error_paths/conftest.py
- Command line
-
hf download hf://spaces/techprotrade/annator-command-center/tests/error_paths/conftest.py
-
curl -L -o conftest.py https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/error_paths/conftest.py
20.6 kB
| """ | |
| Error Path Test Configuration | |
| Shared fixtures and utilities for testing error handling code paths | |
| that are rarely executed in normal operation but critical for production reliability. | |
| Error paths tested: | |
| - Database errors (IntegrityError, OperationalError, DataError) | |
| - Network errors (ConnectionError, TimeoutError) | |
| - LLM errors (APIError, RateLimitError, AuthenticationError) | |
| - Filesystem errors (IOError, OSError) | |
| - Invalid input handling (TypeError, ValueError, KeyError) | |
| - Race conditions and threading errors | |
| - Resource exhaustion (memory, connections, file handles) | |
| """ | |
| import pytest | |
| import logging | |
| from unittest.mock import patch, MagicMock, AsyncMock | |
| from unittest.mock import _patch as _patch_type | |
| from typing import Any, Dict, List, Optional, Type, Callable | |
| from contextlib import contextmanager | |
| from sqlalchemy.exc import IntegrityError, OperationalError, ProgrammingError, DataError | |
| from sqlalchemy.orm import Session | |
| import time | |
| logger = logging.getLogger(__name__) | |
| # ============================================================================ | |
| # Error Injection Fixtures | |
| # ============================================================================ | |
| def mock_database_error(): | |
| """ | |
| Patch database operations to raise SQLAlchemy exceptions. | |
| Usage: | |
| def test_constraint_violation(mock_database_error): | |
| mock_database_error(patch_target, IntegrityError("unique constraint")) | |
| # Test that code handles IntegrityError gracefully | |
| """ | |
| def _patch(target: str, exception: Exception, call_count: int = 1): | |
| """ | |
| Patch target to raise exception on Nth call. | |
| Args: | |
| target: Import path to patch (e.g., "core.database.get_db_session") | |
| exception: Exception to raise | |
| call_count: Raise on Nth call (1 = first call, 2 = second call) | |
| """ | |
| calls = [0] | |
| def side_effect(*args, **kwargs): | |
| calls[0] += 1 | |
| if calls[0] == call_count: | |
| raise exception | |
| # Return mock response for other calls | |
| if hasattr(exception, '__class__') and exception.__class__.__name__ == 'IntegrityError': | |
| # Mock DB session for IntegrityError | |
| return MagicMock() | |
| return MagicMock() | |
| return patch(target, side_effect=side_effect) | |
| return _patch | |
| def mock_network_error(): | |
| """ | |
| Patch HTTP/network operations to raise connection errors. | |
| Usage: | |
| def test_api_timeout(mock_network_error): | |
| mock_network_error("requests.get", TimeoutError("Connection timed out")) | |
| # Test retry logic or graceful degradation | |
| """ | |
| def _patch(target: str, exception: Exception = ConnectionError("Network unreachable")): | |
| """ | |
| Patch target to raise network exception. | |
| Args: | |
| target: Import path to patch | |
| exception: Network exception to raise (default: ConnectionError) | |
| """ | |
| return patch(target, side_effect=exception) | |
| return _patch | |
| def mock_llm_error(): | |
| """ | |
| Patch LLM client operations to raise API errors. | |
| Usage: | |
| def test_llm_rate_limit(mock_llm_error): | |
| mock_llm_error("openai.OpenAI.chat.completions.create", rate_limit_error) | |
| # Test fallback to next provider | |
| """ | |
| def _patch(target: str, error_type: str = "rate_limit"): | |
| """ | |
| Patch LLM client to raise specific error type. | |
| Args: | |
| target: Import path to LLM client method | |
| error_type: One of "rate_limit", "auth", "timeout", "server_error" | |
| """ | |
| errors = { | |
| "rate_limit": Exception("Rate limit exceeded (429)"), | |
| "auth": Exception("Unauthorized (401)"), | |
| "timeout": TimeoutError("Request timed out"), | |
| "server_error": Exception("Internal server error (500)"), | |
| "context_window": Exception("Context window exceeded"), | |
| } | |
| exception = errors.get(error_type, errors["server_error"]) | |
| return patch(target, side_effect=exception) | |
| return _patch | |
| def mock_filesystem_error(): | |
| """ | |
| Patch file operations to raise IO errors. | |
| Usage: | |
| def test_file_not_found(mock_filesystem_error): | |
| mock_filesystem_error("builtins.open", FileNotFoundError("File not found")) | |
| # Test graceful fallback | |
| """ | |
| def _patch(target: str, exception: Exception = IOError("Disk error")): | |
| """ | |
| Patch file operation to raise IO exception. | |
| Args: | |
| target: Import path to file operation (e.g., "builtins.open") | |
| exception: IO exception to raise | |
| """ | |
| return patch(target, side_effect=exception) | |
| return _patch | |
| # ============================================================================ | |
| # Error Verification Fixtures | |
| # ============================================================================ | |
| def assert_logs_error(caplog): | |
| """ | |
| Verify that an error was logged with correct level and message. | |
| Usage: | |
| def test_error_logging(caplog, assert_logs_error): | |
| with caplog.at_level(logging.ERROR): | |
| # Trigger error | |
| service.method_that_fails() | |
| # Verify error was logged | |
| assert_logs_error(caplog, "expected error message") | |
| """ | |
| def _verify(caplog_object, expected_message: str = None, level: int = logging.ERROR): | |
| """ | |
| Verify error log entry exists. | |
| Args: | |
| caplog_object: pytest caplog fixture | |
| expected_message: Substring to match in log message (optional) | |
| level: Expected log level (default: ERROR) | |
| """ | |
| assert caplog_object.records, f"No log records found" | |
| # Check for matching level | |
| level_matches = [r for r in caplog_object.records if r.levelno == level] | |
| assert level_matches, f"No {logging.getLevelName(level)} logs found. Got levels: {[r.levelno for r in caplog_object.records]}" | |
| if expected_message: | |
| # Check for message substring | |
| message_matches = [r for r in level_matches if expected_message.lower() in r.message.lower()] | |
| assert message_matches, f"No log entry contains '{expected_message}'. Messages: {[r.message for r in level_matches]}" | |
| return True | |
| return _verify | |
| def assert_graceful_degradation(): | |
| """ | |
| Verify service remains operational after error. | |
| Usage: | |
| def test_graceful_degradation(assert_graceful_degradation, db_session): | |
| cache = GovernanceCache(max_size=100, ttl_seconds=60) | |
| # Trigger error | |
| with pytest.raises(IntegrityError): | |
| db_session.commit() | |
| # Verify cache still works | |
| assert_graceful_degradation(cache, "get", agent_id="test", action_type="stream_chat") | |
| """ | |
| def _verify(service, method: str, *args, **kwargs): | |
| """ | |
| Verify service method still works after error. | |
| Args: | |
| service: Service instance to test | |
| method: Method name to call | |
| *args: Positional args for method | |
| **kwargs: Keyword args for method | |
| """ | |
| method_obj = getattr(service, method, None) | |
| assert method_obj is not None, f"Service has no method '{method}'" | |
| # Try calling method | |
| try: | |
| result = method_obj(*args, **kwargs) | |
| logger.info(f"Service degraded gracefully: {method}() returned {type(result)}") | |
| return True | |
| except Exception as e: | |
| logger.error(f"Service did NOT degrade gracefully: {method}() raised {type(e).__name__}: {e}") | |
| raise AssertionError(f"Service method '{method}' failed after error: {e}") | |
| return _verify | |
| def assert_error_message_contains(): | |
| """ | |
| Verify exception message contains expected pattern. | |
| Usage: | |
| def test_error_message(assert_error_message_contains): | |
| with pytest.raises(ValueError) as exc_info: | |
| raise ValueError("Invalid agent ID: must be string") | |
| assert_error_message_contains(exc_info, "Invalid agent ID") | |
| """ | |
| def _verify(exc_info: pytest.ExceptionInfo, pattern: str, use_regex: bool = False): | |
| """ | |
| Verify exception message contains pattern. | |
| Args: | |
| exc_info: pytest exception info from pytest.raises() | |
| pattern: String or regex pattern to match | |
| use_regex: True if pattern is regex (default: False for substring) | |
| """ | |
| message = str(exc_info.value) | |
| if use_regex: | |
| import re | |
| assert re.search(pattern, message, re.IGNORECASE), f"Exception message does not match regex '{pattern}': {message}" | |
| else: | |
| assert pattern.lower() in message.lower(), f"Exception message does not contain '{pattern}': {message}" | |
| return True | |
| return _verify | |
| # ============================================================================ | |
| # Common Error Scenarios | |
| # ============================================================================ | |
| def corrupt_cache_entry(): | |
| """ | |
| Create a corrupted cache entry for testing cache recovery. | |
| Usage: | |
| def test_corrupted_cache_entry(corrupt_cache_entry): | |
| cache = GovernanceCache(max_size=100, ttl_seconds=60) | |
| corrupted = corrupt_cache_entry() # Missing 'data' key | |
| cache._cache["test:stream_chat"] = corrupted | |
| # Test that cache handles corruption gracefully | |
| """ | |
| def _create(corruption_type: str = "missing_data") -> Dict[str, Any]: | |
| """ | |
| Create corrupted cache entry. | |
| Args: | |
| corruption_type: Type of corruption | |
| - "missing_data": Entry lacks 'data' key | |
| - "missing_timestamp": Entry lacks 'cached_at' key | |
| - "invalid_data": Entry has non-dict data | |
| - "expired": Entry has ancient timestamp | |
| """ | |
| corrupted_entries = { | |
| "missing_data": {"cached_at": time.time()}, # Missing 'data' key | |
| "missing_timestamp": {"data": {"allowed": True}}, # Missing 'cached_at' key | |
| "invalid_data": ["not", "a", "dict"], # Wrong type | |
| "expired": {"data": {"allowed": True}, "cached_at": 0}, # Ancient timestamp | |
| } | |
| return corrupted_entries.get(corruption_type, corrupted_entries["missing_data"]) | |
| return _create | |
| def invalid_agent_id(): | |
| """ | |
| Generate invalid agent_id values for testing input validation. | |
| Usage: | |
| def test_invalid_agent_id(invalid_agent_id): | |
| cache = GovernanceCache(max_size=100, ttl_seconds=60) | |
| for invalid_id in invalid_agent_id(): | |
| with pytest.raises((TypeError, ValueError)): | |
| cache.get(invalid_id, "stream_chat") | |
| """ | |
| def _generate(format_type: str = "all") -> List[Any]: | |
| """ | |
| Generate invalid agent_id values. | |
| Args: | |
| format_type: Type of invalid values to return | |
| - "type": Wrong types (int, list, dict, None) | |
| - "empty": Empty strings, whitespace | |
| - "special": Special characters, unicode | |
| - "all": All of the above (default) | |
| Returns: | |
| List of invalid agent_id values | |
| """ | |
| invalid_values = { | |
| "type": [12345, None, ["list"], {"dict": "value"}, 3.14], | |
| "empty": ["", " ", "\t", "\n"], | |
| "special": ["agent@example.com", "agent/with/slashes", "agent\\with\\backslashes", "agent with spaces"], | |
| } | |
| if format_type == "all": | |
| all_invalid = [] | |
| for values in invalid_values.values(): | |
| all_invalid.extend(values) | |
| return all_invalid | |
| return invalid_values.get(format_type, []) | |
| return _generate | |
| def malformed_database_result(): | |
| """ | |
| Create malformed database query result for testing ORM error handling. | |
| Usage: | |
| def test_malformed_result(malformed_database_result, db_session): | |
| malformed = malformed_database_result("missing_column") | |
| with patch.object(db_session, 'query', return_value=malformed): | |
| # Test that code handles missing columns gracefully | |
| """ | |
| def _create(malformation_type: str = "missing_column"): | |
| """ | |
| Create malformed database result. | |
| Args: | |
| malformation_type: Type of malformation | |
| - "missing_column": Result lacks required column | |
| - "wrong_type": Column has wrong type | |
| - "null_value": Required column is None | |
| - "empty_result": Query returns empty list | |
| """ | |
| # Create mock result with specific malformation | |
| mock_result = MagicMock() | |
| if malformation_type == "missing_column": | |
| # AttributeError when accessing missing column | |
| mock_result.configure_mock(**{"id": "test-agent", "status": "STUDENT"}) | |
| # Deliberately remove 'name' attribute | |
| del mock_result.name | |
| elif malformation_type == "wrong_type": | |
| mock_result.configure_mock(**{"id": 12345, "status": "STUDENT"}) # id should be str | |
| elif malformation_type == "null_value": | |
| mock_result.configure_mock(**{"id": None, "status": "STUDENT"}) | |
| elif malformation_type == "empty_result": | |
| mock_result = [] | |
| return mock_result | |
| return _create | |
| def race_condition_scenario(): | |
| """ | |
| Simulate race condition for testing thread safety. | |
| Usage: | |
| def test_concurrent_write(race_condition_scenario): | |
| cache = GovernanceCache(max_size=100, ttl_seconds=60) | |
| def write_operation(): | |
| cache.set("agent-1", "stream_chat", {"allowed": True}) | |
| # Run concurrent writes | |
| race_condition_scenario(write_operation, num_threads=10) | |
| """ | |
| def _simulate(operation: Callable, num_threads: int = 5, delay_ms: int = 10): | |
| """ | |
| Simulate concurrent execution of operation. | |
| Args: | |
| operation: Function to execute concurrently | |
| num_threads: Number of threads to spawn | |
| delay_ms: Delay between thread starts (ms) to increase race likelihood | |
| """ | |
| import threading | |
| import time | |
| threads = [] | |
| errors = [] | |
| def thread_func(): | |
| try: | |
| time.sleep(delay_ms / 1000.0) # Stagger starts | |
| operation() | |
| except Exception as e: | |
| errors.append(e) | |
| # Spawn threads | |
| for _ in range(num_threads): | |
| thread = threading.Thread(target=thread_func) | |
| threads.append(thread) | |
| thread.start() | |
| # Wait for completion | |
| for thread in threads: | |
| thread.join(timeout=5.0) | |
| # Check for errors | |
| if errors: | |
| raise AssertionError(f"Race condition detected: {len(errors)} threads raised exceptions: {errors}") | |
| return True | |
| return _simulate | |
| # ============================================================================ | |
| # Database Error Scenarios | |
| # ============================================================================ | |
| def database_constraint_error(): | |
| """ | |
| Create database constraint violation for testing. | |
| Usage: | |
| def test_unique_constraint(database_constraint_error, db_session): | |
| agent1 = AgentRegistry(id="duplicate-id", name="Agent1") | |
| db_session.add(agent1) | |
| db_session.commit() | |
| agent2 = AgentRegistry(id="duplicate-id", name="Agent2") | |
| db_session.add(agent2) | |
| with pytest.raises(IntegrityError): | |
| db_session.commit() | |
| """ | |
| def _create(constraint_type: str = "unique") -> Exception: | |
| """ | |
| Create database constraint error. | |
| Args: | |
| constraint_type: Type of constraint | |
| - "unique": Unique constraint violation | |
| - "foreign_key": Foreign key violation | |
| - "not_null": NOT NULL violation | |
| - "check": CHECK constraint violation | |
| """ | |
| messages = { | |
| "unique": "duplicate key value violates unique constraint", | |
| "foreign_key": "insert or update on table violates foreign key constraint", | |
| "not_null": "null value in column violates not-null constraint", | |
| "check": "new row for relation violates check constraint", | |
| } | |
| orig = Exception(messages[constraint_type]) | |
| return IntegrityError("INSERT INTO agents", {}, orig) | |
| return _create | |
| def database_connection_error(): | |
| """ | |
| Simulate database connection failure. | |
| Usage: | |
| def test_db_connection_failure(database_connection_error): | |
| with patch("core.database.create_engine", side_effect=database_connection_error()): | |
| # Test connection error handling | |
| """ | |
| def _create(error_type: str = "connection_refused") -> Exception: | |
| """ | |
| Create database connection error. | |
| Args: | |
| error_type: Type of connection error | |
| - "connection_refused": Server not reachable | |
| - "timeout": Connection timeout | |
| - "auth_failed": Authentication failed | |
| - "pool_exhausted": Connection pool exhausted | |
| """ | |
| messages = { | |
| "connection_refused": "could not connect to server: Connection refused", | |
| "timeout": "server closed the connection unexpectedly", | |
| "auth_failed": "password authentication failed", | |
| "pool_exhausted": "connection pool exhausted", | |
| } | |
| return OperationalError(messages[error_type], {}, None) | |
| return _create | |
| # ============================================================================ | |
| # LLM Error Scenarios | |
| # ============================================================================ | |
| def llm_provider_error(): | |
| """ | |
| Create LLM provider error for testing fallback logic. | |
| Usage: | |
| def test_provider_fallback(llm_provider_error): | |
| with patch("core.llm.byok_handler.OpenAI", side_effect=llm_provider_error("auth")): | |
| # Test fallback to next provider | |
| """ | |
| def _create(error_type: str = "rate_limit") -> Exception: | |
| """ | |
| Create LLM provider error. | |
| Args: | |
| error_type: Type of LLM error | |
| - "rate_limit": Rate limit exceeded (429) | |
| - "auth": Invalid API key (401) | |
| - "timeout": Request timeout | |
| - "server_error": Internal server error (500) | |
| - "context_window": Prompt too long | |
| """ | |
| errors = { | |
| "rate_limit": Exception("Rate limit exceeded (429)"), | |
| "auth": Exception("Unauthorized (401): Invalid API key"), | |
| "timeout": TimeoutError("Request timed out after 30s"), | |
| "server_error": Exception("Internal server error (500)"), | |
| "context_window": Exception("This model's maximum context length is"), | |
| } | |
| return errors.get(error_type, errors["server_error"]) | |
| return _create | |
| # ============================================================================ | |
| # Helpers for Testing Async Error Paths | |
| # ============================================================================ | |
| def async_error_context(): | |
| """ | |
| Context manager for testing async error paths. | |
| Usage: | |
| async def test_async_error(async_error_context): | |
| async with async_error_context(ValueError("Expected error")): | |
| await async_function_that_fails() | |
| """ | |
| def _context(expected_exception: Exception): | |
| """ | |
| Context manager for async error testing. | |
| Args: | |
| expected_exception: Exception expected to be raised | |
| """ | |
| try: | |
| yield | |
| except Exception as e: | |
| if type(e).__name__ == type(expected_exception).__name__: | |
| logger.info(f"Caught expected error: {type(e).__name__}") | |
| raise | |
| else: | |
| logger.error(f"Unexpected error type: {type(e).__name__} vs {type(expected_exception).__name__}") | |
| raise | |
| return _context | |