| """ |
| Pytest configuration and fixtures for memory leak detection tests. |
| |
| This module provides fixtures for memory leak detection using Bloomberg's memray |
| profiler. All fixtures include graceful degradation if memray is not installed. |
| |
| Key Fixtures: |
| - memray_session: Yields memray.Tracker for memory profiling |
| - check_memory_growth: Helper function to assert memory growth thresholds |
| - amplification_loop: Helper function for 100-iteration amplification loops |
| |
| Usage: |
| def test_my_function_no_leak(memray_session, check_memory_growth): |
| from core.my_module import my_function |
| |
| for i in range(100): |
| my_function(f"test_{i}") |
| |
| # Assert <10MB memory growth |
| check_memory_growth(memray_session, threshold_mb=10) |
| |
| Phase: 243-01 (Memory & Performance Bug Discovery) |
| """ |
|
|
| import sys |
| from pathlib import Path |
| from typing import Optional |
|
|
| import pytest |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture(scope="function") |
| def memray_session(tmp_path): |
| """ |
| Memray session fixture for memory leak detection. |
| |
| This fixture provides a memray.Stats instance for analyzing Python |
| memory allocations during test execution. It handles graceful degradation |
| if memray is not installed or Python version is <3.11. |
| |
| Yields: |
| memray.Tracker: Started tracker instance (use after test for stats) |
| |
| Examples: |
| def test_agent_execution_no_leak(memray_session, check_memory_growth): |
| from core.agent_governance_service import AgentGovernanceService |
| |
| governance = AgentGovernanceService() |
| |
| for i in range(100): |
| governance.execute_agent( |
| agent_id="test_agent", |
| query=f"Test {i}", |
| db_session=db_session |
| ) |
| |
| # Assert <10MB memory growth |
| check_memory_growth(memray_session, threshold_mb=10) |
| |
| Requirements: |
| - Python 3.11+ (memray requirement) |
| - memray>=1.12.0 (install with: pip install memray) |
| |
| Graceful Degradation: |
| - Skips with pytest.skip if Python <3.11 |
| - Skips with pytest.skip if memray not installed |
| - Does NOT fail CI if memray unavailable |
| |
| Phase: 243-01 |
| """ |
| |
| if sys.version_info < (3, 11): |
| pytest.skip( |
| "memray requires Python 3.11+. " |
| f"Current version: {sys.version_info.major}.{sys.version_info.minor}" |
| ) |
|
|
| |
| try: |
| import memray |
| except ImportError: |
| pytest.skip( |
| "memray not installed. Install with: pip install memray>=1.12.0" |
| ) |
|
|
| |
| output_file = tmp_path / "memray.bin" |
|
|
| |
| tracker = memray.Tracker(str(output_file)) |
| tracker.start() |
|
|
| yield tracker |
|
|
| |
| tracker.stop() |
|
|
| |
| stats = memray.Stats(str(output_file)) |
| tracker.stats = stats |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture(scope="function") |
| def check_memory_growth(): |
| """ |
| Helper function to assert memory growth thresholds. |
| |
| This fixture provides a helper function for asserting that memory growth |
| during a test stays within acceptable bounds. It calculates growth between |
| start and peak memory usage. |
| |
| Returns: |
| Callable[[memray.Stats, float, Optional[str]], None]: Assertion function |
| |
| Args: |
| stats: memray.Stats instance from memray_session |
| threshold_mb: Maximum allowed memory growth in MB |
| context_msg: Optional context message for assertion failure |
| |
| Examples: |
| def test_agent_execution_no_leak(memray_session, check_memory_growth): |
| # ... run test ... |
| |
| # Assert <10MB memory growth |
| check_memory_growth(memray_session, threshold_mb=10) |
| |
| Thresholds by Test Category: |
| - Agent execution: <10MB (100 iterations) |
| - Governance cache: <5MB (1000 operations) |
| - LLM streaming: <15MB (1000 tokens) |
| |
| Phase: 243-01 |
| """ |
| def _check_memory_growth( |
| stats, |
| threshold_mb: float, |
| context_msg: Optional[str] = None |
| ) -> None: |
| """ |
| Assert memory growth threshold. |
| |
| Args: |
| stats: memray.Stats instance |
| threshold_mb: Maximum allowed memory growth in MB |
| context_msg: Optional context message for assertion |
| |
| Raises: |
| AssertionError: If memory growth exceeds threshold |
| """ |
| |
| memory_growth_mb = stats.peak_memory_mb - stats.start_memory_mb |
|
|
| |
| base_msg = ( |
| f"Memory leak detected: {memory_growth_mb:.2f} MB growth " |
| f"(threshold: {threshold_mb} MB)" |
| ) |
| full_msg = f"{context_msg}: {base_msg}" if context_msg else base_msg |
|
|
| |
| assert memory_growth_mb < threshold_mb, full_msg |
|
|
| return _check_memory_growth |
|
|
|
|
| @pytest.fixture(scope="function") |
| def assert_allocation_count(): |
| """ |
| Helper function to assert allocation count thresholds. |
| |
| This fixture provides a helper function for asserting that the number |
| of memory allocations during a test stays within acceptable bounds. |
| |
| Returns: |
| Callable[[memray.Stats, int, Optional[str]], None]: Assertion function |
| |
| Args: |
| stats: memray.Stats instance from memray_session |
| max_allocations: Maximum allowed allocation count |
| context_msg: Optional context message for assertion failure |
| |
| Examples: |
| def test_agent_execution_no_leak(memray_session, assert_allocation_count): |
| # ... run test ... |
| |
| # Assert <1000 allocations per execution |
| assert_allocation_count(memray_session, max_allocations=1000) |
| |
| Phase: 243-01 |
| """ |
| def _assert_allocation_count( |
| stats, |
| max_allocations: int, |
| context_msg: Optional[str] = None |
| ) -> None: |
| """ |
| Assert allocation count threshold. |
| |
| Args: |
| stats: memray.Stats instance |
| max_allocations: Maximum allowed allocation count |
| context_msg: Optional context message for assertion |
| |
| Raises: |
| AssertionError: If allocation count exceeds threshold |
| """ |
| |
| |
| try: |
| |
| allocation_data = stats.get_metrics() |
| total_allocations = allocation_data.get("total_allocations", 0) |
| except (AttributeError, KeyError): |
| |
| pytest.skip("Allocation count not available in this memray version") |
|
|
| |
| base_msg = ( |
| f"Too many allocations: {total_allocations} " |
| f"(threshold: {max_allocations})" |
| ) |
| full_msg = f"{context_msg}: {base_msg}" if context_msg else base_msg |
|
|
| |
| assert total_allocations < max_allocations, full_msg |
|
|
| return _assert_allocation_count |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture(scope="function") |
| def amplification_loop(): |
| """ |
| Helper function for 100-iteration amplification loops. |
| |
| This fixture provides a helper function for running test functions |
| in amplification loops (100 iterations) to amplify small memory leaks |
| into detectable growth. |
| |
| Returns: |
| Callable[[Callable, int, Optional[dict]], None]: Loop runner function |
| |
| Args: |
| test_func: Function to execute in loop |
| iterations: Number of iterations (default: 100) |
| context: Optional context dictionary for test data |
| |
| Examples: |
| def test_agent_execution_no_leak(memray_session, amplification_loop): |
| from core.agent_governance_service import AgentGovernanceService |
| |
| governance = AgentGovernanceService() |
| |
| def execute_agent(iteration): |
| governance.execute_agent( |
| agent_id="test_agent", |
| query=f"Test {iteration}", |
| db_session=db_session |
| ) |
| |
| # Run 100 iterations |
| amplification_loop(execute_agent, iterations=100) |
| |
| Amplification Strategy: |
| - Small leaks (1KB/iteration) become detectable (100KB over 100 iterations) |
| - Detects cumulative leaks from repeated operations |
| - Identifies cache misses, unclosed connections, etc. |
| |
| Phase: 243-01 |
| """ |
| def _amplification_loop( |
| test_func: callable, |
| iterations: int = 100, |
| context: Optional[dict] = None |
| ) -> None: |
| """ |
| Run test function in amplification loop. |
| |
| Args: |
| test_func: Function to execute (receives iteration index) |
| iterations: Number of iterations (default: 100) |
| context: Optional context dictionary for test data |
| |
| Raises: |
| Exception: Propagates exceptions from test_func (logged, not swallowed) |
| """ |
| for i in range(iterations): |
| try: |
| |
| test_func(i, context) |
| except Exception as e: |
| |
| |
| pytest.logger.warning( |
| f"Amplification loop iteration {i} failed: {e}" |
| ) |
|
|
| return _amplification_loop |
|
|
|
|
| |
| |
| |
|
|
| def pytest_configure(config): |
| """ |
| Configure pytest markers for memory leak tests. |
| |
| This function is called by pytest at configuration time to register |
| custom markers used in memory leak tests. |
| |
| Markers Registered: |
| - memory_leak: Memory leak detection tests (run sequentially, weekly) |
| |
| Usage: |
| @pytest.mark.memory_leak |
| @pytest.mark.slow |
| def test_my_function_no_leak(memray_session): |
| ... |
| |
| CI/CD Integration: |
| - Run weekly via: pytest -m memory_leak |
| - Excluded from fast PR tests (<10 minutes) |
| - Tests skip gracefully if memray unavailable |
| |
| Phase: 243-01 |
| """ |
| config.addinivalue_line( |
| "markers", |
| "memory_leak: Memory leak detection tests (run sequentially, weekly)" |
| ) |
|
|