From b15979d8e6da3c7cbc4de7ced579983da06e9623 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Sat, 9 May 2026 22:30:58 +0100 Subject: [PATCH] fix(test_tools): Reset unlabeled metrics in `assert_metric` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MetricWrapperBase.clear()` only works on parent metrics — the ones declared with labels. Calling it on an unlabeled metric raises AttributeError because `_lock` is only set on parents. `assert_metric_impl` iterates every collector in the registry and calls `clear()`, so any unlabeled metric registered in the test session would crash the fixture. Reset unlabeled metrics' observable state via `_metric_init` instead, which recreates `_value` (Counter) / `_buckets`+`_sum` (Histogram) / etc. fresh. beep boop --- src/common/test_tools/plugin.py | 11 +++++++++-- tests/conftest.py | 8 ++++++++ tests/unit/common/test_tools/test_plugin.py | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/common/test_tools/plugin.py b/src/common/test_tools/plugin.py index 47e5ee2e..92459631 100644 --- a/src/common/test_tools/plugin.py +++ b/src/common/test_tools/plugin.py @@ -31,10 +31,17 @@ def assert_metric_impl() -> Generator[AssertMetricFixture, None, None]: registry = prometheus_client.REGISTRY collectors = [*registry._collector_to_names] - # Reset registry state + # Reset registry state. `MetricWrapperBase.clear()` is implemented for + # parent metrics (those declared with labels) — calling it on an + # unlabeled metric raises AttributeError because `_lock` is only set on + # parents. Reset unlabeled metrics' observable state via `_metric_init`. for collector in collectors: - if isinstance(collector, prometheus_client.metrics.MetricWrapperBase): + if not isinstance(collector, prometheus_client.metrics.MetricWrapperBase): + continue + if collector._is_parent(): # type: ignore[no-untyped-call] collector.clear() + else: + collector._metric_init() # type: ignore[no-untyped-call] def _assert_metric( *, diff --git a/tests/conftest.py b/tests/conftest.py index afe35873..ad91ead7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -92,3 +92,11 @@ def test_metric() -> prometheus_client.Counter: "Total number of tests run by pytest.", ["test_name"], ) + + +@pytest.fixture(scope="session") +def test_unlabeled_metric() -> prometheus_client.Counter: + return prometheus_client.Counter( + "pytest_unlabeled_total", + "Total number of pytest unlabeled-metric assertions.", + ) diff --git a/tests/unit/common/test_tools/test_plugin.py b/tests/unit/common/test_tools/test_plugin.py index 1ac164d4..14afa29b 100644 --- a/tests/unit/common/test_tools/test_plugin.py +++ b/tests/unit/common/test_tools/test_plugin.py @@ -25,6 +25,22 @@ def test_assert_metrics__metric_incremented__asserts_expected( ) +def test_assert_metrics__unlabeled_metric_incremented__asserts_expected( + assert_metric: AssertMetricFixture, + test_unlabeled_metric: prometheus_client.Counter, +) -> None: + # Given an unlabeled counter incremented during the test (the fixture + # has already reset the registry). The unlabeled-metric reset previously + # raised AttributeError because `MetricWrapperBase.clear()` only works + # on labeled parents; the fixture now uses `_metric_init` for unlabeled. + + # When the counter is incremented + test_unlabeled_metric.inc() + + # Then the assertion sees a fresh count of 1 + assert_metric(name="pytest_unlabeled_total", labels={}, value=1) + + def test_assert_metrics__after_registry_reset__raises_assertion( test_metric: prometheus_client.Counter, ) -> None: