Hypothesis stateful tests for the metrics event reducer (#78) - #247
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Summary
WalkthroughReduce execution events into explicit counter or histogram operations. Apply those operations through ChangesMetrics operation pipeline
Sequence Diagram(s)sequenceDiagram
participant ExecEvent
participant MetricsHook
participant metric_operations
participant MetricsCollector
ExecEvent->>MetricsHook: submit execution event
MetricsHook->>metric_operations: reduce event
metric_operations-->>MetricsHook: return operations
MetricsHook->>MetricsCollector: apply counter or histogram operation
Suggested labels: Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 inconclusive)
✅ Passed checks (18 passed)
📋 Issue PlannerLet us write the prompt for your AI agent so you can ship faster (with fewer bugs). View plan for ticket: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Reviewer's GuideRefactors 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 reducersequenceDiagram
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
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
cuprum/adapters/metrics_adapter.pycuprum/unittests/__snapshots__/test_maturin_build.ambrcuprum/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)
|
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 #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:
None of those are regressions from this PR, so they are flagged rather than folded in. |
|
Both findings verified against current code and fixed in bf74ced. Neither was stale. 1. PEP 695 aliasValid. The project targets type _MetricOp = _CounterOp | _HistogramOpI checked the one thing that could have made this unsafe: a 2.
|
| 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 |
|
@coderabbitai The Linked Issues check is right that the PR is narrower than #78's title, and I have resolved the gap rather than restating the scope. #78 is titled "Hypothesis stateful tests for metrics/tracing/logging hooks", but its body scopes the work to
So tracing was already covered and only logging was outstanding. I have raised #252 for it. Worth noting why that follow-up is not a fourth state machine: Keeping this PR to the metrics reducer therefore matches #78's body, and #252 carries the remainder of its title. |
|
Tip For best results, initiate chat on the files or code changes.
🐇🔍 ✏️ Learnings added
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
docs/cuprum-design.md (1)
1475-1477: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winUse one complete phase contract in both documents.
The shared event contract omits
stdinandstdin_error, although the metrics reducer handles them. Both statements about the “documented phase contract” are therefore incomplete.
docs/cuprum-design.md#L1475-L1477: Add the supported phases to the event and metrics descriptions, or scope the wording to the adapter's phase set.docs/developers-guide.md#L282-L284: Apply the same phase-contract wording used in the design document.🤖 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 `@docs/cuprum-design.md` around lines 1475 - 1477, The phase-contract wording is inconsistent because the shared event contract omits stdin and stdin_error while the metrics reducer supports them. Update docs/cuprum-design.md lines 1475-1477 and docs/developers-guide.md lines 282-284 with the same complete phase-contract wording, either listing stdin and stdin_error among supported phases or explicitly scoping the statements to the adapter’s phase set.cuprum/unittests/test_metrics_adapter_stateful.py (1)
261-277: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUse a named exception for the simulated collector failure.
Replace the generic
RuntimeErrorwith the package exception hierarchy or a narrowly named test exception ending inError. Assert that exact exception type in the failure-path test. Keep the injected backend failure explicit.As per coding guidelines, “Raise specific built-in exceptions or domain-specific exceptions instead of generic
Exceptionor catch-allRuntimeError.”Also applies to: 280-303
🤖 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/unittests/test_metrics_adapter_stateful.py` around lines 261 - 277, Update _FailingHistogramCollector.observe_histogram to raise a specific named exception, using the package exception hierarchy or a narrowly scoped test exception ending in Error, while keeping the injected backend rejection explicit. Update the failure-path test covering this collector to assert that exact exception type instead of RuntimeError.Source: Coding guidelines
🤖 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.
Outside diff comments:
In `@cuprum/unittests/test_metrics_adapter_stateful.py`:
- Around line 261-277: Update _FailingHistogramCollector.observe_histogram to
raise a specific named exception, using the package exception hierarchy or a
narrowly scoped test exception ending in Error, while keeping the injected
backend rejection explicit. Update the failure-path test covering this collector
to assert that exact exception type instead of RuntimeError.
In `@docs/cuprum-design.md`:
- Around line 1475-1477: The phase-contract wording is inconsistent because the
shared event contract omits stdin and stdin_error while the metrics reducer
supports them. Update docs/cuprum-design.md lines 1475-1477 and
docs/developers-guide.md lines 282-284 with the same complete phase-contract
wording, either listing stdin and stdin_error among supported phases or
explicitly scoping the statements to the adapter’s phase set.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8d5f389d-4c42-4a5a-bf63-c4106053e3d5
📒 Files selected for processing (4)
cuprum/adapters/metrics_adapter.pycuprum/unittests/test_metrics_adapter_stateful.pydocs/cuprum-design.mddocs/developers-guide.md
🔗 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)
|
Both outstanding findings are addressed in 1. One complete phase contract in both documents — takenVerified first. The finding is right that the shared event contract was the weaker Rather than scope the wording down to an adapter subset — which would have I extended the fix to two places the finding did not cite, because they
The new wording also states the consequence, which is the part that 2. Named exception for the simulated collector failure — takenChecked what production does with a collector failure before choosing, and Hook exceptions are not isolated. So a bare
Non-vacuity check: replacing 3. A claim the code contradicts — found while verifying (2)Not in either review. Three places asserted that
That is backwards.
Bearing on the #243 / #244 counter declinesBoth PRs declined an "add metrics counters" recommendation on the grounds If anything the declines were understated. Because the exception is Gates
|
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>
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>
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>
The pre-merge Developer Documentation check noted that no developer-guide entry covers the new _metric_operations seam; only the design document records it. Extend the observability section, beside the existing note that MetricsHook consumes ExecEvent values, with the split this branch introduces: the pure reducer decides what to record, _apply is the only step that reaches the collector, and the stateful test drives random event streams through it against an independent phase-count oracle. Records the two consequences a future change must preserve — labels are projected only when an operation is yielded, and an unrecognized phase raises rather than being dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_PHASE_COUNTERS` is documented as the single definition of these metric names, but a plain dict leaves that claim unenforced: any importing module could rewrite an entry and silently redirect a counter. Wrap it in `types.MappingProxyType` so the mapping matches its stated contract, per the project's preference for immutable module-level data. The annotation widens to `cabc.Mapping` accordingly; only `.get` is used at the call site, so nothing else changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the last gap in #78's title. Tracing already had stateful coverage before this PR and metrics gained it here, leaving the logging hook as the only adapter with no randomised event coverage. Use `@given` properties rather than a fourth state machine, because `structured_logging_hook` holds no state: it maps a phase to a level, builds an `extra` mapping, and emits one record per event. A state machine would generate interleavings that cannot distinguish any two implementations, since nothing carries between events. Its real risks are per-event and shape-dependent, and that is what the properties pin: one record per event, each phase at its configured level, every attached field `cuprum_`-prefixed so it cannot shadow a reserved `LogRecord` attribute, a total message formatter, and a JSON round trip. Two of the five properties were vacuous when first written, which mutation testing caught rather than review. The JSON property generated only string tag values, so removing both `_json_serializable` and `default=str` still passed; `ExecEvent.tags` is typed `Mapping[str, object]`, so the generator now produces values that are not JSON-native, which is what those two guards exist for. The level property accepted any configured level, so dropping a phase from the map and silently falling back to DEBUG also passed; it now derives the expected level independently per phase. All four mutants fail the corrected properties: an unprefixed extra key, an empty message for an unknown phase, no JSON coercion, and a phase dropped from the level map. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A bare assert on a shrunk Hypothesis example reports only that two values differed, which is the least useful moment to lose the phase, the byte count, or the expected operations. Attach a message to each, carrying the inputs that produced the failure and the values on both sides. Verified with an AST walk rather than a grep: no `ast.Assert` in the module is left without a `msg`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An exit event yields a failure counter and a duration observation as two independent collector calls, so a collector that raises on the second records a failure without its duration. That was true but undocumented and untested, which left it looking like an oversight rather than a decision. State the contract: the calls are independent and ordered, no atomicity is attempted, and what already landed stays. Atomicity is not achievable here — the collector wraps an arbitrary backend, and buffering to apply together would only move the problem while delaying when metrics appear. Note where the exception goes: `_emit_exec_event` catches it, logs `observe_hook_failed`, and lets the command continue, because a broken metrics backend must not fail the user's command. Pin it with a collector whose histogram writes fail, asserting the counter remains, the observation does not, and the error reaches the caller. Verified by mutation: reversing the operation order fails it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three review findings on the metrics adapter tests. `emit` retyped the four phase-to-counter pairs a third time, after the production `_PHASE_COUNTERS` and the module's own `_UNIT_COUNTER_PHASES`. Key the existing list once and look up through it, so the stateful oracle and the parametrized cases cannot drift. It stays a test-local restatement rather than an import of the adapter's table: an oracle reading the production mapping would agree with it by construction and could not catch a wrong metric name. Dispatch the phases with `match`/`case`, matching the reducer's own style, and give the fall-through an explicit arm — `plan` and an uncounted `stdin` both leave the oracle unchanged, which was previously only implied by the absence of a branch. Caption the metrics-dispatch diagram, which was the only one in the file without one. The number is provisional; `#251` tracks renumbering. Document the non-atomic application contract outside the docstring. An `exit` event applies two independent collector calls in a fixed order, so a collector that raises on the second leaves the first applied — which is something a collector implementer needs before writing one, not something to discover from a source docstring. Add the screen-reader description the figure was also missing, and a pointer from the developers' guide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dispatch contract said a collector should treat each call as "independent and idempotent-safe". The second half is unsupported: `inc_counter` and `observe_histogram` receive no event or operation identifier, so a collector has nothing to deduplicate on and a repeated call increments again. Say what is actually true instead — calls are independent and ordered — and state the absent guarantee explicitly rather than leaving it inferred. The adapter never retries a failed call either, which is why a partial application stays partial; a collector wanting exactly-once has to get the identity from somewhere else. Corrected in all three places the contract is stated: the design document, the `MetricsHook` docstring, and the developers' guide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two documentation claims about the metrics reducer were incomplete or wrong, and one test asserted the weaker of two available contracts. **The phase contract was under-stated.** `ExecPhase` has seven members, and `_metric_operations` has an arm for every one of them, but the shared event contract listed only five — omitting `stdin` and `stdin_error`. Both documents then leant on "the documented phase contract" to describe a reducer that is in fact total over the whole of `ExecPhase`. Name the seven phases where the event contract is introduced, and use one wording in both the design document and the developers' guide. The users' guide's `cuprum_phase` value list had the same five-phase gap; the structured logging adapter is fail-open, so it really does emit those records. **The escalation path was documented backwards.** Three places claimed `_emit_exec_event` "lets the command continue", so that a broken metrics backend cannot fail a user's command. It does not. It logs `observe_hook_failed`, wraps the error in `_ExecEventEmissionError` to carry already-scheduled observe tasks through cleanup, and `_StageObservation.emit` unwraps that and re-raises the collector's original exception — the command dies with it. State that instead, and say what follows: because the reducer's phase match is fail-closed, adding an `ExecPhase` value without an arm would raise for every caller that has already registered `MetricsHook`. **The failure-path test now asserts that.** The simulated backend failure was a bare `RuntimeError`, which `pytest.raises` cannot distinguish from an incidental one. Give it a named `_MetricsBackendError`, following the test-local exception convention in `test_cqrs_helpers.py`, and add a behavioural case driving a real command through a failing collector: the backend's own exception type reaches the caller of `run_sync` unchanged. Removing the re-raise in `_pipeline_types.py` turns that case red, so it pins the contract rather than restating the hook's internals. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fe8f8b8 to
1bb01ad
Compare
|
@coderabbitai Have the following failed checks now been resolved? If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures. Do not treat warnings as optional or aspirational. Where a change is out of scope for this PR, propose a GitHub issue unless one exists already. (Treat o11y, code safety, documentation and validation coverage as in scope). #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. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== Issue `#78` =='
gh issue view 78 --repo leynos/cuprum --json number,state,title,body,url \
--jq '{number,state,title,url,body}' || true
printf '%s\n' '== Relevant test files =='
fd -a -t f '^(test_metrics_adapter_stateful|test_tracing_span_stateful|test_logging_adapter_properties)\.py$' cuprum/unittests
printf '%s\n' '== Relevant symbol outlines =='
for file in \
cuprum/adapters/metrics_adapter.py \
cuprum/adapters/tracing_adapter.py \
cuprum/adapters/logging_adapter.py \
cuprum/unittests/test_metrics_adapter_stateful.py \
cuprum/unittests/test_tracing_span_stateful.py \
cuprum/unittests/test_logging_adapter_properties.py
do
if [ -f "$file" ]; then
echo "--- $file"
ast-grep outline "$file" --items all || true
fi
done
printf '%s\n' '== Direct coverage evidence =='
rg -n -C 3 \
'RuleBasedStateMachine|_active_spans|active\.keys|_active_spans\.keys|assert .*not in .*_active_spans|`@given`|structured_logging_hook|JsonLoggingFormatter|cuprum_' \
cuprum/adapters/tracing_adapter.py \
cuprum/unittests/test_metrics_adapter_stateful.py \
cuprum/unittests/test_tracing_span_stateful.py \
cuprum/unittests/test_logging_adapter_properties.py \
|| trueLength of output: 37274
Verify these completed areas:
Do not add a logging state machine. Do not open a follow-up issue. No remaining work exists for this check. Close You are interacting with an AI system. |
The rebase onto main brought in the metrics reducer and the two adapter property suites from #247, which enumerate `ExecPhase` and the counter table independently of production so a wrong name cannot pass by construction. Those enumerations stopped at `exit` and so never saw `pipeline_fail_fast`, leaving the new arm and its counter unexercised by the very oracles written to catch a bad mapping. Widen both enumerations and give the logging level property its own `fail_fast_level` expectation, sampled rather than defaulted so the mapping is checked and not merely observed. Adding the phase to the level suite failed until the expected-level map gained an arm, which is the point of deriving it independently. Also reconcile the documented lists the two branches each half-updated: the metrics counter set, the tracing drop list, the configurable log levels, and the `cuprum_phase` values.
The rebase onto main brought in the metrics reducer and the two adapter property suites from #247, which enumerate `ExecPhase` and the counter table independently of production so a wrong name cannot pass by construction. Those enumerations stopped at `exit` and so never saw `pipeline_fail_fast`, leaving the new arm and its counter unexercised by the very oracles written to catch a bad mapping. Widen both enumerations and give the logging level property its own `fail_fast_level` expectation, sampled rather than defaulted so the mapping is checked and not merely observed. Adding the phase to the level suite failed until the expected-level map gained an arm, which is the point of deriving it independently. Also reconcile the documented lists the two branches each half-updated: the metrics counter set, the tracing drop list, the configurable log levels, and the `cuprum_phase` values.
The rebase onto main brought in the metrics reducer and the two adapter property suites from #247, which enumerate `ExecPhase` and the counter table independently of production so a wrong name cannot pass by construction. Those enumerations stopped at `exit` and so never saw `pipeline_fail_fast`, leaving the new arm and its counter unexercised by the very oracles written to catch a bad mapping. Widen both enumerations and give the logging level property its own `fail_fast_level` expectation, sampled rather than defaulted so the mapping is checked and not merely observed. Adding the phase to the level suite failed until the expected-level map gained an arm, which is the point of deriving it independently. Also reconcile the documented lists the two branches each half-updated: the metrics counter set, the tracing drop list, the configurable log levels, and the `cuprum_phase` values.
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/_HistogramOprecords describe the intended operations; a_PHASE_COUNTERSlookup collapses the unit-counter phases.MetricsHook.__call__now applies the reducer's operations, resolving labels only when there is at least one operation — soplan(and an unknown phase) still compute no labels, as the existing tests require._increment/_record_stdin_bytes/_record_exithelpers are removed; behaviour is unchanged andtest_metrics_adapter.pystill passes.Tests
cuprum/unittests/test_metrics_adapter_stateful.py:start/stdout/stderr/stdin_error;planyields nothing;stdinyields a bytes counter only when a byte count is present;exitcounts a failure only for a non-zero code and a duration only when measured; an unknown phase raises.RuleBasedStateMachinestreams random events (all seven phases, phase-appropriate fields) through a realMetricsHook/InMemoryMetricsand, 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
#78is titled "Hypothesis stateful tests for metrics/tracing/logging hooks".Its body scopes the work to
cuprum/adapters/metrics_adapter.py, but taking thetitle at its word, all three adapters now have randomised event coverage:
Table 1: verification shape for each observe hook, and why
tracing_adapter.pytest_tracing_span_stateful.py(pre-existing)_active_spans, so correlation and drain are the risksmetrics_adapter.pytest_metrics_adapter_stateful.py(added here)logging_adapter.pytest_logging_adapter_properties.py(added here)@givenproperties — holds no state at allThe logging hook gets properties rather than a fourth state machine because it
carries nothing between events: one record in, one record out. Interleavings
cannot distinguish any two implementations of it. Its risks are per-event and
shape-dependent — a reserved-
LogRecordcollision, a phase falling through thelevel map, a tag value the JSON formatter cannot serialize — and the five
properties pin exactly those.
On active map draining: only
TracingHookhas an active map.metrics_adapter.pyhas none, and neither does the logging hook, so the claimapplies to tracing alone — where
test_tracing_span_stateful.pyasserts itdirectly, cross-checking
hook._active_spansagainst a model after every stepand pinning that an exit removes only its own execution's span.
#252 was raised to track the logging work while it was still outstanding; it is
now delivered here and can be closed.
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:
_metric_operationsreducer that maps execution events to counter and histogram operations for the metrics hook.Enhancements:
MetricsHook.__call__to delegate to the shared reducer and a generic operation applier, avoiding label computation for no-op events.