diff --git a/.github/workflows/package-validation.yaml b/.github/workflows/package-validation.yaml index 302ace3..e26d5f0 100644 --- a/.github/workflows/package-validation.yaml +++ b/.github/workflows/package-validation.yaml @@ -97,6 +97,19 @@ jobs: local baseline_env="$2" local baseline="${!baseline_env:-}" local args=() + # Pin this validation-only pack's own Version to the nuget.org baseline (never + # published - PACK_OUTPUT never leaves this job) whenever one exists. Without this, + # an unversioned `dotnet pack` always defaults to 1.0.0.0, which fails ApiCompat's + # CP0003 ("assembly version should be equal to or higher than baseline") the moment + # the real published baseline crosses 1.0.0 - as happened once 1.1.0-preview.103 was + # published, breaking every subsequent PR's package-validation regardless of content. + # Equal-to-baseline (not higher) is deliberate: this step only needs ApiCompat to run + # its actual signature-diff (additions are never flagged; only removals/incompatible + # changes are), not to assert a specific real version bump - that's computed by the + # separate publish-preview.yaml/publish-release.yaml pipelines at actual publish time. + if [ -n "$baseline" ]; then + args+=("-p:Version=${baseline}") + fi if [ "$BREAKING_CHANGE" != "true" ] && [ -n "$baseline" ]; then args+=("-p:PackageValidationBaselineVersion=${baseline}") fi diff --git a/docs/adr/0044-compono-testdoubles-v2-overloads-generics-verification.md b/docs/adr/0044-compono-testdoubles-v2-overloads-generics-verification.md index 2869c7a..6eb0528 100644 --- a/docs/adr/0044-compono-testdoubles-v2-overloads-generics-verification.md +++ b/docs/adr/0044-compono-testdoubles-v2-overloads-generics-verification.md @@ -2601,3 +2601,209 @@ three-arg case sketched above. risk checks, not a general competitive benchmark). - `docs/packages/compono-testdoubles.md`, `skills/compono/references/testdoubles.md` — updated once implemented (PLAN-0044 Phase 4), not by this ADR directly. + +## Amendment 22 (2026-09-06): `CallVerifier.AtLeast(int)`/`AtMost(int)` added; Requirement 3's minimality preserved, not reversed + +[RESEARCH-0023](../research/0023-compono-logging-1.1-research.md), +[RESEARCH-0024](../research/0024-compono-http-1.1-research.md), and +[RESEARCH-0025](../research/0025-compono-testdoubles-1.1-research.md) +established a fact Requirement 3's original design (2026-08-14) had no +occasion to consider: `Compono.CallVerifier` is no longer a +`Compono.TestDoubles`-only implementation detail. `Compono.Http` +(`HttpResponseRegistration.Verify()`, `src/Compono.Http/HttpResponseRegistration.cs:56`) +returns it directly, and `Compono.Logging` (`LogVerificationBuilder`, +`src/Compono.Logging/LogVerificationBuilder.cs`) constructs one internally +via a private `ToCallVerifier()` and hand-forwards `Once`/`Never`/`Exactly` +through its own fluent surface. Three packages now share this exact +vocabulary. [RESEARCH-0027](../research/0027-compono-callverifier-atleast-atmost-investigation.md) +is the deep-design pass behind this amendment. + +**What Requirement 3 actually rejected, re-read precisely.** The original +text (line 395 of this ADR, quoted verbatim): *"Deliberately minimal, +matching the explicit instruction: `Never`/`Once`/`Exactly(n)` only — no +`AtLeast`/`AtMost`, no argument-aware recording, no call-order +verification, no `ReceivedCalls()`-style enumeration, no strict mode."* +`AtLeast`/`AtMost` were named, but named as one line item in a bundle +whose real target — per the surrounding rationale ("don't allocate just to +support `Once()`", "no dictionary... no allocation per call") — was +argument-aware recording, ordering, and enumeration: capabilities that +would have required new storage, new generated dispatch code, or a bigger +verification DSL. `AtLeast`/`AtMost` do not share that cost profile: both +read the same `observedCount` int `CallVerifier` already receives in its +constructor (`src/Compono/CallVerifier.cs`) — no new field on +`ReturnConfig`, no new generated dispatch code, no additional +`Interlocked` operation, no allocation. This amendment reads Requirement +3's original rejection as bundled-in-with a much more expensive Option 3 +("a full `Received()`-equivalent"), not as an independent verdict that +count-range assertions are undesirable on their own terms. + +**Decision:** add exactly two new instance methods to `CallVerifier`, +nothing else: + +```csharp +public readonly struct CallVerifier(int observedCount, string memberDescription) +{ + public void Never() => Exactly(0); + public void Once() => Exactly(1); + + public void Exactly(int times) { /* unchanged */ } + + public void AtLeast(int times) + { + if (observedCount < times) + throw new TestDoubleVerificationException( + $"Expected at least {times} call(s) to {memberDescription}, but received {observedCount}."); + } + + public void AtMost(int times) + { + if (observedCount > times) + throw new TestDoubleVerificationException( + $"Expected at most {times} call(s) to {memberDescription}, but received {observedCount}."); + } +} +``` + +Failure-message wording matches `Exactly`'s existing shape exactly +("Expected {qualifier} {times} call(s) to {memberDescription}, but +received {observedCount}."), same `TestDoubleVerificationException` type — +no new exception type, no message-format drift. + +**Explicitly rejected, per RESEARCH-0027 and the explicit instruction not +to re-enlarge the vocabulary:** `Between(min, max)`, `AtLeastOnce()`, +`AtMostOnce()`, `Any()`, `None()`. Every one of these is either directly +derivable from `AtLeast`/`AtMost`/`Never` at the call site +(`AtLeastOnce()` is `AtLeast(1)`; `Between(a, b)` is two calls or a tiny +consumer-side helper) or adds a synonym for an existing terminal +(`Any()`/`None()` duplicate `Never()`'s job under a different name). +Requirement 3's "one small, concrete... slot, no dictionary" architecture +stays intact — this amendment completes the lower-bound/upper-bound count +vocabulary `Exactly` already sits inside, it does not open a general +verification DSL. + +**Negative-count semantics — Option A chosen, no new validation on any +count-taking method, including the two new ones.** `Exactly(int)` has +never validated its argument — `Exactly(-1)` today is simply an assertion +that can never pass (impossible to observe -1 calls), not an +`ArgumentOutOfRangeException`. Three options were considered: + +- **A. No validation on any count-taking method** (chosen) — `AtLeast(-1)` + and `AtMost(-1)` behave the same way `Exactly(-1)` already does: a + vacuously-true or vacuously-false assertion, never a thrown + `ArgumentException`. Preserves `Exactly`'s existing, released, pre-this- + amendment behavior exactly, and keeps the three sibling methods + behaviorally consistent with each other. +- **B. Validate only the two new methods.** Rejected: three sibling + methods on the same struct with inconsistent argument-validation + contracts (two throw `ArgumentOutOfRangeException` up front, one doesn't) + is a worse API than either extreme, and gives a consumer no way to guess + which is which without reading the source. +- **C. Validate all three, including changing `Exactly(int)`.** Rejected + outright — this is a real post-1.0 behavioral change to already-shipped, + released behavior (a call that used to reach the "0 != -1" comparison + and throw `TestDoubleVerificationException` would instead throw + `ArgumentOutOfRangeException` before ever reaching that comparison), for + a case (`Exactly(-1)`) with no evidence anyone relies on either behavior + but that this ADR has no license to change silently as a side effect of + an unrelated addition. + +`AtLeast(0)` is meaningful and well-defined (always passes — every count +is at least zero) and is not rejected as redundant; it is a legitimate, +if rarely-needed, no-op-shaped assertion, consistent with not adding +special-case validation. `AtMost(0)` is behaviorally identical to +`Never()` (`observedCount > 0` vs. `observedCount != 0` are equivalent +when `observedCount` can never be negative, which it never can — it's an +`Interlocked.Increment`-only counter starting at zero) — both are kept: +`Never()` remains the discoverable, self-documenting spelling for the +common case, `AtMost(0)` is the mechanical consequence of a general +upper-bound primitive existing at all, matching this ADR's existing +"prefer the general primitive, don't special-case away its edge" posture +elsewhere (e.g. Requirement 1's per-overload identity applies uniformly +rather than special-casing single-overload members). + +**`Compono.Logging` consequence.** `Compono.TestDoubles`'s generated +`Verify()` extension and `Compono.Http`'s `HttpResponseRegistration.Verify()` +both return `Compono.CallVerifier` directly today, so both packages gain +`AtLeast`/`AtMost` automatically the moment this amendment's two methods +exist — zero code changes in either package. `Compono.Logging` does not: +`LogVerificationBuilder` (`src/Compono.Logging/LogVerificationBuilder.cs`) +deliberately keeps `CallVerifier` off its own public surface (per that +type's doc comment, "`CallVerifier` itself is never part of this type's +public API") and hand-forwards `Once()`/`Never()`/`Exactly(int)` through +its private `ToCallVerifier()` bridge. Making `AtLeast`/`AtMost` reach +`Compono.Logging` consumers requires two mechanical one-line companion +forwarders on `LogVerificationBuilder`, following the exact pattern its +existing three terminals already use: + +```csharp +public void AtLeast(int times) => ToCallVerifier().AtLeast(times); +public void AtMost(int times) => ToCallVerifier().AtMost(times); +``` + +This amendment records the consequence and the exact shape of the fix; +it does not itself amend [ADR-0055](0055-compono-logging-testing-support-package.md) +(the ADR that owns `LogVerificationBuilder`'s design) — ADR-0055's own +Decision/Consequences text is not being corrected or reversed by anything +here, and per this repo's documentation convention, `LogVerificationBuilder`'s +own doc comment and ADR-0055's cross-referenced current-state docs get +updated at implementation time, not preemptively by an ADR-0044 amendment +that isn't ADR-0055's own record. + +**Completion criteria for implementation of this amendment** (not +satisfied by this amendment itself — this is a design decision, not an +implementation): + +- Core `Compono` unit tests for `AtLeast`/`AtMost` (pass/fail boundaries, + `AtLeast(0)`, `AtMost(0)` vs. `Never()` equivalence, message wording). +- `Compono.TestDoubles` and `Compono.Http` usage tests confirming the new + methods are reachable with zero package-side code changes (compile-time + proof, not just core unit tests). +- `Compono.Logging`'s two forwarding methods on `LogVerificationBuilder`, + plus tests confirming they preserve the existing level/message/property + filtering `Once`/`Never`/`Exactly` already apply before delegating. +- Public API surface diff review (additive-only) and, if this repo runs a + public-API-shape verification step, confirmation it passes without + requiring a baseline update beyond the addition. +- Native AOT/trimming smoke coverage exercised the same way `Exactly` + already is — no new coverage category, just confirmation the new + methods are hit by whatever AOT smoke path already covers `CallVerifier`. +- `docs/packages/compono-testdoubles.md`, `docs/packages/compono-http.md`, + and `docs/packages/compono-logging.md`-equivalent docs (whichever exist) + updated to mention `AtLeast`/`AtMost` alongside `Once`/`Never`/`Exactly`. +- `skills/compono/references/testdoubles.md` (line 362-363: *"Still + deliberately minimal — `Never`/`Once`/`Exactly(n)` only, no + `AtLeast`/`AtMost`..."*), `skills/compono/references/http.md`, and + `skills/compono/references/logging.md` all currently assert or imply + `AtLeast`/`AtMost` don't exist — every such claim must be corrected, not + left stale, once implemented. +- `skills/compono/evals/evals.json` reviewed for any eval whose expected + output currently asserts `AtLeast`/`AtMost` are unsupported, and updated; + new eval(s) added exercising `AtLeast`/`AtMost` usage (at minimum one + per package: TestDoubles, Http, Logging) plus the "matching is not + capture"-adjacent boundary staying accurate. +- Because the skill changes, run the established skill-evaluation + workflow (snapshot/baseline the pre-change skill, update the skill, run + updated-skill evals in a clean context, run baseline-skill evals in a + clean context, compare, keep generated eval workspaces out of source + control) before treating the skill update as done. + +### Links (Amendment 22) + +- [RESEARCH-0027](../research/0027-compono-callverifier-atleast-atmost-investigation.md) — + the investigation this amendment records the outcome of: original-decision + re-reading, cross-package consistency check against real source, and the + ADR-mechanism recommendation (amend, don't supersede or leave undocumented) + this amendment follows. +- [RESEARCH-0023](../research/0023-compono-logging-1.1-research.md), + [RESEARCH-0024](../research/0024-compono-http-1.1-research.md), + [RESEARCH-0025](../research/0025-compono-testdoubles-1.1-research.md) — + the three per-package 1.1 research passes that first surfaced + `CallVerifier`'s now-cross-package reuse. +- [ADR-0055](0055-compono-logging-testing-support-package.md) — owns + `LogVerificationBuilder`'s design; this amendment's Logging-consequence + section records what that ADR's own future update needs to cover, without + amending ADR-0055 itself. +- `src/Compono/CallVerifier.cs`, `src/Compono/ReturnConfig.cs`, + `src/Compono.Http/HttpResponseRegistration.cs:56`, + `src/Compono.Logging/LogVerificationBuilder.cs` — the real source this + amendment's cross-package claims were checked against, not assumed. diff --git a/docs/adr/0060-testdoubles-received-calls-and-clear-calls.md b/docs/adr/0060-testdoubles-received-calls-and-clear-calls.md new file mode 100644 index 0000000..8aceb44 --- /dev/null +++ b/docs/adr/0060-testdoubles-received-calls-and-clear-calls.md @@ -0,0 +1,590 @@ +# [ADR-0060] Compono.TestDoubles: `ReceivedCalls()` Retrospective Inspection and `ClearCalls()` + +**Status:** Accepted + +**Date:** 2026-09-06 + +**Decision Makers:** Nick Cipollina, Claude (design review) + +## Context + +[RESEARCH-0025](../research/0025-compono-testdoubles-1.1-research.md) +identified argument capture/call inspection as the strongest remaining +`Compono.TestDoubles` 1.1 candidate, and +[RESEARCH-0026](../research/0026-compono-testdoubles-call-capture-design-investigation.md) +is the deep design pass behind this ADR. Its headline finding reframes the +problem: the per-member call log this investigation set out to design +**already exists and is already paid for at runtime**. + +[ADR-0048](0048-testdoubles-argument-matching-and-call-verification.md) +added argument-filtered call verification for a defined eligible-member +set (single-overload members meeting five conditions — see "Eligibility +scope" below). To support `Verify().Member(Match.Is(...)).Once()`, the +generator already emits, per eligible member +(`src/Compono.Generators/Templates/TestDouble.scriban:128-129`): + +```csharp +internal readonly System.Collections.Generic.List<(T1, T2, ...)> {{ member.field_name }}_calls = []; +internal readonly object {{ member.field_name }}_lock = new(); +``` + +Every dispatch to an eligible member appends its real argument values to +this list under `{{ member.field_name }}_lock` +(`TestDouble.scriban:275-277`, `:315-320`). `Verify().Member(...)` +acquires the same lock, iterates `_calls`, counts matches, and returns a +`Compono.CallVerifier` (`TestDouble.scriban:610-655`) — then the matched +data is discarded. **The list is retained for the double's entire +lifetime regardless of whether any test ever calls `Verify()` with an +argument matcher at all** — this ADR is about exposing that already-paid +storage as a first-class public capability, not about inventing a second +capture mechanism. + +Argument-filtered `Verify()` reuses argument values by C# reference/value +semantics as received at the call site — no cloning. The one thing +consumers cannot currently do is inspect those retained values directly, +independent of a count assertion, or ask for them across multiple calls in +one strongly-typed shape. + +Separately, a consumer sharing one double across multiple phases of a +test (configure → exercise phase 1 → verify → **clear observation +history** → exercise phase 2 → verify) has no first-class way to reset +call counts/history while keeping configured behavior. `ReturnConfig` +(`src/Compono/ReturnConfig.cs`) already demonstrates the "clear this, +preserve that" pattern for a different pair of concerns — +`ClearConfiguredResponse()` (line 76) clears `Value`/`Exception`/`Sequence` +state *without* touching `CallCount`. This ADR needs the mirror +operation: clear `CallCount`/call history without touching configured +`Value`/`Exception`/`Sequence` state. + +This ADR treats `ReceivedCalls()` and `ClearCalls()` as new public +consumer capabilities with their own API and lifetime semantics, and +references ADR-0048 for the underlying storage rather than restating it — +it is deliberately not an ADR-0048 amendment, because these are genuinely +new capabilities layered on that infrastructure, not a correction to +ADR-0048's own decision. + +## Decision Drivers + +- Every ADR-0043/ADR-0044/ADR-0048 driver still applies: no + cross-generator dependency, no reflection, Native AOT/trimming safety, + explicit two-gate activation, deterministic generated code, minimal + runtime overhead. +- `Compono` values: low runtime overhead, minimal unnecessary allocations, + deterministic generated code, explicit behavior, predictable + concurrency, AOT/trimming compatibility ([RESEARCH-0026](../research/0026-compono-testdoubles-call-capture-design-investigation.md)'s + measured evidence that the existing ADR-0048 storage already carries + this cost for eligible members — this ADR adds no new per-call cost). +- Consumer expectations set by NSubstitute (`ReceivedCalls()`, + `ClearReceivedCalls()`) and by Rocks' own retrospective-inspection + surface, without copying either API for familiarity alone. +- `skills/compono/references/testdoubles.md`'s existing "matching is not + capture" boundary (lines 520-540) is a real, documented limitation this + ADR closes for the eligible-member subset — the skill content asserting + this is unsupported becomes actively wrong the moment this ships and + must be corrected as part of completion, not left to drift. +- A useful, conventional testing capability does not require a prior + filed issue or dogfooding incident — real consumer value, clean package + fit, and a sound design/cost profile are sufficient justification + (established direction for this round of 1.1 scoping). + +## Considered Options — capture/inspection model + +RESEARCH-0026 compared four architectural models; this ADR does not +reopen model A/B (see that research doc for the full comparison) because +the eligibility-scoped answer is already resolved by ADR-0048's existing +storage, but records the option set for completeness: + +1. **Always-recorded invocation history for every eligible member** + (already true today, per ADR-0048 — no new decision needed here; this + ADR only decides whether to *expose* it). +2. **Opt-in capture**, enabled per-member or per-double before exercising + the SUT. +3. **Callback/observer-based capture** — already shipped as + `ReturnsCallback` (ADR-0053). +4. **Generated received-call records** — a strongly typed accessor over + the existing list. + +**Chosen: Option 4, layered on the Option 1 storage that already exists.** +Option 2 is rejected: the storage already exists unconditionally for +every ADR-0048-eligible member (it has to, to support argument-filtered +`Verify()`), so an opt-in toggle would not reduce any real cost — it would +only add a branch and a "why are there no calls?" surprise for a consumer +who forgot to opt in, for zero performance benefit. Option 3 +(`ReturnsCallback`) remains the answer for single-value, exercise-time +capture and is explicitly preserved as a distinct, complementary +mechanism — not superseded by this ADR. Both NSubstitute (`Arg.Do`) and +Rocks position their primary capture mechanism as callback-based too, +which is independent validation that a callback and a retrospective +history are two different, both-legitimate answers to two different +consumer scenarios (capture-as-it-happens vs. inspect-after-the-fact), +not competing designs where one should replace the other. + +## Considered Options — bridge name + +1. `ReceivedCalls()` — mirrors `Configure()`/`Verify()` as a third + generator-emitted downcast bridge; mirrors NSubstitute's own + `ReceivedCalls()` vocabulary, which lowers the migration-recognition + cost this repo's skill/migration docs already care about. +2. `Calls()` — shorter, but ambiguous next to `Verify().Member().Exactly(n)` + and the internal generated `_calls` field naming already in use + internally; risks reading as "make a call" rather than "the calls that + were made." + +**Chosen: Option 1, `ReceivedCalls()`.** Discoverability and consistency +with `Configure()`/`Verify()`'s existing two-bridge naming pattern outrank +brevity, and the name change from the internal `_calls` field naming has +no consumer-facing consequence (that field is `internal`, never public). + +```csharp +repository.ReceivedCalls().Save; // property or method per member, per "returned call type" below +``` + +## Considered Options — returned call type + +1. **Snapshot of the existing unnamed tuple** (`(T1, T2, ...)`, or bare + `T` for a single-parameter member) — this is the literal type already + stored in `_calls` (`TestDoubleEmitter.cs:219-224`: `CallLogTypeText` + is exactly this shape). Zero new generated type. +2. **A generated named record per eligible member**, with real parameter + names (already tracked per-parameter in the same emitter model used to + build matcher locals — `TestDoubleEmitter.cs:198-216` — as + `EscapedName`/`OriginalName`), following the same per-interface + hash-suffixed naming convention Requirement 3 already uses successfully + for `_DoubleVerifier` (`ADR-0044` line 331). + +**Chosen: Option 2.** Confirmed directly in `TestDoubleEmitter.cs`: the +existing tuple has **no named elements** — `CallLogConstructExpression` +builds a positional `(a, b, c)`, and internal consumption reads it back +positionally (`CallLogAccessExpression`: `call.Item1`, `call.Item2`, ..., +`TestDoubleEmitter.cs:213`). Exposing that type verbatim to consumers +would mean `call.Item1`/`call.Item2` at every call site — a real +usability regression relative to the member's actual parameter names, +which the generator already has on hand and simply never threaded into +the tuple's shape (there was no need to, for internal positional matching +use). A generated named record correcting this needs no new metadata — +the parameter names already exist in the emitter's per-member model, +they've simply never been used to name anything before this ADR. No fresh +compiler spike is required to de-risk the type's *naming scheme*: it +reuses the exact per-interface hash-suffix mechanism Requirement 3 already +proved compiles cleanly for `_DoubleVerifier` +(`docs/adr/0044-...md` lines 322-334) — this is the same generator +naming machinery applied to one more generated type, not new territory. +A small implementation-time spike is still warranted to confirm record +vs. `readonly record struct` (favoring the latter for a zero-allocation, +value-type snapshot) and to confirm no collision with a same-named +interface member (the existing hash-suffix scheme already handles this +class of collision generically, but the specific record-type case should +be spiked before implementation, not assumed). + +## Considered Options — snapshot semantics + +**Decision:** `ReceivedCalls().Member` acquires the existing +ADR-0048 per-member lock (`{{ member.field_name }}_lock`), copies the +current `_calls` entries into a new array/`IReadOnlyList`, releases the +lock, and returns the copy. This is a direct reuse of the exact +lock-then-copy pattern `Verify().Member(...)`'s own scan already performs +(`TestDouble.scriban:610-655`) — no new synchronization primitive, no new +concurrency model. + +**Ordering:** sequential calls preserve append order (the list is +appended-to in dispatch order under the lock). Under genuinely concurrent +invocations, ordering reflects whichever call acquired +`{{ member.field_name }}_lock` first to append its entry — this is the +same ordering guarantee (and the same lack of a stronger one) argument- +filtered `Verify()` already implicitly relies on today; this ADR adds no +new guarantee and documents none beyond what already holds. No timestamps, +no global sequence IDs — there is no concrete consumer scenario evidenced +that needs either, and adding them would be exactly the kind of +unrequested generality this round of 1.1 scoping is explicit about +avoiding. + +**Capture semantics — explicit, no deep copy.** The retained call record +stores the same C# value/reference the caller passed: + +- reference types, mutable objects, arrays, collections: the **same + reference** is retained. If the caller mutates that object after the + call returns, a later `ReceivedCalls()` inspection observes the mutated + state, not a snapshot from invocation time. +- structs, `CancellationToken`, nullable value types: ordinary C# + value-copy semantics — the record holds an independent copy of the + struct's field values as they were at the call site. + +No serialization, cloning, reflection, or general deep-copy machinery is +introduced. This is a direct, explicit consequence of the existing +ADR-0048 storage (it already retains arguments this way for matching +purposes) — this ADR does not change that behavior, only documents and +exposes it. This must be stated plainly in `Compono.TestDoubles` +documentation and the skill (see "Documentation requirements" below); a +consumer capturing a mutable argument and mutating it before inspection is +a real, foreseeable footgun that existing NSubstitute users likely already +have intuitions about (NSubstitute's `Arg.Do`/received-calls storage has +the identical reference-retention behavior), so this is consistent with +prior art, not a novel risk. + +## Considered Options — eligibility scope + +**Decision: `ReceivedCalls()` is available for exactly the ADR-0048 +eligible-member set, unchanged, for 1.1.** That set +(`skills/compono/references/testdoubles.md:373-379`) requires a member to: +be the only overload of its name in the interface; have no real parameter +referencing the member's own open generic type parameter; have no +ref-like-typed parameter; have no derived internal field name colliding +with another member's; and not be a one-parameter `Equals`. + +RESEARCH-0026 correctly flagged that this exclusion set is not +monolithic — some conditions are genuinely fundamental to *retrospective +capture* (a ref-like parameter, e.g. `Span`, cannot be stored in a +`List` element at all — a fundamental storage constraint, not specific +to matching), while others are specific to *argument matching's* +mechanism, not to capture itself (the single-overload restriction exists +because `Match`-based configuration needs an unambiguous per-overload +discriminator name — ADR-0044 Requirement 1/ADR-0048's own +overload-discriminator interaction section — a constraint that plausibly +does not apply the same way to a purely retrospective, no-configuration-time +accessor). This ADR deliberately does **not** resolve that distinction +now: expanding eligibility (e.g. supporting overloaded members for +`ReceivedCalls()` even where argument-matched `Verify()`/`Configure()` +stays excluded) is real, plausible future work, but doing it in the same +pass as "expose the storage that already exists" would turn a +narrow, low-risk change into a second overload-eligibility redesign, +which is explicitly out of scope for this round. **For 1.1: reuse +ADR-0048's eligible set exactly, unchanged, unexpanded.** The +fundamental-vs-matching-specific distinction is recorded here as a named, +credible future extension, not as an open question this ADR needs to +answer to ship. + +## Considered Options — `ClearCalls()` semantics + +Adopted core principle (RESEARCH-0026, confirmed against real +`ReturnConfig` field semantics): **`ClearCalls()` clears +observation/verification history and preserves configured behavior.** +Verified field-by-field against `src/Compono/ReturnConfig.cs`: + +| State | Cleared? | Why | +|---|---|---| +| `CallCount` | **Cleared** (reset to 0) | Observation history — this is exactly what "how many times was this called" means. | +| ADR-0048 `_calls` list (captured arguments) | **Cleared** | Observation history — the retrospective record of what happened. | +| `Value`/`HasValue` (`Returns`) | Preserved | Configured behavior — what happens on the *next* call, unrelated to what already happened. | +| `Exception` (`Throws`) | Preserved | Configured behavior. | +| `ReturnsCallback`'s callback field | Preserved | Configured behavior. | +| `Sequence` (`ReturnsSequence` array) | Preserved | Configured behavior — the array itself is immutable once set (`ReturnConfig.cs:22-24`), unrelated to observation. | +| `SequenceOrdinal` | **Preserved — does not rewind.** | This is the one field that could plausibly be argued either way; resolved below. | +| Argument-matcher/multi-entry configuration (`Entries`, ADR-0050) | Preserved | Configured behavior, same category as `Value`/`Exception`. | +| Generic closed-instantiation configuration (ADR-0049 buckets) | Preserved | Configured behavior. | +| Property backing/configured state | Preserved | Configured behavior — a property's `Configure().Prop().Returns(x)` is arrange-phase state, not observation. | + +**`SequenceOrdinal` does not rewind — confirmed, not merely assumed.** +Example: a member is configured with `ReturnsSequence(A, B, C)`; two +invocations return `A` then `B`; `ClearCalls()` runs; the next invocation +returns `C`, not `A`. Rationale: `SequenceOrdinal` is runtime *progress +through configured behavior* — the same category as `Value`/`Exception` +being "what will happen next" — not a record of *what was observed*. +Rewinding it on `ClearCalls()` would silently re-run part of a configured +sequence a consumer already exercised and moved past, which is a stronger +and more surprising side effect than a call-history reset should ever +have. This also keeps `ClearCalls()` cheap and simple: it only ever +zeroes/clears fields whose sole purpose is observation, never touches a +field that participates in *what response comes next*. + +This principle is what keeps `ClearCalls()` from becoming `Reset()` — a +`Reset()`-shaped operation would need to also decide what happens to every +row in the table above, and getting that decision wrong (or leaving it +ambiguous) is exactly the failure mode a precise, narrow `ClearCalls()` +name and contract avoids. + +### `ClearCalls()` receiver and scope + +**Decision: `ClearCalls()` is a direct operation on the double itself, +clearing every member's observation state at once — no per-member +granularity in 1.1.** + +```csharp +repository.ClearCalls(); +``` + +Considered and rejected: + +- `repository.Verify().ClearCalls()` — rejected: `Verify()`'s wrapper + type exists specifically to host assertion terminals + (`Once`/`Never`/`Exactly`/now `AtLeast`/`AtMost`); attaching a mutating + operation to the same receiver blurs "assert" and "mutate" in one type, + which is exactly the responsibility-blur this ADR's "API intent" + section (below) is designed to avoid. +- `repository.ReceivedCalls().Clear()` — rejected for the same reason in + the other direction: `ReceivedCalls()` is an inspection bridge; a + `.Clear()` sitting on it reads as "clear the snapshot I just returned," + not "clear the double's history," which is a real and likely-common + misreading given `ReceivedCalls()`'s snapshot semantics above. +- **Per-member `ClearCalls()`** (e.g. `repository.ClearCalls().Save()`, + mirroring `Configure()`/`Verify()`'s per-member shape) — deferred, not + rejected outright. The evidenced consumer scenario in RESEARCH-0026 and + this ADR's own Context is phase-based reuse of a *whole* double across + test phases, not selectively forgetting one member's history while + keeping another's. Per-member granularity is real, plausible future + work if a concrete scenario evidences it, but adding it now with no + such scenario is exactly the "don't manufacture granularity" instruction + this round of design work is explicit about. A single, whole-double + `repository.ClearCalls()` is the smaller, more direct primitive that + matches the actual evidenced scenario. + +`ClearCalls()` applies uniformly across every generated member on the +double, including members that only maintain a scalar `CallCount` and are +outside the ADR-0048 eligible set (a member with no argument-aware call +log still has a `CallCount` worth resetting) — this is a deliberate +asymmetry with `ReceivedCalls()` (eligibility-scoped) and is called out +explicitly so a reader does not assume the two share one eligibility +rule. + +### `ClearCalls()` concurrency + +`ClearCalls()` must synchronize with the same storage `RecordCall()`/ +argument-log-append and `Verify()`/`ReceivedCalls()` already use, per +member: + +- **Scalar `CallCount`**: reset via + `System.Threading.Interlocked.Exchange(ref CallCount, 0)` — the same + primitive `RecordCall()`'s `Interlocked.Increment` already uses, so a + concurrent increment and a concurrent clear can never tear the field; + the only ambiguity is *ordering* (does an in-flight call's increment + land before or after the clear), which is the same kind of ordering + ambiguity that already exists between any two concurrent `RecordCall()` + calls today — this ADR introduces no new class of race, just one more + operation subject to the existing one. +- **ADR-0048 `_calls` list**: `ClearCalls()` acquires + `{{ member.field_name }}_lock`, calls `_calls.Clear()`, releases — + identical lock discipline to the existing append and the existing + `Verify()` scan. A call whose dispatch is concurrently appending under + the same lock either fully lands before or fully lands after the clear; + there is no torn/partial list state, by construction of the existing + lock. + +**Simplest acceptable semantics, stated explicitly (no stronger promise +than the implementation provides):** a call either lands before or after +`ClearCalls()` from that member's synchronized perspective; there is no +global coordination across members (clearing member A's state has no +ordering relationship to member B's concurrent calls, which is fine — no +evidenced scenario needs cross-member atomicity); no torn state is ever +observable, because every mutation site already holds the relevant lock +or uses the relevant atomic primitive. + +### `ClearCalls()` releases retained references + +Because `_calls.Clear()` removes every element from the list, any argument +references retained only by that list become eligible for garbage +collection once no other reference exists — `ClearCalls()` is a real, +effective memory-release operation for a double that has captured many +mutable/large arguments over a long-running shared-fixture lifetime, not +merely a logical/observational reset. + +## Performance + +RESEARCH-0026's measured findings, adopted without modification for the +1.1 eligibility scope: + +- Scalar `CallCount`-only recording (members outside the ADR-0048 + eligible set) remains effectively zero-allocation, unaffected by + anything in this ADR. +- ADR-0048-eligible members already pay the unbounded `List<(args...)>` + history cost today, unconditionally — `ReceivedCalls()` exposing that + list adds no new per-call cost for the 1.1 scope; it is a pure read-side + addition. +- Unbounded growth only becomes measurably expensive at call volumes far + beyond realistic single-test usage (RESEARCH-0026's spike found ~80 + bytes/call and a ~20x slowdown only material at multi-million-call + scale, driven by list-growth reallocation, not the per-call write). +- **No default cap, no ring buffer, no configuration knob for maximum + captured calls in 1.1.** A bounded/ring-buffer default would introduce + a genuinely surprising truncation semantic ("why did my 501st call + disappear") for a cost that is not evidenced as a real problem at any + realistic test scale. `ClearCalls()` is the correct, explicit answer to + unbounded growth over a long-lived double, not an implicit cap. + +## Public/Generated API Compatibility + +- **Source compatibility:** additive only. `ReceivedCalls()` and + `ClearCalls()` are new generated extension methods; no existing + generated signature changes. +- **Binary compatibility:** additive only in core `Compono` (no new + members on existing public types other than the ADR-0044-amendment's + `CallVerifier` additions, tracked separately) and additive-only in + generated per-interface code (new extension classes/methods, existing + ones unchanged). +- **Generated-source compatibility:** every existing generated file's + content is byte-for-byte unaffected for a member outside the newly + eligible set; for an ADR-0048-eligible member, the generator emits + additional source (the new record type and the two bridges) alongside + the unchanged existing dispatch/`Verify()` code — no existing emitted + line changes. +- **Analyzer/generator determinism:** the new record type's name is + derived deterministically from the same per-interface hash-suffix + scheme already used for `_DoubleVerifier` — same determinism + guarantee, no new nondeterminism source. +- **Native AOT/trimming:** no reflection, no dynamic code generation, + identical trim-safety profile to the existing ADR-0048 storage this ADR + exposes — validated the same way existing TestDoubles AOT smoke + coverage already validates `Verify()`. +- **SemVer:** purely additive; safe for a 1.1 minor release under + Compono's post-1.0 compatibility posture. + +## Decision Outcome + +**Chosen:** ship `ReceivedCalls()` (generated named-record accessor, over +ADR-0048's existing eligible-member set, snapshot-under-existing-lock +semantics, reference-retention capture semantics documented explicitly) +and `ClearCalls()` (whole-double, clears `CallCount` + captured argument +history only, preserves all configured behavior including sequence +ordinal progress) as two new `Compono.TestDoubles` public capabilities for +1.1. + +### Positive Consequences + +- Closes a real, named, currently-documented-as-unsupported gap + (`skills/compono/references/testdoubles.md`'s "matching is not capture" + section) for the eligible-member subset, without inventing new runtime + machinery — the storage already exists and is already paid for. +- Keeps `Configure()`/arrange, `Verify()`/assert, and `ReceivedCalls()`/ + inspect as three clearly separated concerns, avoiding the responsibility + blur of attaching captured data to `CallVerifier`. +- `ClearCalls()` closes a real phase-based-reuse gap with a narrow, + precisely specified contract that cannot silently degrade into a + `Reset()`. +- No measurable new runtime cost for the 1.1 scope; no new concurrency + model introduced beyond reuse of ADR-0048's existing lock discipline. + +### Negative Consequences + +- `ReceivedCalls()` is scoped narrower than "every generated member" — + consumers with an overloaded member of interest cannot use it there yet. + Mitigation: this is the same eligible set ADR-0048's argument-matched + `Verify()`/`Configure()` already impose, so it introduces no new, + unfamiliar boundary — a consumer already living within ADR-0048's + eligibility rules gains a capability, rather than a previously-unified + surface fragmenting further. +- Reference-retention (not deep-copy) capture semantics are a real, + documented footgun for mutable arguments. Mitigation: explicit + documentation and skill coverage (below), consistent with how + NSubstitute's own equivalent behavior is generally understood by + consumers migrating from it. +- The generated named-record type adds one more generated type per + eligible member to the emitted source — a real, if small, generated-code + volume increase. Mitigation: this is bounded by the existing + ADR-0048-eligible set, and the record type is a small, deterministic, + zero-allocation-at-use (`readonly record struct`, pending implementation + spike) shape. + +## Documentation requirements + +Named explicitly, not left as generic "update docs": + +- `docs/packages/compono-testdoubles.md` (or whichever doc currently + catalogs `Compono.TestDoubles` public capabilities) — add + `ReceivedCalls()`/`ClearCalls()` alongside `Configure()`/`Verify()`. +- XML docs on the new generated bridges, the generated record type(s), + and any new public core-`Compono` infrastructure type backing the + snapshot (if one is introduced at implementation time), matching this + repo's existing XML-doc density/style on `CallVerifier`/`ReturnConfig`. +- Package samples/examples demonstrating: basic call inspection; multiple + received calls across several invocations; `ClearCalls()` used between + two phases of one test; the reference-retention footgun for a mutable + argument (a deliberate "here's what NOT to assume" example). +- Migration guidance: where existing NSubstitute-migration material + discusses `Received()`/`ReceivedCalls()`/`ClearReceivedCalls()` + equivalence, update it to reflect that `Compono.TestDoubles` now covers + this for the eligible-member set, with an explicit note on the + eligibility boundary and reference-retention semantics. + +## Skill requirements + +`ReceivedCalls()`/`ClearCalls()` are public generated consumer +capabilities — the skill **must** be updated, not left to drift: + +- `skills/compono/SKILL.md` — lines 101-105 currently state Compono + "never" exposes `ReceivedCalls()` and that "true argument capture... + [is] not supported"; both claims become false for the eligible-member + set and must be corrected precisely (not blanket-reversed — the + eligibility boundary must survive into the corrected text). +- `skills/compono/references/testdoubles.md` — lines 362-363 ("Still + deliberately minimal... no `ReceivedCalls()`-style enumeration") and + the entire "The #1 AutoFixture/NSubstitute-habit trap: matching is not + capture" section (lines 520-540) must be revised to state precisely + what is now supported (`ReceivedCalls()` for ADR-0048-eligible members) + and what remains genuinely unsupported (call-order verification, strict + mode, overloaded-member capture, classes/delegates/indexers/events). + This is the single most consequential skill edit this ADR requires — + it is the section most likely to actively mislead a consumer or an + agent using the skill if left stale. +- Any capability-matrix-style table across `skills/compono/` asserting + argument capture is unsupported must be found (grep for "capture", + "ReceivedCalls", "matching is not capture") and corrected. + +Because the skill changes, `skills/compono/evals/evals.json` **must** be +updated: + +- Add/revise eval(s) exercising: discovering and correctly using + `ReceivedCalls()` for an eligible member; correctly identifying the + matching-vs-capture boundary post-change (i.e., the eval suite must not + keep testing that the skill says capture is unsupported); `ClearCalls()` + preserving configured behavior including sequence-ordinal progress; not + hallucinating `ReceivedCalls()` support for an overloaded member (a + negative eval, given the deliberately narrow eligibility scope); + migration guidance mentioning the NSubstitute-equivalence where + relevant. +- Run the established skill-evaluation workflow before treating the + skill update as complete: snapshot/baseline the pre-change skill (or use + this repo's established immutable-baseline mechanism if one already + exists); update the skill; update `evals.json`; run the updated skill + against the relevant evals in a clean agent context; run the + baseline/old skill against the same evals in a clean agent context; + compare results; inspect any regression; keep generated eval workspaces + out of source control. + +## ADR completion criteria + +Implementation of this ADR is not complete until all of the following are +done together, not left to drift apart: + +- Generator/runtime changes: the new bridge(s), the generated record + type(s), `ClearCalls()`'s whole-double clearing logic, and (if needed) + a new public core-`Compono` primitive analogous to + `ReturnConfig.ClearConfiguredResponse()` for the `CallCount`/history + side. +- Tests: `Compono.TestDoubles.Tests` coverage for `ReceivedCalls()` + (single call, multiple calls, ordering, reference-retention semantics + for a mutable argument), `ClearCalls()` (all state-preservation/clearing + rules in the table above, explicitly including the sequence-ordinal + non-rewind case), and concurrency (concurrent invocation racing a + `ClearCalls()` call, no torn state). +- AOT/trimming smoke coverage extended to exercise the new surface. +- Public API surface diff review (additive-only). +- Documentation changes listed above. +- Skill changes listed above, including the mandatory baseline-vs-updated + skill evaluation comparison. +- Dogfooding via `scripts/dogfood-validate.sh` once implementation reaches + the dogfooding stage — not an ad hoc validation process. + +## Links + +- [ADR-0048](0048-testdoubles-argument-matching-and-call-verification.md) — + origin of the argument-aware call-log storage this ADR exposes; not + restated in full here. +- [ADR-0044](0044-compono-testdoubles-v2-overloads-generics-verification.md) — + Requirement 3's original `Verify()` bridge design and per-overload + discriminator mechanism this ADR's `ReceivedCalls()` bridge follows the + same pattern of; Amendment 22 (`CallVerifier.AtLeast`/`AtMost`), decided + alongside this ADR, is a related but independent change. +- [ADR-0053](0053-testdoubles-invocation-aware-callback-responses.md) — + `ReturnsCallback`, preserved as the distinct, complementary + exercise-time capture mechanism this ADR does not replace. +- [ADR-0054](0054-testdoubles-sequential-call-count-based-responses.md) — + the sequence/ordinal mechanism whose non-rewind behavior under + `ClearCalls()` this ADR specifies. +- [RESEARCH-0025](../research/0025-compono-testdoubles-1.1-research.md), + [RESEARCH-0026](../research/0026-compono-testdoubles-call-capture-design-investigation.md) — + the research this ADR's decisions are drawn from. +- `src/Compono.Generators/Templates/TestDouble.scriban`, + `src/Compono.Generators/Emitters/TestDoubleEmitter.cs`, + `src/Compono/ReturnConfig.cs` — the real generator/runtime source every + claim in this ADR was checked against. +- `skills/compono/SKILL.md`, `skills/compono/references/testdoubles.md`, + `skills/compono/evals/evals.json` — updated at implementation time per + "Skill requirements" above, not by this ADR directly. diff --git a/docs/adr/README.md b/docs/adr/README.md index 892ccbe..a28a964 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -121,3 +121,4 @@ the mechanics: numbering, status, and the index. | [0057](0057-compono-mstest-package-design.md) | Compono.MSTest Package Design | Accepted | | [0058](0058-public-generator-facing-runtime-infrastructure.md) | Public Generator-Facing Runtime Infrastructure | Accepted | | [0059](0059-compono-nunit-package-design.md) | Compono.NUnit Package Design | Accepted | +| [0060](0060-testdoubles-received-calls-and-clear-calls.md) | Compono.TestDoubles: `ReceivedCalls()` Retrospective Inspection and `ClearCalls()` | Accepted | diff --git a/docs/packages/compono-http.md b/docs/packages/compono-http.md index 27d19f3..1c690eb 100644 --- a/docs/packages/compono-http.md +++ b/docs/packages/compono-http.md @@ -99,7 +99,9 @@ registration.Verify().Once(); next. - **`registration.Verify()`** — returns a `CallVerifier` (the exact type core `Compono` already uses elsewhere): `.Never()`, `.Once()`, - `.Exactly(n)`. Answers "how many times did *this configured behavior* + `.Exactly(n)`, `.AtLeast(n)`, `.AtMost(n)` + ([ADR-0044 Amendment 22](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-22-2026-09-06-callverifieratleastintatmostint-added-requirement-3s-minimality-preserved-not-reversed)). + Answers "how many times did *this configured behavior* match" — kept deliberately separate from `handler.Requests`, which answers "what did the system under test actually send." - **`handler.Requests: IReadOnlyList`** — every request diff --git a/docs/packages/compono-logging.md b/docs/packages/compono-logging.md index 7491a40..5c35073 100644 --- a/docs/packages/compono-logging.md +++ b/docs/packages/compono-logging.md @@ -81,10 +81,15 @@ Console.WriteLine(failure.Properties?.First().Key); // "OrderId", e.g. - **`logger.Verify()`** — the fluent entry point: `.AtLevel(level)`, `.WithEventId(id)`, `.WithException()`, `.WithMessageContaining(text)`, `.Matching(predicate)`, ending in - `.Once()` / `.Never()` / `.Exactly(n)` — the exact same single-verb - vocabulary `Compono.TestDoubles`/`Compono.Http` already use - (`repository.Verify().Save().Once()`, `registration.Verify().Once()`), - reusing core `Compono`'s `CallVerifier` unchanged. + `.Once()` / `.Never()` / `.Exactly(n)` / `.AtLeast(n)` / `.AtMost(n)` + ([ADR-0044 Amendment 22](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-22-2026-09-06-callverifieratleastintatmostint-added-requirement-3s-minimality-preserved-not-reversed)) + — the exact same single-verb vocabulary `Compono.TestDoubles`/ + `Compono.Http` already use (`repository.Verify().Save().Once()`, + `registration.Verify().Once()`), each a thin, one-line forward through + the same shared count-verification semantics core `Compono`'s + `CallVerifier` implements — the filter chain above narrows *which* + captured entries count, then the terminal counts them exactly the same + way regardless of which package it's reached through. - **`CapturingLogger` / `CapturingLogger`** — hand-written, publicly, directly constructible with no composition involved at all: ```csharp diff --git a/docs/packages/compono-testdoubles.md b/docs/packages/compono-testdoubles.md index f44d96e..bdb425e 100644 --- a/docs/packages/compono-testdoubles.md +++ b/docs/packages/compono-testdoubles.md @@ -19,13 +19,16 @@ dotnet add package Compono.TestDoubles [ADR-0042](../adr/0042-compono-owned-source-generated-test-doubles.md)'s Non-Goals — but current generated doubles do support `Configure()`, `Verify()`, literal equality matching, `Match.Any()`, -`Match.Is(predicate)`, argument-filtered `Never()`/`Once()`/`Exactly(n)`, -and multi-entry argument-distinguished response configuration for eligible -member shapes. Use [`Compono.NSubstitute`](compono-nsubstitute.md) when you -intentionally want a familiar runtime-proxy substitute or a capability still -outside generated-double support, such as invocation-aware callbacks, true -argument capture, call-order verification, or partial/strict substitutes; -the two packages are not mutually exclusive. +`Match.Is(predicate)`, argument-filtered +`Never()`/`Once()`/`Exactly(n)`/`AtLeast(n)`/`AtMost(n)`, retrospective +call inspection (`ReceivedCalls()`) and whole-double observation reset +(`ClearCalls()`) for eligible member shapes, invocation-aware callbacks, +and multi-entry argument-distinguished response configuration. Use +[`Compono.NSubstitute`](compono-nsubstitute.md) when you intentionally +want a familiar runtime-proxy substitute or a capability still outside +generated-double support, such as call-order verification, argument +capture for an **overloaded** member, or partial/strict substitutes; the +two packages are not mutually exclusive. ## Compile-time opt-in @@ -419,7 +422,9 @@ and remains unsupported until a future design addresses it. `Verify()` — parallel to and independent from `Configure()` — asserts how many times a member was actually called (v2, [ADR-0044](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md) -Requirement 3). `Never()`/`Once()`/`Exactly(n)` only: +Requirement 3, extended by +[Amendment 22](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-22-2026-09-06-callverifieratleastintatmostint-added-requirement-3s-minimality-preserved-not-reversed)). +`Never()`/`Once()`/`Exactly(n)`/`AtLeast(n)`/`AtMost(n)`: ```csharp service.Repository.Configure().CountAsync().Returns(Task.FromResult(5)); @@ -429,6 +434,8 @@ var order = await service.PlaceAsync(3); service.Repository.Verify().CountAsync().Once(); service.Repository.Verify().Save().Once(); service.Repository.Verify().UtcNow().Never(); // never read in this call path +service.Repository.Verify().CountAsync().AtLeast(1); +service.Repository.Verify().CountAsync().AtMost(1); ``` A failing assertion throws `Compono.TestDoubleVerificationException` (a @@ -441,15 +448,164 @@ discriminator mechanism `Configure()` does: `repository.Verify().Speak("x")` selects the same overload-specific counter `repository.Configure().Speak("x")` would. -**Still deliberately minimal** - `Never`/`Once`/`Exactly(n)` only, no -`AtLeast`/`AtMost`, no `ReceivedCalls()`-style enumeration, and (see below) -no call-order verification. Argument-aware recording is available both for -a non-overloaded eligible member (see "Argument matching and -argument-filtered verification" below) and, per-overload, via the -`Matching` surface ("Overload-safe argument matching" above). If a -test needs anything else this page doesn't cover (call-order verification, -`ReturnsForAnyArgs`, etc.), use `Compono.NSubstitute` for that interface -instead - the two providers can coexist (see below). +**Still deliberately minimal, but no longer just `Never`/`Once`/`Exactly(n)`** - +`AtLeast(n)`/`AtMost(n)` round out the lower-bound/upper-bound count +vocabulary `Exactly` already sits inside (per Amendment 22, this closes a +narrow, low-cost gap Requirement 3's original minimality left open - it +does not reopen a general verification DSL: `Between`, `AtLeastOnce()`, +`AtMostOnce()`, `Any()`, and `None()` all remain deliberately unsupported, +each either derivable from the primitives above at the call site or a +synonym for an existing terminal). Neither method validates its argument +any differently than `Exactly` already doesn't - `AtLeast(-1)` and +`AtMost(-1)` behave the same way `Exactly(-1)` always has (a +vacuously-true or vacuously-false assertion, never a thrown +`ArgumentException`), and `AtMost(0)` is behaviorally identical to +`Never()` for the same reason (an observed call count can never be +negative). Still no call-order verification. Argument-aware recording is +available both for a non-overloaded eligible member (see "Argument +matching and argument-filtered verification" below) and, per-overload, via +the `Matching` surface ("Overload-safe argument matching" above). +If a test needs anything else this page doesn't cover (call-order +verification, `ReturnsForAnyArgs`, etc.), use `Compono.NSubstitute` for +that interface instead - the two providers can coexist (see below). + +## Retrospective call inspection: `ReceivedCalls()` + +`ReceivedCalls()` — a third bridge alongside `Configure()`/`Verify()`, +inspecting rather than arranging or asserting — returns the real argument +values a member was actually invoked with, for the same eligible-member +set "Argument matching and argument-filtered verification" below scopes +`Match`-based matching to (single-overload, no ref-like parameter, no +real parameter referencing the member's own open generic type parameter, +no derived-name collision, not a one-parameter `Equals`; +[ADR-0060](../adr/0060-testdoubles-received-calls-and-clear-calls.md)): + +```csharp +repository.Withdraw("acct-1", 50m, overdraftAllowed: true); +repository.Withdraw("acct-2", 75m, overdraftAllowed: false); + +var calls = repository.ReceivedCalls().Withdraw(); + +calls.Should().HaveCount(2); +calls[0].accountId.Should().Be("acct-1"); // a named record, not the internal call log's .Item1 +calls[1].amount.Should().Be(75m); +``` + +Each call is exposed as a generated, per-member `readonly record struct` +with the member's own real parameter names (not `Item1`/`Item2` - the +internal call log ADR-0048's argument-filtered `Verify()` already +maintains uses an unnamed tuple, which `ReceivedCalls()` maps into this +named shape instead of exposing directly). Calls come back in append +order for sequential invocations; under genuinely concurrent invocations, +order reflects whichever call acquired the member's internal recording +lock first - the same ordering guarantee (and lack of a stronger one) +argument-filtered `Verify()`'s own scan already implicitly relies on. +`ReceivedCalls().Member()` returns a **snapshot** - a fresh, independent +copy taken under that same lock at the moment it's called, never a live +view. A later invocation never retroactively changes an +already-returned snapshot: + +```csharp +repository.Withdraw("acct-1", 10m, overdraftAllowed: false); +var firstSnapshot = repository.ReceivedCalls().Withdraw(); + +repository.Withdraw("acct-2", 20m, overdraftAllowed: true); + +firstSnapshot.Count.Should().Be(1); // unaffected by the second call +``` + +**Capture semantics: no deep copy, ordinary C# value/reference semantics.** +A reference-type argument (a class, an array, a mutable collection) is +retained by the *same reference* the caller passed - if the caller mutates +that object after the call returns, a later `ReceivedCalls()` inspection +observes the mutation, not a snapshot from invocation time: + +```csharp +var record = new MutableRecord { Value = 1 }; +archiver.Archive(record); +record.Value = 2; // mutated AFTER the call + +archiver.ReceivedCalls().Archive()[0].record.Value.Should().Be(2); // observes the mutation +``` + +This is a real, documented footgun for a mutable argument, not a bug - +consistent with NSubstitute's own identical `Received()`/argument-capture +behavior, which most migrating consumers already have the right intuition +for. A value-type argument (`int`, `decimal`, a `struct`) is an ordinary +value copy, unaffected by anything the caller does with its own local +variable afterward. + +**What stays unsupported.** `ReceivedCalls()` uses exactly ADR-0048's +eligible-member set, unchanged - an **overloaded** member has no +`ReceivedCalls()` surface, even though it may have a `Matching` +argument-matching surface (see "Overload-safe argument matching" above). +There's no call-order verification, no strict/unexpected-call mode, no +invocation timestamps, no global sequence IDs, and no bounded/ring-buffer +history or capture cap - a long-running double that accumulates many +calls keeps them all until `ClearCalls()` (below) or the double itself is +discarded. + +## Resetting observation history: `ClearCalls()` + +`ClearCalls()` resets a double's *observation* history - every member's +call count and every eligible member's captured-argument history - while +leaving every *configured* behavior untouched: + +```csharp +repository.Configure().Withdraw().Returns(true); +repository.Withdraw("acct-1", 10m, overdraftAllowed: false); + +repository.ClearCalls(); + +repository.Verify().Withdraw().Never(); // observation reset +repository.ReceivedCalls().Withdraw().Should().BeEmpty(); +repository.Withdraw("acct-2", 20m, overdraftAllowed: false).Should().BeTrue(); // configuration preserved +``` + +It's a direct, **whole-double** operation - `repository.ClearCalls()`, not +`repository.Verify().ClearCalls()` or `repository.ReceivedCalls().Clear()` +(both would blur "assert"/"inspect" with "mutate") and not a per-member +`ClearCalls()` (no evidenced scenario needs selectively forgetting one +member's history while keeping another's - the realistic use case is +resetting a *whole* shared double between phases of one test). Every +generated member is cleared, including one outside the `ReceivedCalls()`- +eligible set (a member with no argument-aware history still has a call +count worth resetting). + +**Preserved, not cleared:** `Returns`/`Throws`/`ReturnsCallback`-configured +behavior, a configured `ReturnsSequence`, [multi-entry](#multiple-response-configurations-per-member) +argument-matched configuration, and +[closed-instantiation](#per-closed-instantiation-configuration-for-self-referencing-generic-returns) +per-`T` configuration all survive `ClearCalls()` unchanged. + +**A configured sequence's progress does not rewind.** This is the one +case worth calling out explicitly, since it's easy to assume otherwise: + +```csharp +repository.Configure().Withdraw().ReturnsSequence("A", "B", "C"); + +repository.Withdraw(/* ... */); // "A" +repository.Withdraw(/* ... */); // "B" + +repository.ClearCalls(); + +repository.Withdraw(/* ... */); // "C" - not "A" +``` + +A sequence's in-progress ordinal is *configured-behavior progress* (the +same category as "what value will `Returns` produce next"), not +observation history - `ClearCalls()` only ever resets state whose sole +purpose is recording what already happened, never state that decides what +happens next. Rewinding it would silently re-run part of a sequence a +test already exercised and moved past, a stronger and more surprising +side effect than a call-history reset should ever have. + +`ClearCalls()` is also a real memory-release operation, not merely a +logical reset: once cleared, any argument references an eligible member's +captured-call history held (per the reference-retention semantics above) +become eligible for garbage collection, which matters for a long-lived +shared double that has captured many large or mutable arguments across a +long-running test fixture. ## Argument matching and argument-filtered verification @@ -795,11 +951,19 @@ concrete implementation is fully supported; see "Static abstract members inherited from a base interface" above ([ADR-0046](../adr/0046-static-abstract-member-conformance-only-generation.md)). Overloaded members, a `ref`/`out`/`in` parameter's own overload, generic -methods independent of their own type parameter, and minimal call -verification (`Never`/`Once`/`Exactly(n)`) are now supported (see above, +methods independent of their own type parameter, and call verification +(`Never`/`Once`/`Exactly(n)`/`AtLeast(n)`/`AtMost(n)`) are now supported +(see above, [ADR-0044](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md)). -An unsupported member shape is a compile-time diagnostic -(`CMP0020`-`CMP0032`), not a silent gap. +Retrospective call inspection (`ReceivedCalls()`) and whole-double +observation reset (`ClearCalls()`) are now supported for the same +eligible-member set argument-filtered `Verify()` already targets — see +"Retrospective call inspection" and "Resetting observation history" above +([ADR-0060](../adr/0060-testdoubles-received-calls-and-clear-calls.md)) — +but **not** for an overloaded member, even one with its own +`Matching` argument-matching surface; that expansion is real, +plausible future work, not resolved here. An unsupported member shape is a +compile-time diagnostic (`CMP0020`-`CMP0032`), not a silent gap. ## Next @@ -809,5 +973,5 @@ An unsupported member shape is a compile-time diagnostic provider sits in the resolution pipeline. - [`Compono.NSubstitute`](compono-nsubstitute.md) — the runtime-proxy alternative, for capabilities still outside generated-double support - (for example invocation-aware callbacks, true argument capture, - call-order verification, or partial/strict substitutes). + (for example call-order verification, argument capture for an + overloaded member, or partial/strict substitutes). diff --git a/docs/plans/0063-callverifier-atleast-atmost-and-testdoubles-received-calls-clear-calls.md b/docs/plans/0063-callverifier-atleast-atmost-and-testdoubles-received-calls-clear-calls.md new file mode 100644 index 0000000..2b7911a --- /dev/null +++ b/docs/plans/0063-callverifier-atleast-atmost-and-testdoubles-received-calls-clear-calls.md @@ -0,0 +1,553 @@ +# [PLAN-0063] `CallVerifier.AtLeast`/`AtMost`, `Compono.TestDoubles` `ReceivedCalls()` + `ClearCalls()` + +**Status:** Done + +**Implements:** [ADR-0044 Amendment 22](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-22-2026-09-06-callverifieratleastintatmostint-added-requirement-3s-minimality-preserved-not-reversed), [ADR-0060](../adr/0060-testdoubles-received-calls-and-clear-calls.md) + +Own sequential plan number (not `0044` or `0060`) per `docs/plans/README.md`'s +multi-ADR rule — this plan spans two separately-numbered ADRs and is kept +as one plan deliberately: both land in the same 1.1 verification/inspection +theme, touch overlapping `Compono.TestDoubles` documentation, touch the +same Compono skill files, and share one `evals.json`/baseline-comparison +pass rather than three partial ones. + +## Goal + +`CallVerifier` gains `AtLeast(int)`/`AtMost(int)`, reachable through +`Compono.TestDoubles`, `Compono.Http`, and `Compono.Logging` with no +consumer-visible inconsistency between them; `Compono.TestDoubles` gains +`ReceivedCalls()` (retrospective, snapshot-based call inspection for the +ADR-0048-eligible member set) and `ClearCalls()` (whole-double observation +reset preserving configured behavior). Done means: all of the above ships +in one PR, with tests, docs, and skill/evals updated and the mandatory +baseline-vs-updated skill comparison run and recorded — not just the code +compiling. + +## Scope + +**In scope**, per ADR-0044 Amendment 22 and ADR-0060's Decision Outcomes — +this plan does not restate those ADRs' reasoning, only the resulting work: + +- `CallVerifier.AtLeast`/`AtMost` (core `Compono`). +- `LogVerificationBuilder.AtLeast`/`AtMost` forwarders (`Compono.Logging`). +- `Compono.TestDoubles` generated `ReceivedCalls()` bridge + generated + named call-record type, scoped to ADR-0048's existing eligible-member + set. +- `Compono.TestDoubles` generated `ClearCalls()`, whole-double, clearing + call counts + ADR-0048 call histories only. +- All documentation, skill, and eval work both ADRs name as mandatory + completion criteria. +- Dogfooding via `scripts/dogfood-validate.sh` at the appropriate stage. + +**Explicitly deferred / out of scope** (per both ADRs and the request that +opened this plan): + +- `Compono.Logging` `WithMessageTemplate`/`WithProperty` (RESEARCH-0023, + queued separately). +- `Compono.Http` async request/body matching, `RespondStream`. +- Overloaded-member `ReceivedCalls()` eligibility expansion. +- Per-member `ClearCalls()`. +- Call-order verification, strict mode, `Between`/`AtLeastOnce`/ + `AtMostOnce`/`Any`/`None`. +- Invocation timestamps/global ordering metadata, bounded/ring-buffer + history, any configurable capture-history cap. +- Any change to `Exactly(int)`'s existing (non-)validation behavior. + +## Tasks + +### 1. Core — `CallVerifier.AtLeast`/`AtMost` + +- [x] Add `AtLeast(int times)`/`AtMost(int times)` to + `src/Compono/CallVerifier.cs`, matching `Exactly`'s existing + structure and `TestDoubleVerificationException` message format + exactly (per ADR-0044 Amendment 22's exact wording). +- [x] No new fields, no new validation on any count-taking method + (`Exactly` included — must not change its existing behavior). +- [x] XML docs matching existing density/style on `Never`/`Once`/`Exactly`. + +### 2. `Compono.Logging` — forwarders + +- [x] Add `AtLeast(int times)`/`AtMost(int times)` to + `src/Compono.Logging/LogVerificationBuilder.cs`, delegating through + the existing private `ToCallVerifier()` — same one-line shape as + `Once`/`Never`/`Exactly`. +- [x] Do not expose `CallVerifier` on `LogVerificationBuilder`'s public + surface; do not otherwise touch its filtering logic. + +### 3. Compiler spike — generated call-record shape (ADR-0060, mandatory before generator work) + +- [x] Spike, against a representative multi-parameter ADR-0048-eligible + member (at least one 2-arg and one 3-arg case, plus one nullable + reference-type parameter and one generic method already covered by + ADR-0049's closed-instantiation configuration), whether a + `readonly record struct` generated per eligible member: + - [x] compiles cleanly using the per-interface hash-suffix naming scheme + already proven for `_DoubleVerifier` (ADR-0044 Requirement 3); + - [x] produces no name collision against the interface's own members/types; + - [x] preserves parameter names and nullable annotations correctly using + the generator's existing identifier-escaping conventions + (`TestDoubleEmitter.cs`'s existing `EscapedName`/`OriginalName` + handling); + - [x] remains valid for a generic-method call record (confirmed: no + ADR-0049 closed-instantiation-eligible member ever reaches this code + path, since that classification is mutually exclusive with + `IsEligibleForMatching` — see Notes. The legitimate generic case + is a generic method with an unused own type parameter, spiked + synthetically since no such member exists in real fixtures today). +- [x] Spike confirmed `readonly record struct` works cleanly: locked in, + proceeding to Task 4. See Notes for full outcome record. + +### 4. Generator/runtime — `ReceivedCalls()` + +- [x] `TestDoubleEmitter.cs`/`TestDoubleMemberInfo.cs`: for each ADR-0048 + eligible member (`IsEligibleForMatching`, not + `IsOverloadMatchingEligible` — ReceivedCalls() is scoped to exactly + the non-overloaded eligible set), added `ReceivedCallClassName` + (`{FieldName}_ReceivedCall`) alongside `EntryClassName`, reserved in + `TestDoubleAnalyzer.AssignCallbackNameSuffixes`'s collision pool. + Existing `CallLogTypeText`/`CallLogConstructExpression` unchanged — + the record is an additional, public-facing representation, the + internal tuple stays for internal matching use. +- [x] `TestDouble.scriban`: emitted the generated + `internal readonly record struct {{ field }}_ReceivedCall(...)` + nested in `_Double` (real parameter names via `EscapedName`, not the + internal-splice-only `OriginalName`), plus the third bridge + (`{{ safe_identifier }}_DoubleReceivedCalls` wrapper struct + + `{{ safe_identifier }}_ReceivedCallsExtension.ReceivedCalls()` + + `{{ safe_identifier }}_DoubleReceivedCallsAccess` per-member + accessors), mirroring Requirement 3's `_DoubleVerifier`/ + `_VerifyExtension`/`_DoubleVerification` pattern exactly. Per-member + accessor snapshots `_calls` under the existing `{{ field }}_lock`, + maps each entry to the new record type, returns + `IReadOnlyList` (a freshly allocated array) — no live mutable + collection ever returned. +- [x] No change to existing `Configure()`/`Verify()` emitted code paths — + confirmed by the generated-source snapshot diff (Task 9): every + pre-existing emitted line is byte-for-byte unchanged, only new lines + added. + +### 5. Generator/runtime — `ClearCalls()` + +- [x] Added `ReturnConfig.ClearObservedCalls()` + (`src/Compono/ReturnConfig.cs`) mirroring `ClearConfiguredResponse()`'s + shape — `Interlocked.Exchange(ref CallCount, 0)`, nothing else. +- [x] `TestDouble.scriban`: emitted `{{ safe_identifier }}_ClearCallsExtension.ClearCalls()` + as a direct extension on the interface (not under `Verify()`/ + `ReceivedCalls()`), iterating every member with three cases: + (a) `is_closed_instantiation_eligible` — iterate every closed-`T` + bucket entry via a new non-generic `{{ safe_identifier }}_IClearableCallState` + interface (implemented by every generated `*_State` class), since + `ClearCalls()` has no static knowledge of which closed `T`'s exist; + (b) `is_eligible_for_matching || is_overload_matching_eligible` — + `lock ({{ field }}_lock) { {{ field }}_calls.Clear(); }` (no separate + `CallCount` to clear for this shape — `RecordCall()` was already + removed from its dispatch by ADR-0050, the call log's own `.Count` + is the count); (c) every other `has_configuration_surface` member — + `{{ field }}.ClearObservedCalls()`. +- [x] **Real generator-fixture-driven correction found and fixed during + implementation, not merely a hypothetical risk**: the ADR-0049 + matched-parameters closed-instantiation state class (multi-entry, + `closed_instantiation_has_matched_parameters`) has **no top-level + `Config` field at all** — `Config` lives on each nested `Entry`, and + that shape's dispatch never calls `RecordCall()` on any `Entry.Config` + either (the shared `Calls` list's own count is authoritative, same + as the non-generic multi-entry shape). Initial implementation wrote + `this.Config.ClearObservedCalls()` unconditionally in both + closed-instantiation branches — a real `CS1061` compile error caught + by `Compono.Generators.Tests`' real fixture suite (not merely + inspection). Fixed: the matched-parameters branch's + `ClearObservedCalls()` now locks `this.Lock`, loops `this.Entries` + clearing each `Entry.Config.ClearObservedCalls()` (a currently-always- + zero no-op given no `RecordCall()` call site exists for this shape, + but the correct, forward-consistent behavior per the field's stated + contract), and clears `this.Calls`. See "Important implementation + caution" evidence class 4 in the request that started this + implementation tranche — this is exactly the kind of storage-model + trace that caution warned was necessary, and it was necessary in + practice, not just in theory. +- [x] A second real compile error was also caught and fixed the same way: + a generic-in-`T` closed-instantiation state class whose type + parameter is itself literally named `Config` + (`ClosedInstantiationMatchedParameterTypeParameterNamedAfterNestedEntryMember_GeneratesSupportedDouble`, + an existing repo fixture covering exactly this collision) makes a + bare `Config` reference inside that class resolve to the *type + parameter*, not the field (`CS0704`). Fixed by qualifying every new + reference as `this.Config`/`this.Lock`/`this.Calls`, which always + resolves to the instance member regardless of a same-named type + parameter. +- [x] Confirmed generated `ClearCalls()` never touches `Value`/`Exception`/ + `Sequence`/`SequenceOrdinal`/matcher entries/closed-instantiation + `Entries` list membership/property-configured state — verified by + generated-source inspection (Task 9) and the state-preservation test + matrix (Task 7). + +### 6. Tests — CallVerifier / cross-package + +- [x] `Compono.Tests`: `AtLeast`/`AtMost` boundary tests (below/equal/above + for both), negative-value behavior matching `Exactly`'s existing + (non-)validation, `AtLeast(0)`, `AtMost(0)` vs. `Never()` equivalence, + exact failure-message assertions. Also added `ClearObservedCalls()` + unit tests (reset/preserves Value/Exception/does-not-rewind-Sequence). +- [x] `Compono.TestDoubles.Tests`/`Compono.TestDoubles.SampleTests`: proved + `Verify().Member().AtLeast(...)`/`.AtMost(...)` reachable (via + `IAccountRepository`/`ILedger` in the AOT smoke test and the + SampleTests' `ReceivedCallsAndClearCallsTests`/existing `MatchingTests` + coverage) with zero package-side code changes. +- [x] `Compono.Http.Tests`: `Verify_AtLeastAndAtMost_AreReachableWithNoPackageCodeChanges` + (`TestHttpHandlerTests.cs`). +- [x] `Compono.Logging.Tests`: `AtLeast_CountsOnlyTheFilteredSubset_NotTheWholeCaptureBuffer`/ + `AtMost_...` (`LogVerificationBuilderTests.cs`) — proves filtering + happens before the count terminal (four entries captured, two match + the level filter, `AtLeast(3)`/`AtMost(1)` fail against the filtered + count of 2, not the unfiltered 4). + +### 7. Tests — `ReceivedCalls()` / `ClearCalls()` + +- [x] `Compono.TestDoubles.SampleTests/ReceivedCallsAndClearCallsTests.cs`: + single-call inspection, multiple-call inspection with order + assertions, snapshot isolation (an earlier snapshot doesn't grow when + a later call happens), reference-retention semantics (`IArchiver`/ + `MutableRecord` — mutate after the call, assert the captured record + observes the mutation), value-type argument copy semantics (mutate a + local `decimal` after the call, assert the captured value is + unaffected), and the bare-`T`-not-a-tuple single-parameter shape + (`Rename`). +- [x] `ClearCalls()`: reset-and-preserve-configured-Returns, preserves + ADR-0050 multi-entry configuration, and the ADR-0060 worked example + (`ReturnsSequence("A","B","C")` → two calls → `ClearCalls()` → next + call → `"C"`, not `"A"`) via `ILedger`. +- [x] Concurrency: `ClearCalls_RacingConcurrentInvocations_NeverThrowsOrCorruptsState` + — a `Barrier`-synchronized pair of tasks (200 iterations each) racing + concurrent `Withdraw()` calls against concurrent `ClearCalls()` calls + on the same double; asserts no exception and a legal (0..200) final + count, not a specific one — deterministic, no sleeps. +- [x] Generated-source snapshot tests (`Compono.Generators.Tests`): all 98 + pre-existing `TestDouble.g.cs` snapshots reviewed (diffed line-by-line + against their prior verified content — every diff purely additive, + confirmed before accepting) and re-verified against the new + `ReceivedCalls()`/`ClearCalls()`/`IClearableCallState` emitted shapes; + 313/313 tests pass on both net10.0 and net11.0. + +### 8. AOT / trimming + +- [x] Extended `Compono.TestDoubles.AotSmokeTest/Program.cs` (the existing + project that already exercises `Configure()`/`Verify()`) to exercise + `AtLeast`/`AtMost` (pass + a caught `TestDoubleVerificationException` + for a deliberately-failing `AtLeast(5)`), `ReceivedCalls()` (asserts + count and first captured `accountId`), `ClearCalls()` (asserts + `Verify().Never()` and an empty `ReceivedCalls()` snapshot + immediately after, then that multi-entry configuration survives), and + the sequence-non-rewind invariant, all via `IAccountRepository`. + Published with `dotnet publish -c Release -f net10.0 -p:PublishAot=true + -r osx-arm64` and ran the resulting native binary directly (not a + rerun of the pre-existing smoke path) — exit code 0, `PASS` printed, + confirming no reflection/dynamic-code-generation path was introduced. + +### 9. Compatibility validation + +- [x] Public API surface diff review: `Compono.TestDoubles.Tests`/ + `Compono.NSubstitute.Tests`/etc.'s `PublicApiSurfaceTests` pattern + locks only the *type* set of each package's own static assembly + (`Compono.TestDoubles.dll` itself never contains generated-double + types — those are emitted into each *consumer's* compiled assembly), + so it is structurally unaffected by this change and needed no update; + confirmed by rerunning it (still passes). No core `Compono`/ + `Compono.Http`/`Compono.Logging` equivalent test exists to update. + Every change is additive: two new methods on `CallVerifier`, two new + forwarders on `LogVerificationBuilder`, one new method on + `ReturnConfig`, and (per eligible interface) new generated + extension classes/types alongside the unchanged existing ones — no + existing signature changed. +- [x] Generated-source snapshot diff review: performed as part of Task 7 — + every one of the 98 changed snapshots' diff was inspected before + acceptance and contained only new lines; zero existing emitted lines + changed for any member outside the newly-touched surface. + +### 10. Documentation + +- [x] `docs/packages/compono-testdoubles.md` — added "Retrospective call + inspection: `ReceivedCalls()`" and "Resetting observation history: + `ClearCalls()`" sections next to "Call verification"; covers + snapshot semantics, ordering semantics, reference-retention + semantics (with a mutable-argument code example), the ADR-0048 + eligibility boundary (overloaded members explicitly called out as + unsupported for `ReceivedCalls()`, in both the new sections and "What + it deliberately doesn't do"), whole-double `ClearCalls()` semantics, + preserved-vs-cleared state, and the sequence-ordinal non-rewind + worked example. Also corrected the top-of-file capability summary and + "What it deliberately doesn't do"/"Next" sections' stale "argument + capture... outside generated-double support" claims. +- [x] `docs/packages/compono-http.md` — added `.AtLeast(n)`/`.AtMost(n)` + to the `registration.Verify()` bullet. +- [x] `docs/packages/compono-logging.md` — added `.AtLeast(n)`/`.AtMost(n)` + to the `logger.Verify()` bullet, stating they forward through the + same shared count-verification semantics as `Once`/`Never`/`Exactly`. + Did not describe `WithMessageTemplate`/`WithProperty` — queued + separately, out of scope for this plan. +- [x] XML docs on every new public/generated-facing type and member + (`CallVerifier.AtLeast`/`AtMost`, `LogVerificationBuilder.AtLeast`/ + `AtMost`, `ReturnConfig.ClearObservedCalls`, the generated + `ReceivedCalls()`/`ClearCalls()` bridges' inline comments). +- [x] Samples/examples: `docs/packages/compono-testdoubles.md`'s new + sections carry runnable-shaped code examples for basic inspection, + multiple calls, snapshot isolation, the mutable-argument + reference-retention footgun, and `ClearCalls()` across two phases + including the sequence non-rewind case; `test/Compono.TestDoubles.SampleTests/ReceivedCallsAndClearCallsTests.cs` + is this repo's established "sample-doubles-as-tests" mechanism + (mirroring `MatchingTests.cs`/`VerificationTests.cs`) and exercises + every one of these scenarios as real, running, packaged-consumer + code — no separate dedicated samples project exists for + `Compono.TestDoubles` to add a duplicate example set to. +- [x] NSubstitute migration guidance: `docs/packages/compono-nsubstitute.md` + was checked (grep for `ReceivedCalls`/`ClearReceivedCalls`/ + `capture`/`migrat`) — it contains no existing statement that + `ReceivedCalls()`/`ClearReceivedCalls()`-equivalent capability is + unsupported, so there was nothing stale to correct there; the + correction landed instead in `compono-testdoubles.md`'s own + capability summary (above), which is where that claim actually lived. + +### 11. Skill + +- [x] Grepped the entire `skills/compono/` tree for: `ReceivedCalls`, + `capture`, `matching is not capture`, `AtLeast`, `AtMost`, `Never`, + `Exactly`, `call verification`, `overloaded`, `ClearCalls` — hits + enumerated before editing (75 total, narrowed to the files below). +- [x] `skills/compono/SKILL.md` — corrected **two** stale capability + summaries (not just the one at the plan's originally-cited + lines 101-105 — a second, near-identical one existed further down + the same file, at what were then lines ~172-183, and needed the + identical fix): both now state what's supported (eligible-member + `ReceivedCalls()`/`ClearCalls()`, `AtLeast`/`AtMost`) and what still + isn't (overloaded-member `ReceivedCalls()`, call-order verification, + strict mode), plus added `ReturnsCallback`-vs-`ReceivedCalls()` + disambiguation (two distinct, complementary mechanisms) and + NSubstitute `ReceivedCalls`/`ClearReceivedCalls` vocabulary mapping. +- [x] `skills/compono/references/testdoubles.md` — revised the "Still + deliberately minimal" line (362-372 as renumbered) to state + `AtLeast`/`AtMost` are supported and name what's still explicitly + rejected (`Between`/`AtLeastOnce`/`AtMostOnce`/`Any`/`None`); added a + new "Retrospective call inspection: `ReceivedCalls()`" section + (mirroring the doc page); rewrote the "matching is not capture" + section (renamed nothing, corrected content) to state plain capture + is now supported for the eligible set and only overloaded-member + capture/call-order verification/strict mode remain unsupported; also + corrected a third, narrower stale claim ("does not expose an + arbitrary call log") in the "Argument matching and filtered + verification" intro paragraph. +- [x] `skills/compono/references/http.md` — added `AtLeast`/`AtMost` in + both places `Once`/`Never`/`Exactly` were listed. +- [x] `skills/compono/references/logging.md` — added `AtLeast`/`AtMost` + forwarding through the shared count-verification semantics; did not + add `WithMessageTemplate`/`WithProperty` content. +- [x] No other file under `skills/compono/` surfaced a stale hit on the + grep term list beyond the four files above. +- [x] Did not overstate: no claim of full NSubstitute call-inspection + parity, no mention of call-order verification as supported, no + mention of per-member `ClearCalls()` — each explicitly called out as + still unsupported everywhere the topic comes up. + +### 12. Evals + +- [x] Snapshotted the pre-change skill via `git show HEAD:...` for + `SKILL.md`, `references/testdoubles.md`, `references/http.md`, + `references/logging.md` into a scratchpad baseline directory — + equivalent to a pre-edit snapshot since none of Task 11's edits had + been committed, so `HEAD` was still the exact pre-change content. +- [x] Updated `skills/compono/evals/evals.json` (46 → 52 evals): revised + eval 30 (NSubstitute-migration vocabulary mapping) to include + `ReceivedCalls`/`ClearReceivedCalls` → `ReceivedCalls().Member()` + mapping and to stop calling capture a blanket-unsupported boundary; + added eval 47 (`AtLeast`/`AtMost` usage), 48 (discovering/using + `ReceivedCalls()` for an eligible member, named-record shape), 49 + (reference-retention semantics — not a bug, with a concrete fix), 50 + (discovering `ClearCalls()` for cross-phase reuse), 51 (`ClearCalls()` + not rewinding a configured sequence — the exact ADR-0060 worked + example, single objectively-correct answer "C"), and 52 (a negative + eval: overloaded-member `ReceivedCalls()` correctly identified as + unsupported, redirected to the existing `Matching` surface + or `Compono.NSubstitute`). +- [x] Ran the **updated** skill (real repo files) against evals + 30/47-52 in a clean subagent context (general-purpose, no + prior-conversation knowledge, reading only the four skill files): + answered all seven correctly and precisely — `AtLeast`/`AtMost` as + two independent terminals (correctly noting no `Between`); + `ReceivedCalls().Withdraw()` returning named-field records; + reference-retention explained as documented behavior with a concrete + mitigation, not a bug; `ClearCalls()` as the whole-double receiver + with configured behavior preserved; the sequence non-rewind case + answered "C" with the correct rationale; overloaded-member + `ReceivedCalls()` correctly declined with a `Matching`/ + `Compono.NSubstitute` redirect. No hallucinated APIs, no overclaiming. +- [x] Ran the **baseline** (pre-change) skill against the same seven evals + in a clean subagent context, reading only the snapshot files: on + every eval touching new 1.1 surface (47-52), it correctly reported + the capability as **not present** in its source material and + redirected to `Compono.NSubstitute`/a project-local fake/logging the + gap as roadmap evidence — it never hallucinated `AtLeast`/`AtMost`, + `ReceivedCalls()`, or `ClearCalls()` into existence, and where it + reasoned about `ClearCalls()`'s hypothetical interaction with a + sequence (eval 51) it explicitly caveated the answer as inference, + not a confirmed API. Eval 30 (NSubstitute vocabulary mapping, the + part of the scenario the baseline skill *does* cover) was answered + correctly by both versions. +- [x] Comparison result: **no regressions** — the baseline's expected + failures on evals 47-52 are exactly the intended evidence that the + updated skill teaches new, real capability (not a skill defect being + masked), and the baseline's own behavior on those evals was safe + (declined/redirected) rather than wrong (hallucinated). No skill/eval + iteration was needed. +- [x] Recorded the comparison above, in this plan's own Tasks section (per + this repo's convention of tracking a plan's own execution detail + inline, since no separate baseline-comparison-log document exists in + this repo for skill evals). +- [x] No generated eval workspace files were created outside the two + subagents' own transcripts (no local eval-runner artifact directory + exists for this repo's skill-eval mechanism) — nothing to keep out + of source control beyond the scratchpad baseline snapshot itself, + which lives outside the repo. + +### 13. Dogfooding / final validation + +- [x] Ran `scripts/dogfood-validate.sh` (default package set: `Compono`, + `Compono.NSubstitute`, `Compono.TestDoubles`, `Compono.XunitV3`) + against the trivia-platform consumer repo — packed local version + `99.0.0-local.20260906155600-93733-19331`, confirmed every resolved + reference used that exact freshly-packed version (not a stale cache + hit), full consumer suite: **783/783 passed, 0 failed**. Consumer's + `git status --porcelain` confirmed byte-identical before and after + (no edits to its `Directory.Packages.props` or anywhere else). + Required starting a local Docker runtime (`colima start`) first, per + the script's own documented Testcontainers prerequisite. +- [x] As expected, trivia-platform's existing test suite doesn't naturally + exercise `ReceivedCalls()`/`ClearCalls()`/`AtLeast`/`AtMost` (it + predates this plan) — per the plan's own instruction, relied on the + generator/unit/SampleTests/AOT/eval coverage above for those APIs + rather than standing up a new ad hoc consumer repo. Dogfooding's + role here is exactly what it's for: proving the packaged artifact + (analyzer, `PrivateAssets`, `CompilerVisibleProperty` flow) still + works end-to-end for a real, unrelated consumer — no packaging + regression, no generator crash processing a large real interface + surface it had never seen before. + +## Critical Files + +- `src/Compono/CallVerifier.cs` — `AtLeast`/`AtMost`. +- `src/Compono/ReturnConfig.cs` — new observed-call reset primitive. +- `src/Compono.Logging/LogVerificationBuilder.cs` — two forwarders. +- `src/Compono.Generators/Emitters/TestDoubleEmitter.cs` — new generated + record-type model per eligible member. +- `src/Compono.Generators/Templates/TestDouble.scriban` — `ReceivedCalls()` + and `ClearCalls()` emission. +- `test/Compono.Tests/`, `test/Compono.TestDoubles.Tests/`, + `test/Compono.Http.Tests/` (or equivalent), `test/Compono.Logging.Tests/` + (or equivalent), `test/Compono.Generators.Tests/` — new coverage per + Tasks 6-7. +- `docs/packages/compono-testdoubles.md`, `docs/packages/compono-http.md`, + `docs/packages/compono-logging.md` (or equivalent existing doc paths). +- `skills/compono/SKILL.md`, `skills/compono/references/testdoubles.md`, + `skills/compono/references/http.md`, `skills/compono/references/logging.md`, + `skills/compono/evals/evals.json`. + +## Test Plan + +Per `references/testing.md`'s conventions: unit tests for `CallVerifier` +boundary/message behavior (core `Compono`), package-level reachability/ +forwarding tests (TestDoubles/Http/Logging), generated-source snapshot +tests for the new emitted shapes, a deterministic concurrency test for +`ClearCalls()` racing invocation, and an AOT smoke extension. Skill changes +are validated by the mandatory baseline-vs-updated eval comparison (Task +12), not by unit tests. Dogfooding (Task 13) is the final real-consumer +check, scoped to what existing dogfood targets already exercise. + +## Notes + +**Task 3 compiler-spike outcome (2026-09-06): PASSED, no limitation found.** +Spiked (scratch console app, not committed) a hand-written stand-in for the +generator's `readonly record struct` output against: a 2-arg eligible +member, a 3-arg eligible member with a nullable reference-type parameter, +a 1-arg eligible member (the bare-`T`, non-tuple `CallLogTypeText` shape), +and a `ReceivedCalls()` bridge/wrapper-struct pair mirroring +`{{ safe_identifier }}_DoubleVerifier`'s exact pattern. All compiled +cleanly, preserved real parameter names, and the reference-retention +proof (mutate an `Order` after the call, confirm the later snapshot +observes the mutation) passed as ADR-0060 specifies. **Locked in: +`readonly record struct`, one per eligible member, named +`{{ safe_identifier }}_{{ InterfaceName }}_{{ member.escaped_name }}_ReceivedCall` +(or equivalent single deterministic scheme reusing the existing per-file +`safe_identifier` hash prefix — no separate new hash needed, since +`safe_identifier` is already unique per interface and the member name is +already unique within it after Requirement 3's own collision handling). + +**Whether a legitimate ADR-0048-eligible generic member exists:** yes, but +only a narrow, currently-unfixtured shape. Tracing +`TestDoubleAnalyzer.cs:1435-1441`, `isEligibleForMatching` requires (among +other conditions) `!(IsGenericMethod && any parameter references the +method's own open type parameter)` — this does **not** exclude +`IsGenericMethod` outright, only a parameter that references the method's +own type parameter. A generic method whose type parameter is unused by any +real parameter (e.g. `bool TryLog(string message)`, `TMarker` +supplied only at the call site, never appearing in a parameter or return +type) legitimately satisfies `isEligibleForMatching` while +`IsGenericMethod` is `true`. No existing `Compono.Generators.Tests` +fixture exercises this shape today — it was spiked synthetically (see +above) rather than added as a real generator fixture, since the record +type's shape depends only on `Parameters` (identical code path regardless +of `IsGenericMethod`), so no generator/emitter special-casing is needed +for it and manufacturing a fixture purely to exercise an already-covered +code path would be exactly the "don't add a case merely to satisfy the +checklist" the ADR/plan warn against. If a future real interface needs +this shape, it is already correctly handled by the eligibility-set-scoped +implementation below — this is a documented fact, not an open risk. + +**PR #134 Codex review round (2026-09-07): three real generator-correctness bugs found and fixed, +each with a real generator-fixture regression test** (`test/Compono.Generators.Tests/TestDoubleVerifyTests.cs`): +1. P1 - `ClearCalls()`/`ReceivedCalls()` are always-emitted, always-zero-argument bridge extensions + exactly like `Configure()`/`Verify()`, but `TestDoubleAnalyzer`'s reserved-name collision check + (CMP0023) only covered `"Configure"`/`"Verify"`. An interface declaring its own zero-argument + `ClearCalls`/`ReceivedCalls` member would silently shadow the generated bridge (ordinary member + lookup wins over an extension method) with no diagnostic. Fixed: widened the reserved-name set; + `ClearCallsNamedMember_ReportsCollisionDiagnostic`/`ReceivedCallsNamedMember_ReportsCollisionDiagnostic` + cover it. +2. P2 - an eligible member's generated `{FieldName}_ReceivedCall` record-class name could collide + with an unrelated real sibling member's own natural field name (e.g. eligible `Foo` alongside a + real member literally named `Foo_ReceivedCall`), producing a real CS0102 duplicate-declaration + compile error - never caught by `AssignCallbackNameSuffixes`' later callback-only disambiguation + pass. Fixed by feeding this derived name into the SAME earlier `derivedAuxiliaryNameOwners` + pre-pass that already handles this class of collision for `_calls`/`_lock`/`_Entry`/`_entries` + (demotes the colliding member out of matching eligibility rather than renaming, the pre-pass's + established convention). `ReceivedCallRecordNameCollidesWithSiblingMember_FallsBackWithoutRejectingEligibleMember` + covers it. +3. P2 - a parameter literally named the same as its own member's generated `_ReceivedCall` record + type (e.g. `Foo(int __Foo_ReceivedCall)`) produced a positional record property sharing its + enclosing type's name - CS0542. Fixed in `TestDouble.scriban`: that one parameter's declared + name is suffixed `_Value` inside the record declaration only (positional construction elsewhere + is order-based, not name-based, so nothing else needed updating). Two real parameters can never + already share a name, so at most one parameter per record ever needs the suffix. + +All three fixes are additive/narrow (no change to any previously-emitted line for a non-colliding +member, confirmed via the same purely-additive-comment snapshot diff review this plan's Task 9 +already established as the review method) - the full 313-per-TFM `Compono.Generators.Tests` suite +plus these 4 new fixture tests (8 across net10.0/net11.0) all pass. Also fixed in the same round: a +pre-existing `.github/workflows/package-validation.yaml` gap (unrelated to this plan's own code, +but blocking this PR's checks) - its local validation-only pack never set `-p:Version`, always +defaulting to `1.0.0.0`, which started failing ApiCompat's CP0003 the moment nuget.org's real +published baseline crossed 1.0.0 (`1.1.0-preview.103`, published by PR #133's merge to `main`). +Fixed by pinning that pack's `Version` to the resolved baseline. Also regenerated +`docs/reference/api/` (API reference drift against this plan's own new public members - `CallVerifier.AtLeast`/`AtMost`, +`LogVerificationBuilder.AtLeast`/`AtMost`, `ReturnConfig.ClearObservedCalls`), which had been +missed before the initial PR push. + +**Generic-in-`T` closed-instantiation-eligible members (ADR-0049) are +mutually exclusive with `IsEligibleForMatching`** +(`TestDoubleMemberInfo.cs:118`, confirmed again at +`TestDoubleAnalyzer.cs:1429-1433`: `isClosedInstantiationEligible` is +computed first and directly excluded from `isEligibleForMatching`'s own +condition list). `ReceivedCalls()`/`ClearCalls()`'s `_calls`/`_lock`-based +storage never applies to that ADR-0049 bucket-based shape — no design +contradiction, no code path shared, so this plan's Task 4/5 emitter/ +scriban changes only ever touch the `member.is_eligible_for_matching` +branch, never the `member.is_closed_instantiation_eligible` branches. +`ClearCalls()` (whole-double, Task 5) still resets a closed-instantiation +member's scalar `CallCount` inside its own per-`T` bucket state (see Task +5 implementation notes below), since that member still has *a* call count +worth clearing even though it has no `ReceivedCalls()` surface. diff --git a/docs/plans/README.md b/docs/plans/README.md index 2a34176..324b995 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -77,3 +77,4 @@ one. This file is just the mechanics: numbering, status, and the index. | [0060](0060-public-generator-facing-runtime-infrastructure.md) | Public Generator-Facing Runtime Infrastructure | Done | | [0061](0061-pre-1-0-cleanup-and-consolidation.md) | Pre-1.0 Cleanup and Consolidation Gate | Done | | [0062](0062-package-validation-gap-fixes.md) | Package-Validation Gap Fixes | Done | +| [0063](0063-callverifier-atleast-atmost-and-testdoubles-received-calls-clear-calls.md) | `CallVerifier.AtLeast`/`AtMost`, `Compono.TestDoubles` `ReceivedCalls()` + `ClearCalls()` | Done | diff --git a/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.AtLeast(int).md b/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.AtLeast(int).md new file mode 100644 index 0000000..0ebc676 --- /dev/null +++ b/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.AtLeast(int).md @@ -0,0 +1,21 @@ +#### [Compono\.Logging](index.md 'index') +### [Compono\.Logging](Compono.Logging.md 'Compono\.Logging').[LogVerificationBuilder](Compono.Logging.LogVerificationBuilder.md 'Compono\.Logging\.LogVerificationBuilder') + +## LogVerificationBuilder\.AtLeast\(int\) Method + +Asserts the accumulated filters matched at least [times](Compono.Logging.LogVerificationBuilder.AtLeast(int).md#Compono.Logging.LogVerificationBuilder.AtLeast(int).times 'Compono\.Logging\.LogVerificationBuilder\.AtLeast\(int\)\.times') times\. + +```csharp +public void AtLeast(int times); +``` +#### Parameters + + + +`times` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +#### Exceptions + +[TestDoubleVerificationException](../Compono/Compono.TestDoubleVerificationException.md 'Compono\.TestDoubleVerificationException') +The filters matched fewer than + [times](Compono.Logging.LogVerificationBuilder.AtLeast(int).md#Compono.Logging.LogVerificationBuilder.AtLeast(int).times 'Compono\.Logging\.LogVerificationBuilder\.AtLeast\(int\)\.times') times\. \ No newline at end of file diff --git a/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.AtMost(int).md b/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.AtMost(int).md new file mode 100644 index 0000000..174e13c --- /dev/null +++ b/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.AtMost(int).md @@ -0,0 +1,21 @@ +#### [Compono\.Logging](index.md 'index') +### [Compono\.Logging](Compono.Logging.md 'Compono\.Logging').[LogVerificationBuilder](Compono.Logging.LogVerificationBuilder.md 'Compono\.Logging\.LogVerificationBuilder') + +## LogVerificationBuilder\.AtMost\(int\) Method + +Asserts the accumulated filters matched at most [times](Compono.Logging.LogVerificationBuilder.AtMost(int).md#Compono.Logging.LogVerificationBuilder.AtMost(int).times 'Compono\.Logging\.LogVerificationBuilder\.AtMost\(int\)\.times') times\. + +```csharp +public void AtMost(int times); +``` +#### Parameters + + + +`times` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +#### Exceptions + +[TestDoubleVerificationException](../Compono/Compono.TestDoubleVerificationException.md 'Compono\.TestDoubleVerificationException') +The filters matched more than + [times](Compono.Logging.LogVerificationBuilder.AtMost(int).md#Compono.Logging.LogVerificationBuilder.AtMost(int).times 'Compono\.Logging\.LogVerificationBuilder\.AtMost\(int\)\.times') times\. \ No newline at end of file diff --git a/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.md b/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.md index c0fe120..459b584 100644 --- a/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.md +++ b/docs/reference/api/Compono.Logging/Compono.Logging.LogVerificationBuilder.md @@ -18,7 +18,9 @@ Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system | Methods | | | :--- | :--- | +| [AtLeast\(int\)](Compono.Logging.LogVerificationBuilder.AtLeast(int).md 'Compono\.Logging\.LogVerificationBuilder\.AtLeast\(int\)') | Asserts the accumulated filters matched at least [times](Compono.Logging.LogVerificationBuilder.AtLeast(int).md#Compono.Logging.LogVerificationBuilder.AtLeast(int).times 'Compono\.Logging\.LogVerificationBuilder\.AtLeast\(int\)\.times') times\. | | [AtLevel\(LogLevel\)](Compono.Logging.LogVerificationBuilder.AtLevel(Microsoft.Extensions.Logging.LogLevel).md 'Compono\.Logging\.LogVerificationBuilder\.AtLevel\(Microsoft\.Extensions\.Logging\.LogLevel\)') | Restricts matches to entries logged at exactly [level](Compono.Logging.LogVerificationBuilder.AtLevel(Microsoft.Extensions.Logging.LogLevel).md#Compono.Logging.LogVerificationBuilder.AtLevel(Microsoft.Extensions.Logging.LogLevel).level 'Compono\.Logging\.LogVerificationBuilder\.AtLevel\(Microsoft\.Extensions\.Logging\.LogLevel\)\.level')\. | +| [AtMost\(int\)](Compono.Logging.LogVerificationBuilder.AtMost(int).md 'Compono\.Logging\.LogVerificationBuilder\.AtMost\(int\)') | Asserts the accumulated filters matched at most [times](Compono.Logging.LogVerificationBuilder.AtMost(int).md#Compono.Logging.LogVerificationBuilder.AtMost(int).times 'Compono\.Logging\.LogVerificationBuilder\.AtMost\(int\)\.times') times\. | | [Exactly\(int\)](Compono.Logging.LogVerificationBuilder.Exactly(int).md 'Compono\.Logging\.LogVerificationBuilder\.Exactly\(int\)') | Asserts the accumulated filters matched exactly [times](Compono.Logging.LogVerificationBuilder.Exactly(int).md#Compono.Logging.LogVerificationBuilder.Exactly(int).times 'Compono\.Logging\.LogVerificationBuilder\.Exactly\(int\)\.times') times\. | | [Matching\(Func<CapturedLogEntry,bool>\)](Compono.Logging.LogVerificationBuilder.Matching(System.Func_Compono.Logging.CapturedLogEntry,bool_).md 'Compono\.Logging\.LogVerificationBuilder\.Matching\(System\.Func\\)') | Restricts matches to entries satisfying an arbitrary [predicate](Compono.Logging.LogVerificationBuilder.Matching(System.Func_Compono.Logging.CapturedLogEntry,bool_).md#Compono.Logging.LogVerificationBuilder.Matching(System.Func_Compono.Logging.CapturedLogEntry,bool_).predicate 'Compono\.Logging\.LogVerificationBuilder\.Matching\(System\.Func\\)\.predicate') \- the escape hatch for anything the named filters above don't cover\. | | [Never\(\)](Compono.Logging.LogVerificationBuilder.Never().md 'Compono\.Logging\.LogVerificationBuilder\.Never\(\)') | Asserts the accumulated filters never matched\. | diff --git a/docs/reference/api/Compono/Compono.CallVerifier.AtLeast(int).md b/docs/reference/api/Compono/Compono.CallVerifier.AtLeast(int).md new file mode 100644 index 0000000..3cea7da --- /dev/null +++ b/docs/reference/api/Compono/Compono.CallVerifier.AtLeast(int).md @@ -0,0 +1,20 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[CallVerifier](Compono.CallVerifier.md 'Compono\.CallVerifier') + +## CallVerifier\.AtLeast\(int\) Method + +Asserts the member was called at least [times](Compono.CallVerifier.AtLeast(int).md#Compono.CallVerifier.AtLeast(int).times 'Compono\.CallVerifier\.AtLeast\(int\)\.times') times\. + +```csharp +public void AtLeast(int times); +``` +#### Parameters + + + +`times` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +#### Exceptions + +[TestDoubleVerificationException](Compono.TestDoubleVerificationException.md 'Compono\.TestDoubleVerificationException') +The member was called fewer than [times](Compono.CallVerifier.AtLeast(int).md#Compono.CallVerifier.AtLeast(int).times 'Compono\.CallVerifier\.AtLeast\(int\)\.times') times\. \ No newline at end of file diff --git a/docs/reference/api/Compono/Compono.CallVerifier.AtMost(int).md b/docs/reference/api/Compono/Compono.CallVerifier.AtMost(int).md new file mode 100644 index 0000000..0ceea2b --- /dev/null +++ b/docs/reference/api/Compono/Compono.CallVerifier.AtMost(int).md @@ -0,0 +1,20 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[CallVerifier](Compono.CallVerifier.md 'Compono\.CallVerifier') + +## CallVerifier\.AtMost\(int\) Method + +Asserts the member was called at most [times](Compono.CallVerifier.AtMost(int).md#Compono.CallVerifier.AtMost(int).times 'Compono\.CallVerifier\.AtMost\(int\)\.times') times\. + +```csharp +public void AtMost(int times); +``` +#### Parameters + + + +`times` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +#### Exceptions + +[TestDoubleVerificationException](Compono.TestDoubleVerificationException.md 'Compono\.TestDoubleVerificationException') +The member was called more than [times](Compono.CallVerifier.AtMost(int).md#Compono.CallVerifier.AtMost(int).times 'Compono\.CallVerifier\.AtMost\(int\)\.times') times\. \ No newline at end of file diff --git a/docs/reference/api/Compono/Compono.CallVerifier.md b/docs/reference/api/Compono/Compono.CallVerifier.md index 1dee3c0..7879ed9 100644 --- a/docs/reference/api/Compono/Compono.CallVerifier.md +++ b/docs/reference/api/Compono/Compono.CallVerifier.md @@ -17,6 +17,8 @@ public readonly struct CallVerifier | Methods | | | :--- | :--- | +| [AtLeast\(int\)](Compono.CallVerifier.AtLeast(int).md 'Compono\.CallVerifier\.AtLeast\(int\)') | Asserts the member was called at least [times](Compono.CallVerifier.AtLeast(int).md#Compono.CallVerifier.AtLeast(int).times 'Compono\.CallVerifier\.AtLeast\(int\)\.times') times\. | +| [AtMost\(int\)](Compono.CallVerifier.AtMost(int).md 'Compono\.CallVerifier\.AtMost\(int\)') | Asserts the member was called at most [times](Compono.CallVerifier.AtMost(int).md#Compono.CallVerifier.AtMost(int).times 'Compono\.CallVerifier\.AtMost\(int\)\.times') times\. | | [Exactly\(int\)](Compono.CallVerifier.Exactly(int).md 'Compono\.CallVerifier\.Exactly\(int\)') | Asserts the member was called exactly [times](Compono.CallVerifier.Exactly(int).md#Compono.CallVerifier.Exactly(int).times 'Compono\.CallVerifier\.Exactly\(int\)\.times') times\. | | [Never\(\)](Compono.CallVerifier.Never().md 'Compono\.CallVerifier\.Never\(\)') | Asserts the member was never called\. | | [Once\(\)](Compono.CallVerifier.Once().md 'Compono\.CallVerifier\.Once\(\)') | Asserts the member was called exactly once\. | diff --git a/docs/reference/api/Compono/Compono.ReturnConfig_T_.ClearObservedCalls().md b/docs/reference/api/Compono/Compono.ReturnConfig_T_.ClearObservedCalls().md new file mode 100644 index 0000000..6e72270 --- /dev/null +++ b/docs/reference/api/Compono/Compono.ReturnConfig_T_.ClearObservedCalls().md @@ -0,0 +1,14 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[ReturnConfig<T>](Compono.ReturnConfig_T_.md 'Compono\.ReturnConfig\') + +## ReturnConfig\\.ClearObservedCalls\(\) Method + +Clears the recorded call count without changing the configured value, exception, or sequence \- +the mirror of [ClearConfiguredResponse\(\)](Compono.ReturnConfig_T_.ClearConfiguredResponse().md 'Compono\.ReturnConfig\\.ClearConfiguredResponse\(\)')\. Backs `ClearCalls()` \(PLAN\-0063/ +ADR\-0060\): observation history is reset, configured behavior \(including in\-progress +`Compono.ReturnConfig<>.SequenceOrdinal` progress\) is untouched, so a subsequent call resumes the +sequence rather than rewinding it\. + +```csharp +public void ClearObservedCalls(); +``` \ No newline at end of file diff --git a/docs/reference/api/Compono/Compono.ReturnConfig_T_.md b/docs/reference/api/Compono/Compono.ReturnConfig_T_.md index 74855db..c052d13 100644 --- a/docs/reference/api/Compono/Compono.ReturnConfig_T_.md +++ b/docs/reference/api/Compono/Compono.ReturnConfig_T_.md @@ -30,5 +30,6 @@ public struct ReturnConfig | Methods | | | :--- | :--- | | [ClearConfiguredResponse\(\)](Compono.ReturnConfig_T_.ClearConfiguredResponse().md 'Compono\.ReturnConfig\\.ClearConfiguredResponse\(\)') | Clears the configured value, exception, or sequence without changing the recorded call count\. This is infrastructure for generator\-emitted member\-specific configuration builders when they replace an ordinary response with an invocation callback \(ADR\-0053\)\. | +| [ClearObservedCalls\(\)](Compono.ReturnConfig_T_.ClearObservedCalls().md 'Compono\.ReturnConfig\\.ClearObservedCalls\(\)') | Clears the recorded call count without changing the configured value, exception, or sequence \- the mirror of [ClearConfiguredResponse\(\)](Compono.ReturnConfig_T_.ClearConfiguredResponse().md 'Compono\.ReturnConfig\\.ClearConfiguredResponse\(\)')\. Backs `ClearCalls()` \(PLAN\-0063/ ADR\-0060\): observation history is reset, configured behavior \(including in\-progress `Compono.ReturnConfig<>.SequenceOrdinal` progress\) is untouched, so a subsequent call resumes the sequence rather than rewinding it\. | | [NextSequenceOutcome\(\)](Compono.ReturnConfig_T_.NextSequenceOutcome().md 'Compono\.ReturnConfig\\.NextSequenceOutcome\(\)') | Consumes and returns \(or throws\) the next outcome in the configured sequence, by invocation ordinal \- the first call gets index 0, the second index 1, and so on\. Only meaningful when [HasConfiguredSequence](Compono.ReturnConfig_T_.HasConfiguredSequence.md 'Compono\.ReturnConfig\\.HasConfiguredSequence') is [true](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool 'https://docs\.microsoft\.com/en\-us/dotnet/csharp/language\-reference/builtin\-types/bool')\. Once the sequence is exhausted, every further call repeats the final configured outcome \(ADR\-0054's chosen exhaustion semantics, matching NSubstitute's own established `Returns(a, b, c)` behavior\)\. | | [RecordCall\(\)](Compono.ReturnConfig_T_.RecordCall().md 'Compono\.ReturnConfig\\.RecordCall\(\)') | Records one call to this member\. Generated dispatch code always calls this rather than incrementing `Compono.ReturnConfig<>.CallCount` directly \- that field is [internal](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/internal 'https://docs\.microsoft\.com/en\-us/dotnet/csharp/language\-reference/keywords/internal') and unwritable from the consumer assembly the generated code actually lives in\. See ADR\-0044 Amendment 2, Finding 1\. | diff --git a/docs/research/0023-compono-logging-1.1-research.md b/docs/research/0023-compono-logging-1.1-research.md new file mode 100644 index 0000000..b8e6cf8 --- /dev/null +++ b/docs/research/0023-compono-logging-1.1-research.md @@ -0,0 +1,289 @@ +# [RESEARCH-0023] Compono.Logging 1.1 Research + +Status: Done (research only; no ADR yet) + +Feeds: a future ADR-0055 amendment or a new ADR, if 1.1 scope is accepted. Does not itself change any code, ADR, or plan. + +This is research only. No production code, ADR, or plan was created or modified. Scope is explicitly **not** GitHub issue #132 (generated-logging-ownership-under-parallel-MSBuild correctness bug) — that is a separate, already-filed defect; it is mentioned below only where it independently surfaced as an architectural observation. + +## 1. Current package responsibility + +`Compono.Logging` (`src/Compono.Logging/Compono.Logging.csproj`) gives first-class `Microsoft.Extensions.Logging` testing support inside Compono composition: `ILogger`/`ILogger` compose as a hand-written, reflection-free `CapturingLogger`/`CapturingLogger` pair, with direct inspection (`GetCapturedEntries()`/`GetLastCapturedEntry()`/`ClearCapturedEntries()`) and fluent verification (`Verify()...Once()/Never()/Exactly(n)`) reusing core `Compono`'s `CallVerifier` unmodified. It is documented in [ADR-0055](../adr/0055-compono-logging-testing-support-package.md) (5 amendments) and [RESEARCH-0013](0013-compono-logging-testing-design-research.md), shipped in 1.0 (`52af46c`, PR #116), and has had exactly one follow-up commit since (`43a5e81`, an unrelated generator-runtime-hook-policy chore). No feature work has touched it since 1.0 shipped. + +## 2. Current architecture + +Eleven files, `src/Compono.Logging/`: + +| File | Role | +|---|---| +| `CapturedLogEntry.cs` | `readonly record struct` — the captured-entry model (raw + derived, §4). | +| `CapturingLogger.cs` / `CapturingLogger{T}.cs` | `ILogger`/`ILogger` implementations. Each composes its own `LogEntryCollector`; `CapturingLogger` does **not** wrap or inherit `CapturingLogger` (composition-over-inheritance, deliberately breaking from `LayeredCraft.StructuredLogging`'s `TestLogger : TestLogger`). | +| `LogEntryCollector.cs` | `internal`. Owns the lock-guarded entry list, one `LoggerExternalScopeProvider`, and the effective `MinimumLevel`. All capture/filter/scope logic lives here. | +| `LoggingOptions.cs` | One setting: `MinimumLevel` (default `LogLevel.Trace`), fixed at construction. | +| `LogVerificationBuilder.cs` | Fluent filter chain (`AtLevel`/`WithEventId`/`WithException`/`WithMessageContaining`/`Matching`) ending in three one-line forwarders to `Compono.CallVerifier` (`Once`/`Never`/`Exactly`). | +| `LoggerTestingExtensions.cs` | Public extension methods on `ILogger` (`GetCapturedEntries`, `GetLastCapturedEntry`, `ClearCapturedEntries`, `Verify`) — throws `InvalidOperationException` if the `ILogger` isn't a Compono.Logging capturing logger. | +| `ICapturingLoggerFacade.cs` | `internal` dispatch interface so the extension methods above can reach either concrete logger's `LogEntryCollector` without a public downcast. | +| `CompositionBuilderExtensions.cs` | `UseLogging(Action?)` — registers `LoggingProvider` as a stage-6 `ICompositionValueProvider`. | +| `LoggingProvider.cs` | `internal`. The stage-6 provider: bare `ILogger` → `new CapturingLogger(options)` directly; closed `ILogger` → looks up `LoggingFactoryRegistry.TryCreate`. | +| `LoggingFactoryRegistry.cs` | `public` (cross-assembly generator-infrastructure reasons, ADR-0055 Amendment 2) `Type`-keyed registry of generated `CapturingLogger` activators, populated by a `[ModuleInitializer]` the shared `Compono.Generators` emits in the *consumer's own assembly*. | +| `build/Compono.Logging.props` | Defaults `ComponoGeneratedLogging` to `true` (packed to both `build/` and `buildTransitive/`). | + +**Generator integration** (`src/Compono.Generators/`): `Compono.Logging` ships **no generator/analyzer DLL of its own** (ADR-0055 Amendment 3). Logging activation generation was moved into the existing, already-packed `Compono.Generators`: +- `WellKnownTypes/LoggingWellKnownTypes.cs` — a dedicated `ILogger`/`ILogger` symbol resolver, deliberately carrying **no** `Microsoft.Extensions.Logging.Abstractions` package reference itself; `Compilation.GetTypeByMetadataName` returns `null` cleanly for a consumer who never referenced `Compono.Logging`, keeping the shared generator project dependency-free. +- `Models/DiscoveredLoggingCategoryInfo.cs`, `Models/LoggingRuntimeSymbolsStatus.cs`, `Models/GeneratorFeatureFlags.cs` — discovery/status models. +- `Templates/LoggingActivation.scriban`, `Emitters/LoggingActivationEmitter.cs` — emits, per discovered closed `ILogger` category, a `[ModuleInitializer]`-registered call to `LoggingFactoryRegistry.Register(...)` that closes `new CapturingLogger(options)`. +- Gated by `ComponoGeneratedLogging` (default `true`, unlike `Compono.TestDoubles`'s pure-opt-in `ComponoGeneratedTestDoubles` — a deliberate product decision, ADR-0055 Amendment 3). +- Per ADR-0055 Amendment 4: when `ComponoGeneratedLogging` is enabled, `Compono.TestDoubles` generation *excludes* `ILogger`/`ILogger` entirely — Logging owns generation for those types; there is no dual-ownership collision by construction (a real collision `PLAN-0055` task 18 dogfooding found and this amendment fixed). + +## 3. Current dependency graph + +`Compono.Logging.csproj`: +```xml + + +``` +No dependency on `Compono.TestDoubles`, `Compono.NSubstitute`, `Microsoft.Extensions.Logging` (the concrete package), `Microsoft.Extensions.DependencyInjection`, or `Microsoft.Extensions.Diagnostics.Testing` — all deliberate, ADR-0055-recorded exclusions. + +**What actually reaches into core `Compono`, file by file** (confirmed by grep across every `.cs` file in `src/Compono.Logging/`, not just the csproj): +- `CompositionBuilderExtensions.cs` — `CompositionBuilder`, `AddTestDoubleProvider` (registration/config). +- `LoggingProvider.cs` — `ICompositionValueProvider`, `CompositionProviderResult`, `CompositionProviderRequest`, `ICompositionContext` (stage-6 provider integration). +- `LogVerificationBuilder.cs` — `CallVerifier` only, one `new CallVerifier(count, description)` call plus its three-method surface (`Once`/`Never`/`Exactly`, which itself throws core `TestDoubleVerificationException`). + +**What does *not* reach into core `Compono` at all**: `CapturedLogEntry.cs`, `CapturingLogger.cs`, `CapturingLogger{T}.cs`, `LogEntryCollector.cs`, `LoggingOptions.cs`, `ICapturingLoggerFacade.cs`, `LoggerTestingExtensions.cs`, `LoggingFactoryRegistry.cs` — every one of these compiles against `Microsoft.Extensions.Logging.Abstractions` alone. Confirmed empirically, §11. + +Categorizing the dependency by kind, as requested: +- **Compile-time / runtime type reuse**: `CallVerifier` (verification terminal) — runtime, not generator. +- **Registration/config integration**: `CompositionBuilder`/`ICompositionValueProvider`/stage-6 pipeline — this is the actual "why does Logging depend on core Compono at all" answer for most of the surface. `UseLogging()` has to plug into the same `Composer.Create(builder => ...)` pipeline every other integration package plugs into. +- **Generator integration**: none at the `Compono.Logging` project level (it ships no generator). The generator dependency is one-directional the *other* way — `Compono.Generators` (packed into `Compono.nupkg`, not `Compono.Logging.nupkg`) knows how to look for `ILogger`/`ILogger` symbols *if present*, gated safely by `LoggingWellKnownTypes.TryCreate` returning `null` when they aren't. +- **Shared infra**: none beyond `CallVerifier` and the stage-6 provider contract — no shared exception hierarchy, no shared `CompositionPath`/diagnostics helper reuse (`LoggingProvider.FriendlyTypeName` is a deliberately re-implemented local helper specifically because `CompositionPath.FriendlyTypeName` is `internal` to core `Compono` with no `InternalsVisibleTo` grant). +- **Merely packaging**: none — every core reference is load-bearing, not vestigial. + +## 4. Existing 1.0 public contract + +Confirmed by reading the actual types (`src/Compono.Logging/*.cs`), not just the ADR's illustrative snippet: + +```csharp +public static class CompositionBuilderExtensions +{ + public static CompositionBuilder UseLogging(this CompositionBuilder builder, Action? configure = null); +} + +public sealed class LoggingOptions +{ + public LogLevel MinimumLevel { get; set; } = LogLevel.Trace; +} + +public readonly record struct CapturedLogEntry +{ + public LogLevel LogLevel { get; } + public EventId EventId { get; } + public Exception? Exception { get; } + public string Message { get; } + public object? State { get; } + public IReadOnlyList>? Properties { get; } + public string? MessageTemplate { get; } + public IReadOnlyList Scopes { get; } + public DateTimeOffset Timestamp { get; } + // constructor is internal — a consumer inspects, never fabricates, an entry +} + +public sealed class CapturingLogger : ILogger +{ + public CapturingLogger(LoggingOptions? options = null); +} + +public sealed class CapturingLogger : ILogger +{ + public CapturingLogger(LoggingOptions? options = null); +} + +public static class LoggerTestingExtensions // extension methods on ILogger +{ + public static IReadOnlyList GetCapturedEntries(this ILogger logger); + public static CapturedLogEntry? GetLastCapturedEntry(this ILogger logger); + public static void ClearCapturedEntries(this ILogger logger); + public static LogVerificationBuilder Verify(this ILogger logger); +} + +public sealed class LogVerificationBuilder +{ + public LogVerificationBuilder AtLevel(LogLevel level); + public LogVerificationBuilder WithEventId(EventId eventId); + public LogVerificationBuilder WithException() where TException : Exception; + public LogVerificationBuilder WithMessageContaining(string text); + public LogVerificationBuilder Matching(Func predicate); + public void Once(); + public void Never(); + public void Exactly(int times); +} + +public static class LoggingFactoryRegistry // generator infrastructure, [EditorBrowsable(Never)] on Register +{ + public static void Register(Func factory); + public static bool TryCreate(Type requestedType, LoggingOptions options, out object? value); +} +``` + +`CallVerifier` itself (`src/Compono/CallVerifier.cs`) is core, reused unmodified: `readonly struct CallVerifier(int observedCount, string memberDescription)` with exactly `Never()`/`Once()`/`Exactly(int)` — **no `AtLeast`/`AtMost`/ordering**, "deliberately minimal... per ADR-0044 Requirement 3." This is a load-bearing fact for §8 below: any `AtLeast(n)`/`AtMost(n)` on `LogVerificationBuilder` cannot be a one-line forward to `CallVerifier` the way `Once`/`Never`/`Exactly` are — `CallVerifier` doesn't have those members, and ADR-0044 deliberately kept it that way for the whole `Compono.TestDoubles` surface, not just Logging. + +## 5. Consumer ergonomics review + +What a call like `logger.LogInformation("Processed order {OrderId} for {CustomerId}", orderId, customerId)` produces in `CapturedLogEntry`, verified by reading `LogEntryCollector.Record`/`ExtractStructuredState` (`src/Compono.Logging/LogEntryCollector.cs:36-119`): + +| Captured today | Publicly accessible today | Notes | +|---|---|---| +| `LogLevel` | Yes (`.LogLevel`, `Verify().AtLevel(...)`) | | +| `EventId` | Yes (`.EventId`, `Verify().WithEventId(...)`) | Only equality filter exists on `Verify()`; no "any EventId with this Id ignoring Name" partial-match helper. | +| `Exception` | Yes (`.Exception`, `Verify().WithException()`) | Type-only filter; no message/property predicate on the exception itself beyond `Matching(...)`. | +| Formatted message (`"Processed order 42 for 7"`) | Yes (`.Message`, `Verify().WithMessageContaining(...)`) | Only a `Contains` filter; no exact-match, prefix, `StringComparison` choice, or regex helper on `Verify()` itself (`Matching` covers it, but is unnamed/undescribed in verification failure output — see §6). | +| Message template (`"Processed order {OrderId} for {CustomerId}"`) | Yes, but **only via direct inspection** — `.MessageTemplate` on `CapturedLogEntry`. **Not reachable through `Verify()` at all.** | A consumer wanting `Verify().WithMessageTemplate("...")` must drop to `.Matching(e => e.MessageTemplate == "...")`. | +| Structured property names/values (`OrderId=42`, `CustomerId=7`) | Yes, but **only via direct inspection** — `.Properties` on `CapturedLogEntry`. **Not reachable through `Verify()` at all.** | Same gap: `Verify().WithProperty("OrderId", 42)` doesn't exist; must use `.Matching(...)`. | +| Scopes | Yes (`.Scopes`) | **Not reachable through `Verify()` at all** — no `Verify().InScope(...)`/`WithScopeContaining(...)`. Must use `.Matching(...)` against `e.Scopes`. | +| Call count semantics | `Once`/`Never`/`Exactly(n)` | No `AtLeast(n)`/`AtMost(n)`/`Between(min,max)` — see §4's `CallVerifier` constraint. | +| Retrieving *which* entries matched a `Verify()` chain | **No.** `Verify()` is fluent-only and terminal (`void`-returning `Once`/`Never`/`Exactly`) — a consumer who wants the matched entries themselves (e.g., to assert further on `Properties` after confirming `Once()`) must re-filter `GetCapturedEntries()` by hand with the same predicate logic already written into the `Verify()` chain. | This is the most concrete "info captured internally, hard to access" gap: `LogVerificationBuilder.ToCallVerifier()` (`src/Compono.Logging/LogVerificationBuilder.cs:66-87`) already computes the exact matching entries internally (it iterates and applies every filter) but discards everything except the count before constructing `CallVerifier`. | +| Ordering across entries (e.g. "the warning happened before the retry log") | Entries are `IReadOnlyList` in append order (`LogEntryCollector._entries`, oldest-first) via `GetCapturedEntries()`, so ordering *is* inspectable directly. | No `Verify()`-level ordering assertion exists (deliberately out of scope for `CallVerifier`-family verification generally, ADR-0044 Requirement 3 — not Logging-specific). | +| Negative verification | `Never()` | Already covered by existing `CallVerifier.Never()` reuse. | + +Bottom line: **nothing about a `LogInformation(...)`/`[LoggerMessage]` call is uncaptured.** Every piece of information this research's brief asked about (message template, structured properties, EventId, exception, scopes) is already extracted and present on `CapturedLogEntry` in 1.0. The actual gap is entirely in `Verify()`'s filter vocabulary and in `Verify()`'s inability to hand back its matched-entry set — a fluent-ergonomics gap, not a capture gap. + +## 6. Observed or likely friction + +1. **Structured-property/template verification requires dropping to `.Matching(...)` with no message-shaping.** A consumer asserting `OrderId == 42` today writes `logger.Verify().Matching(e => e.Properties?.Any(p => p.Key == "OrderId" && Equals(p.Value, 42)) == true).Once()` — verbose, and on failure `LogVerificationBuilder.Describe()` (`LogVerificationBuilder.cs:89-103`) renders it as the generic `"a custom condition"`, losing the specific property/value in the failure message that `AtLevel`/`WithEventId`/etc. already produce (`"level Warning"`, `"event id ..."`). This is the single clearest, most concrete ergonomic gap: named filters get descriptive failure text; the escape hatch doesn't. +2. **No way to get the matched entries back after `Verify()`.** A test that wants both "exactly one warning was logged" and "that warning's OrderId property was 42" must write the filter predicate twice — once inside `Verify().Matching(...)`, once again against `GetCapturedEntries()` — because `Once()`/`Never()`/`Exactly()` are `void`. `LogVerificationBuilder` already has the matched list in hand internally and throws it away. +3. **`AtLeast(n)`/`AtMost(n)` don't exist**, for a real reason (`CallVerifier` doesn't have them, ADR-0044), but this is a plausible ask a consumer used to `TUnit.Mocks.Logging`'s `Times.AtLeastOnce`-style API or `FakeLogger`'s manual-LINQ flexibility might reach for. +4. **No skill/doc/eval evidence of live consumer complaints** — `git log --oneline -- src/Compono.Logging` shows exactly one substantive commit (the original PR #116) since 1.0 shipped, with no follow-up bug reports, no reopened ADR-0055 amendment, and no dogfooding-surfaced friction beyond what Amendment 4 already fixed (the generation-ownership collision, unrelated to consumer-facing verification ergonomics). The friction identified here is derived from reading the actual `LogVerificationBuilder`/`CapturedLogEntry` code against the stated capture set, not from an observed complaint — flagged honestly rather than manufactured as urgent. + +## 7. Candidate improvements + +1. **`WithProperty(string key, object? value)` / `WithMessageTemplate(string template)` filters on `LogVerificationBuilder`** — closes the §5/§6-#1 gap directly: named, described filters for the two pieces of `CapturedLogEntry` (`Properties`, `MessageTemplate`) that are captured but only reachable through `.Matching(...)` today. +2. **A way to retrieve the entries a `Verify()` chain matched**, e.g. a non-terminal `.Entries()`/`.ToList()` returning `IReadOnlyList` alongside (not instead of) the existing `Once()`/`Never()`/`Exactly()` terminals — closes §6-#2. +3. **`AtLeast(int)`/`AtMost(int)` terminals on `LogVerificationBuilder`.** +4. **A `WithMessage(string)` exact-match filter** alongside the existing `WithMessageContaining`, and/or a `StringComparison` overload on `WithMessageContaining`. +5. Standalone-package split (`Compono.Logging` + `Compono.Logging.Composition` or similar) — evaluated as its own axis, §9-§10, not folded into the ranked list. + +## 8. Detailed analysis of each serious candidate + +**Candidate 1 — `WithProperty`/`WithMessageTemplate` filters.** +- Additive: two new methods on `LogVerificationBuilder`, same shape as the five that already exist (`Add(description, predicate)`), zero change to any existing member, zero change to `CapturedLogEntry` or `CapturingLogger`. +- Directly closes the concretely-identified gap (§6-#1): captured-but-only-`Matching`-reachable information gets first-class, described filters. +- `WithProperty` needs an equality semantics decision (ordinal/structural `Equals`, `null`-value handling — `Properties`' value slot is already nullable per ADR-0055's "Properties nullability" decision) but no new abstraction; mirrors `WithEventId`'s existing `Equals`-based filter shape almost exactly. +- `WithMessageTemplate` is a straight `entry.MessageTemplate == template` predicate, ordinal string compare — as simple as `AtLevel`. +- Low risk, no ADR-0055 boundary crossed (§4's "structured property names/values" was always captured; this only extends `Verify()`'s vocabulary to reach what's already on `CapturedLogEntry`). + +**Candidate 2 — retrieve matched entries from `Verify()`.** +- Additive if designed as a new non-terminal method (`.Entries()` or similar) that reads the same internal filtered list `ToCallVerifier()` (`LogVerificationBuilder.cs:66-87`) already computes, called *instead of* (not chained after) `Once()`/`Never()`/`Exactly()` — the existing terminals stay `void`, unmodified, so nothing about the 1.0 contract changes. +- Real, concrete value: eliminates the double-predicate-writing friction in §6-#2 without inventing a second verification concept — it's direct inspection of the same collector, filtered, which is philosophically identical to what `GetCapturedEntries()` already offers, just pre-filtered. +- Naming risk: must not read as a second "verification" terminal (that would blur `LogVerificationBuilder`'s "filter-then-assert" contract, which ADR-0055 was careful to keep as one verb, `Verify()`, ending in one of three assertion terminals). A getter-shaped name (`.Entries()`, `.Matches()`) rather than an assertion-shaped one avoids that. + +**Candidate 3 — `AtLeast(int)`/`AtMost(int)`.** +- **Not a one-line `CallVerifier` forward** like `Once`/`Never`/`Exactly` are — `CallVerifier` has no such members, by ADR-0044's deliberate design ("no call-order verification... deliberately minimal," `src/Compono/CallVerifier.cs:4-6`), a decision that applies to the entire `Compono.TestDoubles`/`Compono.Http`/`Compono.Logging` verification family, not something Logging can unilaterally reinterpret for itself. +- Two honest paths: (a) implement `AtLeast`/`AtMost` locally inside `LogVerificationBuilder` without going through `CallVerifier` (straightforward — `ToCallVerifier()`'s `matchCount` is already computed; a local `if (matchCount < n) throw new TestDoubleVerificationException(...)` is a few lines), which is additive to Logging alone and doesn't touch core; or (b) propose extending `CallVerifier` itself, which is an ADR-0044-boundary change affecting `Compono.TestDoubles`/`Compono.Http` too and is explicitly out of this research's scope (that would need its own ADR amendment against ADR-0044, not a Logging-only 1.1 change). +- Recommendation if pursued: path (a) only — keep it Logging-local, additive, and out of `CallVerifier`'s established minimal contract. This avoids relitigating ADR-0044 to ship a Logging-specific ergonomic want. +- Genuinely useful but the least differentiated of the three — `Once`/`Never`/`Exactly` already cover most real assertions, and no consumer evidence (§6-#4) specifically asks for it. + +**Candidate 4 — `WithMessage` exact-match / `StringComparison` overload.** +- Small, real, but marginal: `WithMessageContaining` already covers the dominant real-world case (substring assertion tolerant of exact wording drift), and an exact-match variant mostly matters for brittle tests that arguably shouldn't assert exact formatted-message text anyway (message templates change wording; `WithMessageTemplate`, candidate 1, is the more future-proof answer to "assert on message content precisely"). Included for completeness, not ranked. + +## 9. Standalone-package feasibility + +**Empirically confirmed** (§11 experiment): the capture core — `CapturedLogEntry`, `CapturingLogger`, `CapturingLogger`, `LogEntryCollector`, `LoggingOptions`, `ICapturingLoggerFacade` — has **zero** references to any core `Compono` type today. It compiles standalone against `Microsoft.Extensions.Logging.Abstractions` alone. This is not a discovered accident; it follows directly from ADR-0055's original architecture (§2-§3 above): the hand-written logger pair was always designed to be "directly, publicly constructible... composing through `UseLogging()` is not required" (doc comments on both `CapturingLogger` and `CapturingLogger`). + +What actually ties `Compono.Logging` to core `Compono` is narrow and named exactly in §3: +1. `CompositionBuilderExtensions.UseLogging`/`LoggingProvider` — the `Composer.Create(builder => ...)` composition integration. +2. `LogVerificationBuilder`'s three terminals forwarding to `CallVerifier`. + +`LoggerTestingExtensions` (`GetCapturedEntries`/`GetLastCapturedEntry`/`ClearCapturedEntries`/`Verify`) touches core only transitively, through `Verify()`'s return type (`LogVerificationBuilder`) — the first three members have no core dependency at all. + +**Conclusion: the capture-and-inspect half of `Compono.Logging` is already, today, a coherent standalone experience independent of Compono composition** — a consumer could `new CapturingLogger()`, pass it to `OrderService`'s constructor by hand (no `Composer.Create` involved), and call `GetCapturedEntries()`/`GetLastCapturedEntry()`/`ClearCapturedEntries()` with no core `Compono` type ever touched at runtime (only at compile time, transitively, through the `Compono.Logging.csproj` `ProjectReference` that exists for `Verify()`/`CallVerifier`, which the standalone consumer simply wouldn't call). This isn't a hypothetical architecture proposal — it's already true of the shipped 1.0 code, and the docs already say so explicitly (ADR-0055 xmldoc on both logger types; RESEARCH-0013 §3's "no factory needed for the common case" framing was itself modeled on `FakeLogger`'s identical standalone-constructibility). + +The only genuinely coupled piece is `Verify()` — and even that coupling is a single `CallVerifier` struct with three members, not a deep dependency. + +## 10. Architectural options for independence + +Given §9's finding, the real question is not "can capture be decoupled" (it already is, at the type level) but "is a **package-level** split (`Compono.Logging` + `Compono.Logging.Composition`) worth doing": + +- **Option A — status quo (one package, already-decoupled types).** The csproj-level `ProjectReference` to core `Compono` exists, but nothing forces a consumer who never calls `UseLogging()`/`Verify()` to pay any *runtime* cost for it — the JIT/AOT compiler only pulls in what's actually called. The only cost is a compile-time transitive reference to `Compono.dll` even for a consumer who only wants direct construction. Given `Compono.Logging` already requires `Compono` as a `PackageReference` anyway (documented "install both" in `docs/packages/compono-logging.md`), this transitive reference costs nothing a consumer wasn't already paying. +- **Option B — split `Compono.Logging` (capture+inspect, zero core dependency) from `Compono.Logging.Composition` (adds `UseLogging()`/`Verify()`/`CallVerifier` reuse, depends on core `Compono`).** This would let a consumer who wants only `CapturingLogger` + `GetCapturedEntries()` (no Compono composition anywhere in their test suite) install one package with **zero** transitive `Compono` reference. Evaluated against real value below. +- **Option C — depend on `Microsoft.Extensions.Diagnostics.Testing`'s `FakeLogger` for capture, keep only a thin Compono-composition/`Verify()` layer.** Already considered and rejected in ADR-0055 itself (Option 3 under "Package/routing," Decision Outcome §"Package identity and dependency graph") for unconfirmed AOT story and heavier transitive surface — nothing in this research changes that calculus; not re-litigated further here. + +**Is Option B worth it?** No — for three concrete reasons, not a vague "keep it simple": +1. **No real consumer asks for `Compono.Logging` without `Compono`.** `Compono.Logging` is a Compono integration package by name and by every real usage example in the repo (`docs/packages/compono-logging.md`, `skills/compono/references/logging.md`, `samples/Compono.Samples.BasicUsage/LoggingTests.cs`) — every one of them composes through `Composer.Create`/`UseLogging()`. A "just give me `CapturingLogger` with no Compono at all" consumer is a *possible* standalone-testing-library use case but is not what anyone installing `Compono.Logging` is actually doing today (there is zero evidence — no issue, no consumer request, no dogfooding finding — that anyone wants this). +2. **A split would create exactly the "awkward behavior around generated `ILogger` ownership" the brief asked to check for.** `LoggingFactoryRegistry`/generated activation (ADR-0055 Amendments 1-3) exists specifically to let `Composer.Create` resolve a closed `ILogger` request through Compono's stage-6 provider pipeline. If capture+inspect lived in a package with no reference to `CompositionBuilder`/`ICompositionValueProvider` at all, `UseLogging()` and `LoggingProvider` would have nowhere to live except the "Composition" half — meaning the generated-activation machinery (`LoggingFactoryRegistry`, the `[ModuleInitializer]`, `Compono.Generators`' `LoggingActivationEmitter`) would *also* need to move to, or straddle, the Composition package, since it exists purely to serve `LoggingProvider`. This doesn't cleanly cut along the "capture vs. composition" seam the brief hypothesized — the generated-activation registry is composition-integration infrastructure, not capture infrastructure, even though it currently lives in the same physical project as the capture types. Splitting would either (a) leave `LoggingFactoryRegistry` in the capture-only package for no architectural reason (it's meaningless without `LoggingProvider`), or (b) move it to the Composition package, which then needs `[ModuleInitializer]`-generated code (compiled into the *consumer's* assembly) to reference a package the consumer may not have installed if they only wanted plain capture — a real, not hypothetical, dependency-direction problem. +3. **The cost being solved for is already close to zero (§9's Option A analysis)** — a compile-time-only transitive `ProjectReference`/`PackageReference` to `Compono`, for a package whose entire raison d'être (per its own name, `Compono.`, and every one of its docs) is Compono composition integration. Splitting trades a real, working, already-decoupled-at-the-type-level design for two packages, two release cadences, two sets of docs, and a real architectural seam mismatch (#2), to save a cost that doesn't meaningfully exist for the audience this package serves. + +**Conclusion: do not split.** The standalone-viable *design* (§9) should be named and preserved as an explicit, documented property of the existing single package — worth a doc callout (§13/§17) — but a package-level split has no real consumer value and one concrete architectural cost. + +## 11. Experiments performed + +**Standalone-capture compile spike** (proves/disproves §9's claim directly, rather than reasoning from the dependency graph alone): copied `CapturedLogEntry.cs`, `CapturingLogger.cs`, `CapturingLogger{T}.cs`, `LogEntryCollector.cs`, `LoggingOptions.cs`, `ICapturingLoggerFacade.cs` unmodified into a throwaway project (`/private/tmp/.../scratchpad/standalone-test/CapturingCore/`) referencing only `Microsoft.Extensions.Logging.Abstractions` — no `Compono` reference at all: + +```xml + + + net9.0 + enable + enable + + + + + +``` + +Result: **`dotnet build` succeeded, 0 warnings, 0 errors.** Confirms §9's claim by direct compilation, not just by absence-of-reference grep — the capture core genuinely has no hidden core-`Compono` coupling (e.g., via a shared `GlobalUsings.cs` or implicit base type) that a csproj-level grep alone might have missed. Scratch files were not committed and are not part of any src/ change. + +## 12. External research + +- **`FakeLogger`/`FakeLogRecord`** (`Microsoft.Extensions.Diagnostics.Testing`, current per Microsoft Learn and NuGet as of this research pass): `FakeLogger` is directly, publicly constructible with no `ILoggerFactory` — the same "no factory needed" ergonomics `Compono.Logging` already adopted. `FakeLogRecord` keeps raw `State` **and** a derived `StructuredState`/`GetStructuredStateValue(key)` surface side by side — the same "raw + derived" shape `CapturedLogEntry` already adopted (RESEARCH-0013 §3, re-confirmed here). Critically for §7-#3: **`FakeLogCollector` itself has no `Times`/`AtLeast`/`AtMost` verification API at all** — its entire query surface is `GetSnapshot()` plus manual LINQ. This means Compono.Logging's existing `Once`/`Never`/`Exactly` fluent terminals are already *ahead* of Microsoft's own official capture library on verification ergonomics; adding `AtLeast`/`AtMost` would extend that lead, not catch up to a gap. +- **`TUnit.Mocks.Logging`** (beta, per RESEARCH-0013 §4, re-confirmed as still the relevant shape): `logger.VerifyLog().AtLevel(...).ContainingMessage(...).WasCalled(Times.Once)` — a fluent filter-then-count-terminal shape structurally identical to `Compono.Logging`'s own `Verify()...Once()`. TUnit's own public surface, per that research, does **not** expose structured-property or scope filters either — narrower than `Compono.Logging`'s `CapturedLogEntry`, not broader. No new evidence from this pass changes that assessment; `Compono.Logging`'s captured-entry model remains the more complete of the two. +- Net effect on ergonomics ideas: external prior art validates the *existing* 1.0 design more than it suggests unmet capability — neither `FakeLogger` nor `TUnit.Mocks.Logging` has a richer verification vocabulary than what candidates 1-2 in §7 would add to `Compono.Logging`. This is inspiration confirming direction, not a parity gap to close. + +Sources: [FakeLogger Class (Microsoft.Extensions.Logging.Testing) — Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.testing.fakelogger), [Testing logging code with Microsoft.Extensions.Logging and FakeLogger](https://blog.elmah.io/testing-logging-code-with-microsoft-extensions-logging-and-fakelogger/), [NuGet Gallery — Microsoft.Extensions.Diagnostics.Testing](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.Testing/). + +## 13. Compatibility implications + +- **Source compatibility**: all three ranked candidates (§16) are pure additions — new methods on `LogVerificationBuilder`, no existing signature changed, no existing method removed or given new required parameters. No consumer code that compiles against 1.0 stops compiling against 1.1. +- **Binary compatibility**: additive members on a `sealed class` are binary-compatible additions (no interface implementation changes on `LogVerificationBuilder` — it's a concrete sealed type, not an interface). `CapturedLogEntry` is untouched by every ranked candidate. +- **Generated-source compatibility**: none of the ranked candidates touch `Compono.Generators`' `LoggingActivationEmitter`/`LoggingActivation.scriban` or `LoggingWellKnownTypes` — the generated `[ModuleInitializer]`/`LoggingFactoryRegistry.Register` shape is entirely unaffected. No regeneration-triggering change. +- **Analyzer/generator behavior**: unaffected — `CMP0038`/`CMP0039` diagnostics are about generated-activation discovery, not verification API surface. +- **Runtime behavior**: `WithProperty`/`WithMessageTemplate`/an `.Entries()`-style accessor only ever run inside a new code path a consumer opts into by calling the new member; no existing `Verify()` chain's behavior changes. +- **Native AOT / trimming**: every ranked candidate is ordinary generic-free, reflection-free C# (predicate delegates and struct/list operations, the same shape `AtLevel`/`WithEventId`/`Matching` already use) — no new reflection, no new dynamic dispatch, consistent with `Compono.Logging`'s existing `true` claim (ADR-0055's own AOT requirement, following `Compono.Http`'s ADR-0051 precedent). +- **Deterministic builds**: unaffected — no new source-generation-time nondeterminism introduced (no ranked candidate touches the generator). +- **Package dependency changes**: none of the three ranked candidates change `Compono.Logging.csproj`'s dependency list. The rejected standalone-split idea (§9-§10) would have been the one candidate with real package-dependency-graph implications; it's rejected specifically so this section stays "none." +- **SemVer**: all three ranked candidates are additive-only — correctly a **minor** version bump (1.1.0), not a major one. No existing public member's signature, nullability, or return type changes. + +## 14. AOT/trimming/generator implications + +Covered in §13 above; restated briefly because the brief calls it out as its own numbered item: no ranked candidate adds reflection, dynamic proxying, or source generation of any kind. `LoggingActivationEmitter`/`LoggingWellKnownTypes`/`LoggingFactoryRegistry` (the actual generator-touching surface) are untouched by every ranked candidate — none of the three candidates changes what gets generated, when, or how activation is discovered. + +## 15. Rejected ideas + +- **`AtLeast`/`AtMost` implemented by extending core `CallVerifier`** — rejected as *this* research's recommendation (not rejected outright as an idea): it's a real ADR-0044 boundary change affecting `Compono.TestDoubles`/`Compono.Http` too, out of scope for a Logging-only 1.1, and would need its own ADR-0044 amendment with its own consumer-evidence bar. If pursued at all, path (a) in §8 (Logging-local implementation, bypassing `CallVerifier`) is the additive, in-scope version. +- **Package split (`Compono.Logging` + `Compono.Logging.Composition`)** — rejected, §10, on concrete grounds (no consumer evidence, real generated-activation-ownership seam mismatch, near-zero cost being solved for), not "keep it simple" hand-waving. +- **`ILoggerFactory` composition support** — already an explicit ADR-0055 v1 non-goal ("no native `ILoggerFactory` support in v1... not becoming general logging infrastructure") with no new evidence in this pass to revisit it; correctly out of a 1.1 scope focused on closing existing-capability ergonomics gaps, not expanding responsibility. +- **A category-string constructor for the non-generic `CapturingLogger`** — an explicit ADR-0055/skill-documented v1 boundary (`skills/compono/references/logging.md`'s "v1 boundaries" list); no new evidence surfaced here to revisit it, and it would blur `CapturingLogger` vs. `CapturingLogger`'s clean split. +- **Per-level `ControlLevel`-style toggle (replacing the single `MinimumLevel` threshold)** — already an explicit ADR-0055 non-goal ("No `ControlLevel`-style per-level toggle is added here — out of scope"); FakeLogger has it, but no real consumer evidence from either the original ADR-0055 research or this pass asks for it, and a single threshold has covered every real case documented so far. +- **Ordering assertions across log entries** (e.g. "warning happened before retry") — `GetCapturedEntries()` already exposes append order directly for a consumer who needs this; adding an ordering-assertion DSL to `Verify()` would be new abstraction on top of information already retrievable by hand, and no consumer evidence asks for it — manufacturing scope, not closing a gap. + +## 16. Ranked recommendations + +1. **`WithProperty(string, object?)` and `WithMessageTemplate(string)` filters on `LogVerificationBuilder`** (§7 candidate 1, §8 detail). Strongest candidate: closes a concretely-identified, code-verified gap (§5/§6-#1) — structured properties and message templates are *already captured* on `CapturedLogEntry` in 1.0 but only reachable through `Verify()`'s generic `.Matching(...)` escape hatch, which also degrades failure-message quality (`"a custom condition"` vs. a named, specific description). Purely additive, zero core-Compono touch, zero AOT/generator implication, mirrors the exact existing pattern of every other filter method on the same class. +2. **A non-terminal accessor to retrieve `Verify()`'s matched entries** (§7 candidate 2, §8 detail), e.g. `.Entries()` returning `IReadOnlyList`, additive alongside the existing `Once`/`Never`/`Exactly` terminals. Closes the double-predicate-writing friction (§6-#2) that's structurally present today — `LogVerificationBuilder` already computes the matched set internally and discards it. Needs careful naming so it reads as inspection, not a second assertion verb, to protect ADR-0055's one-verb `Verify()` contract. +3. **`AtLeast(int)`/`AtMost(int)` terminals, implemented Logging-locally (not via `CallVerifier`)** (§7 candidate 3, §8 detail). Real and additive, but ranked third: less concretely evidenced than 1-2 (no specific captured-but-unreachable data point behind it, just a plausible ergonomic want validated only by TUnit's `Times.AtLeastOnce`-shaped prior art, §12), and it must be implemented carefully to avoid quietly extending `CallVerifier`'s deliberately-minimal ADR-0044 contract. + +`WithMessage`/`StringComparison` overload (§7 candidate 4) and the standalone-package split (§9-§10) are explicitly **not** ranked — the former is too marginal against the already-strong `WithMessageContaining`+`WithMessageTemplate` combination, the latter is a rejected architectural idea (§15), not a feature. + +## 17. Recommended 1.1 scope + +Ship candidates 1 and 2 together (`WithProperty`/`WithMessageTemplate` filters plus a matched-entries accessor) as the core of a `Compono.Logging` 1.1: both are small, additive, evidence-backed extensions of the exact same `LogVerificationBuilder` class, touch no other package, and require no ADR-0044 boundary discussion. Candidate 3 (`AtLeast`/`AtMost`) is a reasonable stretch addition to the same release if there's appetite, implemented Logging-locally per §8's path (a) — but it's the more discretionary of the three and could just as easily wait for real consumer demand. + +Separately, document (not code) that `CapturingLogger`/`CapturingLogger` are already, today, usable with zero Compono composition and zero core-`Compono` runtime dependency — this is true now, is already partially stated in the two types' own xmldoc, but isn't called out anywhere a consumer evaluating "do I need to buy into Compono composition to use this" would see it (`docs/packages/compono-logging.md`'s "When to install" section leads with composition). A documentation clarification, not an ADR or code change — flagged here per the brief's item 13, left as a note for whoever picks up 1.1 rather than actioned in this research pass. + +Do not pursue the standalone-package split (§9-§10) — it addresses a cost that doesn't meaningfully exist for `Compono.Logging`'s actual audience, and introduces a real generated-activation-ownership seam mismatch that doesn't cleanly resolve. + +## 18. Questions or evidence still unresolved + +- No real, filed consumer request (issue, dogfooding finding, or otherwise) drives any of candidates 1-3 — this research derived them from reading `CapturedLogEntry`/`LogVerificationBuilder` against the stated capture set and cross-checking against `FakeLogger`/`TUnit.Mocks.Logging` prior art, not from an observed friction report. Worth validating against `alexa-vox-craft`/`structured-logging` (the two real consumers ADR-0055's own research cited) before committing 1.1 scope, the same way ADR-0055's original research did for v1. +- `WithProperty`'s exact equality semantics (structural vs. reference, how to handle a `null` captured value against a non-null expected value or vice versa) needs a small design decision before implementation — flagged, not resolved, here. +- The naming of the matched-entries accessor (`.Entries()` vs. `.Matches()` vs. something else) needs a decision that protects the "one verb, `Verify()`" contract ADR-0055 was explicit about — this research flags the risk but doesn't pick a name. +- Whether `AtLeast`/`AtMost` belongs on `LogVerificationBuilder` at all, versus waiting for a broader `CallVerifier`-family ADR-0044 amendment that would give `Compono.TestDoubles`/`Compono.Http` the same capability consistently, is a real open product question this research surfaces but does not resolve — recommending the narrower, Logging-local path (§8) is a scoping choice for *this* research pass, not a claim that the broader question is settled. diff --git a/docs/research/0024-compono-http-1.1-research.md b/docs/research/0024-compono-http-1.1-research.md new file mode 100644 index 0000000..e16d3d2 --- /dev/null +++ b/docs/research/0024-compono-http-1.1-research.md @@ -0,0 +1,602 @@ +# [RESEARCH-0024] Compono.Http 1.1 Research + +**Status:** Done (research only; no ADR yet) + +**Feeds:** a future ADR scoping `Compono.Http`'s share of a `1.1.0` +minor release. Builds on `docs/research/0009-compono-http-admission-research.md` +(the original Gate A/B admission research) and `docs/adr/0051-compono-http-handler-based-testing-package.md` +(the accepted design, now with Amendment 1 — path-matcher split — and +Amendment 2 — `RespondBytes`, PR #133). Does not re-litigate admission; +`Compono.Http` exists and ships in 1.0. This document asks only: what, if +anything, in this package deserves a `1.1` addition, and is anything here +usable standalone from core Compono. + +**Trigger:** PR #133 (`RespondBytes`) correctly bumped the next preview to +`1.1.0`, but is too small to justify a minor release alone. This research +looks for other genuinely additive candidates in the same package before +committing to what `1.1` actually contains. + +--- + +## 1. Current package responsibility + +`Compono.Http` is a **non-generated, runtime-only** package providing a +single public surface: `TestHttpHandler`, a reflection-free +`HttpMessageHandler` subclass for testing code built on +`HttpClient`/`HttpMessageHandler`. It answers one question — "let me +control what an `HttpClient` sees as a response, and verify what it sent" +— and deliberately nothing else. ADR-0051's Decision Drivers explicitly +bound scope: no reflection, no generator involvement, minimal package +graph (no `Microsoft.Extensions.Http`, no `Compono.TestDoubles`, no +`Compono.DependencyInjection`). + +## 2. Current architecture + +Four public types, all in `src/Compono.Http/`: + +- `TestHttpHandler : HttpMessageHandler` (`TestHttpHandler.cs`) — holds a + `List`, dispatches last-registered-first/ + first-match-wins (`SendAsync`, lines 156–164), records every request in + a thread-safe log (`Requests`, `RecordRequest`), throws + `UnmatchedHttpRequestException` when nothing matches (strict-by-default, + no implicit 404 fallback). `CreateClient(Uri?)` wraps itself in an + `HttpClient` with `disposeHandler: false` — the handler is caller-owned + and multiple clients may share one. +- `HttpResponseRegistrationBuilder` (`HttpResponseRegistrationBuilder.cs`) + — the fluent finisher returned by `OnGet`/`OnPost`/`OnPut`/`OnPatch`/ + `OnDelete`/`When`. Terminal methods: `Respond(HttpStatusCode)`, + `RespondText(string, mediaType, encoding?)`, `RespondJson(T, + JsonSerializerOptions?)` (AOT-unsafe, `[RequiresDynamicCode]`/ + `[RequiresUnreferencedCode]`), `RespondJson(T, JsonTypeInfo)` + (AOT-safe), `RespondBytes(byte[], mediaType)` (Amendment 2, defensively + clones the input array), `Throws(Exception)` (same instance rethrown + every match — no factory/callback). A `_finished` guard prevents + double-finalizing one builder (line 137–141). +- `HttpResponseRegistration` (`HttpResponseRegistration.cs`) — the + verification handle returned by every terminal method. Holds the + matcher, a `Func` + response-factory (never a stored instance — "factory, not instance"), + and an `Interlocked`-incremented match count. `Verify()` returns a + **core** `Compono.CallVerifier` unchanged — `Never()`/`Once()`/ + `Exactly(n)` only, no `AtLeast`/`AtMost` (ADR-0044 Requirement 3 binds + this at the core level, not an `Http`-specific choice). +- `UnmatchedHttpRequestException` — describes method + URI only. + +Path matching uses **core `Match`** (`Match`) for the +single-scalar `OnX(path)` overload, but a **plain +`Func`** for the whole-request `When(...)` +predicate (ADR-0051 Amendment 1: `Match` exposes no accessor beyond +`Matches()`, so `Compono.Http` can't produce an honest diagnostic string +for an `Is(...)`-based `Match`, only for a literal `string`). + +## 3. Current dependency graph + +``` +Compono.Http.csproj: + + (no other PackageReference) +``` + +Confirmed via `src/Compono.Http/Compono.Http.csproj`: the **only** +dependency is core `Compono`, referenced with `PrivateAssets="none"` +(consumers get a transitive `Compono` reference — this is a real, +first-class dependency, not incidental packaging). No +`Microsoft.Extensions.Http`. No generator project reference — this is the +first Compono integration package with **zero** source-generator +involvement (contrast `Compono.TestDoubles`, `Compono.XunitV3`). + +**Why it depends on core `Compono` — exactly two touch points, both +compile-time and both reuse-not-recreate:** + +1. `Match` (core `src/Compono/Match.cs`) — used only in the + `OnGet(Match path)`-style overloads. This is genuinely a + **shared abstraction** reuse, not composition — `Match` has no + dependency on `CompositionBuilder`/`[Composable]`/anything + composition-graph-shaped; it is a standalone value type. +2. `CallVerifier` (core `src/Compono/CallVerifier.cs`) — used only in + `HttpResponseRegistration.Verify()`. Same story: `CallVerifier` is a + `readonly struct` taking `(int observedCount, string + memberDescription)` in its constructor — no composition dependency + whatsoever. + +Neither dependency touches `CompositionRow`, `[Shared]`, +`CompositionBuilder`, `ICompositionProvider`, or any other actual +*composition* concept. `TestHttpHandler` is plain-constructed +(`new TestHttpHandler()`); nothing about its lifecycle is +composition-owned (ADR-0051 states this explicitly: "Compono composition +does not own or dispose it"). The dependency on core `Compono` exists +**purely to reuse two small, composition-agnostic value/verification +types** — this is a "shared infrastructure" dependency, not a "runtime +composition integration" or "generator integration" one. + +## 4. Existing 1.0 public contract + +```csharp +var handler = new TestHttpHandler(); + +handler.OnGet("/v1/things/42") + .RespondJson(thing, ThingJsonContext.Default.Thing); + +handler.OnPost(Match.Any()) + .Respond(HttpStatusCode.Created); + +handler.When(req => req.Method == HttpMethod.Get && req.Headers.Contains("X-Trace")) + .Throws(new HttpRequestException("simulated transport failure")); + +var registration = handler.OnGet("/v1/things/42").RespondBytes(certBytes, "application/x-x509-ca-cert"); + +using var client = handler.CreateClient(new Uri("https://api.example.test")); +// exercise client... + +registration.Verify().Once(); +handler.Requests.Should().ContainSingle(r => r.RequestUri!.PathAndQuery == "/v1/things/42"); +``` + +No `IHttpClientFactory` integration, no request-body matching, no header +matching, no stream response, no sequential/conditional responses beyond +last-match-wins precedence, no status-only convenience beyond +`Respond(HttpStatusCode)` (which already covers "status-only" — there is +no separate "empty response" concept needed). + +## 5. Consumer ergonomics review + +Walking every payload/matching/failure axis the task calls out, against +what's actually implemented (`grep` confirms zero matches for +`RequestContentMatch`, header-matching, or `StreamContent` handling +anywhere in `src/Compono.Http/` or the 420-line +`test/Compono.Http.Tests/TestHttpHandlerTests.cs`): + +| Axis | State | +|---|---| +| byte arrays | ✅ `RespondBytes` (1.1-adjacent, just shipped) | +| streams | ❌ no `RespondStream`; see §8.1 | +| `HttpContent` (arbitrary) | ❌ no `Respond(HttpContent)` escape hatch | +| strings | ✅ `RespondText` | +| JSON | ✅ `RespondJson` (both overloads) | +| status-only | ✅ `Respond(HttpStatusCode)` | +| empty responses | ✅ subsumed by `Respond(HttpStatusCode)` | +| response headers (non-content) | ❌ no way to add e.g. `Retry-After`, `ETag` to a response | +| content headers beyond `Content-Type` | ❌ no way to set `Content-Encoding`, `Content-Disposition`, etc. | +| media types/content types | ✅ every `Respond*` takes one | +| reason phrases | ❌ not settable (defaults to the framework's for the status code) | +| custom `HttpResponseMessage` | ❌ no "just give me the message to finish myself" escape hatch | +| method matching | ✅ `OnGet`/`OnPost`/`OnPut`/`OnPatch`/`OnDelete` (no `OnHead`/`OnOptions`) | +| URI/path matching | ✅ exact string or `Match` | +| query-string matching | ⚠️ folded into path (`PathAndQuery`) — no independent query-param matcher | +| header matching | ❌ only via `When(...)`'s whole-request predicate | +| request content/body matching | ❌ only via `When(...)`, and only synchronously (see §8.2 — this is the sharpest real gap) | +| JSON body matching | ❌ none; `When` predicate would need synchronous access to an already-buffered body | +| raw body matching | ❌ same | +| multiple configured responses / sequencing | ❌ last-match-wins is static; no "respond X then Y then Z" | +| conditional responses | ⚠️ possible today via `When(...)` + closured mutable state, but not first-class | +| callbacks | ❌ none | +| request inspection | ✅ `handler.Requests` (post-hoc), no live/streaming inspection | +| non-success status codes | ✅ `Respond(HttpStatusCode.InternalServerError)` etc. — already fully solved, not a gap | +| `HttpRequestException` | ✅ `Throws(new HttpRequestException(...))` — already fully solved | +| timeout/cancellation | ❌ no built-in `OperationCanceledException`/`TaskCanceledException` convenience — achievable via `Throws`, but `Throws` never checks `cancellationToken`, so it can't distinguish a caller-cancelled call | +| malformed payloads | ✅ trivially achievable via `RespondBytes`/`RespondText` with garbage content — not a gap, already general enough | +| transport-level failures | ✅ `Throws(new HttpRequestException(...))`/`Throws(new SocketException(...))` — already solved | + +## 6. Observed or likely friction + +Two real friction clusters emerge, not a long tail of small requests: + +**A. Response-body-shape gaps are nearly closed after `RespondBytes`.** +Streams and raw `HttpContent` are the only remaining payload primitives, +and both have real ownership/lifetime traps (§8.1) that argue for +deliberate exclusion rather than quiet omission. + +**B. Request-side matching stops at path/method.** Every other axis +(headers, query params, body) funnels through `When(Func)`, and that predicate is **synchronous**, while reading +`request.Content` (to inspect a JSON body, form data, etc.) is +fundamentally **asynchronous** (`ReadAsStringAsync`/`ReadAsByteArrayAsync` +return `Task`). A consumer wanting to match on request body today must +either: (a) pre-buffer content into a byte array before it's sent +(usually impossible — the body is produced by the system under test at +send time), or (b) call `.GetAwaiter().GetResult()` inside the predicate, +a synchronous-over-asynchronous anti-pattern that risks deadlocks in +`SynchronizationContext`-bound environments (the same class of hazard +ADR-0001 exists to avoid within Compono's own architecture, even though +this instance is at a test-authoring seam rather than the composition +engine itself). + +This is the load-bearing finding: **request body matching isn't merely +missing, it's structurally awkward to add without a design decision**, +because `TestHttpHandler.SendAsync` is itself `async` and *could* await a +body-reading predicate — but every existing matcher type +(`Func`, core `Match`) is synchronous, and +introducing an async matcher shape is a genuine two-way door (§8.2). + +## 7. Candidate improvements + +Ranked list evaluated in §8; two rejected outright to keep scope honest +(§15): + +1. **Async-aware request body matching / inspection** (`WithJsonBody`, + `WithContent(Func>)`, or similar) — + addresses friction cluster B, the one with no workaround that isn't an + anti-pattern. +2. **`RespondStream` for streaming response bodies** — addresses the one + remaining payload-primitive gap, but only if ownership/lifetime + semantics can be made unambiguous (§8.1 — this candidate came in + *rejected* after analysis, see below). +3. **Header-matching convenience (`WithHeader(name, value)` on the builder + chain)** — a narrower, synchronous-only slice of cluster B that could + ship independently of the harder async-body problem. + +## 8. Detailed analysis of each serious candidate + +### 8.1 `RespondStream(...)` — analyzed and NOT recommended for 1.1 + +The task explicitly asks whether `RespondStream(...)` would be natural. +Tracing the existing `RespondBytes` precedent (ADR-0051 Amendment 2) +against stream semantics surfaces a real conflict: + +- **Byte arrays are trivially cloneable and re-fresh-able** — + `RespondBytes` clones the input once at registration time + (`(byte[])content.Clone()`), and every matched call constructs "a fresh + `ByteArrayContent` over that private copy." This is possible *because* + a `byte[]` can be copied and a fresh `ByteArrayContent` can wrap the + same underlying bytes indefinitely, arbitrarily many times, with no + state mutation between reads. +- **A `Stream` cannot be treated the same way.** A `Stream` is + stateful and single-pass by default (`Position`, `CanSeek`) — reading + it once (as `HttpContent`'s `StreamContent` does when the response body + is serialized) advances or exhausts it. A naive `RespondStream(Stream + stream)` signature would work exactly once, then silently return empty + content or throw `ObjectDisposedException` on every subsequent matched + call — a correctness trap the "factory, not instance" architecture + (ADR-0051) was specifically designed to prevent for every other + `Respond*` method. +- Fixing this "for real" needs a **factory**: `RespondStream(Func + streamFactory)`, matching `RespondBytes`'s snapshot for freshness. But + this raises **ownership** ambiguity the task explicitly flags: does + `TestHttpHandler`/the resulting `StreamContent` dispose the stream the + factory returns after each response is consumed? `HttpContent.Dispose()` + disposes its wrapped stream by default — so a factory returning a + `MemoryStream` per call is fine (cheap, always re-creatable), but a + factory wrapping a `FileStream` or a caller-owned stream needs an + explicit non-disposing wrapper, which is exactly the kind of extra API + surface (`leaveOpen`, a wrapping `NonDisposingStream`) that turns one + clean method into a small ownership sub-API. +- **Compono's synchronous composition model** doesn't itself conflict + here (response bodies aren't produced during composition), but stream + *creation* being potentially I/O-bound (e.g., reading a fixture file + per response) does mean a synchronous `Func` factory is the + only fit — an `async` factory would need `TestHttpHandler`'s dispatch to + await it, which is fine (`SendAsync` is already `Task`-returning), but + then compounds with the async-matcher design question in §8.2 rather + than being independent of it. + +**Verdict:** `RespondStream` is not a clean two-line addition. It either +(a) ships with a silent single-use footgun that contradicts the +established "factory, not instance" invariant, or (b) needs a genuine +mini ownership-and-lifetime design (factory + optional disposal policy) — +a small ADR-worthy decision on its own, not a drive-by 1.1 addition. +Given `RespondBytes` already covers the overwhelmingly common binary-body +case (small fixtures, certs, images), and the marginal case (streaming a +genuinely large or I/O-bound body through a test double) is rare in +practice, this doesn't clear the "real, repeated friction" bar ADR-0039's +Gate B sets. **Recommendation: do not add in 1.1; revisit only if a real +dogfooding signal surfaces (per ADR-0039's evidence standard), and only +alongside an explicit ownership-model ADR.** + +### 8.2 Async-aware request body/header matching — top candidate + +**Consumer problem:** A consumer testing an HTTP client that POSTs a JSON +body cannot assert-and-branch on that body's content without either (a) +abusing `When`'s side-effect-capture idiom from `alexa-vox-craft` history +(exactly the anti-pattern ADR-0051's own admission research flagged as a +`Compono.Http`-worthy problem, see `docs/research/0009-...`'s +"Request capture without a real API" section) or (b) blocking on +`.Result`/`.GetAwaiter().GetResult()` inside a synchronous predicate. + +**Proposed conceptual API** (illustrative, not a locked design): + +```csharp +handler.OnPost("/v1/orders") + .WithJsonBody(body => body.CustomerId == expectedId) + .Respond(HttpStatusCode.Created); +``` + +or, more conservatively, a single async-capable escape hatch alongside +the existing synchronous `When`: + +```csharp +handler.WhenAsync(async req => +{ + var body = await req.Content!.ReadAsStringAsync(); + return body.Contains(expectedFragment); +}); +``` + +**Why it belongs in `Compono.Http`:** this is the one matching axis where +the *current* API actively pushes consumers toward an anti-pattern +(sync-over-async) rather than merely lacking a convenience. Every other +missing-matcher gap (query params, headers) has a workable, non-hazardous +`When(...)` workaround today; body matching does not. + +**Implementation complexity:** moderate. `TestHttpHandler.SendAsync` is +already `async`-compatible (returns `Task`), so +awaiting an async predicate during dispatch is mechanically +straightforward. The real design cost is in **not** duplicating +`Match`/`Func` into a third parallel +matching vocabulary — likely needs its own `Func>`-shaped overload set (`WhenAsync`, or an +`IAsyncRequestMatcher` distinct from `When`'s sync `Func`), which is new +API surface, not a body-reading convenience layered on the existing one. + +**Compatibility risk:** low — purely additive (`WhenAsync` alongside +`When`, or a new `WithJsonBody` builder method); no change to existing +generated/public shapes. + +**Testing implications:** needs coverage for ordering semantics when a +registration's matcher must be awaited during dispatch (does an async +matcher change last-match-wins evaluation from "synchronous linear scan" +to "sequential awaited scan"? — yes, mechanically, since `for` + `await` +inside the loop body serializes evaluation; this should be documented, +not just implemented). + +**Should it wait for a later release?** No — of the three candidates, +this is the one with a demonstrable anti-pattern-forcing gap today (not +just an inconvenience), which is the strongest form of the "real friction" +bar this research is measuring against. Recommend it as the anchor +feature for `1.1`. + +### 8.3 Header-matching convenience — secondary candidate + +**Consumer problem:** matching on a request header (e.g. `Authorization`, +a correlation ID, `Accept-Language`) today requires dropping to +`When(req => req.Headers.Contains(...))`, losing the `OnGet(path)`-style +readable diagnostics (`_description` stays `"When(...) request"` rather +than something like `GET /v1/things/42 with header X-Trace`). + +**Proposed conceptual API:** + +```csharp +handler.OnGet("/v1/things/42") + .WithHeader("X-Trace", "abc123") + .RespondJson(thing); +``` + +This needs `HttpResponseRegistrationBuilder` (or a builder-returned +intermediate) to compose an *additional* matcher condition onto the one +`On(method, path)` already built — a real, if small, shape change: today +`OnX` returns a builder whose matcher is already fixed at construction +(`On(method, path)` closes over `description`/`matcher` immediately). +Adding fluent matcher composition (`.WithHeader(...)`) before a terminal +`Respond*`/`Throws` call means either (a) making the matcher mutable +during the builder phase, or (b) `WithHeader` returning a *new* +intermediate builder type layering an `AND` condition — more design +surface than it first looks. + +**Why it belongs in `Compono.Http`:** synchronous, no async complexity +(headers are available on `HttpRequestMessage` without buffering) — a +much smaller, self-contained version of the §8.2 problem. + +**Implementation complexity:** low-to-moderate — mechanically simple +matcher composition, but touches `HttpResponseRegistrationBuilder`'s +current "matcher fixed at construction" shape. + +**Compatibility risk:** low if purely additive (new fluent method, +existing `OnX`/`When` behavior untouched). + +**Should it wait for a later release?** Optional for `1.1` — real but +milder friction than §8.2 (headers are synchronously inspectable today +via `When`, just without nice diagnostics). Worth including only if §8.2 +is being done anyway and the two can share design review; not worth a +release on its own. + +## 9. Standalone-package feasibility + +**Question:** could `Compono.Http`'s HTTP-testing primitives function +without core Compono? + +**Answer: yes, almost trivially — and arguably it already does not +meaningfully depend on Compono's *product* (composition), only on two +small reusable value types.** Concretely: + +- `TestHttpHandler`/`HttpResponseRegistration`/ + `HttpResponseRegistrationBuilder`/`UnmatchedHttpRequestException` reference + **zero** composition concepts (`CompositionBuilder`, `[Composable]`, + `CompositionRow`, `ICompositionProvider` — grepped, no matches in + `src/Compono.Http/*.cs`). +- The only two core types touched (`Match`, `CallVerifier`) are + themselves composition-agnostic value/verification primitives — neither + requires a composition context to construct or use. (`Match` is used + today purely as an ergonomic string-matching helper; `CallVerifier` is + a bare `(int, string)` struct.) +- No source generator involvement at all — nothing to decouple there. + +This means `Compono.Http`'s dependency on core `Compono` is best +classified, per the task's own taxonomy, as **"shared infrastructure"** — +not compile-time-fundamental, not runtime-composition, not +generator-integration, not registration/configuration, and not "merely +packaging" either (it's a real, exercised dependency, just a narrow one). + +## 10. Architectural options for independence + +- **Do nothing (status quo).** `Compono.Http` keeps its + `ProjectReference` to core `Compono` for `Match`/`CallVerifier` + reuse. Cost: a consumer who wants *only* `TestHttpHandler` and nothing + else still pulls in the full `Compono` package as a transitive + dependency (small assembly, no generator, but still a foreign package + name in their dependency tree). +- **Duplicate `Match`/`CallVerifier` into `Compono.Http`, drop the + core reference.** Technically trivial (both types are tiny, no + dependencies of their own) but creates **exactly the "duplicate APIs or + confusing modes" outcome the task warns against** — two independent + `Match` types across packages that happen to look identical is worse + than one shared type, for a savings of one small transitive package + reference that costs nothing at runtime (no generator, no reflection, + no AOT/trim impact — verified: neither type triggers any + `RequiresDynamicCode`/`RequiresUnreferencedCode` behavior). +- **Extract `Match`/`CallVerifier` into a lower-level shared package.** + Rejected per the task's own standard: this is the "generic shared + abstraction with little consumer value" anti-pattern — it would exist + solely to let `Compono.Http` avoid depending on `Compono`, not because + any real consumer need drives a new package boundary. `Compono` itself + isn't a heavy dependency (no generator project reference from + `Compono.Http`, no runtime composition machinery pulled in — the + `Compono.dll` a `Compono.Http` consumer gets is small and free of + reflection). + +**Conclusion:** independence is *architecturally trivial* here but has +**no real payoff** — the existing dependency costs a consumer nothing +measurable (no generator tax, no AOT/trim risk, no runtime behavior +change) and already satisfies "does this feel like a coherent product" +(consumers install `Compono.Http`, get `TestHttpHandler`, and never +directly touch `Match`/`CallVerifier` as "core Compono" — they're used +transparently as return/parameter types). **Do not pursue package +independence for `Compono.Http`.** This is the cleanest of the three +packages on this axis precisely because the coupling was already minimal +by design (ADR-0051's own "Minimal dependency graph" driver) — there's +no architectural cost to remove, so removing it would be motion without +value. + +## 11. Experiments performed + +- **Dependency-graph verification**: read + `src/Compono.Http/Compono.Http.csproj` directly rather than inferring — + confirmed exactly one `ProjectReference` (core `Compono`), no + `PackageReference` entries, `IsAotCompatible=true` (the *only* Compono + package with this set — verified via the csproj's own comment, cross-checked + against `src/Compono/Compono.csproj` and `src/Compono.TestDoubles/Compono.TestDoubles.csproj` + lacking the property). +- **Symbol-usage grep**: `grep -rn "Match<\|CallVerifier\|CompositionBuilder\|\[Composable\]\|CompositionRow" + src/Compono.Http/*.cs` — confirmed exactly two core-type usages + (`Match`, `CallVerifier`) and zero composition-concept + references, supporting §9's conclusion without needing a scratch + compile (the source itself is small enough — 4 files, ~450 lines total + — to read exhaustively rather than sample). +- **Async escape-hatch check**: read `TestHttpHandlerTests.cs` in full + (420 lines) to confirm no existing test exercises request-body + matching — the gap in §6 is a genuine absence, not an + undocumented-but-present capability. +- Did not stand up a throwaway `RespondStream` prototype — §8.1's + ownership analysis is derived directly from `RespondBytes`'s existing + clone-and-refresh contract (ADR-0051 Amendment 2) plus + `HttpContent`/`StreamContent`'s documented disposal behavior, which was + sufficient to reach a confident rejection without writing code. + +## 12. External research + +Not performed for `Compono.Http` specifically — the original admission +research (`docs/research/0009-...`) already surveyed the .NET HTTP-testing +ecosystem (WireMock.Net's numeric-priority model, considered and rejected +per ADR-0051's "Registration precedence" options) as part of Gate A/B. +This document's candidates (async body matching, header matching, +streaming) are internal ergonomic gaps identified directly from the +existing code and tests, not externally inspired — no new competitive +survey was warranted for a package this narrowly scoped. + +## 13. Compatibility implications + +All three candidates in §7 are additive: + +- **§8.2 (async matching):** new methods (`WhenAsync`, or `WithJsonBody`) + alongside existing `When`/`OnX` — no existing public signature changes. + `TestHttpHandler.SendAsync`'s dispatch loop gains an `await` in the + matching loop only when an async matcher is actually registered; + behavior for existing sync-only-configured handlers is unchanged. + Source/binary compatible. No `CallVerifier`/generated-shape changes. +- **§8.3 (header matching):** additive `WithHeader(...)` fluent method; + read §8.3's caveat about `HttpResponseRegistrationBuilder`'s current + "matcher fixed at construction" shape — implementing this without a + breaking internal refactor needs care, but nothing in the *public* + contract needs to change shape. +- **§8.1 (streams) — not recommended,** so no compatibility analysis + needed beyond what's captured in the rejection rationale. + +None of these touch SemVer-significant surfaces (no generated-source +shape, no analyzer/generator behavior, no package dependency changes). + +## 14. AOT/trimming/generator implications + +`Compono.Http` is the only Compono package with `IsAotCompatible=true` +today (verified in its own csproj), enforced by +`test/Compono.Http.AotSmokeTest/AnalyzerContract/`. Any `1.1` addition +must preserve this: + +- §8.2's async matching introduces no JSON/reflection dependency by + itself (`ReadAsStringAsync`/`ReadAsByteArrayAsync` are reflection-free); + a `WithJsonBody` convenience would need the same + `JsonSerializerOptions?`-vs-`JsonTypeInfo` overload split + `RespondJson` already uses, carrying the same + `[RequiresDynamicCode]`/`[RequiresUnreferencedCode]` pair on the + reflection-based overload. +- §8.3's header matching introduces no AOT/trim risk at all (no + serialization involved). +- No generator involvement in any candidate — `Compono.Http` remains + generator-free. + +## 15. Rejected ideas + +- **General-purpose HTTP mocking framework features** (numeric response + priority, regex path matching, `OnHead`/`OnOptions`/arbitrary-verb + matching, request/response middleware pipelines) — explicitly out of + scope per ADR-0051's own bounded intent ("intended responsibility ends" + question from the task): `Compono.Http` is a testing primitive for code + already built on `HttpClient`, not a WireMock.Net competitor. Adding + these would be feature creep against the package's stated + responsibility, not friction-driven. +- **`RespondStream(Stream)`** — rejected in §8.1: real ownership/lifetime + ambiguity, no demonstrated repeated friction (contrast `RespondBytes`, + which had none of these problems and a small, obviously-correct + clone-based fix). +- **Sequential/scripted responses** ("respond 500 once, then 200") — a + real mocking-framework feature, but no evidence of consumer need was + found in tests/docs/dogfooding, and it meaningfully expands + `TestHttpHandler`'s state model (registrations would need an ordered, + consumable queue rather than a static last-match-wins list). Flagged as + "unsupported and intentionally out of scope" per this task's own + guidance to keep the package from becoming a general mocking framework + — revisit only with real dogfooding evidence per ADR-0039 Gate B. +- **Configurable strict/loose unmatched-request mode** — ADR-0051 already + considered and rejected this as Option 3 under "Unmatched-request + behavior"; nothing in this research surfaces new evidence to revisit + that decision. + +## 16. Ranked recommendations + +1. **Async-aware request body/header matching (§8.2)** — highest value, + addresses a real anti-pattern-forcing gap, purely additive, no + AOT/trim risk. **Recommended for `1.1`.** +2. **Header-matching convenience (§8.3)** — secondary, ships well + alongside #1 if the fluent-builder shape work is being done anyway; + optional on its own. **Recommended for `1.1` only if bundled with #1's + design work; otherwise defer.** +3. **`RespondStream` (§8.1)** — analyzed, not recommended; needs its own + ownership-model ADR before it's revisited, and no evidence yet + justifies that investment. **Defer indefinitely pending real + dogfooding signal.** + +## 17. Recommended 1.1 scope + +`Compono.Http`'s contribution to `1.1` should be **§8.2 (async request +matching), possibly bundled with §8.3 (header matching)** — not +`RespondBytes` alone (too small, already the trigger for this research), +and not a broader mocking-framework expansion (out of scope per ADR-0051). +This is a single, coherent, evidence-backed addition rather than a +catalog — consistent with the "don't manufacture features to pad the +release" instruction. + +## 18. Questions or evidence still unresolved + +- **Real dogfooding evidence for async body matching** — this research + identifies the *anti-pattern-forcing* gap analytically (sync predicate + vs. async body read), but per ADR-0039's Gate B standard, a genuine + admission-quality case would benefit from a real consumer repo showing + the sync-over-async workaround in practice (analogous to the + `alexa-vox-craft` evidence that justified `Compono.Http` itself). Not + found in this repo's own `docs/research/0009-...` (that research + predates any body-matching need surfacing) — worth a light dogfooding + pass before finalizing an ADR. +- **Exact async-matcher API shape** — `WhenAsync` vs. `WithJsonBody` + vs. both — needs the same design-dive treatment ADR-0051 gave path + matching (`Match` vs. HTTP-native vs. split), not assumed here. +- **Whether async matching changes dispatch-order guarantees** in a way + that needs its own documented contract (§8.2's note that awaiting + serializes the match loop) — worth confirming isn't a hidden behavior + change under load/parallel test execution. diff --git a/docs/research/0025-compono-testdoubles-1.1-research.md b/docs/research/0025-compono-testdoubles-1.1-research.md new file mode 100644 index 0000000..947af87 --- /dev/null +++ b/docs/research/0025-compono-testdoubles-1.1-research.md @@ -0,0 +1,866 @@ +# [RESEARCH-0025] Compono.TestDoubles 1.1 Research + +**Status:** Done (research only; no ADR yet) + +This is one of three parallel 1.1-scoping investigations (Http and Logging +are separate). It covers `Compono.TestDoubles` only. + +## 1. Current package responsibility + +`Compono.TestDoubles` gives Compono an AOT-safe, source-generated +alternative to `Compono.NSubstitute` for satisfying an otherwise- +unresolvable interface dependency in a composition graph — no runtime +proxy, near-zero reflection. It was admitted narrowly +([ADR-0042](../adr/0042-compono-owned-source-generated-test-doubles.md)) +specifically as "a fallback default-value generator for otherwise- +unresolvable composition-graph leaves," explicitly **not** a +general-purpose mocking framework competing with +NSubstitute/Moq/FakeItEasy on breadth. [ADR-0043](../adr/0043-compono-generated-test-doubles-design.md) +(21 amendments, all pre-implementation PR review) is the deep design; +[ADR-0044](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md) +(overloads/generics/verification, 21 amendments), +[ADR-0045](../adr/0045-testdoubles-configuration-required-members.md) +(configuration-required members), +[ADR-0048](../adr/0048-testdoubles-argument-matching-and-call-verification.md) +(argument matching), +[ADR-0049](../adr/0049-testdoubles-generic-return-closed-instantiation-configuration.md), +[ADR-0050](../adr/0050-testdoubles-multi-entry-argument-distinguished-configuration.md), +[ADR-0053](../adr/0053-testdoubles-invocation-aware-callback-responses.md), and +[ADR-0054](../adr/0054-testdoubles-sequential-call-count-based-responses.md) +each added a real, dogfooding- or requester-evidenced capability on top. +The package has shipped and iterated 16 times +(`git log --oneline -- src/Compono.TestDoubles` = 16 commits, PR #82 +through #118) since 2026-08-13, entirely pre-1.0 — 1.0.0 shipped with the +full feature set described below already in place. + +## 2. Current architecture + +Three physical layers, deliberately split across two assemblies: + +1. **Generator emission** (`src/Compono.Generators`) — `Discovery/TestDoubleAnalyzer.cs` + (2,310 lines), `Discovery/TestDoubleDefaults.cs`, + `Discovery/TestDoubleMemberIdentityResolver.cs`, + `Emitters/TestDoubleEmitter.cs`, `Emitters/TestDoubleIdentifierNaming.cs`, + `Emitters/TestDoubleOverloadIdentity.cs`, `Models/DiscoveredTestDoubleInfo.cs` + and four sibling model types, `Templates/TestDouble.scriban` (690 lines). + Extends `LeafTypeClassifier`'s existing interface-leaf branch + ([ADR-0024](../adr/0024-public-provider-extensibility-model.md) + Amendment 2) with a third, compile-time-gated outcome + (`ComponoGeneratedTestDoubles=true/false`, default `false`, surfaced via + a `CompilerVisibleProperty` core `Compono` ships). Ships inside core + `Compono.Generators` — inert unless the MSBuild property is set — never + inside the optional `Compono.TestDoubles` package. +2. **Core runtime primitives** (`src/Compono/`) — `ReturnConfig.cs`, + `ReturnConfigBuilder.cs`, `Match.cs`, `CallVerifier.cs`, + `SequenceOutcome.cs`, `Unit.cs`, `GeneratedTestDoubleRegistry.cs`, + `TestDoubleNotConfiguredException.cs`, `TestDoubleVerificationException.cs`. + All `namespace Compono`, all public types in core `Compono.dll`. +3. **Optional runtime package** (`src/Compono.TestDoubles/`) — exactly two + public types: `GeneratedTestDoubleProvider` (an + `ICompositionValueProvider`) and `CompositionBuilderExtensions` + (`UseGeneratedTestDoubles()`), proven by + `test/Compono.TestDoubles.Tests/PublicApiSurfaceTests.cs`: + + ```csharp + publicTypeNames.Should().BeEquivalentTo([ + "Compono.GeneratedTestDoubleProvider", + "Compono.CompositionBuilderExtensions", + ]); + ``` + +**End-to-end flow for a discovered interface leaf `IRepository`:** + +- The generator walks the same composition-discovery closure Compono + already computes for every other leaf (`composer.Create()`/ + `CreateMany()` call sites, `[Compose]` test-method parameters, + `[Composable]`-declared graphs). If `ComponoGeneratedTestDoubles=true` + and `IRepository` is reachable there, the generator emits (once per + distinct interface symbol, `SymbolEqualityComparer`-deduplicated) a + single file: an `internal sealed class _Double : IRepository` + (explicit-interface-implemented members only — no public surface on the + concrete type), a companion `internal static class _DoubleConfiguration` + (same-named extension methods, resolved by ordinary overload resolution + since the interface member isn't in scope on the concrete type), + `Configure()`/`Verify()` bridge extensions on the *interface* type, and + a `file`-scoped `[ModuleInitializer]` that calls + `GeneratedTestDoubleRegistry.RegisterFactory(() => new _Double())`. +- At runtime, `GeneratedTestDoubleProvider.TryProvide` calls + `GeneratedTestDoubleRegistry.TryCreate(requestedType, out value)` — a + plain `ConcurrentDictionary>` lookup + (`src/Compono/GeneratedTestDoubleRegistry.cs:33`), first-registration-wins, + populated purely by consumer-generated module initializers, never by + `Compono`/`Compono.TestDoubles` themselves. +- `[Shared] IRepository repository` works with **zero change** to + `CompositionScope`'s existing exact-requested-type storage — the double + is stored under `IRepository`, same mechanism `Compono.NSubstitute` + already uses. `repository.Configure()` is a generator-emitted, provably + safe-by-construction downcast (`repository as _Double`). + +**Why this shape exists** (three assembly-boundary defects found and fixed +during ADR-0043's own pre-implementation review, none discovered by +building and shipping — all caught by PR review before any code existed): +a runtime-package generic `Configure()` method can't return a type that +doesn't exist until the *consumer's* later compilation (Amendment 1); a +precompiled `GeneratedTestDoubleProvider` can't reference a lookup that +only exists in the consumer's own generated code, forcing the registry and +`ReturnConfig`/`ReturnConfigBuilder` into core `Compono` instead +(Amendment 2); and `internal` fields don't cross the +core-`Compono`-to-consumer-assembly boundary, forcing `ReturnConfig`'s +read side and `ReturnConfigBuilder`'s constructor `public` (Amendment 3). + +## 3. Current dependency graph + +``` +src/Compono.TestDoubles.csproj + --ProjectReference--> src/Compono.csproj (PrivateAssets="none") + +src/Compono.csproj + --ProjectReference (analyzer-only)--> src/Compono.Generators.csproj (netstandard2.0) +``` + +Confirmed directly from `src/Compono.TestDoubles/Compono.TestDoubles.csproj:12-14`: + +```xml + + + +``` + +`Compono.TestDoubles.dll` has a real, non-optional compile-time and +runtime dependency on `Compono.dll` — it isn't a thin veneer that merely +*happens* to layer on top; `GeneratedTestDoubleProvider` implements +`Compono.ICompositionValueProvider` and calls +`Compono.GeneratedTestDoubleRegistry.TryCreate`, and +`CompositionBuilderExtensions.UseGeneratedTestDoubles()` extends +`Compono.CompositionBuilder` directly. There is no reverse dependency — +core `Compono` never references `Compono.TestDoubles` (confirmed by +`GeneratedTestDoubleRegistry`'s own doc comment: "Read by +`Compono.TestDoubles`'s `GeneratedTestDoubleProvider` - core `Compono` has +no reference the other way"). + +`Compono.Generators` is shared, analyzer-only infrastructure — the exact +same generator project also emits the composition plan itself, row +invokers, and (per grep evidence below) `Compono.Logging`'s activation +code. It is not a `Compono.TestDoubles`-specific generator; it's core +Compono's own generator, extended with one more compile-time-gated +discovery branch. + +Two other first-party packages already reuse the exact same core +primitives with **no generator involvement of their own**, evidence that +`CallVerifier`/`Match` have already become de facto shared runtime +infrastructure, not `Compono.TestDoubles`-private types: + +- `src/Compono.Http/Compono.Http.csproj:31` (comment): *"Match/CallVerifier + are reused directly from Compono with no generator involvement."* + `HttpResponseRegistration.cs:56`: `public CallVerifier Verify() => new(_matchedCallCount, _description);` +- `src/Compono.Logging/LogVerificationBuilder.cs:8-10`: *"core `CallVerifier` + - `Once`/`Never`/`Exactly` each build a `CallVerifier` from the filtered + match count right here."* + +## 4. Existing 1.0 public contract + +**`Compono.TestDoubles.dll`** (2 public types, locked by +`PublicApiSurfaceTests`): + +- `GeneratedTestDoubleProvider : ICompositionValueProvider` +- `CompositionBuilderExtensions.UseGeneratedTestDoubles()` + +**Core `Compono.dll`** (public types a generated double's own generated +code depends on, per Section 2's assembly-boundary history): + +- `ReturnConfig` (struct — internal mutable fields, public + `HasConfiguredValue`/`HasConfiguredException`/`HasConfiguredSequence`/ + `ConfiguredValue`/`ConfiguredException`/`ConfiguredCallCount`, + `[EditorBrowsable(Never)]`-hidden `RecordCall()`/`ClearConfiguredResponse()`/ + `NextSequenceOutcome()`) +- `ReturnConfigBuilder` (`ref struct`; `Returns`/`Throws`/`ReturnsSequence`, + each last-configuration-wins over the other two) +- `SequenceOutcome` / `SequenceOutcome.Throw(Exception)` +- `Match` / `Match.Any()` / `Match.Is(predicate)` +- `CallVerifier` (`Never()`/`Once()`/`Exactly(n)`) +- `Unit` (void-marker struct) +- `GeneratedTestDoubleRegistry.RegisterFactory`/`.TryCreate` +- `TestDoubleNotConfiguredException`, `TestDoubleVerificationException` + +**Generated per-interface surface** (never hand-referenced by a +consumer): `Configure()`/`Verify()` extension bridges on the interface +type; per-member `Configure().Member(...)`/`Verify().Member(...)` +extensions on the generated double type; `Matching(...)` aliases +for eligible overloads (ADR-0044 Amendment 21); per-closed-`T` +configuration for a self-referencing generic return (ADR-0049). + +**Diagnostics** — `CMP0020`–`CMP0032` and `CMP0035`–`CMP0037`, all +informational (`docs/reference/diagnostics.md:7-34`), never fail the +build; most reject a whole interface leaf back to the ordinary +runtime-provider path, a scoped subset (`CMP0022`, `CMP0029`, `CMP0030`, +`CMP0031`) withholds only one member's surface, `CMP0032` is an +informational count of configuration-required members. + +## 5. Consumer ergonomics review + +Classification against the requested scenario list, evidenced by +`skills/compono/references/testdoubles.md`, the ADRs above, and +`test/Compono.TestDoubles.AotSmokeTest/Program.cs` (a real, exercised +end-to-end proof of every listed capability under Native AOT): + +| Scenario | Classification | Evidence | +|---|---|---| +| Exact call counts | **Already supported cleanly** | `CallVerifier.Exactly(n)` | +| Min/max call counts (`AtLeast`/`AtMost`) | **Unsupported, intentionally out of scope (for now)** | Explicitly and repeatedly excluded across ADR-0044 (`skills/.../testdoubles.md:363`), with ADR-0048's own admission note: *"Call-order verification has zero real evidence"* — same evidence bar applies; no dogfooding case has yet forced this | +| Negative verification | **Already supported cleanly** | `Verify().Member().Never()` | +| Argument capture (arbitrary later inspection) | **Unsupported but high-value** | Skill doc's own explicit boundary: "true argument capture for later arbitrary inspection outside a generated `Verify().Member(Match...)` count assertion" is named as the #1 remaining AutoFixture/NSubstitute-habit trap | +| Call inspection (see full argument list per call) | **Unsupported but high-value** | Same boundary — `Match.Is(predicate)` tests one argument per configured entry at configure time; there is no way to retrieve the actual argument values used across a member's calls | +| Callbacks (invocation-aware responses) | **Already supported cleanly** | `ReturnsCallback((left, right) => left + right)` — ADR-0053, real generated strongly typed delegate per eligible member | +| Side effects (mutate external state from a callback) | **Supported but awkward** | Achievable only by closing over external state inside a `ReturnsCallback` closure — no dedicated `Callback(...)`-without-a-return-value primitive the way Rocks/NSubstitute expose one | +| Throwing exceptions | **Already supported cleanly** | `ReturnConfigBuilder.Throws(exception)` | +| Sequential exceptions/responses | **Already supported cleanly** | `ReturnsSequence(...)` (ADR-0054), mixed value/exception, per-entry independent ordinal | +| Async `Task` responses | **Already supported cleanly** | First-class; deterministic defaults (`Task.CompletedTask`, empty collections) plus configuration-required fallback for non-nullable `T` | +| Async `ValueTask` responses | **Already supported cleanly** | Same treatment as `Task` throughout | +| Cancellation-related behavior | **Supported but awkward** | A `CancellationToken` parameter is just another matchable/discardable argument — no dedicated cancellation-aware default (e.g. auto-throwing `OperationCanceledException` when a configured token is cancelled); consumers configure it manually like any other member | +| Property setters | **Already supported cleanly** | Real auto-property semantics (ADR-0043 Amendment 7) — getter returns last-written or configured value | +| Events | **Unsupported and intentionally out of scope** | ADR-0042 Non-Goal, unchanged through all amendments | +| `ref`/`out`/`in` parameters | **Unsupported and intentionally out of scope** | ADR-0042 Non-Goal; diagnosed (`CMP0026`/scoped `CMP0030`), falls back cleanly | +| Generic methods (return independent of own `T`) | **Already supported cleanly** | Non-generic `Configure()`/`Verify()` slot covers every closed instantiation | +| Generic methods (return depends on own `T`) | **Already supported cleanly** | Per-closed-`T` configuration (ADR-0049) | +| Generic methods (value-type-constrained `T?`) | **Unsupported and technically problematic** | Explicitly still unsupported — `System.Nullable` shapes the generator "cannot represent without reflection or boxing" | +| Overload-heavy interfaces | **Already supported cleanly** | Per-overload `Configure()`/`Verify()` discriminator (ADR-0044), plus `Matching` alias for argument-level distinction within one overload (Amendment 21) | +| Indexers | **Unsupported and intentionally out of scope** | ADR-0042 Non-Goal; diagnosed, falls back cleanly | +| Default interface members | **Already supported cleanly** | Full DIM-fallback support, including the "derived `new` redeclaration wins, base view forwards and shares call-recording state" case (ADR-0044 Amendment 20) | +| Inherited interfaces | **Already supported cleanly** | "Full base-interface closure" — `IRepository : IClock` gets `IClock.UtcNow` too | +| Partial behavior (partial substitutes, some real/some faked) | **Unsupported and intentionally out of scope** | ADR-0042 Non-Goal ("no class/partial mocking"); interfaces only, no wrapping a real implementation | +| Reset/clear call history | **Unsupported but high-value** | No `ClearReceivedCalls()`-equivalent found anywhere in the public surface or docs; a fresh double must currently be re-composed (`composer.Create()` again) to reset state, which is often not what a `[Shared]`-scoped multi-phase test wants | +| Multiple verifier operations (chained/independent assertions) | **Already supported cleanly** | Each `Verify().Member(...)` call is independent and stateless — no shared mutable verifier object to worry about ordering | +| Ordered verification (call-order across members) | **Unsupported and intentionally out of scope** | ADR-0044/ADR-0048 both record this as excluded for lack of real evidence, not infeasibility | +| Configuring based on arguments | **Already supported cleanly** | `Match` (literal/`Any`/`Is`) on eligible members, multi-entry with last-registration-wins precedence (ADR-0050) | + +**The single "next missing primitive."** Weighing architectural leverage, +not raw feature count: **an exposed, per-member received-call record** — +a small, generated, argument-tuple log the existing call-count machinery +already almost has (`ReturnConfig.RecordCall()` already increments a +counter on every dispatch; the missing piece is retaining the arguments, +not just the count). This one primitive would unlock, without a second +separate design effort each: + +- **Argument capture** — directly, by exposing the retained tuples. +- **Call inspection** — directly, same mechanism. +- **A real `Reset()`/`ClearCalls()`** — a natural companion once there's a + log object to clear, rather than only a scalar counter to zero. +- Partially, **min/max call counts** — `AtLeast`/`AtMost` become one-line + additions to `CallVerifier` once `observedCount` already exists (they + need no new state at all, independent of the capture question — this + is genuinely the cheapest of the group and could ship alone). + +By contrast, **generalized call-count constraints** alone (just adding +`AtLeast`/`AtMost` to `CallVerifier`) is real but narrower leverage — it +closes one explicitly-flagged gap without addressing the "matching is not +capture" boundary the skill doc itself names as the more consequential +one. **Response factories based on arguments** already shipped +(`ReturnsCallback`, ADR-0053) — not a remaining primitive. +**Configured exception responses** already shipped (`Throws`, +`ReturnsSequence`). **Exposed received-call records** is the one primitive +from the candidate list that is both still missing and has multi-scenario +leverage, matching the external-research precedent below (Rocks' +`Callback(a => value = a)` is exactly a hand-rolled version of "expose the +call" that a first-class captured-record primitive would make +unnecessary). + +## 6. Observed or likely friction + +- The skill doc's own dedicated section — "The #1 AutoFixture/NSubstitute-habit + trap: matching is not capture" — is the single most load-bearing piece + of friction documentation in the whole reference file, strongly + suggesting real dogfooding pain drove it, not speculation. It tells a + consumer migrating off `Compono.NSubstitute` to fall back to + `Compono.NSubstitute` itself, or a project-local fake, for exactly this + case — a permanent detour under ADR-0042 Amendment 2's own policy + ("any real, evidenced case where `Compono.NSubstitute` can satisfy a + shape `Compono.TestDoubles` cannot is, by definition, a roadmap + candidate"). +- No `Reset()`/`ClearReceivedCalls()` equivalent means a `[Shared]` double + reused across multiple phases of one test (arrange a call, assert it, + then arrange and assert a second distinct call on the same member) + cannot cleanly separate the two phases' verification — the call count + keeps accumulating with no way to zero it short of composing a fresh + double. +- `AtLeast(n)`/`AtMost(n)` is a real, named, cheap gap (`CallVerifier` + already carries `observedCount`) that keeps getting explicitly named + and explicitly deferred across three ADRs (0042, 0044, 0048) for the + same "no real evidence yet" reason — a candidate that is architecturally + trivial but has so far failed Compono's own evidence bar, not failed on + merit. +- Cancellation-aware behavior is a narrower, real .NET-idiom gap: + Async interfaces (`IAmazonDynamoDB`-shaped clients, the repo's own + worked examples) routinely take a `CancellationToken` as the last + parameter, and a common real test wants "throw `OperationCanceledException` + if the token passed in is already cancelled" without hand-wiring it via + `Match.Is(ct => ct.IsCancellationRequested)` plus a + manual `Throws`. + +## 7. Candidate improvements + +1. Exposed received-call records (argument capture + call inspection + + `Reset()`). +2. Generalized call-count constraints (`AtLeast(n)`/`AtMost(n)` on + `CallVerifier`). +3. `Reset()`/`ClearCalls()` as a standalone primitive, independent of (1). +4. Cancellation-aware default/helper for `CancellationToken`-shaped + parameters. +5. A dedicated `Callback(Action<...>)`-only surface distinct from + `ReturnsCallback` for pure side effects on a `void` member (partially + redundant with (1)). +6. Ordered/sequenced verification across members — explicitly weighed and + rejected below (Section 15), not a serious 1.1 candidate. +7. Extracting `Compono.TestDoubles` (or its primitives) into a fully + independent, non-Compono-dependent package — the Part B question, + analyzed in depth in Sections 9–10. + +## 8. Detailed analysis of each serious candidate + +### 8.1 Exposed received-call records + +**Shape.** Extend `ReturnConfig`'s existing `RecordCall()` call site +(already invoked by every generated dispatch body) to optionally retain +the invocation's real arguments in a small, bounded, per-member log — +gated the same way argument-aware `Configure()`/`Verify()` already is +(the five-condition eligibility test in ADR-0048: non-overloaded, no +open-generic-parameter-referencing real parameters, no ref-like +parameter, no field-name collision, not a one-parameter `Equals`). A +natural generated-code surface: `Verify().Member(...)` already returns a +filtered count; a sibling accessor (e.g. a `Calls` property on the +existing `Verify()` handle, or a distinct `Captured()` terminal next to +`.Once()`/`.Exactly(n)`) returning a read-only list of argument tuples, +matching exactly Rocks' own precedent (`Callback(a => value = a)`, +Section 12) but as a first-class, generated, strongly-typed API rather +than a workaround. + +**Cost.** Real, bounded, and squarely inside the pattern this package +already uses everywhere else: a new generated field (an array or list +alongside the existing counter), a generated accessor, and eligibility +gating reusing ADR-0048's already-proven five conditions verbatim — no +new core-engine mechanism, no reflection, no expression trees. The +`ReturnsCallback` precedent (ADR-0053) already proves a generated +strongly-typed delegate per eligible member compiles and performs +correctly under Native AOT; a captured-arguments list is a strictly +simpler shape (no delegate invocation, just a snapshot append). + +**Fit.** Directly closes the one gap the package's own documentation +flags as the most consequential remaining boundary, without expanding +scope toward strict mode, partial substitutes, or general expression-tree +matching — none of which this touches. + +### 8.2 `AtLeast(n)`/`AtMost(n)` + +**Shape.** Two more methods on `CallVerifier` alongside `Never()`/ +`Once()`/`Exactly(n)`: + +```csharp +public void AtLeast(int times) { if (observedCount < times) throw ...; } +public void AtMost(int times) { if (observedCount > times) throw ...; } +``` + +**Cost.** Trivial — `CallVerifier` already carries `observedCount` +(`src/Compono/CallVerifier.cs:12`); this is a same-file, few-line +addition with no generator change at all (the generated `Verify().Member(...)` +surface already returns a `CallVerifier`; consumers would just get two +more terminal methods on the type they already hold). + +**Fit.** Exactly closes a gap ADR-0044/ADR-0048 both explicitly named and +explicitly declined for lack of evidence, not for architectural +difficulty — the cheapest correction on this whole list, and the +2026-08-13-through-08-21 ADR trail already shows the project revisiting +"no real evidence yet" verdicts once evidence does arrive (ADR-0042 +Amendment 2's entire point). Given `Compono.NSubstitute`'s +`Received.InRange`/`AtLeast`/`AtLeastOne` surface exists and is one +migration-mapping table entry away from being a real gap the moment a +consumer actually needs it, this is a low-risk, low-cost inclusion +candidate even without a specific dogfooding incident yet in hand. + +### 8.3 `Reset()`/`ClearCalls()` + +**Shape.** A method on the generated double (or a `Configure()`/`Verify()`-parallel +handle) that zeroes `ReturnConfig.CallCount` for one member, or every +member, without touching configured `Returns`/`Throws`/`ReturnsSequence` +state (or with an overload that also clears configuration, mirroring +NSubstitute's `ClearReceivedCalls()`/`ClearSubstitute()` split). + +**Cost.** Small — `CallCount` is already an ordinary mutable `internal` +field on `ReturnConfig`; a generated `Interlocked.Exchange(ref __member.CallCount, 0)`-style +reset needs no new architecture. Slightly complicated by `ReturnConfig`'s +existing `ClearConfiguredResponse()` (ADR-0053, already used internally +for `ReturnsCallback` transitions) — a `Reset()` would need to *not* +collide with or accidentally reuse that method's different intent +(clearing configured response vs. clearing recorded calls). + +**Fit.** Real, if narrower than (8.1) — most naturally ships *alongside* +exposed received-call records (Section 5's "next missing primitive" +reasoning), since both touch the same call-log state; shipping it alone +is defensible but leaves the bigger capture gap open. + +### 8.4 Cancellation-aware behavior + +**Shape.** Unclear net win. A dedicated "auto-throw when the passed token +is cancelled" default would be new, member-shape-specific dispatch logic +(inspecting a `CancellationToken` parameter's runtime state, not just its +identity) — a real behavioral special case the generator doesn't have +today for any other parameter type, and arguably in tension with +"deterministic defaults" being purely return-shape-driven, never +parameter-content-driven. The existing `Match.Is(ct => ct.IsCancellationRequested)` +plus `.Throws(new OperationCanceledException())` already expresses this +without new generator work. + +**Cost.** Real generator complexity for a narrow, arguably +already-served case. + +**Fit.** Weak — recommend not pursuing without a real, specific +dogfooding incident (Section 15). + +## 9. Standalone-package feasibility (deep) + +### 9.1 Generator dependency + +**Yes, structurally and irreducibly, for the discovery signal.** +`TestDoubleAnalyzer` is not a separate generator — it's an extension of +`LeafTypeClassifier`'s existing interface-leaf branch inside the single +`Compono.Generators` project, sharing the same composition-graph +discovery pass (constructor-parameter walking via the Roslyn semantic +model) every other Compono leaf classification already uses. This is not +an implementation convenience; it's the entire reason ADR-0042 concluded +Compono could do something no external source-generated mocking library +can: "no cross-generator dependency, ever... this is only +architecturally sound because `Compono.Generators` can own discovery and +generation in the same pass." A real two-generator Roslyn spike proved +(ADR-0042's Context) that TUnit.Mocks' own cross-generator bridge fails +identically to no trigger at all when the only trigger comes from a +sibling generator's output — the same failure mode a fully independent +`Compono.TestDoubles`-owned generator would hit trying to react to +Compono's own composition-graph output. + +**Experimentally confirmed in this investigation** (Section 11, Experiment +1): a plain interface with **zero** `composer.Create()`/`CreateMany()`/ +`[Compose]`/`[Composable]` reachability gets **no** generated double at +all, even with `ComponoGeneratedTestDoubles=true` and `Compono.TestDoubles` +referenced — `foo.Configure()` fails `CS1061` exactly like the interface +was never touched by the feature. The zero-declaration UX (the entire +differentiator per ADR-0042's Decision Drivers) depends 100% on the +interface already being reachable through *some* Compono composition +concept. + +### 9.2 Runtime dependency + +**No — the runtime primitives are, and have been since Amendment 2/3, +conceptually decoupled from composition; they live in core `Compono` +purely for cross-assembly accessibility, not because they need anything +`Composer`/`CompositionBuilder`/`CompositionScope` provides.** +`ReturnConfig`, `ReturnConfigBuilder`, `Match`, `CallVerifier`, +`SequenceOutcome`, `Unit`, and `GeneratedTestDoubleRegistry` (Section 2 +above) reference no composition types anywhere in their own source — read +in full during this investigation, confirmed zero mention of `Composer`, +`CompositionBuilder`, `CompositionScope`, `ICompositionContext`, or any +composition-request type. `GeneratedTestDoubleRegistry` is a plain +`Type`-keyed `ConcurrentDictionary>` — the same shape, +independently, `RowInvokerRegistry` uses for a completely different +purpose. **Experimentally confirmed** (Section 11, Experiment 3): a +hand-written double registered and retrieved directly through +`GeneratedTestDoubleRegistry.RegisterFactory`/`.TryCreate`, configured +via `ReturnConfigBuilder`, and asserted via `CallVerifier`, works +correctly with **zero** `Composer`/`CompositionBuilder` anywhere in the +program. `Compono.Http` and `Compono.Logging` already independently prove +this by reusing `CallVerifier`/`Match` directly with "no generator +involvement" of their own (Section 3) — these types are already, in +practice, shared cross-package runtime utilities, not composition +primitives that happen to also serve test doubles. + +**What is genuinely a fundamental (not merely historical) runtime +dependency:** `[Shared]`-scoped identity — the property that a +`[Shared] IRepository repository` test parameter and `Service`'s own +`IRepository` constructor dependency resolve to the *same* double +instance — is a real `CompositionScope` behavior (ADR-0011), not +something the double itself provides. A standalone (non-composing) +consumer never needs or gets this; it constructs and wires the double by +hand, same as any other manually-instantiated test double. + +### 9.3 Activation dependency + +**Currently, activation is 100% composition-mediated in every real usage +this repo has**, even though nothing in the double's own type structure +requires it. `GeneratedTestDoubleProvider` is reached only through +`CompositionBuilder`'s stage-6 provider pipeline +(`ICompositionValueProvider`/`AddTestDoubleProvider`); every existing +consumer — `Compono.TestDoubles.SampleTests`, `Compono.TestDoubles.AotSmokeTest`, +the skill doc's own examples — instantiates a double exclusively via +`composer.Create()`. **No test anywhere in this repo constructs a +generated double directly** (confirmed by grep: no `new _Double()` +or direct `GeneratedTestDoubleRegistry`/`Compono.TestDoubles.Generated` +usage in any `Compono.TestDoubles.*` test project except the runtime +package's own unit tests of the provider/extension methods themselves, +which mock the registry, not a real generated type). + +A `TestDouble.Create()`-shaped standalone factory (this +research prompt's own suggested shape) is architecturally reachable — +`GeneratedTestDoubleRegistry.TryCreate(typeof(T), out value)` already *is* +almost exactly that, minus a thin generic wrapper — but the harder +question is **discovery**, not activation: `TryCreate` only succeeds for +a type the generator already decided to generate a double for, and today +that decision is inseparable from composition-graph reachability. Adding +a standalone `TestDouble.Create()` API without also adding a +standalone *discovery trigger* (e.g., an explicit +`[GenerateTestDouble(typeof(IFoo))]` assembly-level attribute, the exact +shape every external library — TUnit.Mocks, Imposter, Rocks — already +uses, per ADR-0042's own Context) would just be a thin wrapper over a +registry that's empty for any interface a consumer never happened to run +through Compono composition somewhere else in the same compilation. +Native AOT/trimming/reflection posture is unaffected either way — no +reflection is needed for either the composition-mediated or a +hypothetical attribute-mediated trigger, since both ultimately populate +the same `[ModuleInitializer]`-registered, `Type`-keyed dictionary. + +### 9.4 Configuration dependency + +**No.** `Configure()`/`Verify()` and every builder behind them +(`ReturnConfigBuilder`, `CallVerifier`) operate purely on the +generated double instance's own fields — nothing routes through +`CompositionBuilder`, a profile, or any other composition configuration +concept. This was true from the first working sketch (ADR-0043's +"Standalone usability" section: "falls out with essentially no extra +cost... `RepositoryDouble` and its `Configure(...)` surface have zero +dependency on `Composer`/`[Compose]`/`CompositionRow`") and remains true +after every subsequent amendment — none of ADR-0044/0045/0048/0049/0050/0053/0054 +introduced any composition-configuration coupling either. There is no +"two confusing configuration models" risk here: `Configure()`/`Verify()` +already is the one and only configuration model, whether or not the +double was produced via composition. + +## 10. Architectural options for independence + +| Dimension | A: Status quo | B: Fully standalone, Compono integrates via hooks | C: `.Core`/integration split | D: Shared lower-level infra package | +|---|---|---|---|---| +| Consumer experience (Compono user) | Unchanged — `UseGeneratedTestDoubles()` + zero-declaration composition, exactly today | Same or worse — a standalone-first design would likely need an explicit per-type trigger even for Compono users, regressing the zero-declaration UX that is the package's entire reason to exist | Same as A if done carefully; risk of a confusing "which package do I reference" decision for every consumer, not just standalone ones | Unchanged for `Compono.TestDoubles` users; new packages (a hypothetical future non-Compono consumer) would reference the infra package directly | +| Consumer experience (hypothetical non-Compono user) | Not served at all today (proven, Section 11 Experiment 1) | Full standalone product — but with an explicit-trigger UX no better than Rocks/TUnit.Mocks/Mockolate already offer, and less mature | Same as B for the standalone half | Same as B for the standalone half — but the "product" on offer would just be `Match`/`CallVerifier`/`ReturnConfig` as bare utility types with no generator behind them, not a mocking library | +| Package naming | `Compono.TestDoubles` unchanged | New name needed (`Compono.TestDoubles` no longer describes a Compono-coupled thing); risks the exact `Compono.Mocks`/general-purpose-mocking-framework naming ADR-0042 explicitly rejected at admission time | `Compono.TestDoubles.Core` + `Compono.TestDoubles` (integration), mirroring no existing Compono package-split precedent | Something like `Compono.Testing.Primitives` — new, unprecedented naming category | +| Dependency graph | `Compono.TestDoubles` → `Compono` (one edge, confirmed Section 3) | `Compono.TestDoubles.Core` (or new name) has no Compono dependency; `Compono` or a thin adapter package depends on *it* — an inversion of today's direction | `Compono.TestDoubles.Core` (no Compono dep) ← `Compono.TestDoubles` (adapter, depends on both) | New `Compono.Testing.Primitives` (no Compono dep) ← `Compono` ← `Compono.TestDoubles`/`Compono.Http`/`Compono.Logging` all depend on the shared package instead of on `Compono.dll` for these types | +| Public API | Unchanged | `Match`/`ReturnConfig`/`CallVerifier`/etc. move out of `Compono.dll`'s public surface — a breaking change for any consumer (including `Compono.Http`/`Compono.Logging` internally) that references them as `Compono.Match` today | Same breaking-change problem as B unless `Compono` re-exports type-forwards | Same breaking-change problem, mitigated only by type-forwarding shims | +| Binary/source compatibility | No change | Breaking: namespace/assembly move for public types current consumers (including two other first-party packages) already reference | Breaking, same reason, unless carefully forwarded | Breaking, same reason | +| Generator/analyzer packaging | Unchanged — one analyzer, `Compono.Generators`, ships inside core `Compono`'s nupkg | A standalone generator needs its **own** discovery trigger (an attribute), duplicated and maintained forever alongside the existing composition-graph trigger in the same generator, or an entirely separate generator package (reproducing ADR-0042's proven-bad cross-generator-handoff shape if it needs to cooperate with Compono's own discovery at all) | Same duplication problem as B, scoped to whichever half is "core" | N/A — infra package ships no generator at all | +| Generated-API compatibility | Unchanged | New standalone-triggered doubles would need their own (different) generated shape question resolved from scratch — the existing shape assumes the interface is already known to be composition-reachable | Same open question as B | N/A | +| Native AOT/trimming | Already proven (`Compono.TestDoubles.AotSmokeTest`, real `dotnet publish -p:PublishAot=true`) | No new AOT risk in principle (still zero reflection either way) but doubles the AOT-proof surface (composition-triggered path + attribute-triggered path) that needs its own smoke test | Same doubling | N/A — infra package has no generation to prove | +| Build behavior | Unchanged | New package, new pack/publish pipeline, new versioning story synced against `Compono`'s own | Two new packages instead of one | One new package plus type-forwarding shims in every consumer | +| Complexity | Lowest — this is the shape the deep-design ADR already converged on after 21 amendments of real review | Highest — reopens a discovery-trigger design question ADR-0042 spent real effort concluding Compono's zero-declaration answer to, for a benefit (serving non-Compono consumers) nobody has asked for | High — two packages to maintain, version, and keep mutually consistent, for the same underlying benefit as B | Medium — a real, defensible refactor (see below) but with real breaking-change cost for zero current behavioral gain | +| Maintenance cost | Lowest, proven by 16 real ship-and-iterate commits without needing this split | Highest — a second discovery trigger becomes permanent surface area, forever kept in sync with the composition-graph trigger's own eligibility rules (all 21+ ADR-0043/0044 amendments' worth of edge cases) | High, same reason as B, times two packages | Medium — real but bounded (move types, add forwards, update three consuming packages' `.csproj` comments) | +| Ability to evolve independently | Already true where it matters — `Compono.TestDoubles`'s own runtime package is 2 types and changes independently of core `Compono`'s composition-engine work today | No better than A in practice — the generator half still can't evolve independently of `Compono.Generators`' shared discovery infra (Section 9.1) | No better than A — same generator constraint | Somewhat better in principle (the primitives could version independently of `Compono`'s composition-engine surface) but no current evidence any consumer needs that independence | +| Coherent standalone product? | N/A (doesn't attempt this) | **No** — without a real per-type discovery trigger, a "standalone" consumer gets an empty registry; with one, the product is a worse-differentiated clone of Rocks/TUnit.Mocks/Mockolate, not a real Compono-flavored standalone tool | Same "no" as B for the standalone half | N/A — this option doesn't produce a mocking product at all, just relocates utility types | + +## 11. Experiments performed + +All three run from +`/private/tmp/claude-501/.../scratchpad/standalone-experiment` (a +throwaway project outside `src/`/`test/`, git-ignored, nothing committed +to the real repo). `Compono` and `Compono.TestDoubles` were packed +locally (`dotnet pack ... -p:Version=1.0.0 -o feed`) and restored via a +local `nuget.config` pointing at that folder, mirroring +`test/Compono.TestDoubles.SampleTests`' own real consumption pattern +(`PackageReference`, not `ProjectReference` — a `ProjectReference` to +`Compono.csproj` does not transitively carry the `Compono.Generators` +analyzer three hops deep, confirmed as a dead end before switching to the +pack-and-restore approach the AOT smoke test project's own comments +already document as the correct shape). + +**Experiment 1 — does the generator trigger without any composition call +site?** A plain `IFoo { int GetValue(); }` interface, `ComponoGeneratedTestDoubles=true`, +`Compono.TestDoubles` referenced, **no** `composer.Create()`, +`CreateMany()`, `[Compose]`, or `[Composable]` anywhere in the +compilation. `foo!.Configure().GetValue()` fails to compile: + +``` +error CS1061: 'IFoo' does not contain a definition for 'Configure' and no +accessible extension method 'Configure' accepting a first argument of +type 'IFoo' could be found +``` + +**Result: confirmed — the zero-declaration generation trigger is +strictly composition-reachability-gated. No fallback "just referencing +the interface somewhere" trigger exists.** This directly evidences +Section 9.1/9.3's claim rather than merely restating the ADR's own +reasoning. + +**Experiment 2 — positive control.** Identical project, one line added: +`var composer = Composer.Create(b => b.UseGeneratedTestDoubles()); var foo = composer.Create();` +before the `Configure()` call. Compiles and runs, printing `42` — the +configured value. **Result: confirms the trigger is exactly +`composer.Create()` reachability, nothing more, nothing less** — the +smallest possible composition call site is sufficient, no `[Compose]`/`[Composable]` +attribute needed on top of it. + +**Experiment 3 — do the runtime primitives need `Composer` at all?** A +hand-written `FooDouble : IFoo` (standing in for what the generator would +emit) registered directly via `GeneratedTestDoubleRegistry.RegisterFactory(() => new FooDouble())`, +retrieved via `GeneratedTestDoubleRegistry.TryCreate(typeof(IFoo), out value)`, +configured via a hand-written `ReturnConfigBuilder`-based method, and +asserted via `CallVerifier` — with **zero** `Composer`/`CompositionBuilder` +anywhere in the program. Output: + +``` +42 +PASS: registry + ReturnConfig/ReturnConfigBuilder/CallVerifier work with +zero Composer/CompositionBuilder involvement. +``` + +**Result: confirmed — every runtime primitive this feature depends on +(`ReturnConfig`, `ReturnConfigBuilder`, `GeneratedTestDoubleRegistry`, +`CallVerifier`) is already, today, usable with no composition engine +involved at all.** This directly supports Section 9.2's conclusion: +the runtime half is decoupled in substance already; only the *discovery +trigger* (Experiment 1) is genuinely, structurally tied to composition. + +## 12. External/competitive research + +**What was seen externally** (WebSearch against primary GitHub +sources/READMEs, cross-checked against ADR-0042's own prior spike +findings from the same investigation trail): + +- **NSubstitute/Moq**: both rely on runtime proxy generation (Castle + DynamicProxy / `System.Reflection.Emit`-adjacent mechanisms) and are + reported as fundamentally incompatible with Native AOT — "Native AOT + has limited support for features that depend heavily on runtime code + generation." This matches ADR-0042's own stated motivation exactly and + is the reason `Compono.NSubstitute` (not `Compono.TestDoubles`) carries + that limitation. +- **Rocks** (JasonBock/Rocks): source-generator-based, triggered by an + **assembly-level attribute** (`[Rock(typeof(ITarget), BuildType.Create)]`), + generating a `{Type}{Kind}Expectations` class. Consumer flow: build + `Setups` on the expectations object, call `.Instance()` to get the + mock, exercise it, call `Verify()`. Argument matching: exact value, + `Arg.Any()`, `Arg.Validate(predicate)` — directly analogous to + Compono's `Match`'s three cases. **Argument capture is real and + supported**, via `Callback(a => value = a)` — exactly the "expose the + invocation" primitive this research identifies as Compono's own biggest + remaining gap (Section 5/8.1), confirming it as a real, demanded + capability in a comparable ecosystem, not a speculative one. Stated + limitations: no sealed-type mocking, no closed generic types (open + forms only), `[Obsolete]`-marked members rejected, static-abstract + members unimplemented (tracked as an open issue) — no reflection + anywhere; fully AOT-compatible by construction (generates real C# + source, not IL emission). +- **TUnit.Mocks / Imposter**: both require a compile-time-visible, + per-type trigger written directly in consumer source + (`Mock.Of()`/`T.Mock()`/`[assembly: GenerateMock(typeof(T))]` for + TUnit.Mocks; `[assembly: GenerateImposter(typeof(T))]` for Imposter) — + already investigated in depth by ADR-0042's own prior spike (a real, + reproduced cross-generator-handoff failure: a type whose only trigger + came from a *different* generator's own emitted source never gets a + mock, on clean or incremental builds alike). Not re-verified in this + pass; cited as already-established primary evidence this repo already + holds. +- **Newer entrants surfaced this pass** (not previously referenced by any + Compono ADR, genuinely new information): **Skugga** ("Roslyn-based... + leverages Source Generators and Interceptors to create static, + AOT-safe mocks with zero runtime footprint") and **Mockolate** + ("modern, strongly-typed, AOT-compatible mocking library... powered by + source generators") — both confirm the broader .NET ecosystem is + actively moving toward exactly Compono's own chosen mechanism + (compile-time generation over runtime proxying) for AOT compatibility, + independently arriving at the same architectural direction ADR-0042 + chose. Neither was fetched in depth (no primary-source README pull + beyond the search-result summary) — flagged as worth a closer look in + a future pass if either turns out to have real adoption, not + incorporated into any recommendation here. + +**What is recommended for Compono** (inspiration only, not a parity +target): Rocks' `Callback(a => value = a)` argument-capture pattern +directly supports Section 8.1's "exposed received-call records" as a +real, externally-validated capability worth Compono's own smaller, +strongly-typed treatment — not because Compono should chase Rocks' +breadth, but because this is independent evidence the specific gap +Compono's own skill doc already flags as its biggest boundary is a real +gap other libraries in this exact niche (source-generated, AOT-safe) +found worth solving too. Nothing else from this survey is recommended — +Rocks' assembly-attribute trigger, `Instance()`-then-configure two-phase +flow, and open-generics-only stance are all real design choices Compono +should **not** copy: they exist because Rocks has no composition-graph +discovery to piggyback on, which is precisely the differentiator ADR-0042 +found Compono uniquely has and should keep leaning on rather than +abandon for standalone-library parity. + +## 13. Compatibility implications + +- **Source compatibility (candidates in Section 8):** additive only. + `AtLeast`/`AtMost` on `CallVerifier`, a `Reset()`/captured-calls + accessor on the generated `Verify()` surface — none change any existing + generated signature or public type's existing members. No SemVer + concern beyond a routine minor-version addition. +- **Binary compatibility:** `CallVerifier`/`ReturnConfig` are public + types in core `Compono.dll`; adding members to them is binary-compatible + (no existing member signatures change). `ReturnConfig` is a mutable + struct with `internal` backing fields already extended twice before + (Amendment 3 read-accessors, ADR-0054's `Sequence`/`SequenceOrdinal` + fields) without any prior compatibility incident — real precedent this + extends cleanly again. +- **Generated-source compatibility:** a captured-call-log or `AtLeast`/`AtMost` + addition changes *future* generator output (new files emit the new + surface) but never requires regenerating or invalidating any + already-generated file from an older generator version — additive, + matching every prior TestDoubles feature's own rollout shape (v1 → + v2 → v3 in the skill doc's own section numbering, each purely additive). +- **Analyzer/diagnostics compatibility:** no new diagnostic codes are + obviously required for (8.1)/(8.2) — the existing eligibility-gating + diagnostics (`CMP0026`/`CMP0029`/`CMP0030`) already cover the same + member shapes a captured-call-log would need to exclude (ref-like + parameters, overloads, collisions); reusing them rather than adding new + codes is the lower-risk default, to be confirmed at design time. +- **Runtime/AOT/trimming:** unaffected either way — every candidate in + Section 8 is a plain-field-plus-branch addition to the same + already-AOT-proven shape (`Compono.TestDoubles.AotSmokeTest` already + exercises the closest-analogous existing feature, + `ReturnsCallback`/`ReturnsSequence`, under real `PublishAot=true`). +- **Deterministic builds:** unaffected — no new nondeterminism source + (hashing, ordering) is introduced by any Section 8 candidate; existing + hash-suffixed naming and `.Collect()`+`SymbolEqualityComparer` + deduplication are untouched. +- **SemVer:** every Section 8 candidate is additive-only and ships as a + minor version bump (1.1.0), consistent with the "additive capabilities" + framing this whole investigation was scoped against. + +## 14. AOT/trimming/generator implications + +No candidate in Section 8 introduces reflection, `Activator.CreateInstance`, +`MakeGenericMethod`, expression-tree compilation, or any other +AOT-hostile mechanism — every one is a straight extension of the existing +generated-field-plus-branch dispatch shape this package has used +unchanged since v1, already proven end-to-end under +`dotnet publish -p:PublishAot=true` (`Compono.TestDoubles.AotSmokeTest`). +A captured-call-log (8.1) needs a plain array/list field per eligible +member — no different in AOT posture from `ReturnConfig.Sequence` +(ADR-0054), which already ships this exact shape (an array field, set +once, read many times, `Interlocked`-guarded ordinal). `AtLeast`/`AtMost` +(8.2) touch no generated code at all — pure core-library addition. Any +new standalone-discovery trigger (Section 10's Option B/C, explicitly not +recommended) would still be reflection-free by construction (an +attribute-driven Roslyn pass, same mechanism as the existing +composition-graph-driven one) — the AOT argument was never the reason to +reject standalone independence; the discovery-trigger-duplication and +product-differentiation arguments (Sections 9.1, 9.3, 10) are. + +## 15. Rejected ideas + +- **Call-order verification across members.** Explicitly and repeatedly + rejected across ADR-0044 and ADR-0048 for lack of real evidence — "a + direct search... [found] zero real evidence" (ADR-0048). Nothing + surfaced in this investigation changes that; not re-recommended here. +- **Strict mode / partial substitutes / recursive auto-configuration.** + ADR-0042 Non-Goals, unchanged through every subsequent amendment — a + fundamental scope boundary, not a backlog item. +- **`ref`/`out`/`in` parameter support.** ADR-0042 Non-Goal; the generator + already diagnoses and cleanly falls back (`CMP0026`/scoped `CMP0030`) — + no evidence this boundary is causing real friction (unlike argument + capture, which the skill doc calls out unprompted). +- **Class/protected-member mocking.** ADR-0042 Non-Goal, structurally + distinct from this package's interfaces-only scope; would require an + entirely different generation strategy (subclass proxying), not an + incremental extension of the existing explicit-interface-implementation + design. +- **Cancellation-aware auto-throw default (Section 8.4).** Considered + seriously enough to analyze (Section 8.4) but rejected here for lack of + a real dogfooding incident and a plausible-but-unproven complexity cost + (parameter-content-driven dispatch behavior, a new category the + generator doesn't have today) — flagged as worth revisiting only if a + real case surfaces, per this package's own evidence-first policy + (ADR-0042 Amendment 2). +- **Fully standalone `Compono.TestDoubles` (Option B/C, Section 10).** + Analyzed in full depth (Sections 9–10); rejected as a 1.1 (or any + near-term) candidate — see Section 16's verdict. +- **Shared lower-level infra package (Option D, Section 10).** A + defensible refactor in the abstract (the primitives already function as + shared infra in practice — Section 3's `Compono.Http`/`Compono.Logging` + reuse evidence) but a real, avoidable breaking change for zero current + behavioral benefit; no consumer has asked to version these primitives + independently of `Compono.dll`. Revisit only if that changes. + +## 16. Ranked recommendations (1.1 candidates) + +1. **Exposed received-call records (argument capture + call inspection), + paired with a `Reset()`/`ClearCalls()` primitive.** Highest leverage + per Section 5's "next missing primitive" analysis — closes the one gap + the package's own documentation names as its most consequential + boundary, externally validated as a real, demanded capability in a + directly comparable library (Rocks' `Callback(a => value = a)`, + Section 12), and architecturally cheap (extends the existing + `RecordCall()`/eligibility-gating machinery, no new core mechanism). +2. **`AtLeast(n)`/`AtMost(n)` on `CallVerifier`.** Lowest cost of any + candidate on this list (a few lines, no generator change), closes a + gap this project's own ADR trail has named three separate times + without yet clearing its own evidence bar — worth including + proactively in 1.1 given how cheap it is relative to its documented, + repeated visibility, even absent a fresh dogfooding incident. +3. *(Lower priority, ship only if scope allows)* Nothing else on the + candidate list clears the bar independently — cancellation-aware + defaults (8.4) and a standalone side-effect-only `Callback(...)` (item + 5 in Section 7) are both weaker/narrower than 1 and 2 and are better + left for a future pass with real evidence. + +## 17. Recommended 1.1 scope + +Ship recommendation 1 (captured-call records + `Reset()`) and +recommendation 2 (`AtLeast`/`AtMost`) together as `Compono.TestDoubles`'s +1.1 content. Both are additive, low-risk, and directly address the two +concrete, named gaps this investigation found the strongest evidence for +— one from the package's own documentation (argument capture) and one +from its own repeated-but-unresolved ADR history (`AtLeast`/`AtMost`). +Do not pursue standalone-package independence in 1.1 (Section 16's +verdict, below) — no consumer evidence justifies the real breaking-change +and discovery-trigger-duplication costs Section 10 documents. + +## 18. Questions or evidence still unresolved + +- The exact API shape for exposed received-call records (a `.Calls` + property on `Verify()`'s handle vs. a distinct `.Captured()` terminal + vs. something else) needs its own deep-design pass — this research + identifies the primitive and its leverage, not the final API, matching + this repo's own ADR-0029 restraint for problem-recording vs. solution- + designing. +- Whether `AtLeast`/`AtMost` should ship with `Compono.NSubstitute` + migration-mapping-table entries added alongside (matching the existing + `Received(n)`/`DidNotReceive()` table in the skill doc) is a + documentation-scope question for the implementing plan, not resolved + here. +- Skugga and Mockolate (Section 12) were surfaced but not deeply + researched in this pass — if either gains real traction, a future + investigation should assess whether their approach reveals anything + Compono's own design missed, though nothing found so far suggests it + does. +- Whether a captured-call log needs a bounded/capped size (to avoid + unbounded memory growth in a long-running or heavily-looped test) was + not resolved here and should be a specific question for the + implementing design pass. + +## Conclusions + +**(A) Top 1.1 feature recommendations, ranked:** + +1. Exposed received-call records (argument capture + call inspection), + paired with `Reset()`/`ClearCalls()`. +2. `AtLeast(n)`/`AtMost(n)` on `CallVerifier`. +3. No third candidate clears the bar independently in this pass — treat + the rest of Section 7's list as deferred pending real evidence. + +**(B) Standalone-viability verdict: No — coupling is fundamental and +appropriate.** + +The runtime primitives (`ReturnConfig`, `ReturnConfigBuilder`, +`Match`, `CallVerifier`, `SequenceOutcome`, `GeneratedTestDoubleRegistry`) +are already, today, provably decoupled from `Composer`/`CompositionBuilder` +(Experiment 3, Section 11) — that half of the question is not in dispute. +But the capability that makes `Compono.TestDoubles` worth having at all — +zero-declaration double generation with no per-type consumer trigger — is +100% dependent on Compono's own composition-graph discovery (Experiment +1, Section 11: an interface with no `composer.Create()`/`[Compose]`/ +`[Composable]` reachability gets no generated double, full stop, even +with every other opt-in flag set). Making the package "standalone" would +mean inventing a second, non-composition discovery trigger (an +assembly-level attribute, the same shape Rocks/TUnit.Mocks/Imposter/Skugga/Mockolate +already use) that must be permanently maintained alongside the existing +one inside the same generator — precisely the differentiation-destroying, +complexity-adding outcome ADR-0042 evaluated and declined to chase, and +Decision Driver 3 explicitly pre-authorized abandoning ("standalone +usability must never justify added complexity, and must be dropped +rather than distort the architecture if it doesn't fall out cleanly"). +It didn't fall out cleanly — the experiment proves it — so per the +package's own governing ADR, it should stay dropped. diff --git a/docs/research/0026-compono-testdoubles-call-capture-design-investigation.md b/docs/research/0026-compono-testdoubles-call-capture-design-investigation.md new file mode 100644 index 0000000..79c18ee --- /dev/null +++ b/docs/research/0026-compono-testdoubles-call-capture-design-investigation.md @@ -0,0 +1,330 @@ +# [RESEARCH-0026] Compono.TestDoubles Call-Capture and ClearCalls Design Investigation + +**Status:** Done (research only; no ADR yet) + +**Feeds:** a future ADR on `Compono.TestDoubles` retrospective call-capture/inspection and `ClearCalls()` + +**Related:** [RESEARCH-0025](0025-compono-testdoubles-1.1-research.md) (this package's broader 1.1 admission research — this document is the deep-dive on its strongest candidate), [ADR-0044](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md) (`ReturnConfig`/`CallVerifier`/`Verify()` bridge), [ADR-0048](../adr/0048-testdoubles-argument-matching-and-call-verification.md) (argument matching + argument-filtered verification — the ADR whose implementation turns out to already contain most of the infrastructure this investigation needs) + +## 0. Headline finding — read this first + +**The generated per-member call log this investigation was asked to design already exists.** ADR-0048 (accepted 2026-08-21/22) added, as a private implementation detail of argument-filtered `Verify()`, a per-eligible-member, lock-guarded, strongly-typed `List<(T1, T2, ...)>` that records every call's real argument values for the member's lifetime. Today this list is used for exactly one purpose — `Verify().Member(matchers...)` locks it, scans it, counts entries whose arguments satisfy the supplied `Match` predicates, and discards the scan result into a plain `int` handed to `CallVerifier`. The list itself, and every argument value in it, is thrown away after every `Verify()` call. + +This reframes the investigation from "should we build call capture" to "should we expose and manage the lifetime of infrastructure we already pay for on every eligible member." That change of framing drives most of the recommendations below: this is much closer to Option D (generated received-call records) than a green-field design, the eligibility rule is very likely to be reused rather than reinvented, and the marginal cost of *exposing* the log is near zero — the cost already exists. + +## 1. Consumer scenarios + +Four scenarios, per the brief: + +1. **Argument capture** — inspect an argument from a call already made, after exercising the SUT. Already representable in principle by iterating the existing call log; not exposed publicly today. +2. **Multiple-call inspection** — inspect arguments across several invocations, e.g. distinguishing call 1's argument from call 3's. Requires real history, not a single capture slot; the existing call log already retains every call in order. +3. **Verify-then-inspect** — assert "called exactly once" and then read that one call's arguments without re-stating the predicate. Not supported by any existing mechanism: `CallVerifier` (core `Compono`) is constructed from a bare `int` and has no reference back to the call log (a deliberate ADR-0048 decision, see §3.5 below), so today a consumer cannot get from "verification passed" to "here are the arguments" in one step. +4. **`ClearCalls()` across phases of one test** — clear observation history between an exercise phase and a later exercise phase on the same shared double, without losing configured `Returns`/`Throws`/`ReturnsSequence`. No such method exists today. `ReturnConfig.ClearConfiguredResponse()` exists but does the *opposite* of what's wanted here (clears configuration, leaves `CallCount` untouched) — see §1.1. + +### 1.1 `ClearConfiguredResponse()` already sets the naming/semantics precedent, in the wrong direction + +`src/Compono/ReturnConfig.cs:71-80`: + +```csharp +public void ClearConfiguredResponse() +{ + HasValue = false; + Value = default; + Exception = null; + Sequence = null; + SequenceOrdinal = 0; +} +``` + +This method (added for ADR-0053's `ReturnsCallback`, per its doc comment) clears *configuration* and explicitly resets `SequenceOrdinal` to 0 — but it is invoked only when a builder is *replacing* one response mechanism with another (e.g. `Returns` → `ReturnsCallback`), not as a consumer-facing test-lifecycle primitive. It does not touch `CallCount` at all. A future `ClearCalls()` is the mirror image of this: it should clear observation state and leave configuration (including `SequenceOrdinal`) untouched. The existing method is useful evidence that "clear half the state, leave the other half" is already an accepted pattern in this codebase, not a new kind of complexity — but its *direction* (clear config, keep count) is the opposite of what `ClearCalls()` needs (clear count/log, keep config), so it cannot be reused or generalized as-is. Naming it precisely (`ClearCalls`, not `Reset`) avoids the confusion of two same-named methods doing opposite things. + +## 2. Existing architecture relevant to call observation + +### 2.1 Two independent recording mechanisms already exist, for different member shapes + +| Member shape | What's recorded today | Where | +|---|---|---| +| Argument-independent (generic-parameter-referencing, overloaded, ref-like-parameter, `Equals(T)`, or simply not yet touched by ADR-0048) | `ReturnConfig.CallCount` only (`Interlocked.Increment`), no arguments | `src/Compono/ReturnConfig.cs:68`, e.g. `TestDouble.scriban:364,377` | +| ADR-0048-eligible (see §5 for the 5 conditions) | `CallCount`-equivalent (the log's `.Count`) **and** every call's real, strongly-typed argument tuple, in a `List<(...)>` guarded by a dedicated `lock` object, one pair of fields per eligible member | `TestDouble.scriban:127-129`, `:203-211`, ADR-0048 generated-shape example | + +Concretely, per eligible member the generator already emits (ADR-0048's own generated-shape example, `docs/adr/0048-...md`): + +```csharp +internal readonly global::System.Collections.Generic.List<(string CognitoSub, string GameName, CancellationToken Ct)> __getPlayerByCognitoSub_calls = []; +private readonly object __getPlayerByCognitoSub_lock = new(); +``` + +and dispatch: + +```csharp +lock (__getPlayerByCognitoSub_lock) { __getPlayerByCognitoSub_calls.Add((cognitoSub, gameName, ct)); } +``` + +and `Verify()` (`TestDouble.scriban:611-618`): + +```csharp +lock (self.Instance.__getPlayerByCognitoSub_lock) +{ + foreach (var call in self.Instance.__getPlayerByCognitoSub_calls) + if (cognitoSub.Matches(call.CognitoSub) && gameName.Matches(call.GameName) && ct.Matches(call.Ct)) + count++; +} +return new(count, "GetPlayerByCognitoSubAsync"); +``` + +**The list is never cleared, never bounded, and never exposed.** Its only consumer is this filtered `Verify()` count. `count`-only `Verify()` (no arguments) also exists on eligible members and, per `TestDouble.scriban:625-630`, reads `.Count` off the *same* list under the *same* lock rather than a separate counter — meaning the plain-`Verify()`-with-no-arguments path on an eligible member is **already** paying the list's storage cost even though it only ever wants a count. This is an existing, accepted cost (ADR-0048's "Negative Consequences": "Materially larger generated-code volume per eligible member... versus v1/v2's single scalar field — accepted"). + +### 2.2 Concurrency model already established (ADR-0048, "Allocation and concurrency model") + +- Append and filtered-count-read use the *same* lock — "a filtered count is a snapshot-and-count under that lock, not a separate unlocked read, avoiding a collection-enumeration race for negligible cost at unit-test call volumes." +- This was itself a bug fix: `TestDouble.scriban:203-208` documents (Codex review, PR #108 round 5) that an earlier split-lock shape — short lock around `Add()`, then an unlocked scan — let a concurrent `Configure()`/dispatch mutate the list's backing array mid-scan. The fix folds the append and the scan into the same lock acquisition. +- `Configure()` itself stays unsynchronized against concurrent invocation, unchanged from v1/v2 — matcher fields are plain fields, not volatile/interlocked. "Concurrent verification while calls are still in flight is unsupported/undefined, matching today's `CallCount` semantics." +- Sequence ordinal claiming (`ReturnConfig.NextSequenceOutcome`, ADR-0054) uses `Interlocked.Increment` on a separate `int`, independent of the call log's lock — two entirely separate concurrency primitives already coexist on one generated double member with no interaction between them. + +This is directly reusable: exposing the log for read access needs no new concurrency primitive, only a decision about what "reading" means to a consumer (see §7). + +### 2.3 `CallVerifier` deliberately has no path back to the log (ADR-0048's "Matching API shape") + +ADR-0048 explicitly considered and rejected giving `CallVerifier` (or an intermediate wrapper) continued access to the call log after construction: + +> "Rejected: requires `CallVerifier` (or an intermediate wrapper) to retain access to the call log after construction, reopening exactly the architectural question this ADR's review caught — `CallVerifier` cannot perform matching after construction because it no longer has access to the call log." + +This is why scenario 3 (verify-then-inspect) has no answer today: it was a known, named architectural boundary, not an oversight. Any design in this investigation that wants verify-then-inspect in one fluent chain must either (a) change what `Verify()` returns for eligible members specifically, while leaving `CallVerifier` itself untouched for the argument-independent case, or (b) treat inspection as a fully separate accessor consulted before or independently of `Verify()` (§7 recommends (b)). + +## 3. Design alternatives + +### 3.A Always-recorded invocation history + +This is what already exists for ADR-0048-eligible members (§2.1) — the "always record" decision was already made and shipped. The open question is not whether to record (that ship sailed for eligible members) but whether to (a) expose what's already recorded, (b) bound it, and (c) extend eligibility to more member shapes to record for them too. + +**Cost, measured** (§6): recording itself (a lock + tuple write into a list) is cheap per call, but *unbounded* growth is not — list doubling/reallocation dominates at scale. A capped/bounded structure removes nearly all of the cost. See §6 for numbers. + +**Retained references, not deep copies:** the list stores the argument *values* as passed — for a reference type, that's the same reference the SUT passed, not a snapshot. See §5.4 for the consequences (a consumer mutating a captured object after the call sees the mutation reflected in the "captured" value too, exactly like NSubstitute's `Received()`/`Arg.Do` behave, and exactly what "capture" can mean without an unbounded, impossible deep-copy obligation). + +**GC pressure when the consumer never inspects history:** for eligible members, this cost is already paid unconditionally today (every call appends, regardless of whether any test ever calls `Verify()` on that member). Widening eligibility widens this "pay whether or not you look" cost to more member shapes. This is the central tension the recommendation in §8 resolves. + +### 3.B Opt-in capture/history + +Investigated and rejected as the primary mechanism, for reasons specific to this codebase's existing design, not in the abstract: + +- **It cannot be retroactive.** Per the brief's own concern: "prevents retrospective inspection because capture must be requested before exercising the SUT." Compono's whole `Configure()`/exercise/`Verify()` flow already assumes verification is retrospective — a consumer calls `Verify()` *after* exercising the SUT with no advance declaration that they intend to verify. An opt-in capture toggle would need to run *before* the SUT executes, breaking the one convention every existing verification path (`Once`/`Never`/`Exactly`/ADR-0048's filtered `Verify()`) already relies on. This is a bigger ergonomic regression than the allocation cost it would save. +- **It adds branching cost that approaches the always-on cost anyway.** A per-call `if (captureEnabled)` check plus the field to hold that flag is real generated-code and real branch-prediction cost sitting directly in the hot dispatch path of *every* call, not just captured ones — for the eligible-member case, this doesn't beat "always record," it just adds a branch in front of a cost that (per §3.A) is already dominated by *unboundedness*, not the write itself. +- **It duplicates a decision ADR-0048 already made.** ADR-0048 chose "single slot, one configured response" over "ordered, append-only chain" for *configuration*, reasoning that unevidenced generality is a real cost. The same reasoning applies here: there is no consumer scenario in this brief that needs to declare capture in advance rather than always having it available retrospectively for eligible members. + +Opt-in is not recommended anywhere in this design. + +### 3.C Callback/observer-based capture + +`ReturnsCallback` already exists (ADR-0053) and already solves single-value, this-invocation capture cleanly: + +```csharp +repository.Configure().Save(Match.Any(), Match.Any()).ReturnsCallback((item, ct) => captured = item); +``` + +Per `TestDouble.scriban:15-42`, the callback builder's `ReturnsCallback` clears any configured `Returns`/`Throws`/`ReturnsSequence` on that slot (`_config.ClearConfiguredResponse()`) and stores the callback in a separate field — it is mutually exclusive with a configured response, which the callback itself must now supply by returning a value (for non-void members). + +- **Void members:** already supported — the void-member dispatch template (`TestDouble.scriban` void-member block) calls the callback the same way as the value-returning path; a `void`-returning delegate works today. No gap here. +- **Solves single-value capture well** — this is Rocks's own chosen design too (see §9: Rocks's `Callback()` is documented specifically as how you "capture method argument values," with no separate call-history API). It is the industry-precedented answer to "I want this one argument." +- **Users can trivially build history themselves** on top of it: `.ReturnsCallback((item, ct) => history.Add(item))` is a two-line pattern any consumer can write today with zero new API. This is real prior art for "don't build what a one-line workaround already covers cleanly" — but it is *not* equivalent to the log-based approach for scenario 3 (verify-then-inspect in one step) because it requires the consumer to have set up the callback *before* the exercise phase, which is exactly the opt-in framing rejected in §3.B, just opted into voluntarily by a consumer who wants it rather than mandated by the API. +- **Interaction with `Returns`/`Throws`/`ReturnsSequence`:** mutually exclusive today (`ReturnsCallback` clears the others; configuring one of the others after `ReturnsCallback` would need to clear the callback field too — worth double-checking as an existing-code correctness item, out of scope for this investigation but flagged as a candidate follow-up: does `Returns()`/`Throws()`/`ReturnsSequence()` clear a previously configured callback field? A grep of `ReturnConfigBuilder.cs` (§ shape above) shows `Returns`/`Throws`/`ReturnsSequence` clear each other's state but say nothing about the callback field, which lives outside `ReturnConfig` entirely on the double class. This may be a pre-existing gap unrelated to this investigation.) +- **Ordering:** no complication — a callback executes once, synchronously, in the dispatch path, no different from any other member body statement. +- **Does not by itself provide scenario 2 (multi-call, cross-invocation inspection) or scenario 4 (`ClearCalls`)** — a consumer-built `List` via callback has no first-class relationship to `Verify()` and no `ClearCalls()` equivalent; the consumer owns and must manage that list's lifetime entirely themselves. + +**Conclusion:** `ReturnsCallback` is already the right, complete answer for scenario 1 (single capture) and needs no new API. It's real, working prior art that a *pure* capture-and-inspect API doesn't need to duplicate. It does **not** cover scenarios 2–4, which is exactly the residual gap this investigation should focus a new capability on. + +### 3.D Generated received-call records + +This is what already exists as private infrastructure (§2.1). The remaining design work is: + +1. What to expose (a raw tuple? A named record? An enumerable view?). +2. Whether exposing it changes storage shape (does the internal representation need to become richer, e.g. adding metadata like a monotonic sequence number or timestamp, or is the existing bare argument tuple sufficient?). +3. Where it's exposed from (`Verify()`? A new accessor? See §7). +4. Snapshot-vs-live semantics for whatever is returned. + +On (2): the existing tuple (`(string CognitoSub, string GameName, CancellationToken Ct)` in the ADR-0048 example) already gives strongly-typed, named-field access — `call.CognitoSub` reads naturally. No metadata (ordinal, timestamp) is recorded today. Scenario 2 ("call 1 → A, call 2 → B, call 3 → C") is satisfiable by the list's own insertion order under the lock — the brief's concurrency section asks what ordering under concurrent calls should mean; see §6.2. A generated, per-member named record type (rather than a bare `ValueTuple`) would give better call-site ergonomics (`call.CognitoSub` reads the same either way, but a `record` gives a nameable type for a consumer to use in a signature, e.g. a local helper method taking `IReadOnlyList`) at the cost of one more generated type per eligible member. This is a real but small ergonomics-vs.-generated-surface-area tradeoff, not a correctness question — recommended in §8 as a `record` rather than a raw tuple, specifically because a raw `ValueTuple` return type is unnamed at the consumer's use site beyond field names inferred from the member signature, and this repo already prefers named, generated types over raw tuples elsewhere (`SequenceOutcome`, `ReturnConfig` itself) for exactly this reason. + +On semantics of "capture" per the brief's explicit list: + +| Argument kind | What "capture" means here | +|---|---| +| Reference types (classes) | The same reference passed at call time, stored in the list slot. No copy. A consumer inspecting it later sees the object's *current* state, not its state at call time, if the SUT (or anything else) mutates it after the call returns. | +| Mutable objects | Same as above — explicitly a reference, not a snapshot. This must be documented plainly (see §12) since it is the one genuinely surprising semantic a consumer could trip on. | +| Arrays / collections | Same reference-retention rule — an array passed by reference is stored as that same array reference; if the caller mutates the array's contents after the call, the "captured" value reflects the mutation. No `.ToArray()`/`.ToList()` defensive copy is taken automatically. | +| `CancellationToken` | A `struct`, copied by value into the tuple/record slot naturally (already true today, per the generated example storing `Ct` directly) — no special handling needed, this already works. | +| Structs generally | Copied by value automatically (C# value semantics) — already correct and free, needs no design decision. | +| Nullable values | `Nullable`/reference-nullable arguments store exactly whatever was passed, including `null` — no special-casing needed; `Match.Matches` already handles this today for the filtering case (`EqualityComparer.Default.Equals` handles `null` correctly), so the storage side needs no new null-handling logic either. | + +**Deep-copying arbitrary arguments is confirmed undesirable/impossible**, matching the brief's own expectation: Compono has no generic serialization/cloning mechanism, deliberately (no reflection-based fallback per ADR-0001), and forcing one in for this feature would be a large, unjustified addition for a narrow benefit. The reference-retention semantics NSubstitute and Rocks both accept implicitly (their `Arg.Do`/`Callback` callbacks receive the live argument, not a copy) is the same trade this repo should make, and it should be stated as plainly as NSubstitute's own docs do for `Received()`'s argument matching (which has the identical "the substitute stores what was passed, not a clone of it" property, just never surfaced as a call-out because `Received()` doesn't hand the argument back to the consumer the way retrospective capture would). + +## 4. Eligibility and member-shape analysis + +### 4.1 The existing ADR-0048 five-condition rule (ADR-0048 Amendment 1, `TestDoubleAnalyzer.cs`'s `isEligibleForMatching`) + +1. Not part of an overload set. +2. No real parameter references the member's own open method-type-parameter. +3. No real parameter is a ref-like type (`Span`, any other `ref struct`) — can't be a generic type argument (`Match>?`, or a tuple element) — `CS0306`. +4. No derived-auxiliary-name collision (implementation-level naming concern, not conceptually about capture). +5. `Equals` with exactly one parameter excluded (arity collision with `object.Equals(object)`). + +### 4.2 Should retrospective capture reuse this rule exactly, or diverge? + +The brief explicitly asks not to blindly reuse ADR-0048's restrictions. Walking each: + +- **Overloaded members (condition 1):** ADR-0048's reason for excluding these from *argument matching* was a **compiler-proven `Match`-wrapping ambiguity** (the `CS0121` spike) — that reasoning is specific to giving *`Configure()`/`Verify()` parameters* the `Match` type. Retrospective capture does **not** need to change any parameter's type — it only needs a place to *store* what was already passed through the existing, unmodified overload-discriminator signature. **This restriction does not mechanically apply to capture and is worth revisiting** — a per-overload call log (keyed the same way `ReturnConfig` is already keyed per-overload today, per ADR-0044 Requirement 1) is plausible without touching the `Match`-ambiguity problem at all, since capture storage never needs a `Match`-typed parameter. This is a genuine expansion opportunity a future ADR should evaluate, not dismiss by inheritance from ADR-0048. +- **Generic methods whose parameters reference the method's own type parameter (condition 2):** this restriction is **not** about `Match` ambiguity — it's that "a per-member call log cannot hold `TState` — it exists only per closed invocation, not per member declaration" (ADR-0048's own words). This limitation is structural, not a `Match`-specific artifact, and **does** mechanically apply to capture for the same reason: there is nowhere to declare `List<(TState, ...)>` as a member field when `TState` isn't known until each call site. Capture cannot do better than argument-filtered verification here without inventing erased/boxed storage, which ADR-0048 already rejected for the same member shapes on AOT-safety/complexity grounds ("no real evidence justifies the added complexity"). **Reuse this exclusion as-is.** +- **Ref-like parameters (condition 3):** this is a hard CLR/generics constraint (`Span` cannot be a generic type argument), not an ADR-0048 policy choice — it applies identically to any storage mechanism, including a hand-written non-generic record shape, unless the record avoids using `T` as a generic parameter and instead has a concretely-typed field per parameter (which the generator already does — the tuple/record element types are the parameters' real, closed types, not a generic `T`). A `Span` argument specifically still cannot be stored in *any* field for *any* purpose past the call's own stack frame (`Span` cannot escape as a field of a heap-allocated object at all, full stop) — this exclusion is unconditionally structural and must be kept for capture too, independent of ADR-0048. +- **`Equals(T)` arity collision (condition 5):** purely an extension-method-resolution artifact of the *generated public API shape* colliding with `object.Equals(object)` — applies identically to a capture-accessor extension method for the same reason (any public extension method with matching arity has the identical `object.Equals` shadowing problem). **Reuse as-is** for any accessor shaped as an extension method; moot if capture is instead exposed as an instance member or through the existing `Verify()`/`Configure()` wrapper types rather than a raw extension on the double. + +**Net eligibility conclusion:** capture eligibility should be **derived independently**, not inherited wholesale. It converges with ADR-0048's rule on 3 of 5 conditions (generic-method-type-parameter exclusion, ref-like exclusion, `Equals` arity exclusion) for genuinely structural reasons, but the overload exclusion is an ADR-0048-specific artifact that a future ADR should re-evaluate on its own evidence rather than copy forward by default. This matters concretely: **if capture reuses the ADR-0048 log verbatim (piggybacking on the same list already generated for eligible members), overloaded members get no capture, same as today** — that's the pragматic near-term answer (§8's recommended scope), and it's a real, honest scope limit to document, not a silent gap. + +### 4.3 Properties, property setters, inherited members, default interface members, async methods + +- **Properties (get-only, get/set, get/init):** per `TestDouble.scriban`'s property block, `get`/`set`/`init` accessors already call `RecordCall()` on the same field-per-member pattern as methods. A property getter has zero parameters (nothing to capture — `CallCount` only, already exists). A property **setter/init** has exactly one parameter (the assigned value) — this is structurally identical to a one-parameter method and **is** capturable using the same mechanism, with no new design question. Not evidenced as a real consumer need in this investigation's scope, but mechanically free to include if ever prioritized. +- **Inherited members:** no special interaction found — the generator resolves the full interface member set (including inherited interface members) uniformly today; nothing about ADR-0048's log or eligibility rule is inheritance-specific (member declarations are already flattened before analysis). +- **Default interface members (DIMs):** the DIM-fallback path (`member.is_dim_fallback_target`) forwards to a helper object rather than dispatching to a slot at all when the member is unconfigured — but ADR-0048's argument matching/logging applies to a DIM the same as any other member *once it's eligible and has a slot*; the DIM-fallback path is orthogonal (it only fires for the *unconfigured* case). No new exclusion needed. +- **Async methods:** `Task`/`Task`/`ValueTask`/`ValueTask`-returning members are already handled identically to any other return type by `ReturnConfig` (`T` is simply `Task` in the ADR-0048 example) — recording happens synchronously at the point of the (synchronous) dispatch call, before any `await` the caller applies. No async-specific capture design is needed; the call is recorded the instant the double's method body runs, which is exactly when arguments are available, regardless of what the caller does with the returned awaitable afterward. + +### 4.4 Discoverable, natural API shape — evaluating the brief's candidates + +The brief asks whether inspection belongs under `Verify()` at all, and floats several conceptual shapes. Evaluated: + +- **`repository.Verify().Save(...).Calls`** — folds inspection into the same wrapper `Verify()` already returns. Problem: per §2.3, `CallVerifier` (returned by `Verify().Member(...)`) is deliberately *not* the thing with log access — the *per-member extension* has log access, and it currently returns `CallVerifier` directly, by design, specifically to avoid `CallVerifier` needing continued log access. Retrofitting a `.Calls` property onto `CallVerifier` reopens exactly the question ADR-0048 closed. Not recommended as stated, though seeding `CallVerifier` with an optional call-log reference is not impossible — it's a real compatibility question flagged in §12. +- **`repository.ReceivedCalls().Save`** — a *third* generated bridge type, parallel to `Configure()`/`Verify()`, dedicated to inspection. Clean separation of concerns (configuration vs. assertion vs. inspection are three different consumer intents, and NSubstitute's own `ReceivedCalls()` name is a real, evidenced precedent for exactly this separation — see §9). This is the shape recommended in §7/§8. +- **`repository.Calls().Save`** — same shape as `ReceivedCalls()`, shorter name. A naming choice, not an architectural one; `ReceivedCalls()` is recommended for its direct NSubstitute-migration-ergonomics precedent (a real evidenced concern per this repo's own migration-boundary framing — the Compono skill's own docs describe "matching is not capture" as a major NSubstitute migration boundary, i.e. NSubstitute consumers already know to reach for a `ReceivedCalls()`-shaped concept). + +**Recommendation: inspection does not belong under `Verify()`.** `Verify()` communicates assertion semantics — a pass/throw contract — and folding data retrieval into it blurs that contract exactly as the brief anticipated. A **third bridge**, `ReceivedCalls()`, mirroring `Configure()`/`Verify()`'s existing two-bridge pattern, is the cleanest fit: it's additive (new extension methods only), doesn't touch `CallVerifier`'s existing shape or ADR-0048's existing "no log access after `Verify()` construction" boundary, and gives capture its own home consistent with the "one small, concrete... extension, no dedicated verification API" precedent ADR-0044 itself used to justify `Verify()`'s own existence as a bridge distinct from `Configure()`. + +## 5. `ClearCalls()` semantics + +Answering the brief's precise checklist, working from the existing storage split (`ReturnConfig`'s configuration fields vs. the ADR-0048 call log, which are **already separate generated fields today** — this split is not something this investigation has to invent, it already exists): + +| State | Cleared by `ClearCalls()`? | Why | +|---|---|---| +| Call counts (`CallCount`, or the eligible-member log's `.Count`) | **Yes** | This *is* the observation/verification history the brief's own stated principle targets. | +| Captured argument history (the ADR-0048 log's contents) | **Yes** | Same list backs both the count and the arguments — clearing one without the other isn't representable given the current single-list storage (§2.1), and conceptually both are "what happened," not "what's configured." | +9| Configured `Returns` | **No** | Configuration, not observation. Matches NSubstitute's `ClearReceivedCalls()` precedent exactly (§9): "will not clear any results set up for the substitute." | +| Configured `Throws` | **No** | Same reasoning as `Returns`. | +| `ReturnsCallback` | **No** | It's configured behavior, structurally identical to `Returns`/`Throws` in `ReturnConfig`'s model (mutually exclusive alternative response mechanism) — clearing it would silently change future dispatch behavior, which `ClearCalls()` must not do per the brief's own stated boundary against becoming `ResetDouble()`. | +| `ReturnsSequence` configuration (the `Sequence` array itself) | **No** | It's configured behavior — the array of outcomes to hand out. | +| Current sequence ordinal (`SequenceOrdinal`) | **No — continues at its current position.** | This is the brief's own stated hypothesis, and the evidence supports it directly: `ReturnConfig.NextSequenceOutcome()`'s own doc comment describes the ordinal as tracking "the first call... index 0, the second... index 1" — i.e., it's **runtime progress through configured behavior**, not a record of what was observed for assertion purposes. It lives in `ReturnConfig` (the configuration struct), physically adjacent to `Sequence` itself, not in the ADR-0048 call log. If a consumer configures a 3-entry sequence, calls the member twice (consuming entries 0 and 1), then calls `ClearCalls()`, the next call should return entry 2 — clearing observation history doesn't rewind configured behavior any more than it would silently reset a `Returns`-configured constant value's future behavior. Resetting it would make `ClearCalls()` observably change *future* dispatch outcomes, not just clear *past* observation — exactly the `ResetDouble()` scope-creep the brief warns against. | +| Argument-specific configuration entries (the per-parameter `Match?` fields) | **No** | Configuration, unchanged by definition — these are what a *future* call is compared against, set by `Configure()`, structurally parallel to `Returns`/`Throws`. | +| Generic closed-instantiation configuration entries (ADR-0044 Requirement 2's dictionary-free per-instantiation slots) | **No** | Same reasoning — configuration, not observation, regardless of which generic-instantiation bucket it lives in. | +| Property backing state (the value a property getter would currently return) | **No** | This is `ConfiguredValue`/`HasConfiguredValue` — the same `ReturnConfig` configuration fields properties already share with methods (§2.1's table shows properties use the identical field shape). No separate design question here; it falls out of "don't touch `ReturnConfig`'s configuration fields" already stated above. | + +**Recommended contract, stated plainly:** *`ClearCalls()` clears everything a `Verify()` call on this double could observe (counts and, where present, captured argument history) and nothing a `Configure()` call set up (responses, exceptions, sequences, sequence position, argument matchers). It changes what the double remembers having seen, not what it will do next.* + +This is **exactly** NSubstitute's own `ClearOptions.ReceivedCalls` flag semantics (§9) — "clear all the received calls," independently toggleable from `ReturnValues`/`CallActions` — which is strong, direct external validation that this observation/configuration split is the conventional, expected contract for this kind of primitive, not a Compono-specific invention. + +### 5.1 Should `ClearCalls()` release retained references? + +Yes, and this falls out for free: since the call log is a `List` (or the record-based equivalent recommended in §3.D/§8), `list.Clear()` (or reassigning to a fresh empty list) drops the list's references to previously-captured arguments, making them eligible for collection the moment nothing else in the test holds them — no special handling needed beyond calling `.Clear()` (or equivalent) under the existing lock. + +## 6. Performance and allocation findings + +### 6.1 Baseline vs. always-record vs. bounded — measured + +A throwaway, read-only spike (not committed, run under `/tmp`, deleted after use — no `src/`/`test/` changes) compared three shapes at 5,000,000 iterations, Release config, .NET 9, workstation GC: + +| Scenario | Elapsed | Allocated | Bytes/call | +|---|---:|---:|---:| +| Baseline — `Interlocked.Increment` only (today's argument-independent `RecordCall()`) | 15.8 ms | 40 B (fixed, not scaled — the harness's own overhead) | ~0.00 | +| Always-record, **unbounded** `List<(string, int, CancellationToken)>` growth under a `lock` (mirrors the current ADR-0048 shape exactly, at unrealistic 5M-call scale) | 328.4 ms | 402,653,616 B | 80.53 | +| Always-record, **bounded** ring buffer (pre-sized array, `lock` + overwrite, no growth) | 24.1 ms | 0 B | ~0.00 | + +**Interpretation:** the expensive part of "always record" is not the per-call write — it's *unbounded growth* (`List`'s doubling reallocation and copy, dominant at scale). A capped/bounded structure removes essentially all of the measured overhead relative to the existing zero-alloc baseline. At realistic unit-test call volumes (a handful to a few hundred calls per test method, not 5 million), even the unbounded shape's absolute cost is trivially small — this spike deliberately used an unrealistic iteration count specifically to make the *asymptotic* behavior (growth-driven, not per-call-driven) visible; it is not a claim that any real test suite will notice a difference at either end of this table. + +**Consequence for design:** ADR-0048's existing choice ("grows for the double's lifetime (one test method), never trimmed — matches every real site's scale") is fine as-is for real test-method call volumes, and this investigation found no performance reason to change it. Bounding is a real, cheap option available if a future ADR wants to guard against a pathological case (e.g. a double called in a tight loop by a bug in the SUT), but the measured evidence doesn't show it as *necessary* for correctness or acceptable performance at realistic scale — it's a defensive option, not a requirement. **Recommendation: do not bound by default.** Bounding creates a real, surprising semantic (which call gets silently dropped when the buffer wraps?) for a cost that isn't evidenced as a real problem, which is exactly the kind of unevidenced complexity ADR-0044/ADR-0048 both explicitly avoid elsewhere. + +### 6.2 Opt-in and callback-only comparative cost + +Not separately benchmarked — reasoned analytically instead, since the mechanisms are architecturally rejected/already-existing rather than open design questions: + +- **Opt-in (§3.B):** would add exactly one branch (`if (captureEnabled)`) in front of the same write the always-on path already does — strictly *more* generated-code cost than always-on for eligible members, since eligible members already always-record unconditionally today; opt-in would only reduce cost for member shapes that *don't* currently record at all, at the cost of the ergonomic regression in §3.B. Not pursued further. +- **Callback-only (§3.C):** zero additional generated storage — `ReturnsCallback` already exists and costs exactly what a delegate invocation costs, which is less than a list append. But it only covers scenario 1, not 2–4 (§3.C's conclusion). Its cost profile is not a reason to prefer or reject it; its *scope* is. + +### 6.3 Performance verdict + +**Acceptable.** For the member shapes already eligible under ADR-0048 (which already always-record), exposing the existing log adds **zero additional runtime cost** — the recording already happens; only a read-side accessor and a `Clear()` are new. For any expansion of eligibility (e.g. to overloaded members, per §4.2's open question), the added cost is the same shape already accepted for ADR-0048-eligible members today, and the same "accepted, same real generated-code-volume-is-an-expected-cost precedent" reasoning ADR-0048 itself used applies without needing new justification. + +## 7. Recommended conceptual API + +```csharp +// Existing, unchanged: +repository.Configure().GetPlayerByCognitoSubAsync(Match.Any(), Match.Any(), Match.Any()).Returns(player); +repository.Verify().GetPlayerByCognitoSubAsync(Match.Is(s => s == cognitoSub), Match.Any(), Match.Any()).Once(); + +// New — a third bridge, parallel to Configure()/Verify(), for eligible members only: +var calls = repository.ReceivedCalls().GetPlayerByCognitoSubAsync(); // IReadOnlyList, snapshot at read time +Assert.Equal(expectedCognitoSub, calls[0].CognitoSub); + +// New — ClearCalls(), likely hung off the Verify() bridge (it's the "reset what Verify() would see" operation) +// or a fourth minimal bridge — exact receiver type is an ADR-level decision, not resolved here: +repository.Verify().ClearCalls(); // or: repository.ClearCalls(); — see open question in §14 +``` + +Key properties of this shape: + +- **`ReceivedCalls()` is scoped to ADR-0048-eligible members only** (§4.2), same restriction surface as today's argument-filtered `Verify()`, with the overload-exclusion question flagged as open (§4.2) rather than settled. +- **Returns a generated, per-member named type** (a `record` with named properties matching the parameter names, e.g. `GetPlayerByCognitoSubAsyncCall(string CognitoSub, string GameName, CancellationToken Ct)`) rather than a raw `ValueTuple`, for the naming/ergonomics reasons in §3.D. +- **Snapshot semantics on read:** the accessor takes the same lock ADR-0048's filtered `Verify()` already takes, copies the current list contents into an array/list returned to the caller, and releases the lock — the same "snapshot-and-count under the lock" pattern ADR-0048 already established for filtered counting, just returning entries instead of a count. This avoids handing a consumer a live, lock-free-iterated reference to internal generated state (which could otherwise race against a concurrent call still appending). +- **Does not touch `CallVerifier`.** Scenario 3 (verify-then-inspect) is answered by calling `ReceivedCalls()` after `Verify()...Once()` passes, rather than by threading data through `CallVerifier` itself — two separate, composable calls rather than one fused one. This keeps ADR-0048's "`CallVerifier` never needs access to the call log at all" invariant fully intact (a true zero-risk-to-existing-code option), at the cost of the consumer writing two lines instead of one chained expression. Given `CallVerifier` is a **public, core-`Compono`, cross-package-reused type** (per this session's Investigation 2, also in flight), *not* touching its shape at all for this investigation is the conservative, clearly-correct choice, and is recommended specifically because it does not entangle this investigation's outcome with Investigation 2's. + +## 8. Recommended scope for 1.1 (if admitted) + +1. Expose `ReceivedCalls()` for exactly the members already eligible under ADR-0048's five-condition rule (unchanged scope) — zero new runtime cost, infrastructure already exists and is already paid for. +2. Add `ClearCalls()` with the semantics in §5 (clears counts/log, preserves all configuration including sequence ordinal). +3. **Do not** expand eligibility to overloaded members in this same pass, even though §4.2 found no `Match`-ambiguity reason blocking it — that's a real, separate design question (how is a per-overload call log keyed, does it reuse ADR-0044 Requirement 1's per-overload `ReturnConfig` field pattern) that deserves its own compiler-spike-backed pass, matching this repo's own "do not assume, run the compiler" standard used throughout ADR-0048. Flag it explicitly as a follow-up, not a rejection. +4. **Do not** bound the call log (§6.1's finding: unbounded is fine at real scale; bounding adds a surprising semantic for an unevidenced problem). +5. **Do not** change `CallVerifier`'s shape as part of this work (§7 — keep this investigation decoupled from Investigation 2). + +## 9. External comparison + +| Library | Received-call inspection | Argument capture | Clear received calls | Retained history | +|---|---|---|---|---| +| **NSubstitute** | `Received(n)`/`DidNotReceive()` (assertion only, no data returned) | `Arg.Do(action)` — callback-based, same shape as Compono's own existing `ReturnsCallback` | **`ClearReceivedCalls()`** — "will not clear any results set up for the substitute," i.e. observation-only, config preserved. `ClearOptions` enum makes this explicit: `ReceivedCalls` / `ReturnValues` / `CallActions` are independently toggleable flags, `All` clears everything — direct precedent for this investigation's exact configuration/observation split (§5). | Implementation detail, not documented as a first-class retrospective-inspection API (`Received()` is assertion-only; no documented `ReceivedCalls()`-as-data-accessor found in current docs) | +| **Rocks** (source-generated, AOT-friendly, the closest architectural peer) | `Verify()` — assertion only (strict-mock style, fails with `VerificationException`) plus `ExpectedCallCount()` for count-based assertion | **`Callback()`** — explicitly documented as the mechanism to "capture method argument values" — i.e., Rocks made the *same* choice this investigation recommends keeping for single-value capture (§3.C): callback-based, not retained-history-based, as its primary public capture story | Not found documented | Not found documented as a public retrospective-history API | + +**Reading these findings:** neither of the two most relevant peers (one API-mature and widely used, one source-generated/AOT-focused like Compono itself) treats "iterate every captured call as strongly-typed data" as a prominent, separately-branded public feature — NSubstitute's public capture story is callback-based (`Arg.Do`), and so is Rocks's (`Callback()`). This is a genuine signal against over-building: **the callback mechanism (§3.C, already shipped in Compono as `ReturnsCallback`) is doing exactly what the two most relevant peer libraries treat as their primary capture mechanism.** What neither peer's public docs prominently expose is exactly scenario 2/3 (multi-call history, verify-then-inspect) — which is the residual, narrower gap `ReceivedCalls()` is recommended to fill, not a re-implementation of what `ReturnsCallback` already covers. This also means the case for `ReceivedCalls()` should be argued on its own merits (a natural, evidence-independent completion of a fluent surface the package already has three-quarters of — Configure/Verify/[Received]) rather than on "consumers expect this because NSubstitute has it," since NSubstitute's own most-visible, most-documented capture path is the same callback shape Compono already ships. + +`ClearReceivedCalls()`'s `ClearOptions` flag design, however, is unambiguous, strong, directly-applicable precedent for §5's `ClearCalls()` semantics specifically — worth citing directly in any future ADR. + +## 10. Rejected alternatives, summarized + +- **Opt-in capture (§3.B):** rejected — breaks the retrospective-verification convention every existing Compono.TestDoubles verification path relies on, and doesn't clearly reduce cost versus always-on for already-eligible members. +- **Folding `.Calls` onto `CallVerifier`/`Verify()`'s return value (§4.4):** rejected — reopens an architectural boundary ADR-0048 deliberately closed (`CallVerifier` has no log access after construction), and blurs `Verify()`'s single-purpose assertion contract. +- **Bounded/ring-buffer call log by default (§6.1/§8):** rejected as a default — measured cost of unbounded growth at realistic test-method scale is negligible; bounding trades a real, understood cost for a new, surprising "which call got silently dropped" semantic. +- **A new, second full history mechanism independent of ADR-0048's existing log (rather than exposing the existing one):** rejected — would duplicate storage (two lists per eligible member) for no benefit; the existing log already has everything scenario 1/2/3 need. + +## 11. AOT/trimming implications + +None found beyond what ADR-0048 already established and shipped (this feature reuses that exact storage, adding no new generic/reflection-based mechanism): + +- No reflection, no boxing — the recommended `record` return type has real, closed, compile-time-known field types (same types the existing tuple already uses). +- No new trimming-unsafe surface — `List.Clear()`/enumeration and constructing a `record` from already-known-type fields are all fully trim-safe, ordinary generic code, matching every other Compono.TestDoubles mechanism. +- `ReceivedCalls()`/`ClearCalls()` are ordinary generated extension methods and instance-state mutators — no new generator-only-resolvable indirection is introduced. + +## 12. Compatibility implications + +- **Source/binary compatibility:** fully additive. New generated members (a `ReceivedCalls()` bridge type, `ClearCalls()`), no change to any existing generated signature, no change to `CallVerifier`, `ReturnConfig`, or `ReturnConfigBuilder`'s existing public shape. +- **Generated-source compatibility:** every existing eligible/ineligible member keeps generating byte-for-byte the same code for its *existing* surface (`Configure()`/`Verify()`/dispatch) — the new bridge and `ClearCalls()` are pure additions to the generated output, not modifications of existing output. This matches the same "does not modify, break, or supersede any part of [the prior ADR's] existing generated surface" standard ADR-0048 itself met relative to ADR-0044. +- **Behavioral/SemVer:** additive, 1.1-appropriate. No existing consumer code changes behavior; a consumer not using the new members observes nothing different. +- **The one real compatibility-adjacent open question:** if a future ADR ever *does* decide to widen `CallVerifier` itself (Investigation 2, separate track) to carry optional log access, that would be a `CallVerifier`-shape decision made independently, on Investigation 2's own merits — this investigation's recommended design in §7 explicitly does not require or block that; they are compatible but decoupled tracks. + +## 13. Whether this should be admitted into 1.1 + +**Yes, as scoped in §8.** Rationale, applying this session's evidence standard directly (no filed consumer issue is required): + +- It is a conventional testing capability (retrospective call inspection is standard across the category, per §9, even though the *specific mechanism* each peer favors varies) that naturally completes a fluent surface Compono.TestDoubles already has two-thirds of (`Configure()`, `Verify()`). +- Its cost, for the scope recommended in §8, is genuinely zero additional runtime overhead for already-eligible members — the infrastructure already exists and is already paid for; this is as close to "free" as an additive feature gets in this codebase. +- It closes a real, named architectural gap the package's own migration-facing documentation calls out ("matching is not capture" as a stated NSubstitute-migration boundary, per the original brief) — not a manufactured feature, but also not one that required a filed complaint to justify; it was already visible as a documented gap. +- The scope in §8 deliberately excludes the two genuinely open-ended sub-questions (overload eligibility expansion, any `CallVerifier` shape change) rather than trying to resolve them in the same pass — keeping the admitted scope small and evidenced, consistent with this repo's stated aversion to unevidenced generality. + +## 14. Exact decisions a future ADR needs to lock + +1. **Bridge shape and name:** confirm `ReceivedCalls()` (vs. `Calls()` or another name) as the third bridge, parallel to `Configure()`/`Verify()`. +2. **Return type:** confirm a generated `record` per eligible member (vs. reusing the raw `ValueTuple` the internal log already stores) — this investigation recommends `record` for ergonomics but did not compiler-spike it; a spike should confirm no naming/generation conflicts analogous to ADR-0048 Amendment 1's derived-name-collision class of bugs. +3. **Snapshot semantics:** confirm "lock, copy to a new list/array, release lock, return" as the read contract (this investigation recommends it directly from ADR-0048's own established pattern, but it's the ADR's decision to ratify). +4. **`ClearCalls()` receiver:** decide whether it hangs off `Verify()`'s wrapper type, a new minimal wrapper, or the double instance directly (self-`this` extension) — this investigation deliberately left this open (§7) since it's a naming/receiver-ergonomics choice, not a semantics one; the semantics (§5) are the load-bearing decision, already answered. +5. **`ClearCalls()` scope relative to `ReceivedCalls()` eligibility:** confirm `ClearCalls()` applies uniformly to *all* members (clearing `CallCount` for argument-independent members too, not just the ADR-0048 log for eligible ones) rather than only to capture-eligible members — this investigation assumes uniform applicability (§5's table doesn't distinguish member shape) but a future ADR should state this explicitly since it changes the generated surface for *every* member, not just eligible ones. +6. **Overload eligibility for `ReceivedCalls()`:** explicitly deferred (§4.2, §8) — needs its own compiler-spike-backed evaluation, not inherited from ADR-0048 by default. +7. **Whether to also widen `ReceivedCalls()`/argument-matching eligibility to any other currently-excluded shape** (e.g., should the derived-name-collision-driven exclusions in ADR-0048 Amendment 1 be revisited for capture specifically, given they're implementation artifacts rather than fundamental limits) — flagged as a smaller, lower-priority open question, not expected to block 1.1 admission. diff --git a/docs/research/0027-compono-callverifier-atleast-atmost-investigation.md b/docs/research/0027-compono-callverifier-atleast-atmost-investigation.md new file mode 100644 index 0000000..f6dd08e --- /dev/null +++ b/docs/research/0027-compono-callverifier-atleast-atmost-investigation.md @@ -0,0 +1,179 @@ +# [RESEARCH-0027] Compono.CallVerifier: AtLeast/AtMost Cross-Package Investigation + +**Status:** Done (research only; no ADR/amendment yet) + +**Feeds:** a future ADR-0044 amendment decision (or a new ADR — see recommendation below) scoping `AtLeast`/`AtMost` additions to `Compono.CallVerifier` for the 1.1 release, and a small companion change to `Compono.Logging`'s `LogVerificationBuilder`. + +## 1. Summary + +`CallVerifier` (`src/Compono/CallVerifier.cs`) is no longer a `Compono.TestDoubles` implementation detail — it is directly reused, unmodified, by `Compono.Http` and `Compono.Logging` as their own count-verification primitive. All three packages' research documents (RESEARCH-0023/0024/0025) independently converged on the same missing capability: "at least N" / "at most N" call-count assertions, distinct from today's `Never()`/`Once()`/`Exactly(n)`. + +This investigation finds: + +- The original ADR-0044 decision to omit `AtLeast`/`AtMost` was **explicit and specific to those exact names**, not merely a rejection of call-order verification or a bigger DSL. The context has materially changed since then (see §2). +- `AtLeast(int)`/`AtMost(int)` are safe, additive, conventional count assertions that complete the existing abstraction without turning it into a verification framework. `Between`/`AtLeastOnce`/`AtMostOnce`/`Any`/`None` should **not** be added (see §4). +- The change is purely additive at the source/binary/API level in core `Compono`. It works immediately, with zero changes required, for `Compono.TestDoubles` and `Compono.Http`. It requires one small, mechanical companion change in `Compono.Logging`'s `LogVerificationBuilder` to actually surface the new methods through its fluent chain (see §3). +- Recommendation: **amend ADR-0044** (not a new ADR, not undocumented). See §6. + +## 2. Revisiting the original decision + +### 2.1 What ADR-0044 Requirement 3 actually said + +`docs/adr/0044-compono-testdoubles-v2-overloads-generics-verification.md`, Requirement 3's Considered Options list three shapes (lines 128–146): + +1. A dedicated `Verify()` bridge with `.Once()`/`.Never()`/`.Exactly(n)` — **chosen**. +2. Folding verification into `ReturnConfigBuilder` via a raw `CallCount` property, verified with `Assert.Equal(...)`. +3. "A full `Received()`-equivalent — argument-aware call recording, sequence/order verification, `ReceivedCalls()`-style enumeration." + +The Decision Outcome section states, verbatim (lines 393–398): + +> "**Deliberately minimal, matching the explicit instruction:** `Never`/`Once`/`Exactly(n)` only — no `AtLeast`/`AtMost`, no argument-aware recording, no call-order verification, no `ReceivedCalls()`-style enumeration, no strict mode. `Interlocked.Increment` on a plain `int` field is the cheapest possible thread-safe counter — no allocation per call, no dictionary, matching the 'don't allocate just to support `Once()`' instruction directly." + +This is the load-bearing sentence for this investigation. **`AtLeast`/`AtMost` were named and explicitly excluded**, not merely implied by a broader "no DSL" stance. This is a stronger prior than "the ADR only ruled out call-order verification" — it directly addressed count-range semantics and said no. + +However, the *reasoning offered* for the exclusion is entirely about implementation cost and scope discipline, not about count-range semantics being conceptually wrong: + +- The stated cost concern ("cheapest possible thread-safe counter — no allocation per call, no dictionary") is about the **counting mechanism** (`Interlocked.Increment` on an `int`), not about what assertions are built on top of that counter afterward. `AtLeast`/`AtMost` need no additional storage, no additional field, and no change to `RecordCall()` — they read the exact same `observedCount` int that `Exactly` already reads. The performance rationale that justified minimality does not apply to these two methods at all; it applies to the run-time counter, which is already built and already shipped. +- The scope discipline concern ("don't build a verification framework") is squarely about `Received()`-equivalents: argument-aware recording, call-order verification, and enumeration — all fundamentally different capabilities requiring new storage, new generated surface, and new semantics. `AtLeast`/`AtMost` require none of that; they are two more `if` branches inside a struct that already exists. +- Requirement 3's Option 3 (the "full `Received()`-equivalent") is the option genuinely rejected on architectural grounds. `AtLeast`/`AtMost` were bundled into that rejection by name, but not because they share Option 3's cost profile — they don't need per-call storage, per-call allocation, or generated-surface changes of any kind. + +So the honest reading is: **ADR-0044 rejected `AtLeast`/`AtMost` primarily because nothing had evidenced a need for them yet, in the context of a decision that was simultaneously and correctly rejecting a much more expensive DSL.** It was written as one sweeping "no" alongside genuinely expensive asks, not as a considered verdict that count-range assertions themselves are undesirable. + +### 2.2 What changed since + +At the time of ADR-0044 (2026-08-14), `CallVerifier` had exactly one consumer: `Compono.TestDoubles`. Two things have changed: + +1. **`CallVerifier` is now cross-package infrastructure.** `Compono.Http`'s `HttpResponseRegistration.Verify()` returns a `CallVerifier` directly (`src/Compono.Http/HttpResponseRegistration.cs:56`), and `Compono.Logging`'s `LogVerificationBuilder` builds one internally as its terminal step (`src/Compono.Logging/LogVerificationBuilder.cs:66-87`). Three independent research passes (RESEARCH-0023, -0024, -0025), done by three different investigators researching three unrelated packages, converged on the same missing capability without prompting each other. That is a materially different evidentiary posture than "one package's ADR author speculated about a nice-to-have." +2. **ADR-0048 (2026-08-21, seven days after ADR-0044) already extended `CallVerifier`'s consumption pattern** — not `CallVerifier` itself — by adding argument-filtered call verification (`Verify().Member(Match.Is(...))`), reusing `CallVerifier` completely unmodified as the terminal count check after argument filtering. ADR-0048 never revisits or reopens Requirement 3's minimality decision; it treats `CallVerifier` as a fixed, reusable primitive and layers filtering in front of it. This confirms the architectural pattern this investigation proposes to extend (add richness to the terminal count check, not to what feeds it) is already the established shape, just not yet extended to counts themselves. + +Neither of these facts overrides ADR-0044's explicit language — the ADR clearly said no to these two names. But both support the case that the *original constraint has been satisfied and outgrown*, not that it was wrong: the ADR's own words are about lacking evidence and avoiding scope creep, and both conditions have now materially changed in ways ADR-0044's author could not have accounted for when a single package was the only consumer. + +## 3. Cross-package consistency — verified against real code, not illustrative examples + +### 3.1 Compono.TestDoubles + +Generated `Verify()` extensions return `global::Compono.CallVerifier` directly from every generated overload (`src/Compono.Generators/Templates/TestDouble.scriban:585-665`, ten distinct emission sites covering plain members, argument-matched members, and generic members). Because these are ordinary instance methods on a public struct, **any new public method added to `CallVerifier` is immediately callable on every one of these generated call sites with zero generator changes and zero regeneration requirement for existing consumers** (existing generated code is untouched; new consumer code simply calls a method that now exists on the struct it already receives). + +Real shape confirmed: + +```csharp +mediator.Verify().Send().Once(); // exactly 1 call +mediator.Verify().Send().AtLeast(1); // would work immediately once added +``` + +### 3.2 Compono.Http + +`src/Compono.Http/HttpResponseRegistration.cs:56`: + +```csharp +public CallVerifier Verify() => new(_matchedCallCount, _description); +``` + +`Verify()` returns `CallVerifier` itself — no wrapper type. `registration.Verify().AtMost(2)` would compile and work the instant `AtMost` exists on `CallVerifier`, with **zero changes to `Compono.Http`** required. The doc comment at line 51 already states the type is intentionally "unchanged" from core, so this is squarely in scope for a package that has explicitly deferred to core's verification vocabulary. + +### 3.3 Compono.Logging — the one real gap + +`Compono.Logging`'s `Verify()` does **not** return `CallVerifier`. It returns `LogVerificationBuilder` (`src/Compono.Logging/LogVerificationBuilder.cs:14`), a fluent filter chain (`AtLevel`, `WithEventId`, `WithException`, `WithMessageContaining`, `Matching`) that only converts to a `CallVerifier` internally, at the last possible moment, inside a private `ToCallVerifier()` method (line 66). The public terminal methods (`Once()`, `Never()`, `Exactly(int)`, lines 49–58) are **thin one-line forwarders** written by hand: + +```csharp +public void Once() => ToCallVerifier().Once(); +public void Never() => ToCallVerifier().Never(); +public void Exactly(int times) => ToCallVerifier().Exactly(times); +``` + +`CallVerifier` is deliberately never part of `LogVerificationBuilder`'s public surface (per its own class doc, lines 6–13). This means **adding `AtLeast`/`AtMost` to `CallVerifier` does not automatically expose them through `logger.Verify().AtLevel(...).AtLeast(1)`** — that specific illustrative call shape from the brief does not compile today and would not compile after only a core change. `Compono.Logging` needs its own two-line companion addition: + +```csharp +public void AtLeast(int times) => ToCallVerifier().AtLeast(times); +public void AtMost(int times) => ToCallVerifier().AtMost(times); +``` + +This is mechanical, in the same file, following the exact existing pattern — not a design question, just a fact this investigation needs to record so the illustrative cross-package example in the brief is understood correctly: **it is not automatically true today; it requires one small, low-risk, same-shape companion change in `Compono.Logging`, in addition to the core change.** This companion change is additive to `Compono.Logging`'s public API and carries no design risk — it is the same forwarding pattern already used three times in the same class. + +### 3.4 Do any package's semantics make counts misleading? + +No. In all three packages, the underlying `observedCount` represents "number of times the matching condition was satisfied" — a call to a member (TestDoubles), a request matching a registration (Http), or a log entry matching accumulated filters (Logging). `AtLeast`/`AtMost` mean exactly the same thing in all three: a lower/upper bound on that same count. There is no package where "at least" or "at most" would need different semantics or would be misleading given how the count is produced. + +## 4. API semantics + +### 4.1 Signatures + +```csharp +public void AtLeast(int times) +public void AtMost(int times) +``` + +Matching `Exactly(int times)`'s existing parameter name and type exactly (consistency, not incidental). + +### 4.2 Argument validation + +- **Negative counts**: `Exactly(int)` today performs **no argument validation at all** — `Exactly(-1)` simply can never pass (since `observedCount` is never negative) and produces a slightly confusing but harmless message ("Expected exactly -1 call(s)..."). For consistency with existing behavior and to avoid introducing a validation asymmetry between `Exactly` and the two new methods, `AtLeast`/`AtMost` should **not** add `ArgumentOutOfRangeException` guards either. A negative `AtLeast(-1)` is trivially always true (every count is `>= -1`); a negative `AtMost(-1)` can never pass. Both are harmless if slightly nonsensical inputs — exactly as `Exactly(-1)` is today. Adding validation to only the two new methods, while `Exactly` has none, would be an inconsistent, un-evidenced enhancement outside this investigation's scope. +- **`AtLeast(0)`**: Always trivially true (every observed count is `>= 0`). Not forbidden — a consumer might write it for symmetry/self-documentation in generated test scaffolding, and rejecting it would require validation `Exactly(0)`/`Never()` don't have either. Harmless to allow. +- **`AtMost(0)` vs. `Never()`**: `AtMost(0)` is semantically identical to `Never()` (a count can't be negative, so "at most 0" means "exactly 0"). This is not a reason to reject `AtMost(0)` — `Exactly(0)` already exists as a fully redundant spelling of `Never()` today, and no one has proposed removing it. Redundant-but-clear spellings are already the established convention in this type; `AtMost(0)` fits it rather than breaking new ground. +- **Exception type**: `TestDoubleVerificationException`, unchanged — same type `Exactly` already throws, consumed uniformly by all three packages already (Http and Logging both surface it via their own doc comments referencing this exact type). + +### 4.3 Diagnostic wording + +Matching the existing message convention exactly (verified against `test/Compono.Tests/CallVerifierTests.cs:28,49,60,81`, e.g. `"Expected exactly {times} call(s) to {memberDescription}, but received {observedCount}."`): + +```csharp +public void AtLeast(int times) +{ + if (observedCount < times) + { + throw new TestDoubleVerificationException( + $"Expected at least {times} call(s) to {memberDescription}, but received {observedCount}."); + } +} + +public void AtMost(int times) +{ + if (observedCount > times) + { + throw new TestDoubleVerificationException( + $"Expected at most {times} call(s) to {memberDescription}, but received {observedCount}."); + } +} +``` + +This is a direct, minimal-diff extension of `Exactly`'s existing shape and wording style — same sentence template, same clause order, only the comparison operator and the leading adjective change. + +### 4.4 What NOT to add, and why + +Per explicit instruction, evaluated and rejected: + +- **`Between(int min, int max)`** — fully derivable by a consumer calling `AtLeast(min)` then `AtMost(max)` (or vice versa) in two lines; it is pure sugar over two primitives that themselves need to exist first. Adding it now would be exactly the "enlarge the vocabulary before evidence demands it" mistake ADR-0044's original author was rightly wary of. Revisit only if real consumer friction with the two-call spelling is ever evidenced. +- **`AtLeastOnce()` / `AtMostOnce()`** — trivial aliases for `AtLeast(1)` / `AtMost(1)`. NSubstitute has `Received()` (bare, no count, meaning "at least once") as a historical artifact of its fluent-`Received(n)` design, not because "at least once" is an independently meaningful concept worth a dedicated name in a count-based API shaped like this one. Adding both the general and the special-cased-to-1 spelling doubles the vocabulary for zero new expressive power. +- **`Any()` / `None()`** — `Any()` would mean "was called at least once," fully redundant with `AtLeast(1)`; `None()` is exactly `Never()` already. Pure duplication. + +None of these are rejected because they'd be hard to implement — every one of them is a one-line trivial forward. They are rejected because `CallVerifier`'s entire design identity (per ADR-0044's own words: "one small, concrete... no dictionary, no boxing") is a deliberately narrow, unambiguous vocabulary. `AtLeast`/`AtMost` complete the natural mathematical set implied by `Exactly` (exactly / at least / at most is a complete, closed, mutually-orthogonal trio); every rejected name above is a convenience alias *on top of* that trio, not a missing member of it. Stopping at the trio is the same "don't build a framework" discipline ADR-0044 exercised, applied consistently rather than abandoned. + +## 5. Compatibility assessment + +- **Source compatibility**: Fully additive. No existing member signature changes. No existing call site needs modification. +- **Binary compatibility**: Adding two public instance methods to an existing public struct is binary-compatible — it does not change the struct's layout, does not change any existing member's metadata token, and does not affect any assembly compiled against the current `Compono.dll`. Existing compiled consumers (test assemblies, `Compono.Http.dll`, `Compono.Logging.dll`) continue to load and run unmodified; only assemblies wanting to call the new methods need to reference an updated `Compono` package. +- **Public API compatibility**: Purely additive to the public API surface. No public member is removed, renamed, or has its signature changed. +- **Consumers compiled against Compono 1.0**: Unaffected. They don't reference the new methods and never will unless recompiled against a newer `Compono`, at which point they gain access to `AtLeast`/`AtMost` for free with no other code change required. +- **Generated-source compatibility**: No generator changes required for `Compono.TestDoubles` (§3.1) — the generated `Verify()` surface already returns `CallVerifier` by value; existing generated code needs no regeneration and no template change. The `Compono.Http` case similarly needs no source changes (§3.2). `Compono.Logging` needs one small, additive, non-generated hand-written change (§3.3), which is not a generated-source compatibility concern at all — it's ordinary hand-written library code. +- **AOT/trimming**: No new reflection, no new virtual dispatch, no new generic instantiation pattern — the new methods use the exact same field reads and exception-construction pattern `Exactly` already uses, which is already proven AOT/trimming-safe in the existing package. +- **Package versioning/SemVer**: Purely additive — appropriate for a minor version bump (1.1.0) regardless of Compono's pre-/post-1.0 status; this is not a question that depends on SemVer stage at all, since additive API surface is never a breaking change under any SemVer interpretation. + +## 6. ADR mechanism recommendation + +**Recommendation: amend ADR-0044**, not a new ADR, and not "no ADR." + +Reasoning: + +- ADR-0044 Requirement 3 is the specific decision record that named `AtLeast`/`AtMost` and rejected them (§2.1). The natural, discoverable place to record that this specific, named exclusion is being revisited is the same decision record — a future reader investigating "why doesn't `CallVerifier` have `AtLeast`?" will find ADR-0044 first (it is `CallVerifier`'s originating ADR, referenced directly in the type's own XML doc comment: `"Deliberately minimal... per ADR-0044 Requirement 3"`). An amendment there closes the loop exactly where the original constraint lives, consistent with this repo's stated amendment convention (corrections/refinements to a still-valid decision, not reversals). +- This is not a **reversal** of ADR-0044's architecture — the "small, concrete, no dictionary, no boxing" principle stays fully intact; `AtLeast`/`AtMost` are two more branches on the same struct, not a different architecture. That rules out a Superseded status and supports Amendment over a full rewrite. +- A **brand-new ADR** would be disproportionate: there is no new architectural question here requiring its own Context/Decision Drivers/Considered Options framing distinct from what ADR-0044 already established (the struct's shape, its exception type, its "small and concrete" philosophy, its cross-package reuse are all already-settled facts this change operates entirely within). A new ADR would either duplicate ADR-0044's existing content or read strangely thin next to it. +- **"No ADR"** is not appropriate specifically *because* ADR-0044 made an explicit, named decision on this exact question ("no `AtLeast`/`AtMost`"). Even though the code change itself is small, silently contradicting a named decision without a documented amendment would leave the ADR record actively wrong/misleading for any future reader — this is exactly the scenario `design-decisions.md`'s amendment mechanism exists for (a correction to a still-valid decision, not a reversal, but one that must be visible in the record). + +The amendment should also note the small `Compono.Logging` companion change (§3.3) as a consequence, since `LogVerificationBuilder`'s design (deliberately keeping `CallVerifier` out of its public surface) is itself documented in ADR-0055, not ADR-0044 — the amendment to ADR-0044 should cross-reference that ADR-0055 will need a small corresponding note/update when this is implemented, but that is ADR-0055's amendment to make, not ADR-0044's. + +## 7. Final assessment + +- **Recommended `CallVerifier` additions**: `AtLeast(int times)` and `AtMost(int times)` only, per §4. +- **ADR mechanism**: Amend ADR-0044 (§6); flag a small corresponding note for ADR-0055 regarding `Compono.Logging`'s companion forwarding methods. +- **Compatibility verdict**: Fully additive at every layer examined — source, binary, public API, generated-source, AOT/trimming, SemVer. No consumer breakage under any scenario. +- **Proceed to 1.1**: Yes. This is exactly the "conventional, useful, easy-to-understand count assertion that naturally completes an existing abstraction" the investigation was asked to test for — not a manufactured feature, not scope creep, and not blocked by any technical or compatibility risk. diff --git a/docs/roadmap/proposed-adrs.md b/docs/roadmap/proposed-adrs.md index a182699..e96b8b1 100644 --- a/docs/roadmap/proposed-adrs.md +++ b/docs/roadmap/proposed-adrs.md @@ -5,16 +5,32 @@ ADR that's `Proposed`, or `Accepted` but not yet implemented. ## Current state: none proposed or pending implementation +[ADR-0044 Amendment 22](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-22-2026-09-06-callverifieratleastintatmostint-added-requirement-3s-minimality-preserved-not-reversed) +(`CallVerifier.AtLeast(int)`/`AtMost(int)`, plus the `Compono.Logging` +`LogVerificationBuilder` forwarding consequence) and +[ADR-0060](../adr/0060-testdoubles-received-calls-and-clear-calls.md) +(`Compono.TestDoubles` `ReceivedCalls()` retrospective inspection and +`ClearCalls()`) were the immediately preceding entries on this page — both +`Accepted`, now fully implemented by +[PLAN-0063](../plans/0063-callverifier-atleast-atmost-and-testdoubles-received-calls-clear-calls.md) +(`Done`): code, tests (unit/generator-snapshot/AOT-smoke/dogfooding), docs +(`docs/packages/compono-testdoubles.md`, `compono-http.md`, +`compono-logging.md`), and the relevant `skills/compono` files (`SKILL.md`, +`references/testdoubles.md`, `references/http.md`, `references/logging.md`, +`evals/evals.json`, plus the mandatory baseline-vs-updated skill-eval +comparison) landed together, so both entries are removed from this page +per its own "entries removed once implemented" rule. + [ADR-0039](../adr/0039-future-extension-package-admission-gate-and-release-sequence.md) (Future Extension Package Admission Gate and Release Sequence) was the -last entry on this page — `Accepted`, with +prior last entry on this page — `Accepted`, with [PLAN-0039](../plans/0039-future-extension-package-admission-gate-and-release-sequence.md) -now `Done`, putting its two-stage admission model into effect across +`Done`, putting its two-stage admission model into effect across `docs/roadmap/future-packages.md` and `skills/compono`. See [Future Packages](future-packages.md) for the resulting per-candidate disposition it produced. -Every ADR recorded in [`docs/adr/README.md`](../adr/README.md) is +Every other ADR recorded in [`docs/adr/README.md`](../adr/README.md) is currently `Accepted` and implemented, `Superseded`, or (for the two decisions later revisions replaced) implicitly retired by their successor. See the [Historical Decision Log](../architecture/decision-log.md) diff --git a/skills/compono/SKILL.md b/skills/compono/SKILL.md index 0b408f5..210ee1a 100644 --- a/skills/compono/SKILL.md +++ b/skills/compono/SKILL.md @@ -90,20 +90,45 @@ TestDoubles, **read `references/testdoubles.md` before answering**. Do not answer from memory: older Compono guidance said generated doubles had no argument matching, but that is stale. Current TestDoubles supports `Configure()`, `Verify()`, literal equality matching, `Match.Any()`, -`Match.Is(predicate)`, argument-filtered `Never()`/`Once()`/`Exactly(n)`, -multi-entry argument-distinguished response configuration, and -`ReturnsCallback(...)` for supported non-void methods. Translate NSubstitute -vocabulary directly where eligible: -`Arg.Is` → `Match.Is`, `Arg.Any()` → `Match.Any()`, -`Received(1)` → `Verify().Member(...).Once()`, `Received(n)` → -`Verify().Member(...).Exactly(n)`, and `DidNotReceive()` → -`Verify().Member(...).Never()`. Never invent non-existent TestDoubles APIs -such as `CallsTo(...)`, `ReceivedCalls()`, or `[ComponoTest]`, and never -recommend a hand-written recording fake solely because the old test used -NSubstitute argument matchers. Use `ReturnsCallback((arg1, ...) => result)` -when a supported method's result depends on its actual arguments. It is not -an untyped `CallInfo` callback. True argument capture and call-order -verification remain separate capabilities. +`Match.Is(predicate)`, argument-filtered +`Never()`/`Once()`/`Exactly(n)`/`AtLeast(n)`/`AtMost(n)`, multi-entry +argument-distinguished response configuration, `ReturnsCallback(...)` for +supported non-void methods, and — for the same eligible-member set +argument-filtered `Verify()` targets (non-overloaded, no ref-like +parameter, no real parameter referencing the member's own open generic +type parameter, no derived-name collision, not a one-parameter `Equals`) +— `ReceivedCalls()` (retrospective, snapshot-based call inspection, +returning a generated named record per call with real parameter names) +and `ClearCalls()` (a direct, whole-double reset of every member's +observation history — call counts and captured-argument history — that +preserves all configured behavior, including a configured +`ReturnsSequence`'s in-progress ordinal, which never rewinds). Translate +NSubstitute vocabulary directly where eligible: `Arg.Is` → `Match.Is`, +`Arg.Any()` → `Match.Any()`, `Received(1)` → +`Verify().Member(...).Once()`, `Received(n)` → +`Verify().Member(...).Exactly(n)`, `DidNotReceive()` → +`Verify().Member(...).Never()`, and `Received()`/`ReceivedCalls()` on an +eligible member → `.ReceivedCalls().Member()`. `ReceivedCalls()` does +**not** exist for an overloaded member (even one with its own +`Matching` argument-matching surface) — never claim it does; that +expansion is real, plausible future work, not shipped. There is still no +call-order verification and no strict/unexpected-call mode. Never invent +non-existent TestDoubles APIs such as `CallsTo(...)` or `[ComponoTest]`, +and never recommend a hand-written recording fake solely because the old +test used NSubstitute argument matchers or `ReceivedCalls()` on an +interface Compono's eligibility rules exclude. Use +`ReturnsCallback((arg1, ...) => result)` when a supported method's result +depends on its actual arguments. It is not an untyped `CallInfo` callback. +`ReturnsCallback` (exercise-time, single-value capture) and +`ReceivedCalls()` (after-the-fact, retrospective inspection) are two +distinct, complementary mechanisms, not competing designs — recommend +whichever matches the actual scenario (compute a response from the +arguments vs. inspect what was passed after the SUT ran), not one as a +universal substitute for the other. Matching (`Match.Is`/`Match.Any`) +and retrospective inspection (`ReceivedCalls()`) remain distinct concepts +too: matching narrows *which calls count* toward `Verify()`; +`ReceivedCalls()` returns the actual argument values regardless of any +matcher. Call-order verification remains unsupported by either. 1. **Detect** — run the table above. Know which packages are actually installed before recommending any API from them. @@ -145,17 +170,21 @@ verification remain separate capabilities. `UseGeneratedTestDoubles()`, if that package is referenced and the compile-time opt-in is set. Current generated doubles support `Configure()`, `Verify()`, literal equality matching, `Match.Any()`, - `Match.Is(predicate)`, argument-filtered `Never()`/`Once()`/ - `Exactly(n)`, multi-entry argument-distinguished response configuration, - and `ReturnsCallback(...)` for eligible non-void methods. Do not mistake NSubstitute - vocabulary (`Arg.Is`, `Arg.Any`, `Received`, `DidNotReceive`) for a - reason to invent a hand-written recording fake; translate it to the - generated-double surface where the member shape is eligible. A callback - must return the member's declared type exactly, including `Task` or - `ValueTask`; Compono does not wrap a bare result. Argument capture, - call-order verification, classes, delegates, and other explicitly - unsupported shapes remain outside current `Compono.TestDoubles` support - — see `references/testdoubles.md`. + `Match.Is(predicate)`, argument-filtered + `Never()`/`Once()`/`Exactly(n)`/`AtLeast(n)`/`AtMost(n)`, multi-entry + argument-distinguished response configuration, `ReturnsCallback(...)` + for eligible non-void methods, and — for that same eligible-member set + — `ReceivedCalls()` (retrospective call inspection) and `ClearCalls()` + (whole-double observation reset). Do not mistake NSubstitute + vocabulary (`Arg.Is`, `Arg.Any`, `Received`, `DidNotReceive`, + `ReceivedCalls`, `ClearReceivedCalls`) for a reason to invent a + hand-written recording fake; translate it to the generated-double + surface where the member shape is eligible. A callback must return the + member's declared type exactly, including `Task` or `ValueTask`; + Compono does not wrap a bare result. `ReceivedCalls()` for an + **overloaded** member, call-order verification, classes, delegates, + and other explicitly unsupported shapes remain outside current + `Compono.TestDoubles` support — see `references/testdoubles.md`. - A test deliberately needs to exercise the real HTTP client pipeline (real `HttpClient` → `TestHttpHandler` → configured response) rather than substitute an application-level interface away → diff --git a/skills/compono/evals/evals.json b/skills/compono/evals/evals.json index 97a4e28..5a686c3 100644 --- a/skills/compono/evals/evals.json +++ b/skills/compono/evals/evals.json @@ -416,13 +416,14 @@ "id": 30, "category": "behavioral-correctness", "prompt": "I'm reviewing a Compono.TestDoubles migration in AWS Secrets Manager Provider. The old tests are full of NSubstitute vocabulary: Arg.Is, Arg.Any, Received(1), Received(2), and DidNotReceive. The project intentionally removed NSubstitute and Compono.NSubstitute; it now references Compono.XunitV3 and Compono.TestDoubles with generated doubles enabled. Should I write recording fakes for IAmazonSecretsManager, IConfigurationBuilder, and ILoggerFactory because the old tests used those NSubstitute APIs? Assume the members being migrated are eligible non-overloaded interface members.", - "expected_output": "Says no: NSubstitute vocabulary alone is not evidence a fake is required. Translates Arg.Is -> Match.Is, Arg.Any -> Match.Any, Received(1) -> Verify().Member(...).Once(), Received(n) -> Verify().Member(...).Exactly(n), and DidNotReceive() -> Verify().Member(...).Never() for eligible TestDoubles members. Recommends local fakes only for actual unsupported boundaries such as capture/callback/call order, not for ordinary matching/filtering.", + "expected_output": "Says no: NSubstitute vocabulary alone is not evidence a fake is required. Translates Arg.Is -> Match.Is, Arg.Any -> Match.Any, Received(1) -> Verify().Member(...).Once(), Received(n) -> Verify().Member(...).Exactly(n), DidNotReceive() -> Verify().Member(...).Never(), and .ReceivedCalls() (the capture property) -> .ReceivedCalls().Member() for eligible TestDoubles members. Recommends local fakes only for actual unsupported boundaries such as call-order verification or capture on an overloaded member, not for ordinary matching/filtering/retrospective inspection of an eligible member's real arguments.", "files": [], "expectations": [ - "Explicitly rejects writing recording fakes solely because the original tests use Arg.Is/Arg.Any/Received/DidNotReceive", + "Explicitly rejects writing recording fakes solely because the original tests use Arg.Is/Arg.Any/Received/DidNotReceive/ReceivedCalls", "Maps Arg.Is to Match.Is and Arg.Any to Match.Any", "Maps Received(1) to Verify().Once(), Received(n) to Verify().Exactly(n), and DidNotReceive() to Verify().Never() where the member shape is eligible", - "Distinguishes ordinary matching/filtering from actual unsupported boundaries such as capture, callbacks, and call-order verification" + "Maps NSubstitute's ReceivedCalls() capture property to Compono.TestDoubles' ReceivedCalls().Member() for an eligible (non-overloaded) member, rather than claiming capture is unsupported", + "Distinguishes ordinary matching/filtering/capture (all supported for eligible members) from actual unsupported boundaries such as call-order verification or ReceivedCalls() on an overloaded member" ] }, { @@ -565,6 +566,84 @@ "Does not claim or imply Compono.MSTest ships an exactly-once composition guarantee", "Does not invent a workaround (custom caching, a static exactly-once flag) to force single invocation - correctly identifies this as an MSTest-runner-lifecycle property, not something to engineer around" ] + }, + { + "id": 47, + "category": "behavioral-correctness", + "prompt": "This IAccountRepository.Withdraw(string accountId, decimal amount, bool overdraftAllowed) member is a Compono.TestDoubles-generated double, non-overloaded and otherwise eligible. My test calls it several times with different arguments and I need to assert it was called at least twice, and separately that it was called at most three times. What's the idiomatic way to do this with Compono.TestDoubles?", + "expected_output": "Recommends repository.Verify().Withdraw(...).AtLeast(2) and repository.Verify().Withdraw(...).AtMost(3), the same CallVerifier terminals used for Once()/Never()/Exactly(n), not a hand-rolled count comparison against a captured call list.", + "files": [], + "expectations": [ + "Uses Verify().Member(...).AtLeast(n) for a lower-bound count assertion", + "Uses Verify().Member(...).AtMost(n) for an upper-bound count assertion", + "Does not invent a Between(...)/AtLeastOnce()/AtMostOnce() method - none of these exist on CallVerifier", + "Does not fall back to manually counting ReceivedCalls().Member() entries when AtLeast/AtMost already does the job" + ] + }, + { + "id": 48, + "category": "behavioral-correctness", + "prompt": "I have a Compono.TestDoubles generated double for IAccountRepository (non-overloaded, eligible Withdraw(string, decimal, bool) member). After exercising the system under test, I want to inspect the actual accountId and amount values Withdraw() was called with, not just how many times it was called. Is there a way to do this without writing a hand-rolled recording fake?", + "expected_output": "Recommends repository.ReceivedCalls().Withdraw(), which returns a strongly typed, generated record per call with the real parameter names (accountId, amount, overdraftAllowed), not a positional tuple. Explains it's a snapshot taken at call time, and that this only works because Withdraw is a non-overloaded, ADR-0048-eligible member.", + "files": [], + "expectations": [ + "Recommends ReceivedCalls().Member() rather than a hand-written recording fake", + "States the returned type is a generated record with real parameter names, not Item1/Item2 or an untyped tuple", + "Mentions or implies this is a snapshot, not a live view over ongoing calls", + "Does not claim this works for every member unconditionally - ties it to the same eligibility rules as argument-filtered Verify()" + ] + }, + { + "id": 49, + "category": "behavioral-correctness", + "prompt": "I called repository.Save(order) twice in my test, mutated the same `order` object between the two calls, and then inspected repository.ReceivedCalls().Save(). Both captured entries show the object's final, post-mutation state instead of what it looked like at each call. Is this a bug in Compono.TestDoubles?", + "expected_output": "Explains this is expected, documented behavior, not a bug: ReceivedCalls() retains the same reference the caller passed for a reference-type argument, with no deep copy - mutating the object after a call is observed by later inspection of that same captured entry. Compares this to NSubstitute's own identical reference-retention behavior for received-call arguments, and recommends passing a defensive copy at the call site (or capturing the state needed before mutating) if the test needs the value as of the call, not the current value.", + "files": [], + "expectations": [ + "States this is expected behavior per ADR-0060, not a defect", + "Explains ReceivedCalls() stores a reference for reference-type arguments, not a deep copy", + "Does not suggest Compono.TestDoubles is broken or file a bug", + "Offers a concrete fix (pass a copy, or capture needed state before mutating) rather than just describing the limitation" + ] + }, + { + "id": 50, + "category": "behavioral-correctness", + "prompt": "I share one Compono.TestDoubles generated double instance across two phases of the same test - configure it, exercise phase 1, verify phase 1's calls, then I want to exercise phase 2 and verify phase 2's calls independently, without phase 1's calls still counting. The double's configured Returns()/ReturnsSequence() behavior should still apply in phase 2. What should I use?", + "expected_output": "Recommends repository.ClearCalls() between the two phases - a direct, whole-double operation (not repository.Verify().ClearCalls() or repository.ReceivedCalls().Clear()) that resets every member's call count and captured-argument history while leaving all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence, multi-entry and closed-instantiation configuration) untouched.", + "files": [], + "expectations": [ + "Recommends repository.ClearCalls() as a direct call on the double itself", + "Does not suggest Verify().ClearCalls() or ReceivedCalls().Clear() as the receiver", + "States that configured behavior (Returns/Throws/ReturnsSequence/etc.) survives ClearCalls()", + "Does not suggest creating a second double instance or re-composing as the only way to reset observation history" + ] + }, + { + "id": 51, + "category": "behavioral-correctness", + "prompt": "A Compono.TestDoubles member is configured with Configure().NextValue().ReturnsSequence(\"A\", \"B\", \"C\"). I call it twice (getting \"A\" then \"B\"), then call repository.ClearCalls(), then call it a third time. Which value do I get: \"A\" again, or \"C\"?", + "expected_output": "\"C\" - ClearCalls() only resets observation history (call counts, captured-argument history), never a configured sequence's in-progress ordinal. The ordinal is configured-behavior progress (the same category as 'what value Returns produces next'), not a record of what already happened, so it never rewinds.", + "files": [], + "expectations": [ + "Answers \"C\", not \"A\"", + "Explains that ClearCalls() does not rewind a configured ReturnsSequence's ordinal", + "Frames the ordinal as configured-behavior progress, not observation history", + "Does not claim ClearCalls() is equivalent to fully resetting/recomposing the double" + ] + }, + { + "id": 52, + "category": "behavioral-correctness", + "prompt": "IGateway has two overloads of Send: Send(string message) and Send(int retryCount, string message). Both are Compono.TestDoubles generated-double members with an existing Matching argument-matching surface (ADR-0044 Amendment 21). Can I call repository.ReceivedCalls().Send(...) to inspect the actual arguments each overload received?", + "expected_output": "No - ReceivedCalls() is scoped to exactly ADR-0048's non-overloaded eligible-member set for this release; an overloaded member (even one with its own Matching argument-matching surface) has no ReceivedCalls() surface. Recommends Verify()'s existing per-overload count/argument-matching surface for assertions, or Compono.NSubstitute/a project-local fake if retrospective capture of an overloaded member's arguments is genuinely required.", + "files": [], + "expectations": [ + "States that ReceivedCalls() does not support overloaded members, even with an existing Matching surface", + "Does not fabricate a ReceivedCalls() overload-disambiguation API that doesn't exist", + "Points to Verify()'s existing overload-safe argument-matching surface for what IS supported", + "Suggests Compono.NSubstitute or a project-local fake as the real answer if capture on the overloaded member is required" + ] } ] } diff --git a/skills/compono/references/http.md b/skills/compono/references/http.md index f83e6b2..336473b 100644 --- a/skills/compono/references/http.md +++ b/skills/compono/references/http.md @@ -70,7 +70,7 @@ a v1-only limitation that might later be lifted. ```csharp var registration = handler.OnPost("/v1/orders").RespondJson(order); ... - registration.Verify().Once(); // .Never() / .Exactly(n) also available + registration.Verify().Once(); // .Never() / .Exactly(n) / .AtLeast(n) / .AtMost(n) also available ``` ## Matching semantics @@ -99,9 +99,9 @@ unmatched request still appears in `handler.Requests`. Two different questions, two different APIs — never conflate them: -- `registration.Verify().Once()` / `.Never()` / `.Exactly(n)` — "how many - times did *this configured behavior* match." Reuses core `Compono`'s - `CallVerifier` unchanged. +- `registration.Verify().Once()` / `.Never()` / `.Exactly(n)` / + `.AtLeast(n)` / `.AtMost(n)` — "how many times did *this configured + behavior* match." Reuses core `Compono`'s `CallVerifier` unchanged. - `handler.Requests` (`IReadOnlyList`) — "what did the system under test actually send," every request in arrival order, matched or not, snapshotted fresh on every access. diff --git a/skills/compono/references/logging.md b/skills/compono/references/logging.md index de95721..24105ca 100644 --- a/skills/compono/references/logging.md +++ b/skills/compono/references/logging.md @@ -102,7 +102,10 @@ discovered. explicitly considered and rejected): `.AtLevel(level)`, `.WithEventId(id)`, `.WithException()`, `.WithMessageContaining(text)`, `.Matching(predicate)`, ending in - `.Once()` / `.Never()` / `.Exactly(n)`. + `.Once()` / `.Never()` / `.Exactly(n)` / `.AtLeast(n)` / `.AtMost(n)` — + each a thin forward through the same shared count-verification semantics + `Compono.TestDoubles`/`Compono.Http` use, applied to the filtered match + count, not the whole capture buffer. - `new CapturingLogger(options?)` / `new CapturingLogger(options?)` — direct construction, no composition required, identical behavior to a provider-composed instance. diff --git a/skills/compono/references/testdoubles.md b/skills/compono/references/testdoubles.md index a05b06e..708c9b1 100644 --- a/skills/compono/references/testdoubles.md +++ b/skills/compono/references/testdoubles.md @@ -66,11 +66,15 @@ service.Repository.Configure().CountAsync().Returns(Task.FromResult(4)); ## Argument matching and filtered verification -Do not conflate argument matching with argument capture. Current -`Compono.TestDoubles` supports matcher-based configuration and verification -for eligible members. Supported non-void methods also support a strongly typed -`ReturnsCallback(...)`; it does not expose an arbitrary call log or an -untyped `CallInfo` callback API. +Do not conflate argument matching with argument capture — they remain +distinct concepts even though both are now supported for eligible members. +Current `Compono.TestDoubles` supports matcher-based configuration and +verification for eligible members, plus retrospective inspection of the +actual received arguments via `ReceivedCalls()` (see below) for that same +eligible set. Supported non-void methods also support a strongly typed +`ReturnsCallback(...)`; neither that nor `ReceivedCalls()` exposes an +untyped `CallInfo` callback API, and `ReceivedCalls()` is not available +for an overloaded member. ### Invocation-aware responses @@ -359,14 +363,21 @@ A failing assertion throws `Compono.TestDoubleVerificationException` (a plain exception, not a framework assertion type). A call counts whether it hits configured, default, or thrown behavior. -**Still deliberately minimal** — `Never`/`Once`/`Exactly(n)` only, no -`AtLeast`/`AtMost`, no `ReceivedCalls()`-style enumeration, no call-order -verification. An eligible overload's `Matching(Match...)` surface -supports argument matching; same-name matcher-wrapped overload configuration -does not. If a test needs anything this page doesn't cover (call-order -verification, `ReturnsForAnyArgs`, etc.), use `Compono.NSubstitute` for that -interface instead — the two providers can coexist (see "Precedence with -`Compono.NSubstitute`" below). +**`Never`/`Once`/`Exactly(n)`/`AtLeast(n)`/`AtMost(n)`** — a lower-bound/ +upper-bound count vocabulary, still deliberately narrow: no `Between`, +`AtLeastOnce()`, `AtMostOnce()`, `Any()`, or `None()` (each either +derivable from the primitives above at the call site, or a synonym for an +existing terminal — ADR-0044 Amendment 22). Still no call-order +verification. Retrospective inspection of the actual received arguments is +`ReceivedCalls()`, a separate bridge from `Verify()` — see "Retrospective +call inspection: `ReceivedCalls()`" below; it exists for the same +eligible-member set argument-filtered `Verify()` targets, **not** for an +overloaded member (an eligible overload's `Matching(Match...)` +surface supports argument *matching*, not retrospective capture — the two +remain distinct capabilities). If a test needs anything this page doesn't +cover (call-order verification, `ReturnsForAnyArgs`, etc.), use +`Compono.NSubstitute` for that interface instead — the two providers can +coexist (see "Precedence with `Compono.NSubstitute`" below). ## Argument matching and argument-filtered verification (v3) @@ -517,15 +528,58 @@ This applies identically to sync/async/property members and to a fluent self-returning member (`IResponseBuilder`-shaped) — none of those get special-cased, all follow the same rule. -## The #1 AutoFixture/NSubstitute-habit trap: matching is not capture +## Retrospective call inspection: `ReceivedCalls()` + +`ReceivedCalls()` — a third bridge alongside `Configure()`/`Verify()` — +returns the real argument values a member was actually invoked with, for +exactly the same eligible-member set argument-filtered `Verify()` targets +above (single-overload, no ref-like parameter, no real parameter +referencing the member's own open generic type parameter, no derived-name +collision, not a one-parameter `Equals`; ADR-0060): + +```csharp +repository.Withdraw("acct-1", 50m, overdraftAllowed: true); +repository.Withdraw("acct-2", 75m, overdraftAllowed: false); -`Compono.TestDoubles` is not a general-purpose mocking framework, but it -does support argument matching and argument-filtered verification for the -eligible member shapes above. The remaining boundary is stronger behavior -that needs access to the actual invocation as a first-class value: +var calls = repository.ReceivedCalls().Withdraw(); +calls[0].accountId.Should().Be("acct-1"); // real parameter names, not .Item1/.Item2 +calls[1].amount.Should().Be(75m); +``` + +Each call comes back as a generated, per-member `readonly record struct` +with the member's own real parameter names. `ReceivedCalls().Member()` +returns a **snapshot** taken at call time, never a live view — a later +invocation never grows or mutates an already-returned snapshot. **No deep +copy**: a reference-type argument is retained by the same reference the +caller passed (mutate it after the call, and a later inspection observes +the mutation — a real, documented footgun, not a bug, consistent with +NSubstitute's own identical behavior); a value-type argument is an +ordinary value copy. Sequential calls preserve append order; there is no +call-order *verification*, no timestamps, and no global sequence IDs. + +`repository.ClearCalls()` — a direct, whole-double operation, not nested +under `Verify()` or `ReceivedCalls()` — resets every member's observation +history (call counts and captured-argument history) while preserving +every configured behavior (`Returns`/`Throws`/`ReturnsCallback`/ +`ReturnsSequence`, multi-entry and closed-instantiation configuration). A +configured sequence's in-progress ordinal does **not** rewind: after +`ReturnsSequence("A","B","C")`, two calls, then `ClearCalls()`, the next +call returns `"C"`, not `"A"` — the ordinal is configured-behavior +progress, not observation history. There is no per-member `ClearCalls()`. + +## The #1 AutoFixture/NSubstitute-habit trap: matching is not capture -- true argument capture for later arbitrary inspection outside a generated - `Verify().Member(Match...)` count assertion; +`Compono.TestDoubles` is not a general-purpose mocking framework, but for +the eligible member shapes above it now supports argument matching, +argument-filtered verification, **and** retrospective inspection of the +actual invocation via `ReceivedCalls()` (above) — the historical "matching +only, no capture" boundary is gone for that eligible set. What remains a +genuinely stronger, unsupported behavior: + +- `ReceivedCalls()` for an **overloaded** member — even one with its own + `Matching` argument-matching surface, ReceivedCalls() is not + available there (ADR-0060 deliberately did not expand eligibility past + ADR-0048's existing set for 1.1 — do not claim otherwise); - call-order verification; - strict mode, partial substitutes, recursive auto-configuration; - classes, delegates, indexers, events, and other unsupported shapes listed @@ -533,11 +587,15 @@ that needs access to the actual invocation as a first-class value: If a test only needs "this member was called once with an argument matching this predicate," use `Verify().Member(Match.Is(...)).Once()`. If it needs -to store every argument for arbitrary later inspection, invoke a delegate -argument, or verify call order, use an existing project-local fake or -`Compono.NSubstitute` where the project intentionally keeps that dependency. -Treat any real `Compono.NSubstitute`-can/`Compono.TestDoubles`-cannot case as -roadmap evidence under ADR-0042 Amendment 2. +the actual received arguments for an eligible member, use +`ReceivedCalls().Member()` (above) — do not reach for a hand-written +recording fake or `Compono.NSubstitute` for a scenario `ReceivedCalls()` +already covers. If it needs an overloaded member's arguments, invoking a +delegate argument mid-call, or call-order verification, use an existing +project-local fake or `Compono.NSubstitute` where the project intentionally +keeps that dependency. Treat any real `Compono.NSubstitute`-can/ +`Compono.TestDoubles`-cannot case as roadmap evidence under ADR-0042 +Amendment 2. ## Unsupported shapes are compile-time diagnostics, not silent gaps diff --git a/src/Compono.Generators/Diagnostics/DiagnosticDescriptors.cs b/src/Compono.Generators/Diagnostics/DiagnosticDescriptors.cs index c5330ba..b9b156a 100644 --- a/src/Compono.Generators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Compono.Generators/Diagnostics/DiagnosticDescriptors.cs @@ -164,7 +164,7 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor TestDoubleConfigureMemberCollision = new( "CMP0023", - "Test-double interface member collides with a generated Configure()/Verify() bridge", + "Test-double interface member collides with a generated Configure()/Verify()/ReceivedCalls()/ClearCalls() bridge", "'{0}' declares its own member named '{1}', which would silently shadow the generated " + "{1}() extension the double's configuration/verification surface depends on. This leaf falls " + "back to the ordinary runtime-provider path.", diff --git a/src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs b/src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs index 08aba09..33d1fcb 100644 --- a/src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs +++ b/src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs @@ -64,8 +64,15 @@ public static DiscoveredTestDoubleInfo Analyze(INamedTypeSymbol interfaceType, C // generic method. ADR-0044 Requirement 3 widens this reserved-name set to also cover "Verify", // reused by the new Verify() bridge - an interface declaring its own Verify member would // otherwise silently shadow it exactly like an undiagnosed Configure collision would have. + // PLAN-0063/ADR-0060 (Codex review, PR #134): "ClearCalls"/"ReceivedCalls" join the reserved + // set for the same reason - both are always-emitted, always-zero-argument bridge extensions + // (TestDouble.scriban's *_ClearCallsExtension/*_ReceivedCallsExtension are unconditional, not + // gated on the interface having any eligible member), and an interface member of either name + // applicable to a zero-argument call wins ordinary member lookup over the extension exactly + // like an undiagnosed Configure/Verify collision would - e.g. `repository.ClearCalls()` would + // silently invoke the interface's own member and never reach the generated bridge at all. var reservedNameCollision = closure.SelectMany(i => i.GetMembers()) - .Where(m => m.Name is "Configure" or "Verify") + .Where(m => m.Name is "Configure" or "Verify" or "ClearCalls" or "ReceivedCalls") .FirstOrDefault(m => m is not IMethodSymbol method || IsApplicableToZeroArguments(method)); if (reservedNameCollision is not null) { @@ -490,10 +497,23 @@ public static DiscoveredTestDoubleInfo Analyze(INamedTypeSymbol interfaceType, C // phantom collision, silently excluding it from argument matching entirely. Codex // review, PR #108 (round 1). Reserve only what this layout actually emits at this scope: // "_calls"/"_lock"/"_Entry"/"_entries". + // PLAN-0063/ADR-0060 (Codex review, PR #134): "_ReceivedCall" (the generated named + // snapshot-record type backing ReceivedCalls(), TestDoubleMemberInfo.ReceivedCallClassName) + // joins this reservation set for the identical reason as _calls/_lock/_Entry/_entries above + // - a sibling real member whose own natural FieldName happens to equal this derived name + // (e.g. a real member literally named "Foo_ReceivedCall" sitting alongside an eligible + // "Foo") would otherwise silently produce two identically-named declarations (a real + // CS0102/CS0111 duplicate-member compile error in the consumer), never caught by + // AssignCallbackNameSuffixes' later callback-only disambiguation pass, which renames + // colliding *callback* declarations but never this one. Feeding it into this same + // pre-pass means a genuine collision demotes the affected member out of matching + // eligibility (falling back to its plain configuration surface) exactly like any other + // derived-name collision here, rather than reaching the emitter at all. var derivedNames = new[] { $"__{candidateMethod.Name}_calls", $"__{candidateMethod.Name}_lock", $"__{candidateMethod.Name}_Entry", $"__{candidateMethod.Name}_entries", + $"__{candidateMethod.Name}_ReceivedCall", }; foreach (var name in derivedNames) @@ -1752,6 +1772,10 @@ private static List AssignCallbackNameSuffixes(List public string EntriesFieldName => $"{FieldName}_entries"; + /// + /// PLAN-0063/ADR-0060: the generated readonly record struct name backing this + /// member's ReceivedCalls() retrospective-inspection + /// surface - a named, per-parameter-typed snapshot of one entry from the existing + /// {FieldName}_calls list (ADR-0048), reusing 's own already-proven + /// uniqueness (the same guarantee relies on) rather than introducing + /// a new collision-detection pass. Reserved in TestDoubleAnalyzer's derived-name collision + /// pool alongside . Never populated for an overload-matching-eligible + /// member () - `ReceivedCalls()` is scoped to exactly + /// ADR-0048's non-overloaded eligible-member set for 1.1 (ADR-0060, "Considered Options — + /// eligibility scope"). + /// + public string ReceivedCallClassName => $"{FieldName}_ReceivedCall"; + + /// + /// PLAN-0063/ADR-0060 (Codex review, PR #134 round 2): the identifier each parameter's generated + /// positional record property uses, positionally aligned with + /// - normally just that parameter's own , + /// except for the one real parameter (at most one - the compiler already guarantees two real + /// parameters can never share a name) literally named the same as the record's own type, which + /// would otherwise produce a positional property sharing its enclosing type's name (CS0542). That + /// one parameter is renamed by appending "_Value", then "_Value2", "_Value3", ... until the + /// candidate is free of every OTHER real parameter's own name too - round 1's naive unconditional + /// "_Value" suffix collided with a second real parameter already literally named that + /// (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`), a real fixture-catchable bug + /// caught by Codex review round 2, not merely a hypothetical. + /// + /// + /// Rendered here as one ready-to-splice, comma-joined parameter-declaration string (not exposed + /// as a positionally-indexed list) - Scriban's default reflection-based object binding doesn't + /// support indexer access into a plain .NET array/list from template code + /// (member.some_list[for.index] resolves to , confirmed directly), + /// and this repo's other templates never rely on that either (only ever for.index as a + /// plain value, e.g. CompositionPlan.scriban's constructor-parameter descriptors). + /// + public string ReceivedCallRecordParametersText + { + get + { + var names = Parameters.Select(p => p.EscapedName).ToArray(); + var reserved = new HashSet(names, StringComparer.Ordinal); + + for (var i = 0; i < names.Length; i++) + { + if (names[i] != ReceivedCallClassName) + continue; + + var candidate = $"{names[i]}_Value"; + var disambiguator = 2; + while (reserved.Contains(candidate)) + candidate = $"{names[i]}_Value{disambiguator++}"; + + reserved.Add(candidate); + names[i] = candidate; + } + + return string.Join(", ", names.Select((name, i) => $"{Parameters[i].FullyQualifiedTypeName} {name}")); + } + } + /// The generated strongly typed invocation-callback delegate name (ADR-0053). public string CallbackDelegateName => $"{FieldName}{CallbackNameSuffix}_Callback"; diff --git a/src/Compono.Generators/Templates/TestDouble.scriban b/src/Compono.Generators/Templates/TestDouble.scriban index a5549ee..f4b35ca 100644 --- a/src/Compono.Generators/Templates/TestDouble.scriban +++ b/src/Compono.Generators/Templates/TestDouble.scriban @@ -1,6 +1,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// {{ safe_identifier }}_*State class below) is what makes that possible without reflection. +internal interface {{ safe_identifier }}_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "{{ generator_version }}")] internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualified_name }} { @@ -58,7 +67,7 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie {{~ end ~}} {{~ for member in members ~}} {{~ if member.is_closed_instantiation_eligible ~}} - internal sealed class {{ member.closed_instantiation_state_class_name }}<{{ member.closed_instantiation_type_parameter_name }}>{{ member.constraint_clauses_text }} + internal sealed class {{ member.closed_instantiation_state_class_name }}<{{ member.closed_instantiation_type_parameter_name }}> : {{ safe_identifier }}_IClearableCallState{{ member.constraint_clauses_text }} { {{~ if member.closed_instantiation_has_matched_parameters ~}} // ADR-0050: multi-entry response configuration composed inside ADR-0049's @@ -78,11 +87,33 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie internal readonly global::System.Collections.Generic.List Entries = []; internal readonly global::System.Collections.Generic.List<{{ member.call_log_type_text }}> Calls = []; internal readonly object Lock = new(); + + // PLAN-0063/ADR-0060: ClearCalls() clears this closed-T bucket entry's observation state + // (each registered Entry's own call count + the shared captured argument history) without + // touching any Entry's configured Value/Exception/Sequence/SequenceOrdinal state or removing + // any Entry from Entries (ADR-0050's multi-entry configuration is preserved, not reset). + // Reached through the non-generic {{ safe_identifier }}_IClearableCallState interface, since + // ClearCalls() itself has no static knowledge of which closed T's this member has been + // called with. + public void ClearObservedCalls() + { + lock (this.Lock) + { + foreach (var __entry in this.Entries) + __entry.Config.ClearObservedCalls(); + + this.Calls.Clear(); + } + } {{~ else ~}} internal global::Compono.ReturnConfig<{{ member.slot_type_fully_qualified_name }}> Config; {{~ if member.is_callback_eligible ~}} internal {{ member.callback_delegate_name }}{{ member.generic_suffix }}? Callback; {{~ end ~}} + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); {{~ end ~}} } @@ -127,6 +158,24 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie internal readonly global::System.Collections.Generic.List<{{ member.entry_class_name }}> {{ member.entries_field_name }} = []; internal readonly global::System.Collections.Generic.List<{{ member.call_log_type_text }}> {{ member.field_name }}_calls = []; internal readonly object {{ member.field_name }}_lock = new(); +{{~ if member.is_eligible_for_matching ~}} + + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal {{ member.field_name }}_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct {{ member.received_call_class_name }}({{ member.received_call_record_parameters_text }}); +{{~ end ~}} {{~ end ~}} {{~ end ~}} {{~ for member in members ~}} @@ -670,6 +719,85 @@ internal static class {{ safe_identifier }}_DoubleVerification {{~ end ~}} } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct {{ safe_identifier }}_DoubleReceivedCalls +{ + internal global::{{ safe_identifier }}_Double Instance { get; } + + internal {{ safe_identifier }}_DoubleReceivedCalls(global::{{ safe_identifier }}_Double instance) => Instance = instance; +} + +internal static class {{ safe_identifier }}_ReceivedCallsExtension +{ + public static global::{{ safe_identifier }}_DoubleReceivedCalls ReceivedCalls(this {{ interface_fully_qualified_name }} self) => + new(self as global::{{ safe_identifier }}_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the '{{ interface_fully_qualified_name }}' test double generated for this assembly. " + + "If another assembly in this process also generated a double for '{{ interface_fully_qualified_name }}', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class {{ safe_identifier }}_DoubleReceivedCallsAccess +{ +{{~ for member in members ~}} +{{~ if member.is_eligible_for_matching ~}} + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList {{ member.escaped_name }}(this global::{{ safe_identifier }}_DoubleReceivedCalls self) + { + lock (self.Instance.{{ member.field_name }}_lock) + { + var __snapshot = new global::{{ safe_identifier }}_Double.{{ member.received_call_class_name }}[self.Instance.{{ member.field_name }}_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var {{ member.call_loop_variable_name }} = self.Instance.{{ member.field_name }}_calls[__i]; + __snapshot[__i] = new({{ for p in member.parameters }}{{ p.call_log_access_expression }}{{ if !for.last }}, {{ end }}{{ end }}); + } + + return __snapshot; + } + } + +{{~ end ~}} +{{~ end ~}} +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class {{ safe_identifier }}_ClearCallsExtension +{ + public static void ClearCalls(this {{ interface_fully_qualified_name }} self) + { + var __double = self as global::{{ safe_identifier }}_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the '{{ interface_fully_qualified_name }}' test double generated for this assembly. " + + "If another assembly in this process also generated a double for '{{ interface_fully_qualified_name }}', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + +{{~ for member in members ~}} +{{~ if member.is_closed_instantiation_eligible ~}} + lock (__double.{{ member.closed_instantiation_bucket_field_name }}) + { + foreach (var __bucketEntry in __double.{{ member.closed_instantiation_bucket_field_name }}.Values) + (({{ safe_identifier }}_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } +{{~ else if member.is_eligible_for_matching || member.is_overload_matching_eligible ~}} + lock (__double.{{ member.field_name }}_lock) { __double.{{ member.field_name }}_calls.Clear(); } +{{~ else if member.has_configuration_surface ~}} + __double.{{ member.field_name }}.ClearObservedCalls(); +{{~ end ~}} +{{~ end ~}} + } +} + internal static class {{ safe_identifier }}_ConfigureExtension { public static global::{{ safe_identifier }}_Double Configure(this {{ interface_fully_qualified_name }} self) => diff --git a/src/Compono.Logging/LogVerificationBuilder.cs b/src/Compono.Logging/LogVerificationBuilder.cs index b772913..1bd14de 100644 --- a/src/Compono.Logging/LogVerificationBuilder.cs +++ b/src/Compono.Logging/LogVerificationBuilder.cs @@ -57,6 +57,16 @@ public LogVerificationBuilder Matching(Func predicate) = /// times. public void Exactly(int times) => ToCallVerifier().Exactly(times); + /// Asserts the accumulated filters matched at least times. + /// The filters matched fewer than + /// times. + public void AtLeast(int times) => ToCallVerifier().AtLeast(times); + + /// Asserts the accumulated filters matched at most times. + /// The filters matched more than + /// times. + public void AtMost(int times) => ToCallVerifier().AtMost(times); + private LogVerificationBuilder Add(string description, Func predicate) { _filters.Add((description, predicate)); diff --git a/src/Compono/CallVerifier.cs b/src/Compono/CallVerifier.cs index b69e602..1779501 100644 --- a/src/Compono/CallVerifier.cs +++ b/src/Compono/CallVerifier.cs @@ -31,4 +31,30 @@ public void Exactly(int times) $"Expected exactly {times} call(s) to {memberDescription}, but received {observedCount}."); } } + + /// Asserts the member was called at least times. + /// + /// The member was called fewer than times. + /// + public void AtLeast(int times) + { + if (observedCount < times) + { + throw new TestDoubleVerificationException( + $"Expected at least {times} call(s) to {memberDescription}, but received {observedCount}."); + } + } + + /// Asserts the member was called at most times. + /// + /// The member was called more than times. + /// + public void AtMost(int times) + { + if (observedCount > times) + { + throw new TestDoubleVerificationException( + $"Expected at most {times} call(s) to {memberDescription}, but received {observedCount}."); + } + } } diff --git a/src/Compono/ReturnConfig.cs b/src/Compono/ReturnConfig.cs index 61d818f..4a38c4a 100644 --- a/src/Compono/ReturnConfig.cs +++ b/src/Compono/ReturnConfig.cs @@ -82,6 +82,16 @@ public void ClearConfiguredResponse() SequenceOrdinal = 0; } + /// + /// Clears the recorded call count without changing the configured value, exception, or sequence - + /// the mirror of . Backs ClearCalls() (PLAN-0063/ + /// ADR-0060): observation history is reset, configured behavior (including in-progress + /// progress) is untouched, so a subsequent call resumes the + /// sequence rather than rewinding it. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public void ClearObservedCalls() => System.Threading.Interlocked.Exchange(ref CallCount, 0); + /// /// Consumes and returns (or throws) the next outcome in the configured sequence, by invocation /// ordinal - the first call gets index 0, the second index 1, and so on. Only meaningful when diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimDeclaringInterfaceHasUnresolvedStaticAbstractMember_ReportsCmp0036_ConsumerCompiles#TestNamespace.ILeaf7_9555d5b1.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimDeclaringInterfaceHasUnresolvedStaticAbstractMember_ReportsCmp0036_ConsumerCompiles#TestNamespace.ILeaf7_9555d5b1.TestDouble.g.verified.cs index 1b8e925..281b47d 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimDeclaringInterfaceHasUnresolvedStaticAbstractMember_ReportsCmp0036_ConsumerCompiles#TestNamespace.ILeaf7_9555d5b1.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimDeclaringInterfaceHasUnresolvedStaticAbstractMember_ReportsCmp0036_ConsumerCompiles#TestNamespace.ILeaf7_9555d5b1.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ILeaf7_9555d5b1_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ILeaf7_9555d5b1_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ILeaf7_9555d5b1_Double : global::TestNamespace.ILeaf7 { @@ -57,6 +66,22 @@ internal sealed class __CanHandle_Entry internal readonly global::System.Collections.Generic.List __CanHandle_calls = []; internal readonly object __CanHandle_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __CanHandle_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __CanHandle_ReceivedCall(string input); + bool global::TestNamespace.IBase7.CanHandle(string input) { __CanHandle_Callback? __callback = null; @@ -176,6 +201,69 @@ internal static class TestNamespace_ILeaf7_9555d5b1_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ILeaf7_9555d5b1_DoubleReceivedCalls +{ + internal global::TestNamespace_ILeaf7_9555d5b1_Double Instance { get; } + + internal TestNamespace_ILeaf7_9555d5b1_DoubleReceivedCalls(global::TestNamespace_ILeaf7_9555d5b1_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ILeaf7_9555d5b1_ReceivedCallsExtension +{ + public static global::TestNamespace_ILeaf7_9555d5b1_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ILeaf7 self) => + new(self as global::TestNamespace_ILeaf7_9555d5b1_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf7' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf7', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ILeaf7_9555d5b1_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList CanHandle(this global::TestNamespace_ILeaf7_9555d5b1_DoubleReceivedCalls self) + { + lock (self.Instance.__CanHandle_lock) + { + var __snapshot = new global::TestNamespace_ILeaf7_9555d5b1_Double.__CanHandle_ReceivedCall[self.Instance.__CanHandle_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__CanHandle_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ILeaf7_9555d5b1_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ILeaf7 self) + { + var __double = self as global::TestNamespace_ILeaf7_9555d5b1_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf7' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf7', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__CanHandle_lock) { __double.__CanHandle_calls.Clear(); } + } +} + internal static class TestNamespace_ILeaf7_9555d5b1_ConfigureExtension { public static global::TestNamespace_ILeaf7_9555d5b1_Double Configure(this global::TestNamespace.ILeaf7 self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimHelperFieldNameCollidesWithRealMember_ReportsCmp0035_ConsumerCompiles#TestNamespace.ICollision_e0157ffc.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimHelperFieldNameCollidesWithRealMember_ReportsCmp0035_ConsumerCompiles#TestNamespace.ICollision_e0157ffc.TestDouble.g.verified.cs index 3b40927..18dfbc6 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimHelperFieldNameCollidesWithRealMember_ReportsCmp0035_ConsumerCompiles#TestNamespace.ICollision_e0157ffc.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimHelperFieldNameCollidesWithRealMember_ReportsCmp0035_ConsumerCompiles#TestNamespace.ICollision_e0157ffc.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ICollision_e0157ffc_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ICollision_e0157ffc_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ICollision_e0157ffc_Double : global::TestNamespace.ICollision { @@ -105,6 +114,53 @@ internal static class TestNamespace_ICollision_e0157ffc_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ICollision_e0157ffc_DoubleReceivedCalls +{ + internal global::TestNamespace_ICollision_e0157ffc_Double Instance { get; } + + internal TestNamespace_ICollision_e0157ffc_DoubleReceivedCalls(global::TestNamespace_ICollision_e0157ffc_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ICollision_e0157ffc_ReceivedCallsExtension +{ + public static global::TestNamespace_ICollision_e0157ffc_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ICollision self) => + new(self as global::TestNamespace_ICollision_e0157ffc_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ICollision' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ICollision', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ICollision_e0157ffc_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ICollision_e0157ffc_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ICollision self) + { + var __double = self as global::TestNamespace_ICollision_e0157ffc_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ICollision' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ICollision', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Foo.ClearObservedCalls(); + __double.__Foo_dimHelper.ClearObservedCalls(); + } +} + internal static class TestNamespace_ICollision_e0157ffc_ConfigureExtension { public static global::TestNamespace_ICollision_e0157ffc_Double Configure(this global::TestNamespace.ICollision self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimOwnInterfaceDeclaresUnresolvedStaticAbstractMember_ReportsCmp0036_ConsumerCompiles#TestNamespace.ILeaf9_8f55cc3f.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimOwnInterfaceDeclaresUnresolvedStaticAbstractMember_ReportsCmp0036_ConsumerCompiles#TestNamespace.ILeaf9_8f55cc3f.TestDouble.g.verified.cs index 63a080a..2a8e1ee 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimOwnInterfaceDeclaresUnresolvedStaticAbstractMember_ReportsCmp0036_ConsumerCompiles#TestNamespace.ILeaf9_8f55cc3f.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.DimOwnInterfaceDeclaresUnresolvedStaticAbstractMember_ReportsCmp0036_ConsumerCompiles#TestNamespace.ILeaf9_8f55cc3f.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ILeaf9_8f55cc3f_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ILeaf9_8f55cc3f_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ILeaf9_8f55cc3f_Double : global::TestNamespace.ILeaf9 { @@ -57,6 +66,22 @@ internal sealed class __CanHandle_Entry internal readonly global::System.Collections.Generic.List __CanHandle_calls = []; internal readonly object __CanHandle_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __CanHandle_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __CanHandle_ReceivedCall(string input); + bool global::TestNamespace.IBase9.CanHandle(string input) { __CanHandle_Callback? __callback = null; @@ -176,6 +201,69 @@ internal static class TestNamespace_ILeaf9_8f55cc3f_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ILeaf9_8f55cc3f_DoubleReceivedCalls +{ + internal global::TestNamespace_ILeaf9_8f55cc3f_Double Instance { get; } + + internal TestNamespace_ILeaf9_8f55cc3f_DoubleReceivedCalls(global::TestNamespace_ILeaf9_8f55cc3f_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ILeaf9_8f55cc3f_ReceivedCallsExtension +{ + public static global::TestNamespace_ILeaf9_8f55cc3f_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ILeaf9 self) => + new(self as global::TestNamespace_ILeaf9_8f55cc3f_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf9' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf9', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ILeaf9_8f55cc3f_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList CanHandle(this global::TestNamespace_ILeaf9_8f55cc3f_DoubleReceivedCalls self) + { + lock (self.Instance.__CanHandle_lock) + { + var __snapshot = new global::TestNamespace_ILeaf9_8f55cc3f_Double.__CanHandle_ReceivedCall[self.Instance.__CanHandle_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__CanHandle_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ILeaf9_8f55cc3f_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ILeaf9 self) + { + var __double = self as global::TestNamespace_ILeaf9_8f55cc3f_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf9' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf9', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__CanHandle_lock) { __double.__CanHandle_calls.Clear(); } + } +} + internal static class TestNamespace_ILeaf9_8f55cc3f_ConfigureExtension { public static global::TestNamespace_ILeaf9_8f55cc3f_Double Configure(this global::TestNamespace.ILeaf9 self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfaceMethodReimplementationOfConcreteDim_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf12_2124707f.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfaceMethodReimplementationOfConcreteDim_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf12_2124707f.TestDouble.g.verified.cs index f28f09a..b8273d9 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfaceMethodReimplementationOfConcreteDim_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf12_2124707f.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfaceMethodReimplementationOfConcreteDim_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf12_2124707f.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ILeaf12_2124707f_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ILeaf12_2124707f_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ILeaf12_2124707f_Double : global::TestNamespace.ILeaf12 { @@ -109,6 +118,52 @@ internal static class TestNamespace_ILeaf12_2124707f_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ILeaf12_2124707f_DoubleReceivedCalls +{ + internal global::TestNamespace_ILeaf12_2124707f_Double Instance { get; } + + internal TestNamespace_ILeaf12_2124707f_DoubleReceivedCalls(global::TestNamespace_ILeaf12_2124707f_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ILeaf12_2124707f_ReceivedCallsExtension +{ + public static global::TestNamespace_ILeaf12_2124707f_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ILeaf12 self) => + new(self as global::TestNamespace_ILeaf12_2124707f_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf12' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf12', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ILeaf12_2124707f_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ILeaf12_2124707f_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ILeaf12 self) + { + var __double = self as global::TestNamespace_ILeaf12_2124707f_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf12' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf12', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Flag.ClearObservedCalls(); + } +} + internal static class TestNamespace_ILeaf12_2124707f_ConfigureExtension { public static global::TestNamespace_ILeaf12_2124707f_Double Configure(this global::TestNamespace.ILeaf12 self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfaceMethodReimplementation_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf10_232473a5.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfaceMethodReimplementation_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf10_232473a5.TestDouble.g.verified.cs index de31eb6..2acd0ad 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfaceMethodReimplementation_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf10_232473a5.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfaceMethodReimplementation_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf10_232473a5.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ILeaf10_232473a5_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ILeaf10_232473a5_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ILeaf10_232473a5_Double : global::TestNamespace.ILeaf10 { @@ -89,6 +98,52 @@ internal static class TestNamespace_ILeaf10_232473a5_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ILeaf10_232473a5_DoubleReceivedCalls +{ + internal global::TestNamespace_ILeaf10_232473a5_Double Instance { get; } + + internal TestNamespace_ILeaf10_232473a5_DoubleReceivedCalls(global::TestNamespace_ILeaf10_232473a5_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ILeaf10_232473a5_ReceivedCallsExtension +{ + public static global::TestNamespace_ILeaf10_232473a5_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ILeaf10 self) => + new(self as global::TestNamespace_ILeaf10_232473a5_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf10' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf10', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ILeaf10_232473a5_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ILeaf10_232473a5_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ILeaf10 self) + { + var __double = self as global::TestNamespace_ILeaf10_232473a5_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf10' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf10', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Flag.ClearObservedCalls(); + } +} + internal static class TestNamespace_ILeaf10_232473a5_ConfigureExtension { public static global::TestNamespace_ILeaf10_232473a5_Double Configure(this global::TestNamespace.ILeaf10 self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfacePropertyReimplementationOfConcreteDim_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf13_20246eec.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfacePropertyReimplementationOfConcreteDim_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf13_20246eec.TestDouble.g.verified.cs index 0310c68..8cc8afe 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfacePropertyReimplementationOfConcreteDim_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf13_20246eec.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfacePropertyReimplementationOfConcreteDim_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf13_20246eec.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ILeaf13_20246eec_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ILeaf13_20246eec_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ILeaf13_20246eec_Double : global::TestNamespace.ILeaf13 { @@ -72,6 +81,52 @@ internal static class TestNamespace_ILeaf13_20246eec_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ILeaf13_20246eec_DoubleReceivedCalls +{ + internal global::TestNamespace_ILeaf13_20246eec_Double Instance { get; } + + internal TestNamespace_ILeaf13_20246eec_DoubleReceivedCalls(global::TestNamespace_ILeaf13_20246eec_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ILeaf13_20246eec_ReceivedCallsExtension +{ + public static global::TestNamespace_ILeaf13_20246eec_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ILeaf13 self) => + new(self as global::TestNamespace_ILeaf13_20246eec_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf13' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf13', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ILeaf13_20246eec_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ILeaf13_20246eec_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ILeaf13 self) + { + var __double = self as global::TestNamespace_ILeaf13_20246eec_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf13' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf13', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Flag.ClearObservedCalls(); + } +} + internal static class TestNamespace_ILeaf13_20246eec_ConfigureExtension { public static global::TestNamespace_ILeaf13_20246eec_Double Configure(this global::TestNamespace.ILeaf13 self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfacePropertyReimplementation_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf11_22247212.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfacePropertyReimplementation_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf11_22247212.TestDouble.g.verified.cs index be14e83..fffe6a4 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfacePropertyReimplementation_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf11_22247212.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.ExplicitInterfacePropertyReimplementation_ReportsCmp0037_ConsumerCompiles#TestNamespace.ILeaf11_22247212.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ILeaf11_22247212_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ILeaf11_22247212_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ILeaf11_22247212_Double : global::TestNamespace.ILeaf11 { @@ -52,6 +61,52 @@ internal static class TestNamespace_ILeaf11_22247212_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ILeaf11_22247212_DoubleReceivedCalls +{ + internal global::TestNamespace_ILeaf11_22247212_Double Instance { get; } + + internal TestNamespace_ILeaf11_22247212_DoubleReceivedCalls(global::TestNamespace_ILeaf11_22247212_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ILeaf11_22247212_ReceivedCallsExtension +{ + public static global::TestNamespace_ILeaf11_22247212_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ILeaf11 self) => + new(self as global::TestNamespace_ILeaf11_22247212_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf11' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf11', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ILeaf11_22247212_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ILeaf11_22247212_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ILeaf11 self) + { + var __double = self as global::TestNamespace_ILeaf11_22247212_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf11' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf11', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Flag.ClearObservedCalls(); + } +} + internal static class TestNamespace_ILeaf11_22247212_ConfigureExtension { public static global::TestNamespace_ILeaf11_22247212_Double Configure(this global::TestNamespace.ILeaf11 self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.NoSurfacePropertyDim_RoutesThroughDimHelper_ReportsCmp0029#TestNamespace.ILeaf_1497661c.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.NoSurfacePropertyDim_RoutesThroughDimHelper_ReportsCmp0029#TestNamespace.ILeaf_1497661c.TestDouble.g.verified.cs index af254bf..1dcb47c 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.NoSurfacePropertyDim_RoutesThroughDimHelper_ReportsCmp0029#TestNamespace.ILeaf_1497661c.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.NoSurfacePropertyDim_RoutesThroughDimHelper_ReportsCmp0029#TestNamespace.ILeaf_1497661c.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ILeaf_1497661c_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ILeaf_1497661c_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ILeaf_1497661c_Double : global::TestNamespace.ILeaf { @@ -63,6 +72,51 @@ internal static class TestNamespace_ILeaf_1497661c_DoubleVerification { } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ILeaf_1497661c_DoubleReceivedCalls +{ + internal global::TestNamespace_ILeaf_1497661c_Double Instance { get; } + + internal TestNamespace_ILeaf_1497661c_DoubleReceivedCalls(global::TestNamespace_ILeaf_1497661c_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ILeaf_1497661c_ReceivedCallsExtension +{ + public static global::TestNamespace_ILeaf_1497661c_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ILeaf self) => + new(self as global::TestNamespace_ILeaf_1497661c_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ILeaf_1497661c_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ILeaf_1497661c_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ILeaf self) + { + var __double = self as global::TestNamespace_ILeaf_1497661c_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILeaf' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILeaf', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + } +} + internal static class TestNamespace_ILeaf_1497661c_ConfigureExtension { public static global::TestNamespace_ILeaf_1497661c_Double Configure(this global::TestNamespace.ILeaf self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefReadOnlyDimTarget_CallSiteRestatesIn_NoCs9192Warning#TestNamespace.IBase8_4f46e68b.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefReadOnlyDimTarget_CallSiteRestatesIn_NoCs9192Warning#TestNamespace.IBase8_4f46e68b.TestDouble.g.verified.cs index 64e7db9..fe9cff1 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefReadOnlyDimTarget_CallSiteRestatesIn_NoCs9192Warning#TestNamespace.IBase8_4f46e68b.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefReadOnlyDimTarget_CallSiteRestatesIn_NoCs9192Warning#TestNamespace.IBase8_4f46e68b.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IBase8_4f46e68b_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IBase8_4f46e68b_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IBase8_4f46e68b_Double : global::TestNamespace.IBase8 { @@ -63,6 +72,22 @@ internal sealed class __Visit_Entry internal readonly global::System.Collections.Generic.List<__Visit_Entry> __Visit_entries = []; internal readonly global::System.Collections.Generic.List __Visit_calls = []; internal readonly object __Visit_lock = new(); + + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Visit_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Visit_ReceivedCall(string label); // ADR-0044 Amendment 20: owner-forwarding dispatch helper - holds the real generated double, // implements global::TestNamespace.IBase8 but deliberately does NOT // override Flag, so calling Flag through this helper's @@ -217,6 +242,70 @@ internal static class TestNamespace_IBase8_4f46e68b_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IBase8_4f46e68b_DoubleReceivedCalls +{ + internal global::TestNamespace_IBase8_4f46e68b_Double Instance { get; } + + internal TestNamespace_IBase8_4f46e68b_DoubleReceivedCalls(global::TestNamespace_IBase8_4f46e68b_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IBase8_4f46e68b_ReceivedCallsExtension +{ + public static global::TestNamespace_IBase8_4f46e68b_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IBase8 self) => + new(self as global::TestNamespace_IBase8_4f46e68b_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IBase8' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IBase8', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IBase8_4f46e68b_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Visit(this global::TestNamespace_IBase8_4f46e68b_DoubleReceivedCalls self) + { + lock (self.Instance.__Visit_lock) + { + var __snapshot = new global::TestNamespace_IBase8_4f46e68b_Double.__Visit_ReceivedCall[self.Instance.__Visit_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Visit_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IBase8_4f46e68b_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IBase8 self) + { + var __double = self as global::TestNamespace_IBase8_4f46e68b_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IBase8' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IBase8', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Flag.ClearObservedCalls(); + lock (__double.__Visit_lock) { __double.__Visit_calls.Clear(); } + } +} + internal static class TestNamespace_IBase8_4f46e68b_ConfigureExtension { public static global::TestNamespace_IBase8_4f46e68b_Double Configure(this global::TestNamespace.IBase8 self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefReadOnlySiblingParameter_PreservesModifier_ConsumerCompiles#TestNamespace.IBase5_4a46deac.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefReadOnlySiblingParameter_PreservesModifier_ConsumerCompiles#TestNamespace.IBase5_4a46deac.TestDouble.g.verified.cs index 3701d77..b4be0ad 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefReadOnlySiblingParameter_PreservesModifier_ConsumerCompiles#TestNamespace.IBase5_4a46deac.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefReadOnlySiblingParameter_PreservesModifier_ConsumerCompiles#TestNamespace.IBase5_4a46deac.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IBase5_4a46deac_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IBase5_4a46deac_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IBase5_4a46deac_Double : global::TestNamespace.IBase5 { @@ -63,6 +72,22 @@ internal sealed class __Visit_Entry internal readonly global::System.Collections.Generic.List<__Visit_Entry> __Visit_entries = []; internal readonly global::System.Collections.Generic.List __Visit_calls = []; internal readonly object __Visit_lock = new(); + + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Visit_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Visit_ReceivedCall(string label); // ADR-0044 Amendment 20: owner-forwarding dispatch helper - holds the real generated double, // implements global::TestNamespace.IBase5 but deliberately does NOT // override Flag, so calling Flag through this helper's @@ -217,6 +242,70 @@ internal static class TestNamespace_IBase5_4a46deac_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IBase5_4a46deac_DoubleReceivedCalls +{ + internal global::TestNamespace_IBase5_4a46deac_Double Instance { get; } + + internal TestNamespace_IBase5_4a46deac_DoubleReceivedCalls(global::TestNamespace_IBase5_4a46deac_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IBase5_4a46deac_ReceivedCallsExtension +{ + public static global::TestNamespace_IBase5_4a46deac_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IBase5 self) => + new(self as global::TestNamespace_IBase5_4a46deac_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IBase5' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IBase5', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IBase5_4a46deac_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Visit(this global::TestNamespace_IBase5_4a46deac_DoubleReceivedCalls self) + { + lock (self.Instance.__Visit_lock) + { + var __snapshot = new global::TestNamespace_IBase5_4a46deac_Double.__Visit_ReceivedCall[self.Instance.__Visit_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Visit_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IBase5_4a46deac_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IBase5 self) + { + var __double = self as global::TestNamespace_IBase5_4a46deac_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IBase5' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IBase5', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Flag.ClearObservedCalls(); + lock (__double.__Visit_lock) { __double.__Visit_calls.Clear(); } + } +} + internal static class TestNamespace_IBase5_4a46deac_ConfigureExtension { public static global::TestNamespace_IBase5_4a46deac_Double Configure(this global::TestNamespace.IBase5 self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefSiblingParameter_RestatesCallSiteModifier_ConsumerCompiles#TestNamespace.IBase6_4d46e365.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefSiblingParameter_RestatesCallSiteModifier_ConsumerCompiles#TestNamespace.IBase6_4d46e365.TestDouble.g.verified.cs index 1b7cc2a..b1030de 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefSiblingParameter_RestatesCallSiteModifier_ConsumerCompiles#TestNamespace.IBase6_4d46e365.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.RefSiblingParameter_RestatesCallSiteModifier_ConsumerCompiles#TestNamespace.IBase6_4d46e365.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IBase6_4d46e365_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IBase6_4d46e365_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IBase6_4d46e365_Double : global::TestNamespace.IBase6 { @@ -63,6 +72,22 @@ internal sealed class __Visit_Entry internal readonly global::System.Collections.Generic.List<__Visit_Entry> __Visit_entries = []; internal readonly global::System.Collections.Generic.List __Visit_calls = []; internal readonly object __Visit_lock = new(); + + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Visit_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Visit_ReceivedCall(string label); // ADR-0044 Amendment 20: owner-forwarding dispatch helper - holds the real generated double, // implements global::TestNamespace.IBase6 but deliberately does NOT // override Flag, so calling Flag through this helper's @@ -217,6 +242,70 @@ internal static class TestNamespace_IBase6_4d46e365_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IBase6_4d46e365_DoubleReceivedCalls +{ + internal global::TestNamespace_IBase6_4d46e365_Double Instance { get; } + + internal TestNamespace_IBase6_4d46e365_DoubleReceivedCalls(global::TestNamespace_IBase6_4d46e365_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IBase6_4d46e365_ReceivedCallsExtension +{ + public static global::TestNamespace_IBase6_4d46e365_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IBase6 self) => + new(self as global::TestNamespace_IBase6_4d46e365_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IBase6' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IBase6', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IBase6_4d46e365_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Visit(this global::TestNamespace_IBase6_4d46e365_DoubleReceivedCalls self) + { + lock (self.Instance.__Visit_lock) + { + var __snapshot = new global::TestNamespace_IBase6_4d46e365_Double.__Visit_ReceivedCall[self.Instance.__Visit_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Visit_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IBase6_4d46e365_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IBase6 self) + { + var __double = self as global::TestNamespace_IBase6_4d46e365_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IBase6' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IBase6', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Flag.ClearObservedCalls(); + lock (__double.__Visit_lock) { __double.__Visit_calls.Clear(); } + } +} + internal static class TestNamespace_IBase6_4d46e365_ConfigureExtension { public static global::TestNamespace_IBase6_4d46e365_Double Configure(this global::TestNamespace.IBase6 self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.TwoDimFallbackTargetsShareUndisambiguatedName_ReportsCmp0035_ConsumerCompiles#TestNamespace.ICollisionM_72d803a3.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.TwoDimFallbackTargetsShareUndisambiguatedName_ReportsCmp0035_ConsumerCompiles#TestNamespace.ICollisionM_72d803a3.TestDouble.g.verified.cs index 1ad1c35..5e3714e 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.TwoDimFallbackTargetsShareUndisambiguatedName_ReportsCmp0035_ConsumerCompiles#TestNamespace.ICollisionM_72d803a3.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleDefaultInterfaceMemberFallbackTests.TwoDimFallbackTargetsShareUndisambiguatedName_ReportsCmp0035_ConsumerCompiles#TestNamespace.ICollisionM_72d803a3.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ICollisionM_72d803a3_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ICollisionM_72d803a3_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ICollisionM_72d803a3_Double : global::TestNamespace.ICollisionM { @@ -94,6 +103,52 @@ internal static class TestNamespace_ICollisionM_72d803a3_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ICollisionM_72d803a3_DoubleReceivedCalls +{ + internal global::TestNamespace_ICollisionM_72d803a3_Double Instance { get; } + + internal TestNamespace_ICollisionM_72d803a3_DoubleReceivedCalls(global::TestNamespace_ICollisionM_72d803a3_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ICollisionM_72d803a3_ReceivedCallsExtension +{ + public static global::TestNamespace_ICollisionM_72d803a3_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ICollisionM self) => + new(self as global::TestNamespace_ICollisionM_72d803a3_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ICollisionM' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ICollisionM', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ICollisionM_72d803a3_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ICollisionM_72d803a3_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ICollisionM self) + { + var __double = self as global::TestNamespace_ICollisionM_72d803a3_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ICollisionM' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ICollisionM', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__M.ClearObservedCalls(); + } +} + internal static class TestNamespace_ICollisionM_72d803a3_ConfigureExtension { public static global::TestNamespace_ICollisionM_72d803a3_Double Configure(this global::TestNamespace.ICollisionM self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.CallbackGeneratedNames_CollidingWithSiblingBackingFields_FallBackToHashSuffixedNames#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.CallbackGeneratedNames_CollidingWithSiblingBackingFields_FallBackToHashSuffixedNames#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index bfff70f..596586f 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.CallbackGeneratedNames_CollidingWithSiblingBackingFields_FallBackToHashSuffixedNames#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.CallbackGeneratedNames_CollidingWithSiblingBackingFields_FallBackToHashSuffixedNames#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -1,7 +1,16 @@ -//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -137,6 +146,55 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Foo.ClearObservedCalls(); + __double.__Foo_Callback.ClearObservedCalls(); + __double.__Foo_Builder.ClearObservedCalls(); + __double.__Foo_callback.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.CallbackPatternLocals_CollidingWithMemberParameters_AreDisambiguated#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.CallbackPatternLocals_CollidingWithMemberParameters_AreDisambiguated#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 6928030..a2165d2 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.CallbackPatternLocals_CollidingWithMemberParameters_AreDisambiguated#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.CallbackPatternLocals_CollidingWithMemberParameters_AreDisambiguated#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -58,6 +67,22 @@ internal sealed class __Add_Entry internal readonly global::System.Collections.Generic.List<(int, int)> __Add_calls = []; internal readonly object __Add_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Add_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Add_ReceivedCall(int callback, int configuredCallback); + int global::TestNamespace.IRepository.Add(int callback, int configuredCallback) { __Add_Callback? __callback = null; @@ -178,6 +203,69 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Add(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__Add_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__Add_ReceivedCall[self.Instance.__Add_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Add_calls[__i]; + __snapshot[__i] = new(call.Item1, call.Item2); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Add_lock) { __double.__Add_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClearCallsNamedMember_ReportsCollisionDiagnostic#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClearCallsNamedMember_ReportsCollisionDiagnostic#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 0000000..be9605b --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClearCallsNamedMember_ReportsCollisionDiagnostic#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.cs +// +#nullable enable + +namespace TestNamespace +{ + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] + file sealed class OrderServiceCompositionPlan : global::Compono.ICompositionPlan + { + public global::TestNamespace.OrderService Compose(global::Compono.ICompositionContext context) => + new global::TestNamespace.OrderService( + context.Resolve(new global::Compono.CompositionRequestDescriptor(global::Compono.CompositionRequestKind.ConstructorParameter, 0, "repository", typeof(global::TestNamespace.OrderService), global::Compono.Nullability.NotNullable)) + ); + } + + file static class OrderServiceCompositionPlanRegistration + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() => + global::Compono.PlanCache.Instance = new OrderServiceCompositionPlan(); + } +} diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClearCallsNamedMember_ReportsCollisionDiagnostic.verified.txt b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClearCallsNamedMember_ReportsCollisionDiagnostic.verified.txt new file mode 100644 index 0000000..b23d8db --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClearCallsNamedMember_ReportsCollisionDiagnostic.verified.txt @@ -0,0 +1,18 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (14,65)-(14,91), + Message: 'TestNamespace.IRepository' declares its own member named 'ClearCalls', which would silently shadow the generated ClearCalls() extension the double's configuration/verification surface depends on. This leaf falls back to the ordinary runtime-provider path., + Severity: Info, + WarningLevel: 1, + Descriptor: { + Id: CMP0023, + Title: Test-double interface member collides with a generated Configure()/Verify()/ReceivedCalls()/ClearCalls() bridge, + MessageFormat: '{0}' declares its own member named '{1}', which would silently shadow the generated {1}() extension the double's configuration/verification surface depends on. This leaf falls back to the ordinary runtime-provider path., + Category: Compono.TestDoubles, + DefaultSeverity: Info, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberNamedLikeObjectToString_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberNamedLikeObjectToString_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index 22a8841..a9b37b8 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberNamedLikeObjectToString_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberNamedLikeObjectToString_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,10 +52,14 @@ public void ReturnsCallback(__ToString_Callback callback) _callback = callback; } } - internal sealed class __ToString_State where T : class + internal sealed class __ToString_State : TestNamespace_IFactory_993557a2_IClearableCallState where T : class { internal global::Compono.ReturnConfig Config; internal __ToString_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __ToString_buckets = new(); @@ -113,6 +126,56 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__ToString_buckets) + { + foreach (var __bucketEntry in __double.__ToString_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberReturningNullableValueTask_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberReturningNullableValueTask_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index e9b87e5..9282bb3 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberReturningNullableValueTask_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberReturningNullableValueTask_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,10 +52,14 @@ public void ReturnsCallback(__Get_Callback callback) _callback = callback; } } - internal sealed class __Get_State where T : class + internal sealed class __Get_State : TestNamespace_IFactory_993557a2_IClearableCallState where T : class { internal global::Compono.ReturnConfig> Config; internal __Get_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __Get_buckets = new(); @@ -114,6 +127,56 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_buckets) + { + foreach (var __bucketEntry in __double.__Get_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithDiamondCollidingPhantomNameReservation_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithDiamondCollidingPhantomNameReservation_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index b2bc63f..0763ac2 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithDiamondCollidingPhantomNameReservation_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithDiamondCollidingPhantomNameReservation_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,10 +52,14 @@ public void ReturnsCallback(__Get_Callback callback) _callback = callback; } } - internal sealed class __Get_State + internal sealed class __Get_State : TestNamespace_IFactory_993557a2_IClearableCallState { internal global::Compono.ReturnConfig Config; internal __Get_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __Get_buckets = new(); @@ -123,6 +136,56 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_buckets) + { + foreach (var __bucketEntry in __double.__Get_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithDirectNullableReturn_GeneratesDoubleWithNoNullableWarnings#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithDirectNullableReturn_GeneratesDoubleWithNoNullableWarnings#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index a8d0e7a..792abf4 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithDirectNullableReturn_GeneratesDoubleWithNoNullableWarnings#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithDirectNullableReturn_GeneratesDoubleWithNoNullableWarnings#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,10 +52,14 @@ public void ReturnsCallback(__Get_Callback callback) _callback = callback; } } - internal sealed class __Get_State where T : class + internal sealed class __Get_State : TestNamespace_IFactory_993557a2_IClearableCallState where T : class { internal global::Compono.ReturnConfig Config; internal __Get_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __Get_buckets = new(); @@ -114,6 +127,56 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_buckets) + { + foreach (var __bucketEntry in __double.__Get_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithLiterallyCollidingSiblingName_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithLiterallyCollidingSiblingName_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index 266d582..f4a07f0 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithLiterallyCollidingSiblingName_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithLiterallyCollidingSiblingName_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,7 +52,7 @@ public void ReturnsCallback(__Get_Callback callback) _callback = callback; } } - internal sealed class __Get_State where T : class + internal sealed class __Get_State : TestNamespace_IFactory_993557a2_IClearableCallState where T : class { // ADR-0050: multi-entry response configuration composed inside ADR-0049's // per-closed-T state - same Entry shape as the plain matching-eligible branch below, just @@ -58,6 +67,24 @@ internal sealed class Entry internal readonly global::System.Collections.Generic.List Entries = []; internal readonly global::System.Collections.Generic.List Calls = []; internal readonly object Lock = new(); + + // PLAN-0063/ADR-0060: ClearCalls() clears this closed-T bucket entry's observation state + // (each registered Entry's own call count + the shared captured argument history) without + // touching any Entry's configured Value/Exception/Sequence/SequenceOrdinal state or removing + // any Entry from Entries (ADR-0050's multi-entry configuration is preserved, not reset). + // Reached through the non-generic TestNamespace_IFactory_993557a2_IClearableCallState interface, since + // ClearCalls() itself has no static knowledge of which closed T's this member has been + // called with. + public void ClearObservedCalls() + { + lock (this.Lock) + { + foreach (var __entry in this.Entries) + __entry.Config.ClearObservedCalls(); + + this.Calls.Clear(); + } + } } internal readonly global::System.Collections.Generic.Dictionary __Get_buckets = new(); @@ -199,6 +226,57 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_buckets) + { + foreach (var __bucketEntry in __double.__Get_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + __double.__Get_calls.ClearObservedCalls(); + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithPhantomAuxiliaryNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithPhantomAuxiliaryNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index 4e3866f..6df451b 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithPhantomAuxiliaryNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithPhantomAuxiliaryNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,10 +52,14 @@ public void ReturnsCallback(__M_m_x_Callback callback) _callback = callback; } } - internal sealed class __M_m_x_State + internal sealed class __M_m_x_State : TestNamespace_IFactory_993557a2_IClearableCallState { internal global::Compono.ReturnConfig Config; internal __M_m_x_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __M_m_x_buckets = new(); @@ -129,6 +142,57 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__M_m_x_buckets) + { + foreach (var __bucketEntry in __double.__M_m_x_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + __double.__M.ClearObservedCalls(); + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithTypeParameterNamedLikeBoxedLocal_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithTypeParameterNamedLikeBoxedLocal_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index 92dabf7..7b09e44 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithTypeParameterNamedLikeBoxedLocal_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMemberWithTypeParameterNamedLikeBoxedLocal_GeneratesDoubleThatCompiles#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,10 +52,14 @@ public void ReturnsCallback(__Create_Callback callback) _callback = callback; } } - internal sealed class __Create_State + internal sealed class __Create_State : TestNamespace_IFactory_993557a2_IClearableCallState { internal global::Compono.ReturnConfig Config; internal __Create_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __Create_buckets = new(); @@ -113,6 +126,56 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Create_buckets) + { + foreach (var __bucketEntry in __double.__Create_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMembersWithCrossDerivedNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMembersWithCrossDerivedNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index 9393b49..5d1bd1b 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMembersWithCrossDerivedNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMembersWithCrossDerivedNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -81,10 +90,14 @@ public void ReturnsCallback(__B_State_Callback callback) _callback = callback; } } - internal sealed class __B_State + internal sealed class __B_State : TestNamespace_IFactory_993557a2_IClearableCallState { internal global::Compono.ReturnConfig Config; internal __B_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __B_buckets = new(); @@ -102,10 +115,14 @@ internal __B_State __B_Bucket() return (__B_State)__boxed; } } - internal sealed class __B_State_State + internal sealed class __B_State_State : TestNamespace_IFactory_993557a2_IClearableCallState { internal global::Compono.ReturnConfig Config; internal __B_State_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __B_State_buckets = new(); @@ -193,6 +210,61 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__B_buckets) + { + foreach (var __bucketEntry in __double.__B_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + lock (__double.__B_State_buckets) + { + foreach (var __bucketEntry in __double.__B_State_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMembersWithUnrelatedTypeParameterNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMembersWithUnrelatedTypeParameterNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index 950bd26..a3c32f0 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMembersWithUnrelatedTypeParameterNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleMembersWithUnrelatedTypeParameterNameCollision_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -81,10 +90,14 @@ public void ReturnsCallback(__Other_Callback<__Get_State> callback) _callback = callback; } } - internal sealed class __Get_State + internal sealed class __Get_State : TestNamespace_IFactory_993557a2_IClearableCallState { internal global::Compono.ReturnConfig Config; internal __Get_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __Get_buckets = new(); @@ -102,10 +115,14 @@ internal __Get_State __Get_Bucket() return (__Get_State)__boxed; } } - internal sealed class __Other_State<__Get_State> + internal sealed class __Other_State<__Get_State> : TestNamespace_IFactory_993557a2_IClearableCallState { internal global::Compono.ReturnConfig<__Get_State> Config; internal __Other_Callback<__Get_State>? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __Other_buckets = new(); @@ -193,6 +210,61 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_buckets) + { + foreach (var __bucketEntry in __double.__Get_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + lock (__double.__Other_buckets) + { + foreach (var __bucketEntry in __double.__Other_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMemberWithRealParameters_GeneratesDoubleWithArgumentAwareGenericConfiguration#TestNamespace.IContextManager_d05603ce.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMemberWithRealParameters_GeneratesDoubleWithArgumentAwareGenericConfiguration#TestNamespace.IContextManager_d05603ce.TestDouble.g.verified.cs index c46954e..37e821a 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMemberWithRealParameters_GeneratesDoubleWithArgumentAwareGenericConfiguration#TestNamespace.IContextManager_d05603ce.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMemberWithRealParameters_GeneratesDoubleWithArgumentAwareGenericConfiguration#TestNamespace.IContextManager_d05603ce.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IContextManager_d05603ce_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IContextManager_d05603ce_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IContextManager_d05603ce_Double : global::TestNamespace.IContextManager { @@ -43,7 +52,7 @@ public void ReturnsCallback(__GetContextDataAsync_Callback callback) _callback = callback; } } - internal sealed class __GetContextDataAsync_State where T : class + internal sealed class __GetContextDataAsync_State : TestNamespace_IContextManager_d05603ce_IClearableCallState where T : class { // ADR-0050: multi-entry response configuration composed inside ADR-0049's // per-closed-T state - same Entry shape as the plain matching-eligible branch below, just @@ -58,6 +67,24 @@ internal sealed class Entry internal readonly global::System.Collections.Generic.List Entries = []; internal readonly global::System.Collections.Generic.List Calls = []; internal readonly object Lock = new(); + + // PLAN-0063/ADR-0060: ClearCalls() clears this closed-T bucket entry's observation state + // (each registered Entry's own call count + the shared captured argument history) without + // touching any Entry's configured Value/Exception/Sequence/SequenceOrdinal state or removing + // any Entry from Entries (ADR-0050's multi-entry configuration is preserved, not reset). + // Reached through the non-generic TestNamespace_IContextManager_d05603ce_IClearableCallState interface, since + // ClearCalls() itself has no static knowledge of which closed T's this member has been + // called with. + public void ClearObservedCalls() + { + lock (this.Lock) + { + foreach (var __entry in this.Entries) + __entry.Config.ClearObservedCalls(); + + this.Calls.Clear(); + } + } } internal readonly global::System.Collections.Generic.Dictionary __GetContextDataAsync_buckets = new(); @@ -180,6 +207,56 @@ internal static class TestNamespace_IContextManager_d05603ce_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IContextManager_d05603ce_DoubleReceivedCalls +{ + internal global::TestNamespace_IContextManager_d05603ce_Double Instance { get; } + + internal TestNamespace_IContextManager_d05603ce_DoubleReceivedCalls(global::TestNamespace_IContextManager_d05603ce_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IContextManager_d05603ce_ReceivedCallsExtension +{ + public static global::TestNamespace_IContextManager_d05603ce_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IContextManager self) => + new(self as global::TestNamespace_IContextManager_d05603ce_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IContextManager' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IContextManager', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IContextManager_d05603ce_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IContextManager_d05603ce_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IContextManager self) + { + var __double = self as global::TestNamespace_IContextManager_d05603ce_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IContextManager' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IContextManager', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__GetContextDataAsync_buckets) + { + foreach (var __bucketEntry in __double.__GetContextDataAsync_buckets.Values) + ((TestNamespace_IContextManager_d05603ce_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IContextManager_d05603ce_ConfigureExtension { public static global::TestNamespace_IContextManager_d05603ce_Double Configure(this global::TestNamespace.IContextManager self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMemberWithZeroArgSiblingFromDifferentBaseInterface_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMemberWithZeroArgSiblingFromDifferentBaseInterface_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index 7bdbb2a..44f6111 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMemberWithZeroArgSiblingFromDifferentBaseInterface_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMemberWithZeroArgSiblingFromDifferentBaseInterface_GeneratesBothMembersCleanly#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,10 +52,14 @@ public void ReturnsCallback(__Get_467634cd_Callback callback) _callback = callback; } } - internal sealed class __Get_467634cd_State + internal sealed class __Get_467634cd_State : TestNamespace_IFactory_993557a2_IClearableCallState { internal global::Compono.ReturnConfig Config; internal __Get_467634cd_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __Get_467634cd_buckets = new(); @@ -132,6 +145,57 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_467634cd_buckets) + { + foreach (var __bucketEntry in __double.__Get_467634cd_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + __double.__Get.ClearObservedCalls(); + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMember_GeneratesDoubleWithGenericConfigurationExtension#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMember_GeneratesDoubleWithGenericConfigurationExtension#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index d39e69e..e102562 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMember_GeneratesDoubleWithGenericConfigurationExtension#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationEligibleSoloMember_GeneratesDoubleWithGenericConfigurationExtension#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,10 +52,14 @@ public void ReturnsCallback(__Create_Callback callback) _callback = callback; } } - internal sealed class __Create_State + internal sealed class __Create_State : TestNamespace_IFactory_993557a2_IClearableCallState { internal global::Compono.ReturnConfig Config; internal __Create_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __Create_buckets = new(); @@ -113,6 +126,56 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Create_buckets) + { + foreach (var __bucketEntry in __double.__Create_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationMatchedParameterTypeParameterNamedAfterNestedEntryMember_GeneratesSupportedDouble#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationMatchedParameterTypeParameterNamedAfterNestedEntryMember_GeneratesSupportedDouble#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index e439290..b629c0b 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationMatchedParameterTypeParameterNamedAfterNestedEntryMember_GeneratesSupportedDouble#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationMatchedParameterTypeParameterNamedAfterNestedEntryMember_GeneratesSupportedDouble#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,7 +52,7 @@ public void ReturnsCallback(__Create_Callback callback) _callback = callback; } } - internal sealed class __Create_State + internal sealed class __Create_State : TestNamespace_IFactory_993557a2_IClearableCallState { // ADR-0050: multi-entry response configuration composed inside ADR-0049's // per-closed-T state - same Entry shape as the plain matching-eligible branch below, just @@ -58,6 +67,24 @@ internal sealed class Entry internal readonly global::System.Collections.Generic.List Entries = []; internal readonly global::System.Collections.Generic.List Calls = []; internal readonly object Lock = new(); + + // PLAN-0063/ADR-0060: ClearCalls() clears this closed-T bucket entry's observation state + // (each registered Entry's own call count + the shared captured argument history) without + // touching any Entry's configured Value/Exception/Sequence/SequenceOrdinal state or removing + // any Entry from Entries (ADR-0050's multi-entry configuration is preserved, not reset). + // Reached through the non-generic TestNamespace_IFactory_993557a2_IClearableCallState interface, since + // ClearCalls() itself has no static knowledge of which closed T's this member has been + // called with. + public void ClearObservedCalls() + { + lock (this.Lock) + { + foreach (var __entry in this.Entries) + __entry.Config.ClearObservedCalls(); + + this.Calls.Clear(); + } + } } internal readonly global::System.Collections.Generic.Dictionary __Create_buckets = new(); @@ -179,6 +206,56 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Create_buckets) + { + foreach (var __bucketEntry in __double.__Create_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationShapedRefParameterOverloadFallback_CompilesWithoutConfigurationSurface#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationShapedRefParameterOverloadFallback_CompilesWithoutConfigurationSurface#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs index 20afa73..764b42b 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationShapedRefParameterOverloadFallback_CompilesWithoutConfigurationSurface#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ClosedInstantiationShapedRefParameterOverloadFallback_CompilesWithoutConfigurationSurface#TestNamespace.IFactory_993557a2.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IFactory_993557a2_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IFactory_993557a2_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IFactory_993557a2_Double : global::TestNamespace.IFactory { @@ -43,7 +52,7 @@ public void ReturnsCallback(__Get_Callback callback) _callback = callback; } } - internal sealed class __Get_State where T : class + internal sealed class __Get_State : TestNamespace_IFactory_993557a2_IClearableCallState where T : class { // ADR-0050: multi-entry response configuration composed inside ADR-0049's // per-closed-T state - same Entry shape as the plain matching-eligible branch below, just @@ -58,6 +67,24 @@ internal sealed class Entry internal readonly global::System.Collections.Generic.List Entries = []; internal readonly global::System.Collections.Generic.List Calls = []; internal readonly object Lock = new(); + + // PLAN-0063/ADR-0060: ClearCalls() clears this closed-T bucket entry's observation state + // (each registered Entry's own call count + the shared captured argument history) without + // touching any Entry's configured Value/Exception/Sequence/SequenceOrdinal state or removing + // any Entry from Entries (ADR-0050's multi-entry configuration is preserved, not reset). + // Reached through the non-generic TestNamespace_IFactory_993557a2_IClearableCallState interface, since + // ClearCalls() itself has no static knowledge of which closed T's this member has been + // called with. + public void ClearObservedCalls() + { + lock (this.Lock) + { + foreach (var __entry in this.Entries) + __entry.Config.ClearObservedCalls(); + + this.Calls.Clear(); + } + } } internal readonly global::System.Collections.Generic.Dictionary __Get_buckets = new(); @@ -187,6 +214,56 @@ internal static class TestNamespace_IFactory_993557a2_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IFactory_993557a2_DoubleReceivedCalls +{ + internal global::TestNamespace_IFactory_993557a2_Double Instance { get; } + + internal TestNamespace_IFactory_993557a2_DoubleReceivedCalls(global::TestNamespace_IFactory_993557a2_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IFactory_993557a2_ReceivedCallsExtension +{ + public static global::TestNamespace_IFactory_993557a2_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IFactory self) => + new(self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IFactory_993557a2_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IFactory_993557a2_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IFactory self) + { + var __double = self as global::TestNamespace_IFactory_993557a2_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IFactory' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IFactory', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_buckets) + { + foreach (var __bucketEntry in __double.__Get_buckets.Values) + ((TestNamespace_IFactory_993557a2_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IFactory_993557a2_ConfigureExtension { public static global::TestNamespace_IFactory_993557a2_Double Configure(this global::TestNamespace.IFactory self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithDifferentArity_GeneratesDoubleWithoutCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithDifferentArity_GeneratesDoubleWithoutCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index ff6dbf9..861e139 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithDifferentArity_GeneratesDoubleWithoutCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithDifferentArity_GeneratesDoubleWithoutCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -58,6 +67,22 @@ internal sealed class __Configure_Entry internal readonly global::System.Collections.Generic.List __Configure_calls = []; internal readonly object __Configure_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Configure_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Configure_ReceivedCall(int mode); + void global::TestNamespace.IRepository.Configure(int mode) { // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): @@ -189,6 +214,70 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Configure(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__Configure_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__Configure_ReceivedCall[self.Instance.__Configure_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Configure_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Configure_lock) { __double.__Configure_calls.Clear(); } + __double.__GetName.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithOptionalParameter_ReportsCollisionDiagnostic.verified.txt b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithOptionalParameter_ReportsCollisionDiagnostic.verified.txt index 2d6dc6c..17a9d1c 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithOptionalParameter_ReportsCollisionDiagnostic.verified.txt +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithOptionalParameter_ReportsCollisionDiagnostic.verified.txt @@ -7,7 +7,7 @@ WarningLevel: 1, Descriptor: { Id: CMP0023, - Title: Test-double interface member collides with a generated Configure()/Verify() bridge, + Title: Test-double interface member collides with a generated Configure()/Verify()/ReceivedCalls()/ClearCalls() bridge, MessageFormat: '{0}' declares its own member named '{1}', which would silently shadow the generated {1}() extension the double's configuration/verification surface depends on. This leaf falls back to the ordinary runtime-provider path., Category: Compono.TestDoubles, DefaultSeverity: Info, diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithParamsArray_ReportsCollisionDiagnostic.verified.txt b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithParamsArray_ReportsCollisionDiagnostic.verified.txt index 2d6dc6c..17a9d1c 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithParamsArray_ReportsCollisionDiagnostic.verified.txt +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureMemberWithParamsArray_ReportsCollisionDiagnostic.verified.txt @@ -7,7 +7,7 @@ WarningLevel: 1, Descriptor: { Id: CMP0023, - Title: Test-double interface member collides with a generated Configure()/Verify() bridge, + Title: Test-double interface member collides with a generated Configure()/Verify()/ReceivedCalls()/ClearCalls() bridge, MessageFormat: '{0}' declares its own member named '{1}', which would silently shadow the generated {1}() extension the double's configuration/verification surface depends on. This leaf falls back to the ordinary runtime-provider path., Category: Compono.TestDoubles, DefaultSeverity: Info, diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureNamedMember_ReportsCollisionDiagnostic.verified.txt b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureNamedMember_ReportsCollisionDiagnostic.verified.txt index 2d6dc6c..17a9d1c 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureNamedMember_ReportsCollisionDiagnostic.verified.txt +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ConfigureNamedMember_ReportsCollisionDiagnostic.verified.txt @@ -7,7 +7,7 @@ WarningLevel: 1, Descriptor: { Id: CMP0023, - Title: Test-double interface member collides with a generated Configure()/Verify() bridge, + Title: Test-double interface member collides with a generated Configure()/Verify()/ReceivedCalls()/ClearCalls() bridge, MessageFormat: '{0}' declares its own member named '{1}', which would silently shadow the generated {1}() extension the double's configuration/verification surface depends on. This leaf falls back to the ordinary runtime-provider path., Category: Compono.TestDoubles, DefaultSeverity: Info, diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedDynamicAndObjectOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedDynamicAndObjectOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index dda0904..cc7daf3 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedDynamicAndObjectOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedDynamicAndObjectOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -41,6 +50,51 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification { } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedNintAndIntPtrOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedNintAndIntPtrOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index da128c5..49023a6 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedNintAndIntPtrOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedNintAndIntPtrOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -41,6 +50,51 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification { } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedNullableAnnotationOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedNullableAnnotationOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 90ab162..e7b0126 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedNullableAnnotationOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedNullableAnnotationOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -41,6 +50,51 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification { } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedSameNameProperty_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedSameNameProperty_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 5c2d629..56e09bb 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedSameNameProperty_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedSameNameProperty_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -45,6 +54,51 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification { } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedTupleElementNameOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedTupleElementNameOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index f492348..7563caf 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedTupleElementNameOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DiamondInheritedTupleElementNameOverload_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -41,6 +50,51 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification { } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DictionaryReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DictionaryReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 1dc9398..840ab3c 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DictionaryReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.DictionaryReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -89,6 +98,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetCounts.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 65d99d1..188df03 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -239,6 +248,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Process_6729db8e_lock) { __double.__Process_6729db8e_calls.Clear(); } + lock (__double.__Process_ac263cc1_lock) { __double.__Process_ac263cc1_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericMethodsIndependentOfTypeParameter_GeneratesDoubleWithNonGenericConfigurationExtensions#TestNamespace.ILoggerLike_a9dd3ec3.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericMethodsIndependentOfTypeParameter_GeneratesDoubleWithNonGenericConfigurationExtensions#TestNamespace.ILoggerLike_a9dd3ec3.TestDouble.g.verified.cs index d392004..1d50987 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericMethodsIndependentOfTypeParameter_GeneratesDoubleWithNonGenericConfigurationExtensions#TestNamespace.ILoggerLike_a9dd3ec3.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericMethodsIndependentOfTypeParameter_GeneratesDoubleWithNonGenericConfigurationExtensions#TestNamespace.ILoggerLike_a9dd3ec3.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_ILoggerLike_a9dd3ec3_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_ILoggerLike_a9dd3ec3_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_ILoggerLike_a9dd3ec3_Double : global::TestNamespace.ILoggerLike { @@ -65,6 +74,53 @@ internal static class TestNamespace_ILoggerLike_a9dd3ec3_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_ILoggerLike_a9dd3ec3_DoubleReceivedCalls +{ + internal global::TestNamespace_ILoggerLike_a9dd3ec3_Double Instance { get; } + + internal TestNamespace_ILoggerLike_a9dd3ec3_DoubleReceivedCalls(global::TestNamespace_ILoggerLike_a9dd3ec3_Double instance) => Instance = instance; +} + +internal static class TestNamespace_ILoggerLike_a9dd3ec3_ReceivedCallsExtension +{ + public static global::TestNamespace_ILoggerLike_a9dd3ec3_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.ILoggerLike self) => + new(self as global::TestNamespace_ILoggerLike_a9dd3ec3_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILoggerLike' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILoggerLike', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_ILoggerLike_a9dd3ec3_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_ILoggerLike_a9dd3ec3_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.ILoggerLike self) + { + var __double = self as global::TestNamespace_ILoggerLike_a9dd3ec3_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.ILoggerLike' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.ILoggerLike', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Log.ClearObservedCalls(); + __double.__BeginScope.ClearObservedCalls(); + } +} + internal static class TestNamespace_ILoggerLike_a9dd3ec3_ConfigureExtension { public static global::TestNamespace_ILoggerLike_a9dd3ec3_Double Configure(this global::TestNamespace.ILoggerLike self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.HashSetReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.HashSetReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index ec72f9e..c9e874e 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.HashSetReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.HashSetReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -89,6 +98,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetIds.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.IDictionaryReturn_GeneratesDoubleWithConcreteEmptyDictionary#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.IDictionaryReturn_GeneratesDoubleWithConcreteEmptyDictionary#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 71e58fd..2510f15 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.IDictionaryReturn_GeneratesDoubleWithConcreteEmptyDictionary#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.IDictionaryReturn_GeneratesDoubleWithConcreteEmptyDictionary#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -145,6 +154,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetCounts.ClearObservedCalls(); + __double.__GetReadOnlyCounts.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.InheritedGenericOverloadsWithDifferentlyNamedTypeParameters_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.InheritedGenericOverloadsWithDifferentlyNamedTypeParameters_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index d7f5afb..c6c28e4 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.InheritedGenericOverloadsWithDifferentlyNamedTypeParameters_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.InheritedGenericOverloadsWithDifferentlyNamedTypeParameters_ReportsScopedOverloadedDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -41,6 +50,51 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification { } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultiTypeParameterGenericMethod_GeneratesDoubleWithConstrainedExplicitImplementation#TestNamespace.IMultiMapper_d3a016dc.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultiTypeParameterGenericMethod_GeneratesDoubleWithConstrainedExplicitImplementation#TestNamespace.IMultiMapper_d3a016dc.TestDouble.g.verified.cs index 57778ea..f8c4d47 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultiTypeParameterGenericMethod_GeneratesDoubleWithConstrainedExplicitImplementation#TestNamespace.IMultiMapper_d3a016dc.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultiTypeParameterGenericMethod_GeneratesDoubleWithConstrainedExplicitImplementation#TestNamespace.IMultiMapper_d3a016dc.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IMultiMapper_d3a016dc_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IMultiMapper_d3a016dc_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IMultiMapper_d3a016dc_Double : global::TestNamespace.IMultiMapper { @@ -49,6 +58,52 @@ internal static class TestNamespace_IMultiMapper_d3a016dc_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IMultiMapper_d3a016dc_DoubleReceivedCalls +{ + internal global::TestNamespace_IMultiMapper_d3a016dc_Double Instance { get; } + + internal TestNamespace_IMultiMapper_d3a016dc_DoubleReceivedCalls(global::TestNamespace_IMultiMapper_d3a016dc_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IMultiMapper_d3a016dc_ReceivedCallsExtension +{ + public static global::TestNamespace_IMultiMapper_d3a016dc_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IMultiMapper self) => + new(self as global::TestNamespace_IMultiMapper_d3a016dc_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IMultiMapper' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IMultiMapper', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IMultiMapper_d3a016dc_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IMultiMapper_d3a016dc_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IMultiMapper self) + { + var __double = self as global::TestNamespace_IMultiMapper_d3a016dc_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IMultiMapper' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IMultiMapper', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Map.ClearObservedCalls(); + } +} + internal static class TestNamespace_IMultiMapper_d3a016dc_ConfigureExtension { public static global::TestNamespace_IMultiMapper_d3a016dc_Double Configure(this global::TestNamespace.IMultiMapper self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultidimensionalArrayReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultidimensionalArrayReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 6837bc5..b2ec2ae 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultidimensionalArrayReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultidimensionalArrayReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -90,6 +99,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetGrid.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultipleConfigurationRequiredMembers_ReportsSingleCmp0032WithCorrectCount#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultipleConfigurationRequiredMembers_ReportsSingleCmp0032WithCorrectCount#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 831850b..c949f91 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultipleConfigurationRequiredMembers_ReportsSingleCmp0032WithCorrectCount#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.MultipleConfigurationRequiredMembers_ReportsSingleCmp0032WithCorrectCount#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -166,6 +175,54 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetName.ClearObservedCalls(); + __double.__Description.ClearObservedCalls(); + __double.__GetCount.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullablePropertyReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullablePropertyReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 1a37b6b..8b7724d 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullablePropertyReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullablePropertyReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -53,6 +62,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Name.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullableReferenceReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullableReferenceReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index cf13512..7d631ec 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullableReferenceReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullableReferenceReturn_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -90,6 +99,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetName.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullableValueTaskOfReference_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullableValueTaskOfReference_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index ca6369c..e6d4eae 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullableValueTaskOfReference_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NonNullableValueTaskOfReference_GeneratesConfigurationRequiredMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -90,6 +99,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetNameAsync.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NullableCollectionReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NullableCollectionReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 2016d2d..a035845 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NullableCollectionReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NullableCollectionReturn_GeneratesDoubleWithEmptyDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -89,6 +98,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetValues.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NullableReferenceReturnAndParameter_PreservesAnnotationInGeneratedCode#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NullableReferenceReturnAndParameter_PreservesAnnotationInGeneratedCode#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 76445af..6efd76c 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NullableReferenceReturnAndParameter_PreservesAnnotationInGeneratedCode#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.NullableReferenceReturnAndParameter_PreservesAnnotationInGeneratedCode#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -56,6 +65,22 @@ internal sealed class __FindNameAsync_Entry internal readonly global::System.Collections.Generic.List<__FindNameAsync_Entry> __FindNameAsync_entries = []; internal readonly global::System.Collections.Generic.List __FindNameAsync_calls = []; internal readonly object __FindNameAsync_lock = new(); + + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __FindNameAsync_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __FindNameAsync_ReceivedCall(global::System.Guid id); // ADR-0050: multi-entry response configuration - replaces the single // __Save/__Save_m_{param} shape with an ordered, append-only // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). @@ -69,6 +94,22 @@ internal sealed class __Save_Entry internal readonly global::System.Collections.Generic.List __Save_calls = []; internal readonly object __Save_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Save_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Save_ReceivedCall(string? name); + global::System.Threading.Tasks.Task global::TestNamespace.IRepository.FindNameAsync(global::System.Guid id) { __FindNameAsync_Callback? __callback = null; @@ -276,6 +317,87 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList FindNameAsync(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__FindNameAsync_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__FindNameAsync_ReceivedCall[self.Instance.__FindNameAsync_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__FindNameAsync_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Save(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__Save_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__Save_ReceivedCall[self.Instance.__Save_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Save_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__FindNameAsync_lock) { __double.__FindNameAsync_calls.Clear(); } + lock (__double.__Save_lock) { __double.__Save_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OrdinaryMemberReturningShadowedValueTaskOfT_GeneratesDoubleThatCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OrdinaryMemberReturningShadowedValueTaskOfT_GeneratesDoubleThatCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 59c9b76..eb490a0 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OrdinaryMemberReturningShadowedValueTaskOfT_GeneratesDoubleThatCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OrdinaryMemberReturningShadowedValueTaskOfT_GeneratesDoubleThatCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -89,6 +98,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetNameAsync.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasArityDiffersFromRealMemberOfSameName_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasArityDiffersFromRealMemberOfSameName_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index cbb7399..d6bf63e 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasArityDiffersFromRealMemberOfSameName_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasArityDiffersFromRealMemberOfSameName_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -120,6 +129,22 @@ internal sealed class __FooMatching_Entry internal readonly global::System.Collections.Generic.List __FooMatching_calls = []; internal readonly object __FooMatching_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __FooMatching_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __FooMatching_ReceivedCall(int value); + bool global::TestNamespace.IRepository.Foo(int id) { // ADR-0050: reverse-scan the ordered entry list - last matching registration wins. Both @@ -417,6 +442,71 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__FooMatching_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__FooMatching_ReceivedCall[self.Instance.__FooMatching_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__FooMatching_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Foo_e5607478_lock) { __double.__Foo_e5607478_calls.Clear(); } + lock (__double.__Foo_1a56931a_lock) { __double.__Foo_1a56931a_calls.Clear(); } + lock (__double.__FooMatching_lock) { __double.__FooMatching_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesDespiteNullableAnnotationMismatch_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesDespiteNullableAnnotationMismatch_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 3fb04e9..e190b83 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesDespiteNullableAnnotationMismatch_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesDespiteNullableAnnotationMismatch_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -159,6 +168,22 @@ internal sealed class __GetMatching_Entry internal readonly global::System.Collections.Generic.List __GetMatching_calls = []; internal readonly object __GetMatching_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __GetMatching_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __GetMatching_ReceivedCall(string? value); + bool global::TestNamespace.IRepository.Get(int id) { __Get_b9dfaa09_Callback? __callback = null; @@ -464,6 +489,71 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__GetMatching_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__GetMatching_ReceivedCall[self.Instance.__GetMatching_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__GetMatching_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_b9dfaa09_lock) { __double.__Get_b9dfaa09_calls.Clear(); } + lock (__double.__Get_1a56931a_lock) { __double.__Get_1a56931a_calls.Clear(); } + lock (__double.__GetMatching_lock) { __double.__GetMatching_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithClosedInstantiationEligibleRealMember_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithClosedInstantiationEligibleRealMember_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 43f5254..72a35d8 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithClosedInstantiationEligibleRealMember_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithClosedInstantiationEligibleRealMember_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -81,7 +90,7 @@ public void ReturnsCallback(__FooMatching_Callback callback) _callback = callback; } } - internal sealed class __FooMatching_State where T : class + internal sealed class __FooMatching_State : TestNamespace_IRepository_e3198068_IClearableCallState where T : class { // ADR-0050: multi-entry response configuration composed inside ADR-0049's // per-closed-T state - same Entry shape as the plain matching-eligible branch below, just @@ -96,6 +105,24 @@ internal sealed class Entry internal readonly global::System.Collections.Generic.List Entries = []; internal readonly global::System.Collections.Generic.List Calls = []; internal readonly object Lock = new(); + + // PLAN-0063/ADR-0060: ClearCalls() clears this closed-T bucket entry's observation state + // (each registered Entry's own call count + the shared captured argument history) without + // touching any Entry's configured Value/Exception/Sequence/SequenceOrdinal state or removing + // any Entry from Entries (ADR-0050's multi-entry configuration is preserved, not reset). + // Reached through the non-generic TestNamespace_IRepository_e3198068_IClearableCallState interface, since + // ClearCalls() itself has no static knowledge of which closed T's this member has been + // called with. + public void ClearObservedCalls() + { + lock (this.Lock) + { + foreach (var __entry in this.Entries) + __entry.Config.ClearObservedCalls(); + + this.Calls.Clear(); + } + } } internal readonly global::System.Collections.Generic.Dictionary __FooMatching_buckets = new(); @@ -421,6 +448,58 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Foo_e5607478_lock) { __double.__Foo_e5607478_calls.Clear(); } + lock (__double.__Foo_1a56931a_lock) { __double.__Foo_1a56931a_calls.Clear(); } + lock (__double.__FooMatching_buckets) + { + foreach (var __bucketEntry in __double.__FooMatching_buckets.Values) + ((TestNamespace_IRepository_e3198068_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithOverloadedRealFamily_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithOverloadedRealFamily_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 1c8c41d..ae5317b 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithOverloadedRealFamily_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithOverloadedRealFamily_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -609,6 +618,55 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Foo_b9dfaa09_lock) { __double.__Foo_b9dfaa09_calls.Clear(); } + lock (__double.__Foo_1a56931a_lock) { __double.__Foo_1a56931a_calls.Clear(); } + lock (__double.__FooMatching_8ac5d184_lock) { __double.__FooMatching_8ac5d184_calls.Clear(); } + lock (__double.__FooMatching_1a56931a_lock) { __double.__FooMatching_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index e46ed48..1d2d455 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -159,6 +168,22 @@ internal sealed class __GetMatching_Entry internal readonly global::System.Collections.Generic.List __GetMatching_calls = []; internal readonly object __GetMatching_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __GetMatching_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __GetMatching_ReceivedCall(string value); + bool global::TestNamespace.IRepository.Get(int id) { __Get_b9dfaa09_Callback? __callback = null; @@ -464,6 +489,71 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__GetMatching_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__GetMatching_ReceivedCall[self.Instance.__GetMatching_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__GetMatching_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_b9dfaa09_lock) { __double.__Get_b9dfaa09_calls.Clear(); } + lock (__double.__Get_1a56931a_lock) { __double.__Get_1a56931a_calls.Clear(); } + lock (__double.__GetMatching_lock) { __double.__GetMatching_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithSoloGenericRealMemberEmittingNonGenericExtension_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithSoloGenericRealMemberEmittingNonGenericExtension_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index cbfa817..bffbc6d 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithSoloGenericRealMemberEmittingNonGenericExtension_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithSoloGenericRealMemberEmittingNonGenericExtension_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -81,6 +90,22 @@ internal sealed class __FooMatching_Entry internal readonly global::System.Collections.Generic.List __FooMatching_calls = []; internal readonly object __FooMatching_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __FooMatching_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __FooMatching_ReceivedCall(int value); + bool global::TestNamespace.IRepository.Foo(int id) { __Foo_b9dfaa09_Callback? __callback = null; @@ -370,6 +395,71 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__FooMatching_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__FooMatching_ReceivedCall[self.Instance.__FooMatching_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__FooMatching_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Foo_b9dfaa09_lock) { __double.__Foo_b9dfaa09_calls.Clear(); } + lock (__double.__Foo_97bed815_lock) { __double.__Foo_97bed815_calls.Clear(); } + lock (__double.__FooMatching_lock) { __double.__FooMatching_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 0c568c6..72e1b76 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -159,6 +168,22 @@ internal sealed class __GetMatching_Entry internal readonly global::System.Collections.Generic.List __GetMatching_calls = []; internal readonly object __GetMatching_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __GetMatching_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __GetMatching_ReceivedCall(bool flag); + bool global::TestNamespace.IRepository.Get(int id) { __Get_b9dfaa09_Callback? __callback = null; @@ -464,6 +489,71 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__GetMatching_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__GetMatching_ReceivedCall[self.Instance.__GetMatching_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__GetMatching_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_b9dfaa09_lock) { __double.__Get_b9dfaa09_calls.Clear(); } + lock (__double.__Get_1a56931a_lock) { __double.__Get_1a56931a_calls.Clear(); } + lock (__double.__GetMatching_lock) { __double.__GetMatching_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadSuffixCollidesWithDifferentlyNamedRealMember_GeneratesDoubleWithDistinctFieldNames#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadSuffixCollidesWithDifferentlyNamedRealMember_GeneratesDoubleWithDistinctFieldNames#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 0705939..61659bf 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadSuffixCollidesWithDifferentlyNamedRealMember_GeneratesDoubleWithDistinctFieldNames#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadSuffixCollidesWithDifferentlyNamedRealMember_GeneratesDoubleWithDistinctFieldNames#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -251,6 +260,54 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__M_b9dfaa09_2_lock) { __double.__M_b9dfaa09_2_calls.Clear(); } + lock (__double.__M_1a56931a_lock) { __double.__M_1a56931a_calls.Clear(); } + __double.__M_b9dfaa09.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithOutParameterHavingDefault_FallsBackWithoutRejectingSiblingOverload#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithOutParameterHavingDefault_FallsBackWithoutRejectingSiblingOverload#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 95f1e80..e3b41fb 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithOutParameterHavingDefault_FallsBackWithoutRejectingSiblingOverload#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithOutParameterHavingDefault_FallsBackWithoutRejectingSiblingOverload#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -57,6 +66,22 @@ internal sealed class __TryGet_Entry internal readonly global::System.Collections.Generic.List __TryGet_calls = []; internal readonly object __TryGet_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __TryGet_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __TryGet_ReceivedCall(int id); + bool global::TestNamespace.IRepository.TryGet(int id, out string? value) { value = default; @@ -182,6 +207,69 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList TryGet(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__TryGet_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__TryGet_ReceivedCall[self.Instance.__TryGet_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__TryGet_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__TryGet_lock) { __double.__TryGet_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithScopedByValueRefLikeParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithScopedByValueRefLikeParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 18bcd20..2b0d014 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithScopedByValueRefLikeParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithScopedByValueRefLikeParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -150,6 +159,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Seek_37c3f22f.ClearObservedCalls(); + lock (__double.__Seek_b9dfaa09_lock) { __double.__Seek_b9dfaa09_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithScopedRefParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithScopedRefParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 13be42e..ca878b8 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithScopedRefParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithScopedRefParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -18,6 +27,22 @@ internal sealed class __Seek_Entry internal readonly global::System.Collections.Generic.List __Seek_calls = []; internal readonly object __Seek_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Seek_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Seek_ReceivedCall(int value); + void global::TestNamespace.IRepository.Seek(scoped ref global::System.Span value) { } @@ -137,6 +162,69 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Seek(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__Seek_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__Seek_ReceivedCall[self.Instance.__Seek_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Seek_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Seek_lock) { __double.__Seek_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithUnscopedRefOutParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithUnscopedRefOutParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index b714896..0ab3de7 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithUnscopedRefOutParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadWithUnscopedRefOutParameter_GeneratesDoubleWithMatchingRefSafetyContract#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -18,6 +27,22 @@ internal sealed class __Seek_Entry internal readonly global::System.Collections.Generic.List __Seek_calls = []; internal readonly object __Seek_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Seek_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Seek_ReceivedCall(int value); + void global::TestNamespace.IRepository.Seek([global::System.Diagnostics.CodeAnalysis.UnscopedRef] out int value) { value = default; @@ -138,6 +163,69 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Seek(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__Seek_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__Seek_ReceivedCall[self.Instance.__Seek_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Seek_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Seek_lock) { __double.__Seek_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedClosedInstantiationEligibleMember_GeneratesDoubleWithPerOverloadGenericConfiguration#TestNamespace.IContextManager_d05603ce.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedClosedInstantiationEligibleMember_GeneratesDoubleWithPerOverloadGenericConfiguration#TestNamespace.IContextManager_d05603ce.TestDouble.g.verified.cs index 6e00d9c..c7718ac 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedClosedInstantiationEligibleMember_GeneratesDoubleWithPerOverloadGenericConfiguration#TestNamespace.IContextManager_d05603ce.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedClosedInstantiationEligibleMember_GeneratesDoubleWithPerOverloadGenericConfiguration#TestNamespace.IContextManager_d05603ce.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IContextManager_d05603ce_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IContextManager_d05603ce_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IContextManager_d05603ce_Double : global::TestNamespace.IContextManager { @@ -81,10 +90,14 @@ public void ReturnsCallback(__GetDataAsync_1aae9cd0_Callback callback) _callback = callback; } } - internal sealed class __GetDataAsync_97bed815_State where T : class + internal sealed class __GetDataAsync_97bed815_State : TestNamespace_IContextManager_d05603ce_IClearableCallState where T : class { internal global::Compono.ReturnConfig> Config; internal __GetDataAsync_97bed815_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __GetDataAsync_97bed815_buckets = new(); @@ -102,10 +115,14 @@ internal __GetDataAsync_97bed815_State __GetDataAsync_97bed815_Bucket() wh return (__GetDataAsync_97bed815_State)__boxed; } } - internal sealed class __GetDataAsync_1aae9cd0_State where T : class + internal sealed class __GetDataAsync_1aae9cd0_State : TestNamespace_IContextManager_d05603ce_IClearableCallState where T : class { internal global::Compono.ReturnConfig> Config; internal __GetDataAsync_1aae9cd0_Callback? Callback; + + // PLAN-0063/ADR-0060: see the matched-parameters branch above for the full rationale - this + // shape has no argument history to clear, only the scalar call count. + public void ClearObservedCalls() => this.Config.ClearObservedCalls(); } internal readonly global::System.Collections.Generic.Dictionary __GetDataAsync_1aae9cd0_buckets = new(); @@ -195,6 +212,61 @@ internal static class TestNamespace_IContextManager_d05603ce_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IContextManager_d05603ce_DoubleReceivedCalls +{ + internal global::TestNamespace_IContextManager_d05603ce_Double Instance { get; } + + internal TestNamespace_IContextManager_d05603ce_DoubleReceivedCalls(global::TestNamespace_IContextManager_d05603ce_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IContextManager_d05603ce_ReceivedCallsExtension +{ + public static global::TestNamespace_IContextManager_d05603ce_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IContextManager self) => + new(self as global::TestNamespace_IContextManager_d05603ce_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IContextManager' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IContextManager', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IContextManager_d05603ce_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IContextManager_d05603ce_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IContextManager self) + { + var __double = self as global::TestNamespace_IContextManager_d05603ce_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IContextManager' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IContextManager', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__GetDataAsync_97bed815_buckets) + { + foreach (var __bucketEntry in __double.__GetDataAsync_97bed815_buckets.Values) + ((TestNamespace_IContextManager_d05603ce_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + lock (__double.__GetDataAsync_1aae9cd0_buckets) + { + foreach (var __bucketEntry in __double.__GetDataAsync_1aae9cd0_buckets.Values) + ((TestNamespace_IContextManager_d05603ce_IClearableCallState)__bucketEntry).ClearObservedCalls(); + } + } +} + internal static class TestNamespace_IContextManager_d05603ce_ConfigureExtension { public static global::TestNamespace_IContextManager_d05603ce_Double Configure(this global::TestNamespace.IContextManager self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithAttributeOnlyOptionalParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithAttributeOnlyOptionalParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index fb84ae4..6739ab6 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithAttributeOnlyOptionalParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithAttributeOnlyOptionalParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -237,6 +246,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Equals_b9dfaa09.ClearObservedCalls(); + lock (__double.__Equals_07b0838c_lock) { __double.__Equals_07b0838c_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithOptionalParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithOptionalParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 8e9a629..1b8d1a9 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithOptionalParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithOptionalParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -237,6 +246,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Equals_b9dfaa09.ClearObservedCalls(); + lock (__double.__Equals_07b0838c_lock) { __double.__Equals_07b0838c_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithParamsArray_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithParamsArray_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index ff85821..e04a122 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithParamsArray_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithParamsArray_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -237,6 +246,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Equals_4731af18.ClearObservedCalls(); + lock (__double.__Equals_07b0838c_lock) { __double.__Equals_07b0838c_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithRefLikeParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithRefLikeParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index a2395d7..f532e6a 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithRefLikeParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedEqualsWithRefLikeParameter_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Equals_37c3f22f.ClearObservedCalls(); + lock (__double.__Equals_693d6b44_lock) { __double.__Equals_693d6b44_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodWithAllowsRefStructConstraint_GeneratesDoubleWithAntiConstraintPreserved#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodWithAllowsRefStructConstraint_GeneratesDoubleWithAntiConstraintPreserved#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs index 6ad3f09..9d87714 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodWithAllowsRefStructConstraint_GeneratesDoubleWithAntiConstraintPreserved#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodWithAllowsRefStructConstraint_GeneratesDoubleWithAntiConstraintPreserved#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IWidget_34aa79b8_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IWidget_34aa79b8_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IWidget_34aa79b8_Double : global::TestNamespace.IWidget { @@ -65,6 +74,53 @@ internal static class TestNamespace_IWidget_34aa79b8_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IWidget_34aa79b8_DoubleReceivedCalls +{ + internal global::TestNamespace_IWidget_34aa79b8_Double Instance { get; } + + internal TestNamespace_IWidget_34aa79b8_DoubleReceivedCalls(global::TestNamespace_IWidget_34aa79b8_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IWidget_34aa79b8_ReceivedCallsExtension +{ + public static global::TestNamespace_IWidget_34aa79b8_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IWidget self) => + new(self as global::TestNamespace_IWidget_34aa79b8_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IWidget' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IWidget', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IWidget_34aa79b8_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IWidget_34aa79b8_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IWidget self) + { + var __double = self as global::TestNamespace_IWidget_34aa79b8_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IWidget' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IWidget', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Process_9f03e88f.ClearObservedCalls(); + __double.__Process_5792c437.ClearObservedCalls(); + } +} + internal static class TestNamespace_IWidget_34aa79b8_ConfigureExtension { public static global::TestNamespace_IWidget_34aa79b8_Double Configure(this global::TestNamespace.IWidget self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodWithTypeParameterNamedDunderSelf_GeneratesDoubleWithDistinctReceiverName#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodWithTypeParameterNamedDunderSelf_GeneratesDoubleWithDistinctReceiverName#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs index 696180c..97bba5a 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodWithTypeParameterNamedDunderSelf_GeneratesDoubleWithDistinctReceiverName#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodWithTypeParameterNamedDunderSelf_GeneratesDoubleWithDistinctReceiverName#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IWidget_34aa79b8_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IWidget_34aa79b8_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IWidget_34aa79b8_Double : global::TestNamespace.IWidget { @@ -65,6 +74,53 @@ internal static class TestNamespace_IWidget_34aa79b8_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IWidget_34aa79b8_DoubleReceivedCalls +{ + internal global::TestNamespace_IWidget_34aa79b8_Double Instance { get; } + + internal TestNamespace_IWidget_34aa79b8_DoubleReceivedCalls(global::TestNamespace_IWidget_34aa79b8_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IWidget_34aa79b8_ReceivedCallsExtension +{ + public static global::TestNamespace_IWidget_34aa79b8_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IWidget self) => + new(self as global::TestNamespace_IWidget_34aa79b8_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IWidget' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IWidget', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IWidget_34aa79b8_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IWidget_34aa79b8_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IWidget self) + { + var __double = self as global::TestNamespace_IWidget_34aa79b8_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IWidget' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IWidget', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Process_9f03e88f.ClearObservedCalls(); + __double.__Process_22a72316.ClearObservedCalls(); + } +} + internal static class TestNamespace_IWidget_34aa79b8_ConfigureExtension { public static global::TestNamespace_IWidget_34aa79b8_Double Configure(this global::TestNamespace.IWidget self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethod_GeneratesDoubleWithPerOverloadGenericConfigurationExtensions#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethod_GeneratesDoubleWithPerOverloadGenericConfigurationExtensions#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs index 13de2e3..d27f690 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethod_GeneratesDoubleWithPerOverloadGenericConfigurationExtensions#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethod_GeneratesDoubleWithPerOverloadGenericConfigurationExtensions#TestNamespace.IWidget_34aa79b8.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IWidget_34aa79b8_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IWidget_34aa79b8_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IWidget_34aa79b8_Double : global::TestNamespace.IWidget { @@ -65,6 +74,53 @@ internal static class TestNamespace_IWidget_34aa79b8_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IWidget_34aa79b8_DoubleReceivedCalls +{ + internal global::TestNamespace_IWidget_34aa79b8_Double Instance { get; } + + internal TestNamespace_IWidget_34aa79b8_DoubleReceivedCalls(global::TestNamespace_IWidget_34aa79b8_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IWidget_34aa79b8_ReceivedCallsExtension +{ + public static global::TestNamespace_IWidget_34aa79b8_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IWidget self) => + new(self as global::TestNamespace_IWidget_34aa79b8_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IWidget' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IWidget', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IWidget_34aa79b8_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IWidget_34aa79b8_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IWidget self) + { + var __double = self as global::TestNamespace_IWidget_34aa79b8_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IWidget' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IWidget', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Process_9f03e88f.ClearObservedCalls(); + __double.__Process_22a72316.ClearObservedCalls(); + } +} + internal static class TestNamespace_IWidget_34aa79b8_ConfigureExtension { public static global::TestNamespace_IWidget_34aa79b8_Double Configure(this global::TestNamespace.IWidget self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodsOfDifferentArity_DoNotCollideAsZeroArgumentExtensions#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodsOfDifferentArity_DoNotCollideAsZeroArgumentExtensions#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs index 4abccee..dbdca19 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodsOfDifferentArity_DoNotCollideAsZeroArgumentExtensions#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericMethodsOfDifferentArity_DoNotCollideAsZeroArgumentExtensions#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IThing_7b7b47c0_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IThing_7b7b47c0_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IThing_7b7b47c0_Double : global::TestNamespace.IThing { @@ -65,6 +74,53 @@ internal static class TestNamespace_IThing_7b7b47c0_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IThing_7b7b47c0_DoubleReceivedCalls +{ + internal global::TestNamespace_IThing_7b7b47c0_Double Instance { get; } + + internal TestNamespace_IThing_7b7b47c0_DoubleReceivedCalls(global::TestNamespace_IThing_7b7b47c0_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IThing_7b7b47c0_ReceivedCallsExtension +{ + public static global::TestNamespace_IThing_7b7b47c0_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IThing self) => + new(self as global::TestNamespace_IThing_7b7b47c0_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IThing' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IThing', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IThing_7b7b47c0_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IThing_7b7b47c0_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IThing self) + { + var __double = self as global::TestNamespace_IThing_7b7b47c0_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IThing' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IThing', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__M_467634cd.ClearObservedCalls(); + __double.__M_4478703e.ClearObservedCalls(); + } +} + internal static class TestNamespace_IThing_7b7b47c0_ConfigureExtension { public static global::TestNamespace_IThing_7b7b47c0_Double Configure(this global::TestNamespace.IThing self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericToStringMember_DoesNotCollideWithObjectMember#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericToStringMember_DoesNotCollideWithObjectMember#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs index 1c08303..536a0ba 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericToStringMember_DoesNotCollideWithObjectMember#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedGenericToStringMember_DoesNotCollideWithObjectMember#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IThing_7b7b47c0_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IThing_7b7b47c0_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IThing_7b7b47c0_Double : global::TestNamespace.IThing { @@ -65,6 +74,53 @@ internal static class TestNamespace_IThing_7b7b47c0_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IThing_7b7b47c0_DoubleReceivedCalls +{ + internal global::TestNamespace_IThing_7b7b47c0_Double Instance { get; } + + internal TestNamespace_IThing_7b7b47c0_DoubleReceivedCalls(global::TestNamespace_IThing_7b7b47c0_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IThing_7b7b47c0_ReceivedCallsExtension +{ + public static global::TestNamespace_IThing_7b7b47c0_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IThing self) => + new(self as global::TestNamespace_IThing_7b7b47c0_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IThing' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IThing', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IThing_7b7b47c0_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IThing_7b7b47c0_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IThing self) + { + var __double = self as global::TestNamespace_IThing_7b7b47c0_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IThing' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IThing', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__ToString_467634cd.ClearObservedCalls(); + __double.__ToString_9f03e88f.ClearObservedCalls(); + } +} + internal static class TestNamespace_IThing_7b7b47c0_ConfigureExtension { public static global::TestNamespace_IThing_7b7b47c0_Double Configure(this global::TestNamespace.IThing self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithAttributeOnlyOptionalParameter_ConfigureIsCallableWithoutTheOptionalArgument#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithAttributeOnlyOptionalParameter_ConfigureIsCallableWithoutTheOptionalArgument#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 06b9e1e..e0691c5 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithAttributeOnlyOptionalParameter_ConfigureIsCallableWithoutTheOptionalArgument#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithAttributeOnlyOptionalParameter_ConfigureIsCallableWithoutTheOptionalArgument#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__M_b9dfaa09_lock) { __double.__M_b9dfaa09_calls.Clear(); } + lock (__double.__M_1a56931a_lock) { __double.__M_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithNonZeroEnumDefault_GeneratesDoubleWithTypeCompatibleDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithNonZeroEnumDefault_GeneratesDoubleWithTypeCompatibleDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 5bbb947..2448103 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithNonZeroEnumDefault_GeneratesDoubleWithTypeCompatibleDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithNonZeroEnumDefault_GeneratesDoubleWithTypeCompatibleDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__M_f8c02d84_lock) { __double.__M_f8c02d84_calls.Clear(); } + lock (__double.__M_1a56931a_lock) { __double.__M_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithNonZeroNullableEnumDefault_GeneratesDoubleWithTypeCompatibleDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithNonZeroNullableEnumDefault_GeneratesDoubleWithTypeCompatibleDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 19a2d4e..dd097cd 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithNonZeroNullableEnumDefault_GeneratesDoubleWithTypeCompatibleDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithNonZeroNullableEnumDefault_GeneratesDoubleWithTypeCompatibleDefault#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__M_0e236ea3_lock) { __double.__M_0e236ea3_calls.Clear(); } + lock (__double.__M_1a56931a_lock) { __double.__M_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithOptionalParameter_ConfigureIsCallableWithoutTheOptionalArgument#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithOptionalParameter_ConfigureIsCallableWithoutTheOptionalArgument#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 27985a5..9443034 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithOptionalParameter_ConfigureIsCallableWithoutTheOptionalArgument#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithOptionalParameter_ConfigureIsCallableWithoutTheOptionalArgument#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__M_b9dfaa09_lock) { __double.__M_b9dfaa09_calls.Clear(); } + lock (__double.__M_1a56931a_lock) { __double.__M_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithParameterNamedDunderSelf_GeneratesDoubleWithoutParameterNameCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithParameterNamedDunderSelf_GeneratesDoubleWithoutParameterNameCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 2245bb6..38db203 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithParameterNamedDunderSelf_GeneratesDoubleWithoutParameterNameCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithParameterNamedDunderSelf_GeneratesDoubleWithoutParameterNameCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Save_b9dfaa09_lock) { __double.__Save_b9dfaa09_calls.Clear(); } + lock (__double.__Save_1a56931a_lock) { __double.__Save_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithParameterNamedSelf_GeneratesDoubleWithoutParameterNameCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithParameterNamedSelf_GeneratesDoubleWithoutParameterNameCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index d46b718..3506f42 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithParameterNamedSelf_GeneratesDoubleWithoutParameterNameCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithParameterNamedSelf_GeneratesDoubleWithoutParameterNameCollision#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Save_b9dfaa09_lock) { __double.__Save_b9dfaa09_calls.Clear(); } + lock (__double.__Save_1a56931a_lock) { __double.__Save_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 015d7d0..d77e78f 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -321,6 +330,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Delete_b9dfaa09_lock) { __double.__Delete_b9dfaa09_calls.Clear(); } + lock (__double.__Delete_1a56931a_lock) { __double.__Delete_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMember_GeneratesDoubleWithPerOverloadConfiguration#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMember_GeneratesDoubleWithPerOverloadConfiguration#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 3bb5fe8..3cf7d71 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMember_GeneratesDoubleWithPerOverloadConfiguration#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMember_GeneratesDoubleWithPerOverloadConfiguration#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Get_b9dfaa09_lock) { __double.__Get_b9dfaa09_calls.Clear(); } + lock (__double.__Get_1a56931a_lock) { __double.__Get_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedToStringSharingNameWithAnotherOverload_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedToStringSharingNameWithAnotherOverload_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 8def350..5c04a86 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedToStringSharingNameWithAnotherOverload_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedToStringSharingNameWithAnotherOverload_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -321,6 +330,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__ToString_b9dfaa09_lock) { __double.__ToString_b9dfaa09_calls.Clear(); } + lock (__double.__ToString_1a56931a_lock) { __double.__ToString_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedToStringWithParamsArray_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedToStringWithParamsArray_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index ccc406e..d6a7eaf 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedToStringWithParamsArray_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedToStringWithParamsArray_DoesNotCollideWithObjectMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -321,6 +330,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__ToString_4a96ce8f_lock) { __double.__ToString_4a96ce8f_calls.Clear(); } + lock (__double.__ToString_b9dfaa09_lock) { __double.__ToString_b9dfaa09_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadsWithDifferentContainingTypeGenericArguments_BothKeepConfigurationSurface#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadsWithDifferentContainingTypeGenericArguments_BothKeepConfigurationSurface#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 32aa75f..0ce1db6 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadsWithDifferentContainingTypeGenericArguments_BothKeepConfigurationSurface#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadsWithDifferentContainingTypeGenericArguments_BothKeepConfigurationSurface#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Handle_50c4849c_lock) { __double.__Handle_50c4849c_calls.Clear(); } + lock (__double.__Handle_3b0d1db3_lock) { __double.__Handle_3b0d1db3_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadsWithHashCollidingParameterTypes_BothKeepConfigurationSurface#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadsWithHashCollidingParameterTypes_BothKeepConfigurationSurface#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index fa52c41..878126a 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadsWithHashCollidingParameterTypes_BothKeepConfigurationSurface#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadsWithHashCollidingParameterTypes_BothKeepConfigurationSurface#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -235,6 +244,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Handle_3b7828aa_lock) { __double.__Handle_3b7828aa_calls.Clear(); } + lock (__double.__Handle_b5b0a4ae_lock) { __double.__Handle_b5b0a4ae_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ParamsAndNullableStringOverloads_GeneratesDoubleWithPerOverloadConfiguration#TestNamespace.IResponseBuilder_ed393682.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ParamsAndNullableStringOverloads_GeneratesDoubleWithPerOverloadConfiguration#TestNamespace.IResponseBuilder_ed393682.TestDouble.g.verified.cs index fd7bd65..8c10d64 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ParamsAndNullableStringOverloads_GeneratesDoubleWithPerOverloadConfiguration#TestNamespace.IResponseBuilder_ed393682.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ParamsAndNullableStringOverloads_GeneratesDoubleWithPerOverloadConfiguration#TestNamespace.IResponseBuilder_ed393682.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IResponseBuilder_ed393682_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IResponseBuilder_ed393682_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IResponseBuilder_ed393682_Double : global::TestNamespace.IResponseBuilder { @@ -235,6 +244,53 @@ internal static class TestNamespace_IResponseBuilder_ed393682_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IResponseBuilder_ed393682_DoubleReceivedCalls +{ + internal global::TestNamespace_IResponseBuilder_ed393682_Double Instance { get; } + + internal TestNamespace_IResponseBuilder_ed393682_DoubleReceivedCalls(global::TestNamespace_IResponseBuilder_ed393682_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IResponseBuilder_ed393682_ReceivedCallsExtension +{ + public static global::TestNamespace_IResponseBuilder_ed393682_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IResponseBuilder self) => + new(self as global::TestNamespace_IResponseBuilder_ed393682_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IResponseBuilder' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IResponseBuilder', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IResponseBuilder_ed393682_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IResponseBuilder_ed393682_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IResponseBuilder self) + { + var __double = self as global::TestNamespace_IResponseBuilder_ed393682_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IResponseBuilder' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IResponseBuilder', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Speak_1a56931a_lock) { __double.__Speak_1a56931a_calls.Clear(); } + lock (__double.__Speak_22f1fe0a_lock) { __double.__Speak_22f1fe0a_calls.Clear(); } + } +} + internal static class TestNamespace_IResponseBuilder_ed393682_ConfigureExtension { public static global::TestNamespace_IResponseBuilder_ed393682_Double Configure(this global::TestNamespace.IResponseBuilder self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PrivateDefaultInterfaceMethod_GeneratesDoubleIgnoringIt#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PrivateDefaultInterfaceMethod_GeneratesDoubleIgnoringIt#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index b50214a..9437615 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PrivateDefaultInterfaceMethod_GeneratesDoubleIgnoringIt#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PrivateDefaultInterfaceMethod_GeneratesDoubleIgnoringIt#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -89,6 +98,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__GetName.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PrivateDefaultMethodSharesNameWithPublicMember_DoesNotFalselyReportOverload#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PrivateDefaultMethodSharesNameWithPublicMember_DoesNotFalselyReportOverload#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index ba1ab9c..9181982 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PrivateDefaultMethodSharesNameWithPublicMember_DoesNotFalselyReportOverload#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PrivateDefaultMethodSharesNameWithPublicMember_DoesNotFalselyReportOverload#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -49,6 +58,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Get.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyAndMethodBothNamedToStringCollideOnlyViaZeroArgumentCheck#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyAndMethodBothNamedToStringCollideOnlyViaZeroArgumentCheck#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index ff302a6..c80f666 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyAndMethodBothNamedToStringCollideOnlyViaZeroArgumentCheck#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyAndMethodBothNamedToStringCollideOnlyViaZeroArgumentCheck#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -43,6 +52,51 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification { } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyAndZeroParameterMethodShareName_ReportsZeroArgumentExtensionCollisionDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyAndZeroParameterMethodShareName_ReportsZeroArgumentExtensionCollisionDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 0e5b7db..68bb5b7 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyAndZeroParameterMethodShareName_ReportsZeroArgumentExtensionCollisionDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyAndZeroParameterMethodShareName_ReportsZeroArgumentExtensionCollisionDiagnostic#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -43,6 +52,51 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification { } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyCollidesWithZeroParameterOverloadButSiblingOverloadIsUnaffected#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyCollidesWithZeroParameterOverloadButSiblingOverloadIsUnaffected#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 2729229..8f21a8a 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyCollidesWithZeroParameterOverloadButSiblingOverloadIsUnaffected#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyCollidesWithZeroParameterOverloadButSiblingOverloadIsUnaffected#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -187,6 +196,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Value_b9dfaa09_lock) { __double.__Value_b9dfaa09_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyWithNonPublicDefaultSetter_GeneratesGetOnlyDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyWithNonPublicDefaultSetter_GeneratesGetOnlyDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index dd4e1d2..4b40da9 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyWithNonPublicDefaultSetter_GeneratesGetOnlyDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.PropertyWithNonPublicDefaultSetter_GeneratesGetOnlyDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -72,6 +81,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Value.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordNameCollidesWithSiblingMember_FallsBackWithoutRejectingEligibleMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordNameCollidesWithSiblingMember_FallsBackWithoutRejectingEligibleMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs new file mode 100644 index 0000000..4b21db3 --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordNameCollidesWithSiblingMember_FallsBackWithoutRejectingEligibleMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,181 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + internal delegate int __Foo_Callback(int value); + + internal readonly ref struct __Foo_Builder + { + private readonly ref global::Compono.ReturnConfig _config; + private readonly ref __Foo_Callback? _callback; + + internal __Foo_Builder(ref global::Compono.ReturnConfig config, ref __Foo_Callback? callback) + { + _config = ref config; + _callback = ref callback; + } + + public void Returns(int value) + { + _callback = null; + new global::Compono.ReturnConfigBuilder(ref _config).Returns(value); + } + + public void Throws(global::System.Exception exception) + { + _callback = null; + new global::Compono.ReturnConfigBuilder(ref _config).Throws(exception); + } + + public void ReturnsSequence(params global::Compono.SequenceOutcome[] outcomes) + { + _callback = null; + new global::Compono.ReturnConfigBuilder(ref _config).ReturnsSequence(outcomes); + } + + public void ReturnsCallback(__Foo_Callback callback) + { + global::System.ArgumentNullException.ThrowIfNull(callback); + _config.ClearConfiguredResponse(); + _callback = callback; + } + } + internal global::Compono.ReturnConfig __Foo; + internal __Foo_Callback? __Foo_callback; + internal global::Compono.ReturnConfig __Foo_ReceivedCall; + + int global::TestNamespace.IRepository.Foo(int value) + { + __Foo.RecordCall(); + return __Foo_callback is { } callback ? callback(value) + : __Foo.HasConfiguredSequence ? __Foo.NextSequenceOutcome() + : __Foo.HasConfiguredException ? throw __Foo.ConfiguredException + : __Foo.HasConfiguredValue ? __Foo.ConfiguredValue + : default; + } + + void global::TestNamespace.IRepository.Foo_ReceivedCall() + { + __Foo_ReceivedCall.RecordCall(); + if (__Foo_ReceivedCall.HasConfiguredSequence) + __Foo_ReceivedCall.NextSequenceOutcome(); + else if (__Foo_ReceivedCall.HasConfiguredException) + throw __Foo_ReceivedCall.ConfiguredException; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + public static global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder Foo(this global::TestNamespace_IRepository_e3198068_Double self) => + new global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder(ref self.__Foo, ref self.__Foo_callback); + + public static global::Compono.ReturnConfigBuilder Foo_ReceivedCall(this global::TestNamespace_IRepository_e3198068_Double self) => + new global::Compono.ReturnConfigBuilder(ref self.__Foo_ReceivedCall); + +} + +internal readonly struct TestNamespace_IRepository_e3198068_DoubleVerifier +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleVerifier(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_VerifyExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleVerifier Verify(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleVerification +{ + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) => + new(self.Instance.__Foo.ConfiguredCallCount, "global::TestNamespace.IRepository.Foo"); + + public static global::Compono.CallVerifier Foo_ReceivedCall(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) => + new(self.Instance.__Foo_ReceivedCall.ConfiguredCallCount, "global::TestNamespace.IRepository.Foo_ReceivedCall"); + +} + +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Foo.ClearObservedCalls(); + __double.__Foo_ReceivedCall.ClearObservedCalls(); + } +} + +internal static class TestNamespace_IRepository_e3198068_ConfigureExtension +{ + public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => + self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); +} + +file static class TestNamespace_IRepository_e3198068_DoubleRegistration +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() => + global::Compono.GeneratedTestDoubleRegistry.RegisterFactory( + () => new global::TestNamespace_IRepository_e3198068_Double()); +} diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordNameCollidesWithSiblingMember_FallsBackWithoutRejectingEligibleMember#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordNameCollidesWithSiblingMember_FallsBackWithoutRejectingEligibleMember#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 0000000..be9605b --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordNameCollidesWithSiblingMember_FallsBackWithoutRejectingEligibleMember#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.cs +// +#nullable enable + +namespace TestNamespace +{ + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] + file sealed class OrderServiceCompositionPlan : global::Compono.ICompositionPlan + { + public global::TestNamespace.OrderService Compose(global::Compono.ICompositionContext context) => + new global::TestNamespace.OrderService( + context.Resolve(new global::Compono.CompositionRequestDescriptor(global::Compono.CompositionRequestKind.ConstructorParameter, 0, "repository", typeof(global::TestNamespace.OrderService), global::Compono.Nullability.NotNullable)) + ); + } + + file static class OrderServiceCompositionPlanRegistration + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() => + global::Compono.PlanCache.Instance = new OrderServiceCompositionPlan(); + } +} diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithBothRecordTypeAndItsNaiveRenameCandidate_ConsumerCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithBothRecordTypeAndItsNaiveRenameCandidate_ConsumerCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs new file mode 100644 index 0000000..1063f6b --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithBothRecordTypeAndItsNaiveRenameCandidate_ConsumerCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,286 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + internal delegate int __Foo_Callback(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value); + + internal readonly ref struct __Foo_Builder + { + private readonly ref global::Compono.ReturnConfig _config; + private readonly ref __Foo_Callback? _callback; + + internal __Foo_Builder(ref global::Compono.ReturnConfig config, ref __Foo_Callback? callback) + { + _config = ref config; + _callback = ref callback; + } + + public void Returns(int value) + { + _callback = null; + new global::Compono.ReturnConfigBuilder(ref _config).Returns(value); + } + + public void Throws(global::System.Exception exception) + { + _callback = null; + new global::Compono.ReturnConfigBuilder(ref _config).Throws(exception); + } + + public void ReturnsSequence(params global::Compono.SequenceOutcome[] outcomes) + { + _callback = null; + new global::Compono.ReturnConfigBuilder(ref _config).ReturnsSequence(outcomes); + } + + public void ReturnsCallback(__Foo_Callback callback) + { + global::System.ArgumentNullException.ThrowIfNull(callback); + _config.ClearConfiguredResponse(); + _callback = callback; + } + } + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo/__Foo_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_Entry + { + internal global::Compono.Match? Matcher___Foo_ReceivedCall; + internal global::Compono.Match? Matcher___Foo_ReceivedCall_Value; + internal global::Compono.ReturnConfig Config; + internal __Foo_Callback? Callback; + } + + internal readonly global::System.Collections.Generic.List<__Foo_Entry> __Foo_entries = []; + internal readonly global::System.Collections.Generic.List<(int, int)> __Foo_calls = []; + internal readonly object __Foo_lock = new(); + + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Foo_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Foo_ReceivedCall(int __Foo_ReceivedCall_Value2, int __Foo_ReceivedCall_Value); + + int global::TestNamespace.IRepository.Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value) + { + __Foo_Callback? __callback = null; + // ADR-0050: reverse-scan the ordered entry list - last matching registration wins. Both + // the call-log append and the full scan stay under the SAME lock acquisition as + // Configure()'s Add() (Codex review, PR #108 round 5) - the prior split-lock shape (a + // short lock around _calls.Add() only, then an unlocked scan) let a concurrent Configure() + // call mutate List's backing array while dispatch was still iterating it. `return`/ + // `throw` inside a C# `lock` block still releases the lock (try/finally under the hood). + lock (__Foo_lock) + { + __Foo_calls.Add((__Foo_ReceivedCall, __Foo_ReceivedCall_Value)); + for (var __i = __Foo_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_entries[__i]; + if ((__entry.Matcher___Foo_ReceivedCall is not { } __m___Foo_ReceivedCall || __m___Foo_ReceivedCall.Matches(__Foo_ReceivedCall)) && (__entry.Matcher___Foo_ReceivedCall_Value is not { } __m___Foo_ReceivedCall_Value || __m___Foo_ReceivedCall_Value.Matches(__Foo_ReceivedCall_Value))) + { + if (__entry.Callback is { } configuredCallback) + { + __callback = configuredCallback; + break; + } + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - if this entry + // matched but has neither a configured exception nor a configured value (e.g. + // its builder is still being set up when this call arrives), it must NOT shadow + // an older, fully-configured matching entry; the scan continues to the next + // (older) entry instead of falling through to the default/required-config rule. + // ADR-0054: a configured sequence is checked first - Returns/Throws/ReturnsSequence + // are mutually exclusive on one Config, so order between this and the two checks + // below doesn't change behavior, but leads with the newest-added capability. + if (__entry.Config.HasConfiguredSequence) return __entry.Config.NextSequenceOutcome(); + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return __entry.Config.ConfiguredValue; + } + } + } + if (__callback is { } callback) + return callback(__Foo_ReceivedCall, __Foo_ReceivedCall_Value); + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + public static global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match __Foo_ReceivedCall, global::Compono.Match __Foo_ReceivedCall_Value) + { + // ADR-0050: appends a new entry - see the closed-instantiation Configure() + // above for the reallocation-hazard proof, identical reasoning applies here. The Add() + // itself is under the same member lock dispatch scans under (Codex review, PR #108 + // round 5). + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_Entry(); + __entry.Matcher___Foo_ReceivedCall = __Foo_ReceivedCall; + __entry.Matcher___Foo_ReceivedCall_Value = __Foo_ReceivedCall_Value; + lock (__self.__Foo_lock) { __self.__Foo_entries.Add(__entry); } + return new global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder(ref __entry.Config, ref __entry.Callback); + } + + // Compatibility overload (Codex review, PLAN-0048): v1/v2 gave every non-overloaded member a + // zero-argument Configure(), regardless of real arity. ADR-0050: under multi-entry, + // this no longer needs to null out prior matchers to reproduce "last wins" - it just appends its + // own new, all-null-matcher (always-matching) entry; being the most-recently-appended entry, the + // reverse scan finds it before any earlier, more specific entry, exactly reproducing v1/v2's + // argument-independent override behavior without mutating any earlier entry's state at all. + public static global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder Foo(this global::TestNamespace_IRepository_e3198068_Double self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_Entry(); + lock (self.__Foo_lock) { self.__Foo_entries.Add(__entry); } + return new global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder(ref __entry.Config, ref __entry.Callback); + } + +} + +internal readonly struct TestNamespace_IRepository_e3198068_DoubleVerifier +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleVerifier(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_VerifyExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleVerifier Verify(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleVerification +{ + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match __Foo_ReceivedCall, global::Compono.Match __Foo_ReceivedCall_Value) + { + int __count; + lock (__self.Instance.__Foo_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_calls) + { + if (__Foo_ReceivedCall.Matches(call.Item1) && __Foo_ReceivedCall_Value.Matches(call.Item2)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // Compatibility overload - ADR-0050: the removed single-slot field no longer + // tracks a call count at all (RecordCall() is gone from dispatch for this shape) - the call + // log's own Count, under its existing lock, is exactly the same number and is already + // maintained regardless of how many response entries exist. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) + { + int __count; + lock (self.Instance.__Foo_lock) { __count = self.Instance.__Foo_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + +} + +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Foo(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__Foo_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__Foo_ReceivedCall[self.Instance.__Foo_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Foo_calls[__i]; + __snapshot[__i] = new(call.Item1, call.Item2); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Foo_lock) { __double.__Foo_calls.Clear(); } + } +} + +internal static class TestNamespace_IRepository_e3198068_ConfigureExtension +{ + public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => + self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); +} + +file static class TestNamespace_IRepository_e3198068_DoubleRegistration +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() => + global::Compono.GeneratedTestDoubleRegistry.RegisterFactory( + () => new global::TestNamespace_IRepository_e3198068_Double()); +} diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithBothRecordTypeAndItsNaiveRenameCandidate_ConsumerCompiles#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithBothRecordTypeAndItsNaiveRenameCandidate_ConsumerCompiles#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 0000000..be9605b --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithBothRecordTypeAndItsNaiveRenameCandidate_ConsumerCompiles#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.cs +// +#nullable enable + +namespace TestNamespace +{ + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] + file sealed class OrderServiceCompositionPlan : global::Compono.ICompositionPlan + { + public global::TestNamespace.OrderService Compose(global::Compono.ICompositionContext context) => + new global::TestNamespace.OrderService( + context.Resolve(new global::Compono.CompositionRequestDescriptor(global::Compono.CompositionRequestKind.ConstructorParameter, 0, "repository", typeof(global::TestNamespace.OrderService), global::Compono.Nullability.NotNullable)) + ); + } + + file static class OrderServiceCompositionPlanRegistration + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() => + global::Compono.PlanCache.Instance = new OrderServiceCompositionPlan(); + } +} diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithRecordType_ConsumerCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithRecordType_ConsumerCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs new file mode 100644 index 0000000..156239e --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithRecordType_ConsumerCompiles#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,284 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + internal delegate int __Foo_Callback(int __Foo_ReceivedCall); + + internal readonly ref struct __Foo_Builder + { + private readonly ref global::Compono.ReturnConfig _config; + private readonly ref __Foo_Callback? _callback; + + internal __Foo_Builder(ref global::Compono.ReturnConfig config, ref __Foo_Callback? callback) + { + _config = ref config; + _callback = ref callback; + } + + public void Returns(int value) + { + _callback = null; + new global::Compono.ReturnConfigBuilder(ref _config).Returns(value); + } + + public void Throws(global::System.Exception exception) + { + _callback = null; + new global::Compono.ReturnConfigBuilder(ref _config).Throws(exception); + } + + public void ReturnsSequence(params global::Compono.SequenceOutcome[] outcomes) + { + _callback = null; + new global::Compono.ReturnConfigBuilder(ref _config).ReturnsSequence(outcomes); + } + + public void ReturnsCallback(__Foo_Callback callback) + { + global::System.ArgumentNullException.ThrowIfNull(callback); + _config.ClearConfiguredResponse(); + _callback = callback; + } + } + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo/__Foo_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_Entry + { + internal global::Compono.Match? Matcher___Foo_ReceivedCall; + internal global::Compono.ReturnConfig Config; + internal __Foo_Callback? Callback; + } + + internal readonly global::System.Collections.Generic.List<__Foo_Entry> __Foo_entries = []; + internal readonly global::System.Collections.Generic.List __Foo_calls = []; + internal readonly object __Foo_lock = new(); + + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Foo_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Foo_ReceivedCall(int __Foo_ReceivedCall_Value); + + int global::TestNamespace.IRepository.Foo(int __Foo_ReceivedCall) + { + __Foo_Callback? __callback = null; + // ADR-0050: reverse-scan the ordered entry list - last matching registration wins. Both + // the call-log append and the full scan stay under the SAME lock acquisition as + // Configure()'s Add() (Codex review, PR #108 round 5) - the prior split-lock shape (a + // short lock around _calls.Add() only, then an unlocked scan) let a concurrent Configure() + // call mutate List's backing array while dispatch was still iterating it. `return`/ + // `throw` inside a C# `lock` block still releases the lock (try/finally under the hood). + lock (__Foo_lock) + { + __Foo_calls.Add(__Foo_ReceivedCall); + for (var __i = __Foo_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_entries[__i]; + if ((__entry.Matcher___Foo_ReceivedCall is not { } __m___Foo_ReceivedCall || __m___Foo_ReceivedCall.Matches(__Foo_ReceivedCall))) + { + if (__entry.Callback is { } configuredCallback) + { + __callback = configuredCallback; + break; + } + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - if this entry + // matched but has neither a configured exception nor a configured value (e.g. + // its builder is still being set up when this call arrives), it must NOT shadow + // an older, fully-configured matching entry; the scan continues to the next + // (older) entry instead of falling through to the default/required-config rule. + // ADR-0054: a configured sequence is checked first - Returns/Throws/ReturnsSequence + // are mutually exclusive on one Config, so order between this and the two checks + // below doesn't change behavior, but leads with the newest-added capability. + if (__entry.Config.HasConfiguredSequence) return __entry.Config.NextSequenceOutcome(); + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return __entry.Config.ConfiguredValue; + } + } + } + if (__callback is { } callback) + return callback(__Foo_ReceivedCall); + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + public static global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match __Foo_ReceivedCall) + { + // ADR-0050: appends a new entry - see the closed-instantiation Configure() + // above for the reallocation-hazard proof, identical reasoning applies here. The Add() + // itself is under the same member lock dispatch scans under (Codex review, PR #108 + // round 5). + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_Entry(); + __entry.Matcher___Foo_ReceivedCall = __Foo_ReceivedCall; + lock (__self.__Foo_lock) { __self.__Foo_entries.Add(__entry); } + return new global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder(ref __entry.Config, ref __entry.Callback); + } + + // Compatibility overload (Codex review, PLAN-0048): v1/v2 gave every non-overloaded member a + // zero-argument Configure(), regardless of real arity. ADR-0050: under multi-entry, + // this no longer needs to null out prior matchers to reproduce "last wins" - it just appends its + // own new, all-null-matcher (always-matching) entry; being the most-recently-appended entry, the + // reverse scan finds it before any earlier, more specific entry, exactly reproducing v1/v2's + // argument-independent override behavior without mutating any earlier entry's state at all. + public static global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder Foo(this global::TestNamespace_IRepository_e3198068_Double self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_Entry(); + lock (self.__Foo_lock) { self.__Foo_entries.Add(__entry); } + return new global::TestNamespace_IRepository_e3198068_Double.__Foo_Builder(ref __entry.Config, ref __entry.Callback); + } + +} + +internal readonly struct TestNamespace_IRepository_e3198068_DoubleVerifier +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleVerifier(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_VerifyExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleVerifier Verify(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleVerification +{ + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match __Foo_ReceivedCall) + { + int __count; + lock (__self.Instance.__Foo_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_calls) + { + if (__Foo_ReceivedCall.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // Compatibility overload - ADR-0050: the removed single-slot field no longer + // tracks a call count at all (RecordCall() is gone from dispatch for this shape) - the call + // log's own Count, under its existing lock, is exactly the same number and is already + // maintained regardless of how many response entries exist. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) + { + int __count; + lock (self.Instance.__Foo_lock) { __count = self.Instance.__Foo_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + +} + +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Foo(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__Foo_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__Foo_ReceivedCall[self.Instance.__Foo_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Foo_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Foo_lock) { __double.__Foo_calls.Clear(); } + } +} + +internal static class TestNamespace_IRepository_e3198068_ConfigureExtension +{ + public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => + self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); +} + +file static class TestNamespace_IRepository_e3198068_DoubleRegistration +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() => + global::Compono.GeneratedTestDoubleRegistry.RegisterFactory( + () => new global::TestNamespace_IRepository_e3198068_Double()); +} diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithRecordType_ConsumerCompiles#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithRecordType_ConsumerCompiles#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 0000000..be9605b --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallRecordParameterNameCollidesWithRecordType_ConsumerCompiles#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.cs +// +#nullable enable + +namespace TestNamespace +{ + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] + file sealed class OrderServiceCompositionPlan : global::Compono.ICompositionPlan + { + public global::TestNamespace.OrderService Compose(global::Compono.ICompositionContext context) => + new global::TestNamespace.OrderService( + context.Resolve(new global::Compono.CompositionRequestDescriptor(global::Compono.CompositionRequestKind.ConstructorParameter, 0, "repository", typeof(global::TestNamespace.OrderService), global::Compono.Nullability.NotNullable)) + ); + } + + file static class OrderServiceCompositionPlanRegistration + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() => + global::Compono.PlanCache.Instance = new OrderServiceCompositionPlan(); + } +} diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallsNamedMember_ReportsCollisionDiagnostic#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallsNamedMember_ReportsCollisionDiagnostic#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 0000000..be9605b --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallsNamedMember_ReportsCollisionDiagnostic#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.cs +// +#nullable enable + +namespace TestNamespace +{ + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] + file sealed class OrderServiceCompositionPlan : global::Compono.ICompositionPlan + { + public global::TestNamespace.OrderService Compose(global::Compono.ICompositionContext context) => + new global::TestNamespace.OrderService( + context.Resolve(new global::Compono.CompositionRequestDescriptor(global::Compono.CompositionRequestKind.ConstructorParameter, 0, "repository", typeof(global::TestNamespace.OrderService), global::Compono.Nullability.NotNullable)) + ); + } + + file static class OrderServiceCompositionPlanRegistration + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() => + global::Compono.PlanCache.Instance = new OrderServiceCompositionPlan(); + } +} diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallsNamedMember_ReportsCollisionDiagnostic.verified.txt b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallsNamedMember_ReportsCollisionDiagnostic.verified.txt new file mode 100644 index 0000000..df79014 --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.ReceivedCallsNamedMember_ReportsCollisionDiagnostic.verified.txt @@ -0,0 +1,18 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (14,65)-(14,91), + Message: 'TestNamespace.IRepository' declares its own member named 'ReceivedCalls', which would silently shadow the generated ReceivedCalls() extension the double's configuration/verification surface depends on. This leaf falls back to the ordinary runtime-provider path., + Severity: Info, + WarningLevel: 1, + Descriptor: { + Id: CMP0023, + Title: Test-double interface member collides with a generated Configure()/Verify()/ReceivedCalls()/ClearCalls() bridge, + MessageFormat: '{0}' declares its own member named '{1}', which would silently shadow the generated {1}() extension the double's configuration/verification surface depends on. This leaf falls back to the ordinary runtime-provider path., + Category: Compono.TestDoubles, + DefaultSeverity: Info, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 24ac55c..ca79f89 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -327,6 +336,53 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Seek_b9dfaa09_lock) { __double.__Seek_b9dfaa09_calls.Clear(); } + lock (__double.__Seek_1a56931a_lock) { __double.__Seek_1a56931a_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefReadOnlyParameter_GeneratesDoubleWithMatchingExplicitImplementationSignature#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefReadOnlyParameter_GeneratesDoubleWithMatchingExplicitImplementationSignature#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index e8d3d41..0c95cff 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefReadOnlyParameter_GeneratesDoubleWithMatchingExplicitImplementationSignature#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefReadOnlyParameter_GeneratesDoubleWithMatchingExplicitImplementationSignature#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -18,6 +27,22 @@ internal sealed class __Seek_Entry internal readonly global::System.Collections.Generic.List __Seek_calls = []; internal readonly object __Seek_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Seek_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Seek_ReceivedCall(int offset); + void global::TestNamespace.IRepository.Seek(ref readonly int offset) { } @@ -137,6 +162,69 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Seek(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__Seek_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__Seek_ReceivedCall[self.Instance.__Seek_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Seek_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__Seek_lock) { __double.__Seek_calls.Clear(); } + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.SoloGenericConfigureMember_DoesNotCollideWithBridge#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.SoloGenericConfigureMember_DoesNotCollideWithBridge#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs index e546d79..143397c 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.SoloGenericConfigureMember_DoesNotCollideWithBridge#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.SoloGenericConfigureMember_DoesNotCollideWithBridge#TestNamespace.IThing_7b7b47c0.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IThing_7b7b47c0_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IThing_7b7b47c0_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IThing_7b7b47c0_Double : global::TestNamespace.IThing { @@ -49,6 +58,52 @@ internal static class TestNamespace_IThing_7b7b47c0_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IThing_7b7b47c0_DoubleReceivedCalls +{ + internal global::TestNamespace_IThing_7b7b47c0_Double Instance { get; } + + internal TestNamespace_IThing_7b7b47c0_DoubleReceivedCalls(global::TestNamespace_IThing_7b7b47c0_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IThing_7b7b47c0_ReceivedCallsExtension +{ + public static global::TestNamespace_IThing_7b7b47c0_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IThing self) => + new(self as global::TestNamespace_IThing_7b7b47c0_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IThing' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IThing', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IThing_7b7b47c0_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IThing_7b7b47c0_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IThing self) + { + var __double = self as global::TestNamespace_IThing_7b7b47c0_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IThing' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IThing', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Configure.ClearObservedCalls(); + } +} + internal static class TestNamespace_IThing_7b7b47c0_ConfigureExtension { public static global::TestNamespace_IThing_7b7b47c0_Double Configure(this global::TestNamespace.IThing self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractMemberResolvedByDerivedInterface_DoesNotCollideWithSameNamedInstanceMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractMemberResolvedByDerivedInterface_DoesNotCollideWithSameNamedInstanceMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 2e65e46..1da7102 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractMemberResolvedByDerivedInterface_DoesNotCollideWithSameNamedInstanceMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractMemberResolvedByDerivedInterface_DoesNotCollideWithSameNamedInstanceMember#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -90,6 +99,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Name.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractMethodResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractMethodResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index a7917be..1950fcb 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractMethodResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractMethodResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -52,6 +61,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Name.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractOperatorResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractOperatorResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index a7917be..1950fcb 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractOperatorResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractOperatorResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -52,6 +61,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Name.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractPropertyResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractPropertyResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index a7917be..1950fcb 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractPropertyResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.StaticAbstractPropertyResolvedByDerivedInterface_GeneratesUnaffectedDouble#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -52,6 +61,52 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + __double.__Name.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.TestDoublesEnabled_InterfaceLeaf_GeneratesDoubleReachableFromAnotherNamespace#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.TestDoublesEnabled_InterfaceLeaf_GeneratesDoubleReachableFromAnotherNamespace#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs index 64df451..376d0df 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.TestDoublesEnabled_InterfaceLeaf_GeneratesDoubleReachableFromAnotherNamespace#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.TestDoublesEnabled_InterfaceLeaf_GeneratesDoubleReachableFromAnotherNamespace#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -2,6 +2,15 @@ // #nullable enable +// PLAN-0063/ADR-0060: ClearCalls() reaches every ADR-0049 closed-instantiation-eligible member's +// per-closed-T bucket entries generically, with no static knowledge of which closed T's exist at +// runtime - this non-generic interface (implemented by every generated +// TestNamespace_IRepository_e3198068_*State class below) is what makes that possible without reflection. +internal interface TestNamespace_IRepository_e3198068_IClearableCallState +{ + void ClearObservedCalls(); +} + [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { @@ -57,6 +66,22 @@ internal sealed class __FindNameAsync_Entry internal readonly global::System.Collections.Generic.List<__FindNameAsync_Entry> __FindNameAsync_entries = []; internal readonly global::System.Collections.Generic.List __FindNameAsync_calls = []; internal readonly object __FindNameAsync_lock = new(); + + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __FindNameAsync_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __FindNameAsync_ReceivedCall(global::System.Guid id); // ADR-0050: multi-entry response configuration - replaces the single // __Save/__Save_m_{param} shape with an ordered, append-only // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). @@ -70,6 +95,22 @@ internal sealed class __Save_Entry internal readonly global::System.Collections.Generic.List __Save_calls = []; internal readonly object __Save_lock = new(); + // PLAN-0063/ADR-0060: named snapshot record backing ReceivedCalls() for this eligible member - + // one field per real parameter, real parameter names (not the positional "Item1"/"Item2" the + // internal __Save_calls tuple above uses for its own, unrelated, internal-only + // matching purpose). Not emitted for an overload-matching-eligible member - ReceivedCalls() is + // scoped to exactly ADR-0048's non-overloaded eligible-member set for 1.1. + // A parameter literally named the same as this record's own type (e.g. a real member + // `Foo(int __Foo_ReceivedCall)`) would otherwise produce a positional property with the same + // name as its enclosing type - CS0542 - since a record's declared parameter name IS its public + // property name. member.received_call_record_parameters_text (TestDoubleMemberInfo.ReceivedCallRecordParametersText) + // renames just that one parameter to a name guaranteed free of every OTHER real parameter's own + // name too, not merely an unconditional "_Value" suffix - Codex review, PR #134 round 2 caught a + // real fixture where the unconditional suffix collided with an actual second parameter already + // named that (`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`). Rendered fully in C# + // rather than as an indexed Scriban loop - see the property's own remarks for why. + internal readonly record struct __Save_ReceivedCall(string name); + global::System.Threading.Tasks.Task global::TestNamespace.IRepository.FindNameAsync(global::System.Guid id) { __FindNameAsync_Callback? __callback = null; @@ -300,6 +341,88 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification } +// PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, scoped to +// exactly the ADR-0048 eligible-member set (member.is_eligible_for_matching), unexpanded. A +// distinct third bridge from Configure()/Verify() - never attaches captured data to CallVerifier. +internal readonly struct TestNamespace_IRepository_e3198068_DoubleReceivedCalls +{ + internal global::TestNamespace_IRepository_e3198068_Double Instance { get; } + + internal TestNamespace_IRepository_e3198068_DoubleReceivedCalls(global::TestNamespace_IRepository_e3198068_Double instance) => Instance = instance; +} + +internal static class TestNamespace_IRepository_e3198068_ReceivedCallsExtension +{ + public static global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls ReceivedCalls(this global::TestNamespace.IRepository self) => + new(self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test.")); +} + +internal static class TestNamespace_IRepository_e3198068_DoubleReceivedCallsAccess +{ + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList FindNameAsync(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__FindNameAsync_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__FindNameAsync_ReceivedCall[self.Instance.__FindNameAsync_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__FindNameAsync_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + + // Snapshot under the same ADR-0048 per-member lock Verify()'s own scan already uses - no live + // mutable collection is ever returned, per ADR-0060's snapshot-semantics decision. + public static global::System.Collections.Generic.IReadOnlyList Save(this global::TestNamespace_IRepository_e3198068_DoubleReceivedCalls self) + { + lock (self.Instance.__Save_lock) + { + var __snapshot = new global::TestNamespace_IRepository_e3198068_Double.__Save_ReceivedCall[self.Instance.__Save_calls.Count]; + for (var __i = 0; __i < __snapshot.Length; __i++) + { + var call = self.Instance.__Save_calls[__i]; + __snapshot[__i] = new(call); + } + + return __snapshot; + } + } + +} + +// PLAN-0063/ADR-0060: ClearCalls() - a direct, whole-double operation clearing every member's +// observation state (scalar call counts + ADR-0048 captured argument histories) while preserving +// all configured behavior (Returns/Throws/ReturnsCallback/ReturnsSequence - including in-progress +// SequenceOrdinal, which does not rewind - argument-matcher/multi-entry configuration, and generic +// closed-instantiation configuration). No per-member ClearCalls(), no global whole-double lock - +// each member's own existing synchronization primitive is used independently, per member. +internal static class TestNamespace_IRepository_e3198068_ClearCallsExtension +{ + public static void ClearCalls(this global::TestNamespace.IRepository self) + { + var __double = self as global::TestNamespace_IRepository_e3198068_Double + ?? throw new global::System.InvalidOperationException( + $"'{self.GetType()}' is not the 'global::TestNamespace.IRepository' test double generated for this assembly. " + + "If another assembly in this process also generated a double for 'global::TestNamespace.IRepository', only one " + + "registration wins process-wide (Compono.GeneratedTestDoubleRegistry, first-registration-wins) " + + "- this is a known v1 limitation, not a bug in your test."); + + lock (__double.__FindNameAsync_lock) { __double.__FindNameAsync_calls.Clear(); } + lock (__double.__Save_lock) { __double.__Save_calls.Clear(); } + __double.__Count.ClearObservedCalls(); + } +} + internal static class TestNamespace_IRepository_e3198068_ConfigureExtension { public static global::TestNamespace_IRepository_e3198068_Double Configure(this global::TestNamespace.IRepository self) => diff --git a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.VerifyNamedMember_ReportsCollisionDiagnostic.verified.txt b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.VerifyNamedMember_ReportsCollisionDiagnostic.verified.txt index 0395f89..449ba82 100644 --- a/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.VerifyNamedMember_ReportsCollisionDiagnostic.verified.txt +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.VerifyNamedMember_ReportsCollisionDiagnostic.verified.txt @@ -7,7 +7,7 @@ WarningLevel: 1, Descriptor: { Id: CMP0023, - Title: Test-double interface member collides with a generated Configure()/Verify() bridge, + Title: Test-double interface member collides with a generated Configure()/Verify()/ReceivedCalls()/ClearCalls() bridge, MessageFormat: '{0}' declares its own member named '{1}', which would silently shadow the generated {1}() extension the double's configuration/verification surface depends on. This leaf falls back to the ordinary runtime-provider path., Category: Compono.TestDoubles, DefaultSeverity: Info, diff --git a/test/Compono.Generators.Tests/TestDoubleVerifyTests.cs b/test/Compono.Generators.Tests/TestDoubleVerifyTests.cs index 3f40209..a384749 100644 --- a/test/Compono.Generators.Tests/TestDoubleVerifyTests.cs +++ b/test/Compono.Generators.Tests/TestDoubleVerifyTests.cs @@ -1157,6 +1157,162 @@ public static class EntryPoint "CMP0023", TestContext.Current.CancellationToken); + // PLAN-0063/ADR-0060 (Codex review, PR #134): ClearCalls()/ReceivedCalls() are always-emitted, + // always-zero-argument bridge extensions exactly like Configure()/Verify() - an interface member + // of either name applicable to a zero-argument call wins ordinary member lookup over the + // extension, silently leaving e.g. ClearCalls() a no-op. Must be caught by the same CMP0023 + // reserved-name collision check. + [Fact] + public Task ClearCallsNamedMember_ReportsCollisionDiagnostic() => + GeneratorTestHelpers.VerifyFailure( + new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + void ClearCalls(); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run() => Compono.Composer.Create().Create(); + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, + "CMP0023", + TestContext.Current.CancellationToken); + + [Fact] + public Task ReceivedCallsNamedMember_ReportsCollisionDiagnostic() => + GeneratorTestHelpers.VerifyFailure( + new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + void ReceivedCalls(); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run() => Compono.Composer.Create().Create(); + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, + "CMP0023", + TestContext.Current.CancellationToken); + + // PLAN-0063/ADR-0060 (Codex review, PR #134): a real sibling member whose own natural field name + // exactly equals an eligible member's generated ReceivedCallClassName ("__{Name}_ReceivedCall") + // must demote the eligible member out of matching eligibility (its plain configuration surface + // still works) rather than emit two identically-named declarations (a real CS0102 in the consumer). + [Fact] + public Task ReceivedCallRecordNameCollidesWithSiblingMember_FallsBackWithoutRejectingEligibleMember() => + GeneratorTestHelpers.Verify( + new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + int Foo(int value); + void Foo_ReceivedCall(); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run() => Compono.Composer.Create().Create(); + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, + TestContext.Current.CancellationToken); + + // PLAN-0063/ADR-0060 (Codex review, PR #134): a parameter literally named the same as its own + // eligible member's generated ReceivedCallClassName must not produce a record positional property + // with the same name as its enclosing type (CS0542) - the generated record renames just that one + // property, and the assertion here is simply that the whole double still compiles. + [Fact] + public Task ReceivedCallRecordParameterNameCollidesWithRecordType_ConsumerCompiles() => + GeneratorTestHelpers.Verify( + new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + int Foo(int __Foo_ReceivedCall); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run() => Compono.Composer.Create().Create(); + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, + TestContext.Current.CancellationToken); + + // PLAN-0063/ADR-0060 (Codex review, PR #134 round 2): fresh evidence for the fix above - a real + // SECOND parameter already literally named the naive "_Value"-suffixed candidate + // (__Foo_ReceivedCall_Value) makes an unconditional rename collide with it too, producing a + // duplicate positional property. The generated record must pick a name free of every real + // parameter, not just the one it's renaming - here that means "__Foo_ReceivedCall_Value2". + [Fact] + public Task ReceivedCallRecordParameterNameCollidesWithBothRecordTypeAndItsNaiveRenameCandidate_ConsumerCompiles() => + GeneratorTestHelpers.Verify( + new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + int Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run() => Compono.Composer.Create().Create(); + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, + TestContext.Current.CancellationToken); + [Fact] public Task NullableReferenceReturnAndParameter_PreservesAnnotationInGeneratedCode() => GeneratorTestHelpers.Verify(new CodeGenerationOptions diff --git a/test/Compono.Http.Tests/TestHttpHandlerTests.cs b/test/Compono.Http.Tests/TestHttpHandlerTests.cs index 5fb578e..ef723fd 100644 --- a/test/Compono.Http.Tests/TestHttpHandlerTests.cs +++ b/test/Compono.Http.Tests/TestHttpHandlerTests.cs @@ -170,6 +170,26 @@ public async Task RepeatedMatches_GetFreshResponseAndContentEachTime() registration.Verify().Exactly(2); } + // PLAN-0063/ADR-0044 Amendment 22: AtLeast/AtMost are reachable through + // HttpResponseRegistration.Verify() with zero package-side code changes (CallVerifier is + // returned directly by HttpResponseRegistration.Verify()). + [Fact] + public async Task Verify_AtLeastAndAtMost_AreReachableWithNoPackageCodeChanges() + { + using var handler = new TestHttpHandler(); + var registration = handler.OnGet("/users/42").Respond(HttpStatusCode.OK); + + using var client = handler.CreateClient(new Uri("https://api.example.com/")); + await client.GetAsync("/users/42", TestContext.Current.CancellationToken); + await client.GetAsync("/users/42", TestContext.Current.CancellationToken); + + registration.Verify().AtLeast(2); + registration.Verify().AtMost(2); + + var act = () => registration.Verify().AtLeast(3); + act.Should().Throw(); + } + [Fact] public async Task RespondJson_SetsJsonContentTypeWithUtf8Charset() { diff --git a/test/Compono.Logging.Tests/LogVerificationBuilderTests.cs b/test/Compono.Logging.Tests/LogVerificationBuilderTests.cs index 74b6a51..2acbd66 100644 --- a/test/Compono.Logging.Tests/LogVerificationBuilderTests.cs +++ b/test/Compono.Logging.Tests/LogVerificationBuilderTests.cs @@ -62,6 +62,44 @@ public void Exactly_ThrowsOnWrongCount() act.Should().Throw(); } + // PLAN-0063/ADR-0044 Amendment 22: AtLeast/AtMost forwarders - proving filtering happens BEFORE + // the count terminal, not merely that the methods compile. + + [Fact] + public void AtLeast_CountsOnlyTheFilteredSubset_NotTheWholeCaptureBuffer() + { + var logger = new CapturingLogger(); + logger.LogWarning("one"); + logger.LogWarning("two"); + logger.LogInformation("unrelated"); + logger.LogInformation("also unrelated"); + + // Four entries total, only two at Warning - AtLeast(2) must pass against the filtered count, + // and AtLeast(3) must fail even though four entries exist overall. + logger.Verify().AtLevel(LogLevel.Warning).AtLeast(2); + + var act = () => logger.Verify().AtLevel(LogLevel.Warning).AtLeast(3); + act.Should().Throw() + .WithMessage("Expected at least 3 call(s) to a log entry matching level Warning, but received 2."); + } + + [Fact] + public void AtMost_CountsOnlyTheFilteredSubset_NotTheWholeCaptureBuffer() + { + var logger = new CapturingLogger(); + logger.LogWarning("one"); + logger.LogWarning("two"); + logger.LogInformation("unrelated"); + logger.LogInformation("also unrelated"); + + // AtMost(2) passes against the filtered Warning-only count even though four entries exist. + logger.Verify().AtLevel(LogLevel.Warning).AtMost(2); + + var act = () => logger.Verify().AtLevel(LogLevel.Warning).AtMost(1); + act.Should().Throw() + .WithMessage("Expected at most 1 call(s) to a log entry matching level Warning, but received 2."); + } + [Fact] public void AtLevel_Alone_NarrowsToThatLevel() { diff --git a/test/Compono.TestDoubles.AotSmokeTest/Program.cs b/test/Compono.TestDoubles.AotSmokeTest/Program.cs index 110b694..694ed38 100644 --- a/test/Compono.TestDoubles.AotSmokeTest/Program.cs +++ b/test/Compono.TestDoubles.AotSmokeTest/Program.cs @@ -246,6 +246,51 @@ private static async Task Main() .Once(); accountRepository.Verify().Withdraw().Exactly(4); + // PLAN-0063/ADR-0044 Amendment 22: CallVerifier.AtLeast/AtMost, reachable through the + // generated Verify() bridge with no package-side code changes, under Native AOT. + accountRepository.Verify().Withdraw().AtLeast(4); + accountRepository.Verify().Withdraw().AtMost(4); + + var atLeastShouldFail = true; + try + { + accountRepository.Verify().Withdraw().AtLeast(5); + atLeastShouldFail = false; + } + catch (TestDoubleVerificationException) + { + // expected + } + + if (!atLeastShouldFail) + throw new InvalidOperationException("Expected AtLeast(5) to throw when only 4 calls were observed."); + + // PLAN-0063/ADR-0060: ReceivedCalls() - retrospective, snapshot-based call inspection, + // under Native AOT (no reflection, no dynamic code generation). + var receivedWithdrawCalls = accountRepository.ReceivedCalls().Withdraw(); + + if (receivedWithdrawCalls.Count != 4) + throw new InvalidOperationException($"Expected 4 received Withdraw() calls, got {receivedWithdrawCalls.Count}."); + + if (receivedWithdrawCalls[0].accountId != "acct-1") + throw new InvalidOperationException("Expected the first received call's accountId to be 'acct-1'."); + + // PLAN-0063/ADR-0060: ClearCalls() - whole-double observation reset, preserving configured + // behavior, under Native AOT. + accountRepository.ClearCalls(); + + accountRepository.Verify().Withdraw().Never(); + + if (accountRepository.ReceivedCalls().Withdraw().Count != 0) + throw new InvalidOperationException("Expected ReceivedCalls() to be empty immediately after ClearCalls()."); + + var postClearCall = accountRepository.Withdraw("acct-1", 1m, overdraftAllowed: true); + + if (!postClearCall) + throw new InvalidOperationException("Expected ClearCalls() to preserve the configured Match.Is entry for acct-1."); + + accountRepository.Verify().Withdraw().Once(); + // ADR-0053: the generated strongly typed callback delegate and member-specific builder // survive trimming/AOT and receive the invocation's real arguments. accountRepository.Configure() @@ -386,6 +431,18 @@ private static async Task Main() $"Expected independent per-entry sequence ordinals (false,true / true,false), got " + $"({seq1First},{seq2First},{seq1Second},{seq2Second})."); + // PLAN-0063/ADR-0060: ClearCalls() must never rewind a configured sequence's ordinal, + // under Native AOT - acct-seq-1's sequence (false, true) has already been fully consumed + // above; after ClearCalls(), the next call must repeat the final outcome (true, per + // ADR-0054's exhaustion semantics), not rewind to the first (false). + accountRepository.ClearCalls(); + var seq1AfterClear = accountRepository.Withdraw("acct-seq-1", 1m, true); + + if (!seq1AfterClear) + throw new InvalidOperationException( + "Expected ClearCalls() to leave the acct-seq-1 sequence ordinal exhausted at its " + + "final (true) outcome, not rewind it back to the first (false) outcome."); + // ADR-0044 Amendment 21: overload-safe argument matching under Native AOT - coexistence/ // precedence (a broad discriminator-only Configure() registered first, a narrower // .Matching(...) override registered after it - the SUT-visible dispatch always goes diff --git a/test/Compono.TestDoubles.SampleTests/ReceivedCallsAndClearCallsTests.cs b/test/Compono.TestDoubles.SampleTests/ReceivedCallsAndClearCallsTests.cs new file mode 100644 index 0000000..d8077e8 --- /dev/null +++ b/test/Compono.TestDoubles.SampleTests/ReceivedCallsAndClearCallsTests.cs @@ -0,0 +1,232 @@ +using Compono.XunitV3; + +namespace Compono.TestDoubles.SampleTests; + +// PLAN-0063/ADR-0060: a mutable reference-type argument, used to prove ReceivedCalls()'s explicit +// no-deep-copy, reference-retention capture semantics - the retained record observes a mutation made +// to the argument object after the call returns. +public sealed class MutableRecord +{ + public int Value { get; set; } +} + +public interface IArchiver +{ + void Archive(MutableRecord record); +} + +// PLAN-0063/ADR-0060: a single-parameter, ADR-0048-eligible, non-void member used for the +// ClearCalls()-does-not-rewind-ReturnsSequence proof (ADR-0060's own worked example). +public interface ILedger +{ + string NextValue(string key); +} + +public sealed class ReceivedCallsTests +{ + [Theory] + [Compose] + public void SingleCall_ReturnsOneRecordWithTheRealArgumentValues( + [Shared] IAccountRepository repository) + { + repository.Withdraw("acct-1", 50m, overdraftAllowed: true); + + var calls = repository.ReceivedCalls().Withdraw(); + + calls.Should().HaveCount(1); + calls[0].accountId.Should().Be("acct-1"); + calls[0].amount.Should().Be(50m); + calls[0].overdraftAllowed.Should().BeTrue(); + } + + [Theory] + [Compose] + public void MultipleCalls_PreservesAppendOrder( + [Shared] IAccountRepository repository) + { + repository.Withdraw("acct-1", 10m, overdraftAllowed: false); + repository.Withdraw("acct-2", 20m, overdraftAllowed: true); + repository.Withdraw("acct-3", 30m, overdraftAllowed: false); + + var calls = repository.ReceivedCalls().Withdraw(); + + calls.Should().HaveCount(3); + calls[0].accountId.Should().Be("acct-1"); + calls[1].accountId.Should().Be("acct-2"); + calls[2].accountId.Should().Be("acct-3"); + } + + [Theory] + [Compose] + public void SingleParameterMember_ReceivedCallsUsesTheBareParameterShape_NotATuple( + [Shared] IAccountRepository repository) + { + repository.Rename("acct-1"); + + var calls = repository.ReceivedCalls().Rename(); + + calls.Should().ContainSingle().Which.accountId.Should().Be("acct-1"); + } + + // ADR-0060 snapshot semantics: a later invocation must never retroactively grow an + // already-returned ReceivedCalls() snapshot. + [Theory] + [Compose] + public void SnapshotIsolation_LaterInvocationDoesNotAffectAnEarlierSnapshot( + [Shared] IAccountRepository repository) + { + repository.Withdraw("acct-1", 10m, overdraftAllowed: false); + + var firstSnapshot = repository.ReceivedCalls().Withdraw(); + repository.Withdraw("acct-2", 20m, overdraftAllowed: true); + var secondSnapshot = repository.ReceivedCalls().Withdraw(); + + firstSnapshot.Should().HaveCount(1, "the earlier snapshot must not observe a call made after it was taken"); + secondSnapshot.Should().HaveCount(2); + } + + // ADR-0060 capture semantics: reference types are retained by reference, not deep-copied - a + // mutation made to the argument object after the call returns IS observed by a later + // ReceivedCalls() inspection. This is a documented, deliberate footgun, not a bug. + [Theory] + [Compose] + public void ReferenceTypeArgument_IsRetainedByReference_LaterMutationIsObserved( + [Shared] IArchiver archiver) + { + var record = new MutableRecord { Value = 1 }; + archiver.Archive(record); + + record.Value = 2; + + var calls = archiver.ReceivedCalls().Archive(); + calls.Should().ContainSingle().Which.record.Value.Should().Be(2, + "ReceivedCalls() stores the same reference the caller passed, per ADR-0060 - no deep copy"); + } + + // ADR-0060 capture semantics: value-type arguments are ordinary C# value copies - mutating the + // caller's own local after the call has no effect on the already-captured value. + [Theory] + [Compose] + public void ValueTypeArgument_IsCopiedAtCallTime_LaterLocalMutationIsNotObserved( + [Shared] IAccountRepository repository) + { + var amount = 10m; + repository.Withdraw("acct-1", amount, overdraftAllowed: false); + amount = 999m; + + var calls = repository.ReceivedCalls().Withdraw(); + + calls.Should().ContainSingle().Which.amount.Should().Be(10m); + } +} + +public sealed class ClearCallsTests +{ + [Theory] + [Compose] + public void ClearCalls_ResetsCallCountAndReceivedCalls( + [Shared] IAccountRepository repository) + { + repository.Withdraw("acct-1", 10m, overdraftAllowed: false); + repository.Withdraw("acct-2", 20m, overdraftAllowed: true); + + repository.ClearCalls(); + + repository.Verify().Withdraw().Never(); + repository.ReceivedCalls().Withdraw().Should().BeEmpty(); + } + + [Theory] + [Compose] + public void ClearCalls_PreservesConfiguredReturnValue( + [Shared] IAccountRepository repository) + { + repository.Configure().Withdraw().Returns(true); + repository.Withdraw("acct-1", 10m, overdraftAllowed: false); + + repository.ClearCalls(); + + repository.Withdraw("acct-2", 20m, overdraftAllowed: false).Should().BeTrue( + "ClearCalls() must preserve configured behavior - only observation history is reset"); + } + + [Theory] + [Compose] + public void ClearCalls_PreservesMultiEntryConfiguration( + [Shared] IAccountRepository repository) + { + repository.Configure() + .Withdraw("acct-1", Compono.Match.Any(), Compono.Match.Any()) + .Returns(true); + repository.Configure() + .Withdraw("acct-2", Compono.Match.Any(), Compono.Match.Any()) + .Returns(false); + repository.Withdraw("acct-1", 1m, overdraftAllowed: false); + + repository.ClearCalls(); + + repository.Withdraw("acct-1", 1m, overdraftAllowed: false).Should().BeTrue(); + repository.Withdraw("acct-2", 1m, overdraftAllowed: false).Should().BeFalse(); + repository.Verify() + .Withdraw(Compono.Match.Is(id => id == "acct-1"), Compono.Match.Any(), Compono.Match.Any()) + .Once(); + } + + // ADR-0060's own worked example: ReturnsSequence(A, B, C), two calls consume A then B, + // ClearCalls() runs, the next call must return C - not rewind to A. + [Theory] + [Compose] + public void ClearCalls_DoesNotRewindAConfiguredSequence( + [Shared] ILedger ledger) + { + ledger.Configure().NextValue().ReturnsSequence("A", "B", "C"); + + ledger.NextValue("key").Should().Be("A"); + ledger.NextValue("key").Should().Be("B"); + + ledger.ClearCalls(); + + ledger.NextValue("key").Should().Be("C", + "SequenceOrdinal is configured-behavior progress, not observation history - ClearCalls() must never rewind it"); + // ClearCalls() reset the call count even though the sequence itself did not rewind - only + // the third (post-clear) call is observed. + ledger.Verify().NextValue().Once(); + } + + // Deterministic concurrency proof: many concurrent invocations racing one ClearCalls() call must + // never corrupt the call log or throw - each call either lands fully before or fully after the + // clear, per ADR-0060's synchronization contract. + [Theory] + [Compose] + public async Task ClearCalls_RacingConcurrentInvocations_NeverThrowsOrCorruptsState( + [Shared] IAccountRepository repository) + { + const int iterations = 200; + using var barrier = new Barrier(2); + + var cancellationToken = TestContext.Current.CancellationToken; + + var callerTask = Task.Run(() => + { + barrier.SignalAndWait(); + for (var i = 0; i < iterations; i++) + repository.Withdraw("acct-1", 1m, overdraftAllowed: false); + }, cancellationToken); + + var clearerTask = Task.Run(() => + { + barrier.SignalAndWait(); + for (var i = 0; i < iterations; i++) + repository.ClearCalls(); + }, cancellationToken); + + var act = async () => await Task.WhenAll(callerTask, clearerTask); + + await act.Should().NotThrowAsync(); + + // Whatever call count survives the race is a legal outcome (0..iterations) - the invariant + // under test is "no exception, no torn state", not a specific final count. + var finalCalls = repository.ReceivedCalls().Withdraw(); + finalCalls.Count.Should().BeGreaterThanOrEqualTo(0).And.BeLessThanOrEqualTo(iterations); + } +} diff --git a/test/Compono.Tests/CallVerifierTests.cs b/test/Compono.Tests/CallVerifierTests.cs index d7e6aed..bf81b6b 100644 --- a/test/Compono.Tests/CallVerifierTests.cs +++ b/test/Compono.Tests/CallVerifierTests.cs @@ -81,6 +81,180 @@ public void Exactly_WhenCountDiffers_ThrowsWithMessage() .WithMessage("Expected exactly 5 call(s) to IFoo.Bar, but received 3."); } + // PLAN-0063/ADR-0044 Amendment 22: AtLeast/AtMost boundary tests (below/equal/above), negative- + // count behavior matching Exactly's existing (non-)validation, and AtLeast(0)/AtMost(0) edge cases. + + [Fact] + public void AtLeast_WhenObservedCountIsBelowThreshold_ThrowsWithMessage() + { + var verifier = new CallVerifier(2, "IFoo.Bar"); + + var act = () => verifier.AtLeast(3); + + act.Should().Throw() + .WithMessage("Expected at least 3 call(s) to IFoo.Bar, but received 2."); + } + + [Fact] + public void AtLeast_WhenObservedCountEqualsThreshold_DoesNotThrow() + { + var verifier = new CallVerifier(3, "IFoo.Bar"); + + var act = () => verifier.AtLeast(3); + + act.Should().NotThrow(); + } + + [Fact] + public void AtLeast_WhenObservedCountIsAboveThreshold_DoesNotThrow() + { + var verifier = new CallVerifier(4, "IFoo.Bar"); + + var act = () => verifier.AtLeast(3); + + act.Should().NotThrow(); + } + + [Fact] + public void AtLeast_Zero_AlwaysPasses() + { + var verifier = new CallVerifier(0, "IFoo.Bar"); + + var act = () => verifier.AtLeast(0); + + act.Should().NotThrow(); + } + + [Fact] + public void AtLeast_NegativeThreshold_BehavesLikeExactlyAlwaysVacuouslyTrue() + { + var verifier = new CallVerifier(0, "IFoo.Bar"); + + var act = () => verifier.AtLeast(-1); + + act.Should().NotThrow("observedCount can never be negative, so AtLeast(-1) can never fail, " + + "matching Exactly's existing no-argument-validation behavior (ADR-0044 Amendment 22)"); + } + + [Fact] + public void AtMost_WhenObservedCountIsAboveThreshold_ThrowsWithMessage() + { + var verifier = new CallVerifier(4, "IFoo.Bar"); + + var act = () => verifier.AtMost(3); + + act.Should().Throw() + .WithMessage("Expected at most 3 call(s) to IFoo.Bar, but received 4."); + } + + [Fact] + public void AtMost_WhenObservedCountEqualsThreshold_DoesNotThrow() + { + var verifier = new CallVerifier(3, "IFoo.Bar"); + + var act = () => verifier.AtMost(3); + + act.Should().NotThrow(); + } + + [Fact] + public void AtMost_WhenObservedCountIsBelowThreshold_DoesNotThrow() + { + var verifier = new CallVerifier(2, "IFoo.Bar"); + + var act = () => verifier.AtMost(3); + + act.Should().NotThrow(); + } + + [Fact] + public void AtMost_Zero_EquivalentToNever_WhenNeverCalled() + { + var verifier = new CallVerifier(0, "IFoo.Bar"); + + var act = () => verifier.AtMost(0); + + act.Should().NotThrow(); + } + + [Fact] + public void AtMost_Zero_EquivalentToNever_WhenCalled() + { + var verifier = new CallVerifier(1, "IFoo.Bar"); + + var act = () => verifier.AtMost(0); + + act.Should().Throw() + .WithMessage("Expected at most 0 call(s) to IFoo.Bar, but received 1."); + } + + [Fact] + public void AtMost_NegativeThreshold_ThrowsBecauseObservedCountCanNeverBeNegative() + { + var verifier = new CallVerifier(0, "IFoo.Bar"); + + var act = () => verifier.AtMost(-1); + + act.Should().Throw( + "0 > -1, matching Exactly's existing no-argument-validation behavior (ADR-0044 Amendment 22)"); + } + + // PLAN-0063/ADR-0060: ReturnConfig.ClearObservedCalls() - the mirror of ClearConfiguredResponse(), + // clearing only CallCount, never Value/Exception/Sequence/SequenceOrdinal. + + [Fact] + public void ClearObservedCalls_ResetsCallCountToZero() + { + var slot = new ReturnConfig(); + slot.RecordCall(); + slot.RecordCall(); + + slot.ClearObservedCalls(); + + slot.ConfiguredCallCount.Should().Be(0); + } + + [Fact] + public void ClearObservedCalls_DoesNotAffectConfiguredValue() + { + var slot = new ReturnConfig(); + new ReturnConfigBuilder(ref slot).Returns("configured"); + slot.RecordCall(); + + slot.ClearObservedCalls(); + + slot.HasConfiguredValue.Should().BeTrue(); + slot.ConfiguredValue.Should().Be("configured"); + } + + [Fact] + public void ClearObservedCalls_DoesNotAffectConfiguredException() + { + var slot = new ReturnConfig(); + var exception = new InvalidOperationException("boom"); + new ReturnConfigBuilder(ref slot).Throws(exception); + slot.RecordCall(); + + slot.ClearObservedCalls(); + + slot.HasConfiguredException.Should().BeTrue(); + slot.ConfiguredException.Should().BeSameAs(exception); + } + + [Fact] + public void ClearObservedCalls_DoesNotRewindSequenceOrdinal() + { + var slot = new ReturnConfig(); + new ReturnConfigBuilder(ref slot).ReturnsSequence("A", "B", "C"); + slot.NextSequenceOutcome().Should().Be("A"); + slot.NextSequenceOutcome().Should().Be("B"); + + slot.ClearObservedCalls(); + + slot.NextSequenceOutcome().Should().Be("C", "SequenceOrdinal is runtime progress through configured " + + "behavior, not observation history - ClearCalls() must never rewind it (ADR-0060)"); + } + [Fact] public void RecordCall_IncrementsConfiguredCallCount() {