Spaces:
Sleeping
Sleeping
File size: 11,205 Bytes
aef804e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | """
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)"
)
|