diff --git a/README.md b/README.md index 179a92f..3f90394 100644 --- a/README.md +++ b/README.md @@ -175,8 +175,9 @@ directions: They coexist in one suite. Reach for **pytest-memray** to assert a ceiling, catch a leak, or see which function allocated; reach for **pytest-benchmem** to track a measured number over time and gate on a delta. One caveat: memray won't nest two -trackers, so don't run `pytest --memray` and a benchmem fixture on the *same* -test. +trackers, so don't run `pytest --memray` and a benchmem fixture on the *same* test +— if you do, benchmem fails that test with an actionable error rather than a terse +memray one. ## Install diff --git a/src/pytest_benchmem/memray.py b/src/pytest_benchmem/memray.py index de3f2b6..63a1c30 100644 --- a/src/pytest_benchmem/memray.py +++ b/src/pytest_benchmem/memray.py @@ -145,15 +145,38 @@ def _compute_statistics() -> Callable[[str], Any]: return compute_statistics +#: Substring of memray's error when a second ``Tracker`` is started while one is active. +#: memray raises a bare ``RuntimeError("No more than one Tracker instance can be active +#: at the same time")``; we match on this to turn it into an actionable message. +_NESTED_TRACKER_MARKER = "more than one Tracker" + + def _track_once(action: Action) -> Measurement: - """One fresh tracker run → a :class:`Measurement` via memray stats.""" + """One fresh tracker run → a :class:`Measurement` via memray stats. + + memray allows only one active ``Tracker`` per process, so if one is already + running — most often pytest-memray's ``--memray`` profiling the same test — the + nested ``Tracker`` raises. We translate that into an actionable error rather than + surfacing memray's terse one. + """ import memray compute_statistics = _compute_statistics() with tempfile.TemporaryDirectory(prefix="pytest-benchmem-") as tmp: out = Path(tmp) / "track.bin" # memray must create the file itself - with memray.Tracker(out): - action() + try: + with memray.Tracker(out): + action() + except RuntimeError as exc: + if _NESTED_TRACKER_MARKER in str(exc): + raise RuntimeError( + "pytest-benchmem couldn't start its memray pass because another memray " + "Tracker is already active — most likely pytest-memray's `--memray` running " + "on the same test. memray allows only one Tracker per process. Run the two " + "on different tests (or different sessions): pytest-memray for whole-test " + "limits/leaks, pytest-benchmem for the benchmarked action's memory." + ) from exc + raise s = compute_statistics(str(out)) return Measurement( peak_bytes=int(s.peak_memory_allocated), diff --git a/tests/test_memray.py b/tests/test_memray.py index b481836..c146b9c 100644 --- a/tests/test_memray.py +++ b/tests/test_memray.py @@ -21,6 +21,19 @@ def test_measure_peak_returns_positive_bytes(): assert peak > 1_000_000 # a list of 1e6 ints is several MB; bytes, not MiB +def test_nested_tracker_raises_actionable_error(tmp_path): + """If a memray Tracker is already active (e.g. pytest-memray's --memray on the same + test), benchmem's pass re-raises memray's terse error as an actionable one.""" + import memray + + # outer Tracker stands in for pytest-memray's --memray; it must be active first + with ( + memray.Tracker(tmp_path / "outer.bin"), + pytest.raises(RuntimeError, match="another memray Tracker is already active"), + ): + measure_memory(lambda: [0] * 1000) + + def test_repeats_takes_min_of_n(): # min-of-N: a smaller allocation never reports more than a larger one would. small = measure_peak(lambda: [0] * 100_000, repeats=3)