# pytest-memray Reference

pytest-memray integrates memray into pytest, enabling per-test memory tracking, memory budgets, and leak detection.

## Installation & Activation

```bash
# Install as dev dep using project's package manager:
# uv add --dev pytest-memray | poetry add -G dev pytest-memray | pip install pytest-memray
$RUNNER -m pytest --memray tests/
```

## CLI Flags

| Flag | Description | Default |
|------|-------------|---------|
| `--memray` | Enable memory tracking | Off |
| `--most-allocations=N` | Show top N memory-consuming tests (0=all) | 5 |
| `--hide-memray-summary` | Suppress the MEMRAY REPORT section | Off |
| `--memray-bin-path DIR` | Directory for `.bin` dump files | Temp directory |
| `--memray-bin-prefix PREFIX` | Prefix for dump filenames | Random UUID4 |
| `--stacks=N` | Show N stack frames in failure tracebacks | 1 |
| `--native` | Include native (C/C++) frames | Off |
| `--trace-python-allocators` | Record pymalloc allocations | Off |
| `--fail-on-increase` | Fail `limit_memory` tests if memory increased since last run | Off |

## INI Configuration (pytest.ini / pyproject.toml)

```ini
[pytest]
memray = true
most_allocations = 10
hide_memray_summary = false
stacks = 3
native = true
trace_python_allocators = false
fail-on-increase = false
```

Or in `pyproject.toml`:
```toml
[tool.pytest.ini_options]
memray = true
most_allocations = 10
```

## Markers

### `@pytest.mark.limit_memory(limit, *, current_thread_only=False)`

Fail the test if peak memory (high watermark) exceeds `limit`.

```python
@pytest.mark.limit_memory("100 MB")
def test_processing():
    result = process_large_file("data.csv")
    assert len(result) > 0

@pytest.mark.limit_memory("50 MB", current_thread_only=True)
def test_single_thread():
    # Only tracks allocations on the main thread
    result = compute()
```

**Memory format:** `<NUMBER> ([KMGTP]B|B)` — e.g., `"100 MB"`, `"1.5 GB"`, `"512 KB"`

**How it works internally:**
1. Wraps the test function body in a `memray.Tracker` context manager
2. Uses `AGGREGATED_ALLOCATIONS` file format
3. After the test, reads `get_high_watermark_allocation_records()`
4. Sums all record sizes; fails if total exceeds limit

**Key behavior:** Only memory allocated DURING the test function body is measured. Module imports, fixtures, and setup code that run before the test function starts are NOT counted. This means:
- Import-time allocations are invisible
- Model loading in fixtures won't count unless the fixture runs inside the test
- `conftest.py` setup is excluded

### `@pytest.mark.limit_leaks(location_limit, *, filter_fn=None, current_thread_only=False)`

Fail if any single call stack leaks more than `location_limit`.

```python
@pytest.mark.limit_leaks("1 MB")
def test_no_leaks():
    for _ in range(100):
        process_item()

# With filter to ignore known caches
def my_filter(stack):
    for frame in stack.frames:
        if "lru_cache" in frame.function:
            return False  # Don't report this leak
    return True  # Report all other leaks

@pytest.mark.limit_leaks("100 KB", filter_fn=my_filter)
def test_filtered_leaks():
    process_with_caching()
```

**How it works:**
1. Automatically enables `--native` and `--trace-python-allocators` for the test
2. After the test, reads `get_leaked_allocation_records()`
3. Groups by call stack; fails if any stack exceeds `location_limit`
4. If `filter_fn` is provided, calls it for each exceeding stack — return `False` to suppress

**Performance:** Significantly slower due to forced native + python allocator tracking.

**False positives are common** from:
- Python regex compilation cache
- Logging level caches
- Object reuse pools
- Bytecode compilation
- pytest's own message capture

**Best practice:** Run the code in a loop so that one-time caches are amortized, and allow a small per-location budget.

### `filter_fn` API

```python
from pytest_memray import Stack, StackFrame

def my_filter(stack: Stack) -> bool:
    """Return True to report the leak, False to suppress it."""
    for frame in stack.frames:
        # frame.function: str (or "???")
        # frame.filename: str (or "???")
        # frame.lineno: int (or 0)
        if "known_cache" in frame.filename:
            return False
    return True
```

## Output Format

When `--memray` is enabled, the test summary shows:

```
======================================== MEMRAY REPORT ========================================
Allocations results for tests/test_example.py::test_func at the high watermark

     Total memory allocated: 117.2KiB
     Total allocations: 30
     Histogram of allocation sizes: |█|
     Biggest allocating functions:
         - <listcomp>:tests/test_example.py:17 -> 117.2KiB
```

Tests that fail memory limits show `M` status and `MEMORY PROBLEMS` in the summary.

## Saving Binary Dumps for Further Analysis

```bash
# Save .bin files to a specific directory
pytest --memray --memray-bin-path=/tmp/memray-bins tests/

# Then analyze with memray CLI
memray flamegraph /tmp/memray-bins/<uuid>.bin -o /tmp/flame.html
memray stats /tmp/memray-bins/<uuid>.bin
```

This is how you bridge pytest-memray to the full memray analysis toolchain.

## `--fail-on-increase` Mode

When enabled, tests with `limit_memory` markers will also fail if they use more memory than their previous successful run (stored in pytest's cache).

```bash
pytest --memray --fail-on-increase tests/
```

This catches regressions even when total usage is still under the limit.

## Combining with `memray run`

For deeper investigation beyond what pytest-memray provides:

```bash
# Capture full process-wide data via memray run wrapping pytest
memray run --native --trace-python-allocators -o /tmp/full.bin -m pytest tests/test_foo.py -k "test_name"

# Analyze with any memray reporter
memray flamegraph /tmp/full.bin -o /tmp/flame.html
memray stats /tmp/full.bin
```

This captures everything including imports, giving a process-wide view. The peak will be higher than pytest-memray reports because it includes all module-level allocations.

## pytest-xdist Compatibility

pytest-memray works with pytest-xdist (parallel test execution). The main process and workers share a result directory via the `MEMRAY_RESULT_PATH` environment variable. Results are aggregated using pickle metadata files.

## Common Gotchas

1. **Only one marker per test**: You cannot combine `limit_memory` and `limit_leaks` on the same test.

2. **Memory format parsing**: Must match `<NUMBER> ([KMGTP]B|B)` — e.g., `"100 MB"` works, `"100M"` does not.

3. **pymalloc overhead**: Python's allocator may not immediately release memory, so tests may need extra budget to account for arena overhead.

4. **Long test names**: Very long test parameterization strings can cause filesystem errors on macOS due to filename length limits for `.bin` files. Use `--memray-bin-path` with shorter paths if needed.

5. **Config object cache attribute**: `--fail-on-increase` requires pytest's cache plugin. If cache is disabled (`-p no:cacheprovider`), you'll get an `AttributeError`.
