Skip to content

Hypothesis stateful tests for the metrics event reducer (#78) - #247

Open
leynos wants to merge 3 commits into
mainfrom
python-metrics-adapter-tests
Open

Hypothesis stateful tests for the metrics event reducer (#78)#247
leynos wants to merge 3 commits into
mainfrom
python-metrics-adapter-tests

Conversation

@leynos

@leynos leynos commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

MetricsHook.__call__ was a phase-dispatch that reached straight into the collector, with no seam to verify which counters and histograms each event yields across varied phase/order/pid combinations (#78).

Seam

Extract the pure event-to-operation reducer _metric_operations(event) -> tuple[_MetricOp, ...]:

  • _CounterOp / _HistogramOp records describe the intended operations; a _PHASE_COUNTERS lookup collapses the unit-counter phases.
  • MetricsHook.__call__ now applies the reducer's operations, resolving labels only when there is at least one operation — so plan (and an unknown phase) still compute no labels, as the existing tests require.
  • The former _increment/_record_stdin_bytes/_record_exit helpers are removed; behaviour is unchanged and test_metrics_adapter.py still passes.

Tests

cuprum/unittests/test_metrics_adapter_stateful.py:

  • Property tests pin the operations produced per phase — unit counters for start/stdout/stderr/stdin_error; plan yields nothing; stdin yields a bytes counter only when a byte count is present; exit counts a failure only for a non-zero code and a duration only when measured; an unknown phase raises.
  • A Hypothesis RuleBasedStateMachine streams random events (all seven phases, phase-appropriate fields) through a real MetricsHook/InMemoryMetrics and, after every step, checks the accumulated counters and histograms against an independent phase-count oracle (not the reducer). This proves counters and observations are created exactly when intended, and only then, across arbitrary event orders.

Scope note

This addresses the reducer extraction and verification for the named target, cuprum/adapters/metrics_adapter.py. The metrics hook is stateless per event (no active-span map); the "active maps drain" concern applies to the tracing adapter and is out of scope for this file.

Validation

Full gates green: make check-fmt, make lint (ruff, interrogate 100%, pylint 10.00/10), make test (764 passed / 47 skipped incl. the existing metrics suite; Rust nextest 57/57). Wheel-manifest snapshot regenerated.

Closes #78

🤖 Generated with Claude Code

Summary by Sourcery

Extract a pure event-to-metrics reducer for the metrics hook and add property-based and stateful tests to verify metrics behavior across execution phases.

New Features:

  • Introduce a pure _metric_operations reducer that maps execution events to counter and histogram operations for the metrics hook.
  • Add Hypothesis-based property and stateful tests that validate metrics accumulation over randomized execution event streams.

Enhancements:

  • Refactor MetricsHook.__call__ to delegate to the shared reducer and a generic operation applier, avoiding label computation for no-op events.

MetricsHook.__call__ was a phase-dispatch that reached straight into the
collector, with no seam to verify which counters and histograms each
event yields across varied phase/order/pid combinations.

Extract the pure event-to-operation reducer _metric_operations(event) ->
tuple[_MetricOp, ...] (with _CounterOp/_HistogramOp records and a
_PHASE_COUNTERS lookup for the unit-counter phases). __call__ now applies
the reducer's operations, resolving labels only when there is at least
one operation, so plan and unknown phases still compute no labels. The
former _increment/_record_stdin_bytes/_record_exit helpers are removed;
behaviour is unchanged and the existing test_metrics_adapter.py suite
still passes.

Add cuprum/unittests/test_metrics_adapter_stateful.py:
- property tests pinning the operations produced per phase (unit
  counters, plan no-op, stdin bytes only when counted, exit failure/
  duration only when present, unknown phase raises);
- a Hypothesis RuleBasedStateMachine that streams random events through a
  real MetricsHook/InMemoryMetrics and checks the accumulated counters
  and histograms against an independent phase-count oracle — proving
  counters and observations are created exactly when intended.

Regenerate the maturin wheel-manifest snapshot for the new test file.

Closes #78

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Metrics collection now reduces execution events into explicit counter or histogram operations before applying them through MetricsHook. New unit and Hypothesis stateful tests validate phase handling, optional fields, unknown phases, accumulation, and wheel packaging.

Changes

Metrics operation pipeline

Layer / File(s) Summary
Reduce events into metric operations
cuprum/adapters/metrics_adapter.py
Immutable operations and reducers map phases and optional event fields to metric updates, raising a structured error for unknown phases.
Apply operations through the metrics hook
cuprum/adapters/metrics_adapter.py
MetricsHook reduces events before extracting labels and dispatches counter and histogram operations to the collector.
Validate reducer and accumulation behaviour
cuprum/unittests/test_metrics_adapter_stateful.py, cuprum/unittests/__snapshots__/test_maturin_build.ambr
Unit, property-based, and stateful tests verify metric output and include the new test module in the wheel snapshot.

Possibly related PRs

  • leynos/cuprum#154 — Updates the same metrics adapter phase dispatch and unhandled-phase handling areas.

Suggested labels: Issue

Poem

Events flow, then counters rise,
Histograms catch durations’ skies.
Unknown phases meet their fate,
Hypothesis checks each stream’s state.
Metrics march in tidy rows.

🚥 Pre-merge checks | ✅ 18 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers metrics only, but issue #78 also asks for tracing and logging hook stateful tests. Add the tracing and logging stateful tests required by #78, or split those requirements into separate linked issues.
Developer Documentation ⚠️ Warning No developer-guide or design/ADR update documents the new _metric_operations seam; git diff shows only code, tests, and snapshot changes. Add docs/developers-guide.md and cuprum-design.md/ADR coverage for the reducer boundary, then sync any related roadmap/execplan checkboxes.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the change and includes the linked issue reference (#78).
Description check ✅ Passed The description accurately summarises the metrics reducer refactor and the new property and stateful tests.
Out of Scope Changes check ✅ Passed The snapshot update and new tests stay within the metrics reducer and test coverage work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Testing (Overall) ✅ Passed New reducer tests cover phase-by-phase outputs, no-op plan/unknown handling, and a state machine checks real hook/collector accumulation against an independent oracle.
User-Facing Documentation ✅ Passed The PR only refactors internal metrics-hook plumbing and adds tests; no user-facing API or behaviour changed, and the users guide already covers observe hooks/metrics generally.
Module-Level Documentation ✅ Passed Approve: both touched Python modules begin with clear docstrings describing purpose and relation to the metrics hook/reducer.
Testing (Unit And Behavioural) ✅ Passed Pass: keep the reducer unit tests for edge cases, and the stateful machine drives the public MetricsHook with a real collector and an independent oracle.
Testing (Property / Proof) ✅ Passed Use Hypothesis property and stateful tests for the new phase/order invariants; the PR already adds them with an independent oracle.
Testing (Compile-Time / Ui) ✅ Passed Treat the compile-time clause as inapplicable: this PR is Python runtime-only and already uses a focused wheel-manifest snapshot.
Unit Architecture ✅ Passed Pass: keep side-effects in MetricsHook, keep _metric_operations pure, and use injected collectors plus stateful tests to expose the boundary.
Domain Architecture ✅ Passed Keep this change: the pure reducer and stateful tests stay inside the metrics adapter; no domain model picks up transport, storage, or vendor concerns.
Observability ✅ Passed Keep existing metrics only; labels stay low-cardinality (program/project) and no new logging, tracing, or alerts were introduced.
Security And Privacy ✅ Passed PASS: The patch adds no secrets, credentials, env vars, auth logic, or broad permissions; tests and snapshot only cover synthetic metrics data.
Performance And Resource Use ✅ Passed Keep the hot path bounded: the reducer emits at most two ops per event, labels are skipped on no-ops, and stateful tests cap examples and steps.
Concurrency And State ✅ Passed InMemoryMetrics owns one lock via _LockedStore, MetricsHook stays stateless, and the Hypothesis state machine exercises random event orders against an independent oracle.
Architectural Complexity And Maintainability ✅ Passed The new op layer is narrow and local; _metric_operations isolates a real seam, _apply keeps collector wiring explicit, and Hypothesis is already in-repo.
Rust Compiler Lint Integrity ✅ Passed PR changes only Python and a snapshot; no Rust source, lint suppressions, or suspicious clone-based ownership changes were introduced.
📋 Issue Planner

Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).

View plan for ticket: #78

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch python-metrics-adapter-tests

Comment @coderabbitai help to get the list of available commands.

codescene-access[bot]

This comment was marked as outdated.

@sourcery-ai

sourcery-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors the metrics adapter to introduce a pure event-to-metric-operations reducer and wires MetricsHook.call through it, then adds focused property tests and a Hypothesis rule-based state machine to verify that metrics counters and histograms are produced exactly as intended across all execution phases and event streams.

Sequence diagram for MetricsHook event-to-operations reducer

sequenceDiagram
    participant ExecEvent
    participant MetricsHook
    participant Reducer as _metric_operations
    participant Collector as MetricsCollector

    ExecEvent ->> MetricsHook: __call__(event)
    MetricsHook ->> Reducer: _metric_operations(event)
    Reducer -->> MetricsHook: tuple[_MetricOp]

    alt no operations
        MetricsHook -->> ExecEvent: return
    else has operations
        MetricsHook ->> MetricsHook: _extract_labels(event)
        loop for each operation
            MetricsHook ->> MetricsHook: _apply(operation, labels)
            alt _CounterOp
                MetricsHook ->> Collector: inc_counter(name, value, labels)
            else _HistogramOp
                MetricsHook ->> Collector: observe_histogram(name, value, labels)
            end
        end
    end
Loading

File-Level Changes

Change Details Files
Introduce pure event-to-operation reducer and small operation types to decouple event logic from the metrics collector.
  • Define immutable _CounterOp and _HistogramOp dataclasses plus the _MetricOp union for describing intended metric operations.
  • Add the _PHASE_COUNTERS mapping for simple unit-counter phases and implement _exit_operations for exit-phase-specific logic.
  • Implement _metric_operations(event) as the single event-to-operations reducer, handling plan/stdin/exit phases and enforcing unknown phases via _UnhandledMetricsPhaseError.
cuprum/adapters/metrics_adapter.py
Rewire MetricsHook to use the reducer and a generic apply path, simplifying phase dispatch and label handling.
  • Replace the match/case phase dispatch in MetricsHook.call with a call to _metric_operations and early-return when there are no operations so labels are not computed for no-op events.
  • Introduce MetricsHook._apply to translate _CounterOp/_HistogramOp instances into collector calls, replacing the previous _increment/_record_stdin_bytes/_record_exit helpers.
  • Remove the old helper methods and ensure behavior remains equivalent by reusing the same counter and histogram names and values.
cuprum/adapters/metrics_adapter.py
Add property-based and stateful tests to pin the reducer’s behavior and cross-check real metric accumulation against an independent oracle.
  • Add focused tests that assert each known phase yields the correct operations, including unit-counter phases, plan/no-op behavior, stdin with/without byte_count, exit combinations of exit_code and duration_s, and unknown-phase error raising.
  • Introduce composable Hypothesis strategies for ExecEvent instances with phase-appropriate fields, used by both property tests and the state machine.
  • Implement a RuleBasedStateMachine that feeds random ExecEvents into a real MetricsHook backed by InMemoryMetrics while maintaining an independent per-phase counter/duration oracle, with invariants asserting the collector’s counters and histograms match the oracle at every step and only expected histograms exist.
  • Register the state machine as TestMetricsAccumulation with custom Hypothesis settings to control the number of examples and steps.
cuprum/unittests/test_metrics_adapter_stateful.py
Regenerate test snapshot metadata to reflect the updated wheel manifest.
  • Update the maturin build snapshot file so snapshot tests remain consistent with the current wheel manifest.
cuprum/unittests/__snapshots__/test_maturin_build.ambr

Assessment against linked issues

Issue Objective Addressed Explanation
#78 Extract a pure event-to-operation reducer from the metrics hook in cuprum/adapters/metrics_adapter.py to make metric behaviour verifiable independently of the collector.
#78 Add Hypothesis-based property and stateful tests for the metrics hook in cuprum/adapters/metrics_adapter.py to systematically exercise varied event phase/order combinations and prove counters and histograms are created exactly when intended.
#78 Add similar Hypothesis stateful tests for tracing and logging hooks (including proving active maps drain correctly) as referenced in the issue title. The PR explicitly scopes its work to the metrics adapter only, adding reducer extraction and stateful tests for MetricsHook and InMemoryMetrics. It does not modify or add tests for tracing or logging hooks, nor does it address active map draining.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-access[bot]

This comment was marked as outdated.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added the Issue label Jul 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cuprum/adapters/metrics_adapter.py`:
- Line 200: Update the _MetricOp type alias to use the PEP 695 type statement
for the _CounterOp | _HistogramOp union, preserving the existing member types
and avoiding the legacy assignment syntax.
- Around line 226-249: Refactor `_metric_operations` to dispatch on
`event.phase` using a `match`/`case` statement rather than the current chained
top-level `if` branches. Preserve the existing behavior for `plan`, mapped
counter phases, `stdin` byte counts, `exit` via `_exit_operations`, and unknown
phases raising `_UnhandledMetricsPhaseError`; keep the nested `stdin` byte-count
check intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4739c9b9-c96c-4693-85fe-4df36863bc7f

📥 Commits

Reviewing files that changed from the base of the PR and between 302858c and 9d352de.

📒 Files selected for processing (3)
  • cuprum/adapters/metrics_adapter.py
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/test_metrics_adapter_stateful.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)

Comment thread cuprum/adapters/metrics_adapter.py Outdated
value: float


_MetricOp = _CounterOp | _HistogramOp

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the PEP 695 type statement for the union alias.

The snapshot confirms requires_python '>=3.12', so declare _MetricOp with the modern type statement instead of a bare assignment.

🔧 Proposed fix
-_MetricOp = _CounterOp | _HistogramOp
+type _MetricOp = _CounterOp | _HistogramOp

As per coding guidelines, "Use the type keyword for type aliases in modern Python; when supporting Python versions before 3.12, retain typing.TypeAlias syntax and add # noqa: UP040."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_MetricOp = _CounterOp | _HistogramOp
type _MetricOp = _CounterOp | _HistogramOp
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cuprum/adapters/metrics_adapter.py` at line 200, Update the _MetricOp type
alias to use the PEP 695 type statement for the _CounterOp | _HistogramOp union,
preserving the existing member types and avoiding the legacy assignment syntax.

Source: Coding guidelines

Comment thread cuprum/adapters/metrics_adapter.py Outdated
Comment on lines +226 to +249
def _metric_operations(event: ExecEvent) -> tuple[_MetricOp, ...]:
"""Map an execution event to the metric operations it should produce.

This is the pure event-to-operation reducer behind
[`MetricsHook.__call__`][cuprum.adapters.metrics_adapter.MetricsHook.__call__]
— the single source of truth for which counters and histogram observations
each phase yields, so the operations can be verified without a collector.
Labels are applied by the caller. ``plan`` yields nothing; a ``stdin`` event
without a byte count yields nothing; an unknown phase is a contract
violation and raises ``_UnhandledMetricsPhaseError``.
"""
phase = event.phase
if phase == "plan":
return ()
counter_name = _PHASE_COUNTERS.get(phase)
if counter_name is not None:
return (_CounterOp(counter_name, 1.0),)
if phase == "stdin":
if event.byte_count is None:
return ()
return (_CounterOp("cuprum_stdin_bytes_total", float(event.byte_count)),)
if phase == "exit":
return _exit_operations(event)
raise _UnhandledMetricsPhaseError(event.phase)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dispatch on event.phase with match/case instead of chained ifs.

event.phase is a closed, discriminated literal — exactly the shape structural pattern matching is meant for. The current chain also stacks four sequential branches at the top level before the final raise.

♻️ Proposed refactor
 def _metric_operations(event: ExecEvent) -> tuple[_MetricOp, ...]:
     """..."""
-    phase = event.phase
-    if phase == "plan":
-        return ()
-    counter_name = _PHASE_COUNTERS.get(phase)
-    if counter_name is not None:
-        return (_CounterOp(counter_name, 1.0),)
-    if phase == "stdin":
-        if event.byte_count is None:
-            return ()
-        return (_CounterOp("cuprum_stdin_bytes_total", float(event.byte_count)),)
-    if phase == "exit":
-        return _exit_operations(event)
-    raise _UnhandledMetricsPhaseError(event.phase)
+    match event.phase:
+        case "plan":
+            return ()
+        case "stdin":
+            if event.byte_count is None:
+                return ()
+            return (_CounterOp("cuprum_stdin_bytes_total", float(event.byte_count)),)
+        case "exit":
+            return _exit_operations(event)
+        case phase if phase in _PHASE_COUNTERS:
+            return (_CounterOp(_PHASE_COUNTERS[phase], 1.0),)
+        case _:
+            raise _UnhandledMetricsPhaseError(event.phase)

As per path instructions, "Prefer structural pattern matching over isinstance() or imperative decomposition" and "Move conditionals with >2 branches to predicate/helper functions." As per coding guidelines, "Use structural pattern matching (match/case) for branching over structured data such as enums, discriminated unions, and pattern-rich data structures."

[source_path_instructions,source_coding_guidelines]

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _metric_operations(event: ExecEvent) -> tuple[_MetricOp, ...]:
"""Map an execution event to the metric operations it should produce.
This is the pure event-to-operation reducer behind
[`MetricsHook.__call__`][cuprum.adapters.metrics_adapter.MetricsHook.__call__]
the single source of truth for which counters and histogram observations
each phase yields, so the operations can be verified without a collector.
Labels are applied by the caller. ``plan`` yields nothing; a ``stdin`` event
without a byte count yields nothing; an unknown phase is a contract
violation and raises ``_UnhandledMetricsPhaseError``.
"""
phase = event.phase
if phase == "plan":
return ()
counter_name = _PHASE_COUNTERS.get(phase)
if counter_name is not None:
return (_CounterOp(counter_name, 1.0),)
if phase == "stdin":
if event.byte_count is None:
return ()
return (_CounterOp("cuprum_stdin_bytes_total", float(event.byte_count)),)
if phase == "exit":
return _exit_operations(event)
raise _UnhandledMetricsPhaseError(event.phase)
def _metric_operations(event: ExecEvent) -> tuple[_MetricOp, ...]:
"""Map an execution event to the metric operations it should produce.
This is the pure event-to-operation reducer behind
[`MetricsHook.__call__`][cuprum.adapters.metrics_adapter.MetricsHook.__call__]
the single source of truth for which counters and histogram observations
each phase yields, so the operations can be verified without a collector.
Labels are applied by the caller. ``plan`` yields nothing; a ``stdin`` event
without a byte count yields nothing; an unknown phase is a contract
violation and raises ``_UnhandledMetricsPhaseError``.
"""
match event.phase:
case "plan":
return ()
case "stdin":
if event.byte_count is None:
return ()
return (_CounterOp("cuprum_stdin_bytes_total", float(event.byte_count)),)
case "exit":
return _exit_operations(event)
case phase if phase in _PHASE_COUNTERS:
return (_CounterOp(_PHASE_COUNTERS[phase], 1.0),)
case _:
raise _UnhandledMetricsPhaseError(event.phase)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cuprum/adapters/metrics_adapter.py` around lines 226 - 249, Refactor
`_metric_operations` to dispatch on `event.phase` using a `match`/`case`
statement rather than the current chained top-level `if` branches. Preserve the
existing behavior for `plan`, mapped counter phases, `stdin` byte counts, `exit`
via `_exit_operations`, and unknown phases raising
`_UnhandledMetricsPhaseError`; keep the nested `stdin` byte-count check intact.

Record the seam this branch extracts in design-doc 8.4, where the
telemetry adapter decisions already live.

Adds an "Event-to-operation reduction" subsection stating why the split
exists — the pure _metric_operations reducer decides what to record and
_apply is the only step that reaches the collector, so the mapping is
property-testable without one — plus the two consequences worth pinning:
labels are projected only when the reducer yields an operation, so a plan
event never touches them, and an unrecognized phase raises rather than
being silently dropped.

The sequence diagram carries a screen-reader caption describing the whole
flow in prose, including the empty-tuple early return and which collector
call each operation variant becomes.

The caption is deliberately unnumbered rather than continuing the Figure N
sequence used elsewhere in section 8. PR #245 renumbers the later figures
in that section, so any number chosen here would be wrong under one merge
order; no prose cross-references figure numbers, and section 13 already
uses unnumbered screen-reader captions. Worth a tidying pass once both
land.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No quality gates enabled for this code.

@pandalump

Copy link
Copy Markdown
Collaborator

Raised #251 to number this diagram once both PRs land.

The §8.4 diagram here is deliberately unnumbered: #245 inserts two figures earlier in §8 and pushes the old 4 and 5 to 6 and 7, so the correct number for this one is Figure 6 if #247 merges first and Figure 8 once #245 lands. Any fixed number would be wrong under one of the two merge orders, so it carries only its screen-reader caption, which is never wrong. No prose cross-references figure numbers, so nothing breaks in the interim.

#251 records the expected end state (Figures 3–8 with sections and subjects) and asks for a check that #245's renumbering survived the merge, since both PRs touch §8 prose.

While auditing for that issue I found three adjacent things and put them in #251 as decisions rather than acting on them here:

  • docs/cuprum-design.md has 8 Mermaid diagrams but only 3 numbered figures; §8 numbers without screen-reader captions and §13 does the reverse.
  • One diagram (§"Implementation notes for the first iteration", ~line 548) has neither caption, which contravenes both "caption every diagram" and "add screen reader descriptions before complex diagrams" in the documentation style guide. That is the only real accessibility gap of the eight and is worth fixing regardless of the numbering decision.
  • Numbering starts at 3 — there is no Figure 1 or 2 — and the style guide's own example uses the italic _Figure N: ..._ form while this document uses plain Figure N:.

None of those are regressions from this PR, so they are flagged rather than folded in.

Two review findings, both valid against current code.

_MetricOp used the legacy assignment form for its union. The project
targets Python 3.12 and already declares aliases with the PEP 695 type
statement in cuprum/sh.py and cuprum/events.py, so this now matches. The
alias is used only in annotations, and the module has postponed
evaluation, so the lazy TypeAliasType introduces no runtime concern.

_metric_operations dispatched through chained top-level ifs. Restore a
match/case on the phase, which is the idiom used elsewhere in this module
(_apply) and in _subprocess_timeout. The mapped unit-counter phases stay
keyed by _PHASE_COUNTERS behind a guard clause rather than being repeated
as a literal alternation in the pattern, so the metric names keep exactly
one definition and cannot drift from the table. plan, the nested stdin
byte-count check, exit via _exit_operations, and the unknown-phase
_UnhandledMetricsPhaseError all behave as before.

An earlier revision of this function was converted away from match to
satisfy the complexity and return-count lints; keeping the table lookup as
a guard clause rather than expanding it into separate cases holds the
structure under both limits, and ruff is clean.

Behaviour is unchanged, verified by mutation rather than assumed: dropping
the stdin byte-count check and silently returning for an unknown phase
each fail the existing property and stateful tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pandalump

Copy link
Copy Markdown
Collaborator

Both findings verified against current code and fixed in bf74ced. Neither was stale.

1. PEP 695 alias

Valid. The project targets >=3.12, and PEP 695 type statements are already the established form here — four in cuprum/sh.py and two in cuprum/events.py (including ExecPhase, which this module consumes). The legacy assignment was the outlier.

type _MetricOp = _CounterOp | _HistogramOp

I checked the one thing that could have made this unsafe: a type alias produces a lazy TypeAliasType, which breaks runtime uses such as isinstance or a class pattern. _MetricOp appears only in annotations (_exit_operations, _metric_operations, _apply, and one list[_MetricOp] local), and the module has postponed evaluation, so there is no runtime impact. make typecheck is clean.

2. match/case dispatch

Valid, and it restores the idiom used elsewhere — MetricsHook._apply in this same module, and _subprocess_timeout.

One deliberate detail worth flagging. The mapped unit-counter phases stay keyed by _PHASE_COUNTERS behind a guard clause rather than being repeated as a literal alternation:

match phase:
    case "plan":
        return ()
    case "stdin":
        if event.byte_count is None:
            return ()
        return (_CounterOp("cuprum_stdin_bytes_total", float(event.byte_count)),)
    case "exit":
        return _exit_operations(event)
    case _ if (counter_name := _PHASE_COUNTERS.get(phase)) is not None:
        return (_CounterOp(counter_name, 1.0),)
    case _:
        raise _UnhandledMetricsPhaseError(phase)

Writing case "start" | "stdout" | "stderr" | "stdin_error": instead would duplicate the table's key list in the pattern, giving two places to update when a phase is added. The guard keeps the metric names defined exactly once. The guard is also order-safe: none of the table's keys overlap plan, stdin, or exit.

There is history here worth recording. An earlier revision of this function was a match over all eight phases, and was converted to chained ifs because ruff flagged C901 (complexity 9 > 8) and PLR0911 (8 returns > 6). Keeping the table lookup as a single guard clause rather than expanding it into four separate cases holds the structure under both limits — ruff is clean on this form.

plan, the nested stdin byte-count check, exit via _exit_operations, and the unknown-phase _UnhandledMetricsPhaseError all behave exactly as before.

Behaviour preserved, verified rather than assumed. Two mutations of the new dispatch are both caught by the existing suite:

  • dropping the stdin byte-count check → test_stdin_yields_bytes_counter_only_when_counted and the stateful machine fail;
  • returning () for an unknown phase instead of raising → both test_unknown_phase_raises_structured_error tests fail with DID NOT RAISE.

Validation

Command Outcome
uv run pytest cuprum/unittests/test_metrics_adapter.py 13 passed
uv run pytest cuprum/unittests/test_metrics_adapter_stateful.py 9 passed
make check-fmt pass
make lint pass (ruff — no C901/PLR0911 — interrogate 100%, pylint 10.00/10, clippy)
make typecheck pass (ty clean, confirming the lazy alias is fine)
make test pass — Rust nextest 57/57, full Python suite green
make markdownlint / make nixie pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hypothesis stateful tests for metrics/tracing/logging hooks (cuprum/adapters/metrics_adapter.py)

2 participants