techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 8)
aef804e verified
Raw
History Blame Contribute Delete
11.2 kB
"""
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
# =============================================================================
# Memory Leak Detection Fixture
# =============================================================================
@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
"""
# Python 3.11+ version check (memray requirement)
if sys.version_info < (3, 11):
pytest.skip(
"memray requires Python 3.11+. "
f"Current version: {sys.version_info.major}.{sys.version_info.minor}"
)
# Import memray with graceful degradation
try:
import memray
except ImportError:
pytest.skip(
"memray not installed. Install with: pip install memray>=1.12.0"
)
# Create output file for memray binary
output_file = tmp_path / "memray.bin"
# Start tracking memory allocations
tracker = memray.Tracker(str(output_file))
tracker.start()
yield tracker
# Stop tracking and analyze results
tracker.stop()
# Load stats for memory analysis and attach to tracker object
stats = memray.Stats(str(output_file))
tracker.stats = stats
# =============================================================================
# Memory Leak Assertion Helpers
# =============================================================================
@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
"""
# Calculate memory growth in MB
memory_growth_mb = stats.peak_memory_mb - stats.start_memory_mb
# Build assertion message
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 threshold
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
"""
# Get allocation metadata from stats
# Note: memray.Stats provides various metrics, we use total allocations
try:
# Get allocation data (may vary by memray version)
allocation_data = stats.get_metrics()
total_allocations = allocation_data.get("total_allocations", 0)
except (AttributeError, KeyError):
# Fallback: skip allocation count check if not available
pytest.skip("Allocation count not available in this memray version")
# Build assertion message
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 threshold
assert total_allocations < max_allocations, full_msg
return _assert_allocation_count
# =============================================================================
# Amplification Loop Helper
# =============================================================================
@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:
# Call test function with iteration index
test_func(i, context)
except Exception as e:
# Log but continue (testing memory, not functionality)
# Use pytest's logging to avoid print statements
pytest.logger.warning(
f"Amplification loop iteration {i} failed: {e}"
)
return _amplification_loop
# =============================================================================
# Pytest Marker Configuration
# =============================================================================
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)"
)