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)"
    )