diff --git a/.agents/skills/engineering-workflow/references/coding-standards.md b/.agents/skills/engineering-workflow/references/coding-standards.md index e1ba399b..6d410a38 100644 --- a/.agents/skills/engineering-workflow/references/coding-standards.md +++ b/.agents/skills/engineering-workflow/references/coding-standards.md @@ -527,6 +527,27 @@ every other section of this file like any hand-written code): "prove it, don't assume it" standard (see e.g. ADR-0042/0044/0045's own use of it for AOT verification) applies here just as much as it does to composition-engine or generator behavior. +- **When a generator must predict whether two emitted signatures would + collide (`CS0111`), compare real `ITypeSymbol`s via + `SymbolEqualityComparer.Default`, never nullable-aware display-string + text.** C#'s own signature/overload identity never considers + nullable-reference annotations (`TestDoubleOverloadIdentity`'s own + canonical-signature helper already documents this), but + `SymbolDisplayFormat`-based text comparison does when the + nullable-aware format is used — `string` and `string?` compare unequal + as text even though the real compiler treats them as the same + signature, silently missing a real collision. `SymbolEqualityComparer.Default` + is nullability-insensitive by construction, so it matches what the + compiler actually decides. Caught by real-world review on ADR-0044 + Amendment 21's `Matching` collision check (PR #115): the first + draft also only considered *non-overloaded* real members as possible + collision sources, missing that an ordinary overloaded member's own + discriminator extension can just as easily collide (a real declared + parameter type, unwrapped, coincidentally matching a Match-wrapped + alias signature) — when predicting a collision against "every real + member sharing this name," that must genuinely mean every real member, + not just the ones reachable through the code path you happened to be + writing. - **Hint names (`AddSource`) are readable + stable-hash-suffixed**: the sanitized fully-qualified name for a human scanning generated-file lists, plus a short stable hash of the raw pre-sanitization identity — diff --git a/.github/scripts/inspect-packed-nupkgs.sh b/.github/scripts/inspect-packed-nupkgs.sh index 85b75c81..d2ddf121 100755 --- a/.github/scripts/inspect-packed-nupkgs.sh +++ b/.github/scripts/inspect-packed-nupkgs.sh @@ -188,7 +188,7 @@ for pkg in Compono Compono.XunitV3 Compono.NSubstitute Compono.Bogus Compono.TUn Compono.TUnit) assert_manifest_field "$nuspec" "$pkg" "title" "Compono — TUnit Integration" assert_exact_pin_dependency "$nuspec" "$pkg" "Compono" - assert_dependency_range "$nuspec" "$pkg" "TUnit.Core" "[1.64.13, 2.0.0)" + assert_dependency_range "$nuspec" "$pkg" "TUnit.Core" "[1.65.38, 2.0.0)" ;; Compono.TestDoubles) assert_manifest_field "$nuspec" "$pkg" "title" "Compono — Generated Test Doubles" diff --git a/AGENTS.md b/AGENTS.md index 44ce6406..c142a78d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -183,6 +183,13 @@ list). The load-bearing ones: - A compile-time diagnostic beats emitting code that might not compile — reject an unsupported shape with a clear `CMPxxxx` diagnostic rather than generating something and hoping. +- When predicting whether two generated signatures would collide + (`CS0111`), compare `ITypeSymbol`s via `SymbolEqualityComparer.Default` + (nullability-insensitive, matching real C# signature identity) — never + nullable-aware display-string text, and never only the subset of real + members reachable through one particular code path. See + `references/coding-standards.md`'s "Generated code" section for the + real collision this missed (ADR-0044 Amendment 21, PR #115). - Generated code should be low-allocation by construction (pre-sized collections, no LINQ, expression-bodied where there's no branching) — this is on the hot path of every composed test. 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 0c6320e3..2869c7ac 100644 --- a/docs/adr/0044-compono-testdoubles-v2-overloads-generics-verification.md +++ b/docs/adr/0044-compono-testdoubles-v2-overloads-generics-verification.md @@ -2353,6 +2353,228 @@ admission points together). the rubric applied above; "Bug handling"'s "no new capability ADR" rule is why this is an Amendment plus a plan, not a new roadmap ADR. +## Amendment 21 (2026-08-27): argument matching for overloaded members is now a pre-1.0 product requirement; Amendment 18's boundary is superseded, not merely evidenced-around + +`dynamodb-distributed-lock` dogfooding (real migration, not a synthetic +case) surfaced the same boundary Amendment 18 already documented — an +overloaded member's `Configure()`/`Verify()` stays discriminator-only, real +parameter types only, never `Match` — against `Amazon.DynamoDBv2 +.IAmazonDynamoDB`'s real `PutItemAsync`/`DeleteItemAsync` overload +families. A careful re-audit of the migrating consumer's own test suite +(prompted by this ADR's own evidence-over-prediction discipline, not +assumed from the presence of NSubstitute vocabulary alone) found that +**most** of the apparent need disappeared under scrutiny: of the call +sites originally routed to a real NSubstitute substitute for this reason, +all but one turned out to need only a single blanket response regardless +of arguments — already fully expressible through today's existing +discriminator-only surface, requiring no new capability at all. Exactly +**one** real call site +(`AcquireLockHandleAsync_DisposeHandle_ShouldCallReleaseLock`, asserting +`DeleteItemRequest.ConditionExpression`/`ExpressionAttributeValues` +*content*) genuinely needs argument-content matching on an overloaded +member. This distinction is recorded deliberately: it is evidence against +`Compono.TestDoubles` growing into a general-purpose NSubstitute clone +merely because NSubstitute vocabulary (`Arg.Is`, `Arg.Any`) appears in a +migrating consumer's old test — see ADR-0042's own Non-Goals, unaffected +by this Amendment. + +**Product decision (explicit, 2026-08-27): the *capability* — overload-safe +argument matching and argument-filtered verification for an overloaded +member — is `Accepted` as a pre-1.0 requirement, superseding Amendment +18's framing of the discriminator-only boundary as the permanent answer.** +Amendment 18's own finding is **not** overturned as engineering fact: the +real compiler spike it recorded (wrapping overloaded parameters directly +in `Match` and relying on implicit-conversion overload resolution +produces genuine `CS0121` ambiguity in realistic families — numeric +widening, base/derived hierarchies, array-vs-`IEnumerable`) remains the +reason that *specific* API shape (`Match` participating directly in the +*existing, real-arity-overloaded* call itself) is rejected, permanently, +not merely deferred. What changes is the conclusion drawn from that +finding: Amendment 18 treated it as proof the *capability* isn't +achievable; it only proved that *one* shape isn't. + +**Two structurally different shapes were spiked and rejected in turn +(2026-08-27) before landing on the recommended one below — recorded so the +same dead ends aren't re-derived later:** + +1. **A nested `.For()` call under a nullary member-name property** + (`Configure().PutItemAsync.For()`). + **Rejected: does not compile.** `{{safe_identifier}}_DoubleConfiguration` + already declares `PutItemAsync` as a **method** (the existing + discriminator-only overload, which must stay for source compatibility) + — a property and a method cannot share one name on the same type + (`CS0102`), confirmed by inspecting the real generated + `{{...}}_DoubleConfiguration` shape directly. +2. **A flattened, purely-generic overload of the member name itself** + (`Configure().PutItemAsync()`, zero real parameters, reusing + Amendment 1's "overloaded member's extension becomes generic" shape + with a new trigger). Compiles cleanly and avoids (1)'s collision — but + **rejected on type-safety grounds, confirmed by a real `dotnet build`, + not assumed:** the type parameters are pure arity witnesses, completely + unenforced against the real overload's parameter types. Given a real + two-parameter overload `M(PutItemRequest, CancellationToken)`, + `self.M()` **compiles and silently selects that + same overload** — nothing in the language ties `T1`/`T2` to + `PutItemRequest`/`CancellationToken` at all. An API that visually reads + as "select this parameter-type signature" while actually only meaning + "select the overload with this many parameters" is misleading enough on + its own terms to reject, independent of whether it happens to work for + a careful caller. + +**Recommended shape: a separate, matching-specific member name, taking +real `Match` parameters directly** — spiked fresh (real `dotnet build`, +not predicted) against every family that made Amendment 18's original +"`Match` wrapped directly in the call" shape ambiguous, plus the real +`IAmazonDynamoDB`-shaped arity family, using `Match.Any()`/`Match.Is(predicate)`-style +call sites (not bare literals — see the literal-shorthand finding below): + +```csharp +public static Configurator DeleteItemAsyncMatching( + this Double self, Match request, Match cancellationToken) => ...; +``` + +| Family (the same ones Amendment 18's own spike used) | Result with `Match.Any()`/`Match.Is(...)` args | +|---|---| +| Numeric widening (`Match` vs `Match`) | Resolves correctly, no ambiguity | +| Base/derived (`Match` vs `Match`) | Resolves correctly, no ambiguity | +| Array vs `IEnumerable` (`Match` vs `Match>`) | Resolves correctly, no ambiguity | +| Real `IAmazonDynamoDB`-shaped arities (2-arg/3-arg/4-arg, all different real types per arity) | Resolves correctly, no ambiguity | + +**Why this works where Amendment 18's shape didn't:** the ambiguity there +came specifically from `Match`'s own `implicit operator Match(T +literal)` — a *literal* argument has multiple valid conversion paths +across sibling overloads (widen then convert, or convert directly). Once +the caller already supplies an already-`Match`-typed value +(`Match.Any()`/`Match.Is(predicate)`), the argument's static type +*is* `Match` exactly — an identity match against exactly one overload, +no conversion competition at all. This is a materially different +mechanism from wrapping `Match` into the *existing real-arity-overloaded* +member name (rejected shape, still rejected) — it is a *new, separate* +member name whose own overload set is written directly in terms of +`Match`, so the ambiguity-causing implicit-conversion competition never +has a second candidate to compete against. + +**Literal shorthand does not carry over to this surface — confirmed by +compile check, not assumed.** Passing a bare literal (relying on +`Match`'s own implicit `T → Match` conversion, e.g. +`self.M2(new Derived())` against `M2(Match)`/`M2(Match)` +siblings) reproduces the exact `CS0121` ambiguity Amendment 18 found, +because now *two* implicit conversions (the literal's own widening/ +reference conversions, composed with `Match`'s conversion) compete +again. **Decision: the matching-specific surface requires +`Match.Is(predicate)`/`Match.Any()`/an already-`Match`-typed +value — literal shorthand is not offered on this surface.** A consumer +writing a same-value equality check spells it as +`Match.Is(x => x == literal)`. This is a real, deliberate ergonomics +loss relative to a non-overloaded member's existing literal-shorthand +`Configure().Member(literal)` — recorded here, not silently absorbed — +justified because the alternative is reintroducing genuine compiler +ambiguity for the exact overload families this capability exists to +support. + +**Naming convention for the new member name — left to the implementation +dive, not decided here:** a `Matching` suffix (`DeleteItemAsyncMatching`) +is used illustratively above and reads clearly, but the exact convention +(suffix vs. some other disambiguator) is Phase 2 implementation detail, +not a product decision — record whatever is chosen in +`references/testdoubles.md` once picked. + +**Classification (ADR-0029 applied explicitly):** this is the +[ADR-0042 Amendment 2](0042-compono-owned-source-generated-test-doubles.md#amendment-2-2026-08-18-full-compononsubstitute-substitutability-is-a-goal-not-an-aspiration) +override again — a real, evidenced case where `Compono.NSubstitute` +satisfies a shape `Compono.TestDoubles` cannot, which Amendment 2 makes a +roadmap candidate by policy regardless of frequency (one real site, one +real project, is sufficient evidence under that policy). Per explicit +product direction recorded here, that classification is resolved further, +past "roadmap candidate," to **Accepted requirement, `Proposed` API** — +the same split status [ADR-0052](0052-compile-time-composition-discovery-boundary-for-registered-and-nested-resolved-types.md) +itself carries between "is this needed" and "what exactly is the +mechanism." **Update (2026-08-27): implemented and validated per +[PLAN-0054](../plans/0054-testdoubles-overload-safe-matching-and-sequential-responses-impl-plan.md) +— the `Matching` shape below is now `Accepted` API, not merely +`Proposed`.** + +**Scope, held deliberately narrow per the re-audit above:** this Amendment +does not reopen `Match` participating directly in the *existing* +discriminator-only overloaded call (Amendment 18's own rejected shape +stays rejected — the new matching-specific member name is a genuinely +different call site, not a reopening of that one), does not change +anything about non-overloaded members (already fully matching-eligible, +untouched), and does not imply every NSubstitute vocabulary sighting in a +migrating consumer justifies a new capability — the opposite: the re-audit +that found nearly every apparent site needed nothing new is exactly the +evidence discipline this Amendment expects future dogfooding to keep +applying. + +**Same-arity, different-type overloads are fully supported by the +recommended shape — confirmed by compile check, this is better news than +the rejected `.For()` shape's own boundary would have allowed.** +The now-rejected `.For()` design's limitation (generic-arity-only +disambiguation, `CS0111` on same-arity constraint overloading — see the +prior revision of this Amendment, preserved in git history) does not apply +to the matching-specific-member-name shape at all: since it uses **real, +ordinary C# method overloading** on real `Match` parameter types (not +generic type-parameter constraints), same-arity siblings distinguish +exactly the way any two ordinarily-overloaded real methods already do. +Verified directly: a 3-member same-arity overload set with a base/derived +type appearing in different parameter positions across siblings +(`M(Match, Match)` / `M(Match, Match)` / `M(Match, Match)` +where `D : A`) resolves every call correctly via `Match.Any()`-style +arguments, no ambiguity. **No same-arity exclusion or new diagnostic is +needed for this design** — the implementation dive should not build the +`CMP0038`-style carve-out a `.For()`-shaped design would have +required; that need evaporates with the shape change. (Whether some +*other*, narrower boundary exists — e.g. a member that is both generic and +overloaded, or a `ref`/`out`/`in` parameter interacting with this new +surface — remains for the implementation dive to spike against, per the +paragraph below; only the same-arity case is now confirmed clear.) + +**Also not decided here, left to the implementation dive:** the exact +generated shape of the matching-specific member name and its own +naming convention (a `Matching`-suffixed name is illustrative, not +decided — see above), how its own `Configure()`/`Verify()` composes with +[ADR-0050](0050-testdoubles-multi-entry-argument-distinguished-configuration.md)'s +existing entry model (the new member name's own matching entries, most +likely — not the existing discriminator-only member's single slot), and +how it behaves against a member that is both generic *and* overloaded +(Amendment 18's own already-established "overloaded and generic together" +shape — the discriminator extension itself becomes generic, purely for +compile-time overload selection; whether the same treatment is needed for +the new matching-specific name too is not re-derived by this Amendment) +and `ref`/`in`/`out` parameters (`in` is already supported today per +`CMP0004`'s existing rule and needs no new behavior here; a `ref`/`out` +overload already falls back to a deterministic default today and whether +that boundary extends to the new matching-specific name is a real open +question, not assumed either way) — a real generator spike against those +specific shapes, not assumed to generalize cleanly from the two-arg/ +three-arg case sketched above. + +### Links (Amendment 21) + +- `dynamodb-distributed-lock` dogfood evidence report (this session, + 2026-08-27) — the re-audit finding. +- This Amendment's own revision history (same file, git blame) — the + original `.For()` sketch, its `CS0111` same-arity boundary + finding, and why both were superseded, kept here rather than deleted so + a future reader doesn't re-derive the same rejected shape. +- Real compiler spikes (this session, 2026-08-27, against the corrected + matching-specific-member-name shape): (1) the property/method `CS0102` + collision that killed the nested `.For()` shape; (2) the + flattened `Member()` shape compiling but leaving `T1`/`T2` + completely unenforced against the real overload's parameter types + (`self.M()` silently selecting a real + `(PutItemRequest, CancellationToken)` overload) — rejected on + type-safety grounds; (3) the matching-specific-name shape resolving + correctly for every family Amendment 18's own spike found ambiguous, + including same-arity/different-type, using `Match.Any()`/`Match.Is(...)` + call sites; (4) literal shorthand reproducing the original `CS0121` + ambiguity on this new surface, hence its exclusion. +- [ADR-0052](0052-compile-time-composition-discovery-boundary-for-registered-and-nested-resolved-types.md) — + the "Accepted requirement, `Proposed` API" split status precedent this + Amendment's own status still follows. +- [ADR-0042 Amendment 2](0042-compono-owned-source-generated-test-doubles.md#amendment-2-2026-08-18-full-compononsubstitute-substitutability-is-a-goal-not-an-aspiration) — + the classification policy this finding falls under. + ## Links (original, 2026-08-14) - [RESEARCH-0004](../research/0004-lightsaber-skill-testdoubles-v2-dogfood.md) — diff --git a/docs/adr/0054-testdoubles-sequential-call-count-based-responses.md b/docs/adr/0054-testdoubles-sequential-call-count-based-responses.md new file mode 100644 index 00000000..eb724c2c --- /dev/null +++ b/docs/adr/0054-testdoubles-sequential-call-count-based-responses.md @@ -0,0 +1,325 @@ +# [ADR-0054] Compono.TestDoubles: Sequential/Call-Count-Based Responses + +**Status:** Accepted (capability, API, and implementation) — implemented and validated per [PLAN-0054](../plans/0054-testdoubles-overload-safe-matching-and-sequential-responses-impl-plan.md) (2026-08-27). + +**Date:** 2026-08-27 + +**Decision Makers:** solo + +## Context + +`dynamodb-distributed-lock` dogfooding (real migration, not a synthetic +case) surfaced a `Compono.TestDoubles` capability gap distinct from both +[ADR-0053](0053-testdoubles-invocation-aware-callback-responses.md) +(invocation-aware callbacks) and +[ADR-0044 Amendment 21](0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-21-2026-08-27-argument-matching-for-overloaded-members-is-now-a-pre-10-product-requirement-amendment-18s-boundary-is-superseded-not-merely-evidenced-around) +(overload-safe argument matching). Three real tests need +`IAmazonDynamoDB.PutItemAsync` to return a **predetermined sequence of +outcomes, consumed by invocation ordinal**, across calls the system under +test (`DynamoDbDistributedLock`'s own `ExponentialBackoffRetryPolicy` +integration) makes **internally**, inside one public SUT operation, with +no opportunity for the test to reconfigure the double between calls: + +```csharp +// AcquireLockAsync_WhenRetryEnabledAndEventuallySucceeds_ShouldReturnTrue +var callCount = 0; +dynamo.PutItemAsync(Arg.Any(), Arg.Any()) + .Returns(ci => + { + callCount++; + if (callCount < 3) + throw new ConditionalCheckFailedException("Lock exists"); + return new PutItemResponse(); + }); + +var result = await sut.AcquireLockAsync(resourceId, ownerId, ct); // one call; 3 PutItemAsync calls happen inside it +``` + +Real acceptance sequences from this migration: `exception, exception, +value` (twice, different exception types) and `exception, value`. The +general capability must also naturally express a simpler shape like +`false, false, true` for a sync member — not evidenced by this migration +directly, but the natural minimal generalization of what *is* evidenced +(a fixed, ordinal-indexed list of outcomes, some of which may be +exceptions). + +### Why this is not ADR-0053 + +[ADR-0053](0053-testdoubles-invocation-aware-callback-responses.md) is +about computing a response from the *real invocation's own arguments*, or +invoking a captured delegate argument and recording side effects around +it — genuinely invocation-aware behavior. This capability needs neither: +every call in the evidenced sequences uses the same (or don't-care) +arguments; nothing about the response depends on what was passed. The two +capabilities are related only in that both are "the current single-slot +`ReturnConfig` isn't enough," not in mechanism or in the API shape a +consumer would reach for. Per explicit product direction, this ADR +records sequential responses as its own capability rather than folding it +into ADR-0053's scope — their user-facing semantics should not become +callback-shaped merely because a callback could technically emulate a +sequence. + +### Why this is not ADR-0050 + +[ADR-0050](0050-testdoubles-multi-entry-argument-distinguished-configuration.md) +explicitly and separately scoped sequential/call-count-based returns +*out* of its own multi-entry design ("no sequential/call-count-based +returns" is named alongside, not the same as, "no callback responses"). +ADR-0050's multi-entry model selects *which* configured entry applies +based on the *arguments of a given call* (argument-distinguished +dispatch); this capability selects *which outcome within one already- +selected entry* applies based on *how many times that entry has already +fired* (ordinal-distinguished dispatch). The two compose, they don't +overlap — see "Entry interaction" below. + +## Dogfood evidence discipline: what this ADR is not evidence for + +A careful re-audit of the same migration's other apparent NSubstitute +usage (see ADR-0044 Amendment 21's own discipline note) found that +**most** call sites needed nothing beyond today's existing +discriminator-only `Configure()`/`Verify()` surface — only 3 of roughly +15 apparent sites genuinely need sequential responses, and all 3 are the +*same underlying shape* (the SUT's own internal retry loop). This ADR +scopes to exactly that evidenced shape, not a general "any +NSubstitute-vocabulary sighting justifies a new capability" reading — see +ADR-0042's Non-Goals, unaffected here. + +## Applying ADR-0029's Gap decision rubric + +1. **Observed frequency:** 3 real, distinct call sites, one real project, + all the same underlying shape (an internal retry loop the test cannot + intervene in). +2. **Was this ever intended to work?** No — `Compono.TestDoubles` has + never claimed sequential responses; ADR-0050 explicitly named the + exclusion. Not a bug. +3. **Workaround cost:** real — the 3 sites remain on a real NSubstitute + substitute (`Register(_ => Substitute.For())`) + for this reason alone, unable to move to `Compono.TestDoubles` even + after the overload-matching gap (ADR-0044 Amendment 21) is closed. +4. **Principle alignment:** no reflection or hidden state required — a + fixed array of outcomes plus an atomic ordinal counter, both entirely + within existing source-generation and no-reflection constraints. + +**Classification:** per +[ADR-0042 Amendment 2](0042-compono-owned-source-generated-test-doubles.md#amendment-2-2026-08-18-full-compononsubstitute-substitutability-is-a-goal-not-an-aspiration), +a real evidenced case where `Compono.NSubstitute` satisfies a shape +`Compono.TestDoubles` cannot is a roadmap candidate by policy regardless +of frequency. Per explicit product direction recorded here, resolved +further to **Accepted requirement, `Proposed` API** — the same split +status ADR-0052 and ADR-0044 Amendment 21 both carry. + +## Accepted design direction (product-directed, API details still open) + +The following properties are **Accepted** — not open questions for the +implementation dive, only their exact mechanism is: + +- Sequence state belongs to the **matched ADR-0050 entry**, not to the + member as a whole — `Configure().Foo(Match.Is(x => x.Id == 1))` and + `Configure().Foo(Match.Is(x => x.Id == 2))` own independent sequences + with independent ordinal counters. +- **Independent ordinal per entry**, consumed deterministically by + invocation order. +- **Thread-safe, deterministic ordinal consumption** — no locks; the + outcomes list is immutable once configured (single-writer at + `Configure()` time), so an atomic increment (`Interlocked.Increment`, + the same primitive `ReturnConfig.RecordCall()` already uses for + `CallCount`) to claim the next ordinal, then an index read, is + sufficient. No new concurrency primitive introduced. +- **Call recording stays independent of response consumption** — + `RecordCall()`/`CallCount` (and ADR-0050's argument-filtered `Verify()` + built on it) already fire on every dispatch regardless of which + response path executes; a sequence changes only what gets *returned*, + never what gets *recorded*. No new design needed here — true by + construction once sequence state is attached to the entry rather than + replacing `RecordCall()`'s own mechanism. +- **Reconfiguration replaces the sequence and resets its ordinal** — + reusing the already-documented `ReturnConfigBuilder.Returns`/ + `.Throws` "last-configuration-wins" contract, extended naturally: a new + `Returns(...)`/sequence call on what resolves to the same entry + identity replaces that entry's whole response state (single value, + exception, or sequence — whichever it holds) and resets any ordinal to + 0. No new diagnostic, no separate "reset" vs. "replace" concept. +- **Exhaustion repeats the final configured response** — matches + NSubstitute's own long-established `Returns(a, b, c)` behavior (which + repeats `c` on call 4+), the exact behavior this feature's own + migration audience already expects. An explicit "throw when exhausted" + variant was considered and is likely redundant with the already-shipped + `Verify().Member(...).Exactly(n)`; not adopted without further evidence + a real case needs it distinct from that existing assertion. + +## Response representation: one open question resolved here, against product direction + +An earlier design pass considered making a sequence's configured values +**logical**, unwrapped outcomes (e.g. a bare `PutItemResponse` for a +`Task`-returning member), generator-wrapped +(`Task.FromResult(...)`) at dispatch time — diverging from today's +single-entry `Returns(T value)` contract, where `T` is the member's own +declared return type and the consumer already constructs the `Task` +directly. **Rejected, per explicit product direction**: introducing two +different "what do I pass" conventions for the same member depending on +whether it's configured via `Returns(...)` or a sequence API is exactly +the kind of inconsistency this repo's own `docs/manifesto.md` +explicit-over-implicit bias warns against — a consumer should not need to +learn a second mental model only because they reached for sequencing. + +**Decision:** a sequence's configured outcomes use the **same +declared-return-type contract** `Returns(T value)` already uses today — +for a `Task`-returning member, sequence entries are +`Task` values (e.g. `Task.FromResult(response)`), not +bare `PutItemResponse`. If today's declared-return-type contract itself +deserves better async ergonomics (a real, separately-worth-investigating +question, since this migration's own timer-test fix needed +`Task.FromResult(...)`/a hand-written async helper to construct one), that +improvement — if pursued at all — must apply **consistently to both** +`Returns(...)` and any sequence API, not to sequences alone. **Not decided +by this ADR**; flagged as a distinct, optional follow-up candidate, not a +prerequisite for this capability. + +## `SequenceOutcome` representation — implicit dual conversion rejected, replaced with an explicit `Throw` factory + +An earlier implementation pass gave `SequenceOutcome` two implicit +conversions — one from `T` (the value case) and one from `System.Exception` +(the throw case), mirroring `Match`'s own single-implicit-conversion +shape. **Rejected, confirmed unsafe by real compile-and-run checks, not by +inspection.** For any `T` where `System.Exception` is itself assignable to +or from `T` — `T = Exception` itself, a concrete subtype like +`InvalidOperationException`, `T = object`, or a nullable reference `T` — +both conversions become simultaneously applicable to the same argument, +and C#'s overload-resolution betterness rules pick one **silently and +deterministically**, with no compile error to flag the ambiguity to the +author: + +| `T` | `SequenceOutcome x = new InvalidOperationException(...)` resolves to | +|---|---| +| `InvalidOperationException` | the **value** conversion (`IsException = false`) — the exception is treated as *data*, not a signal to throw | +| `object` | the **exception** conversion (`IsException = true`) — and there is *no* way to express "return this exception boxed as a plain `object` value" through the implicit surface at all, for any `T` `Exception` is assignable to | +| `Exception` | the **exception** conversion | + +Both outcomes are individually legitimate, real return shapes for a member +whose declared type actually is (or is assigned from) `Exception` — a +`GetLastFault(): Exception` member returning a value is a completely +ordinary shape. An API whose meaning silently depends on which of two +*equally plausible* readings the compiler's betterness rules happen to +prefer is exactly the "obscure user-defined-conversion resolution" +category rejected here, independent of whether any given case also +produces a genuine `CS0121` (some do; the table above shows cases that +compile cleanly to the *wrong* reading, which is worse, not better). + +**Decision: drop the implicit `Exception → SequenceOutcome` conversion. +Keep the single implicit `T → SequenceOutcome` conversion** (safe by +construction — nothing else competes with it, confirmed unambiguous for +every `T` tested, including `T = Exception`/`InvalidOperationException`/ +`object`/`Exception?`, and for a `null` reference-typed value). **Add an +explicit factory, `Compono.SequenceOutcome.Throw(Exception exception)`, +returning a small non-generic marker type with its own implicit +conversion to `SequenceOutcome` for every `T`:** + +```csharp +public readonly struct SequenceOutcome +{ + public static implicit operator SequenceOutcome(T value) => ...; + public static implicit operator SequenceOutcome(SequenceOutcome.ThrownOutcome thrown) => ...; +} + +public static class SequenceOutcome +{ + public readonly struct ThrownOutcome { /* internal-only, carries the Exception */ } + public static ThrownOutcome Throw(Exception exception) => ...; +} +``` + +Because `ThrownOutcome` is its own distinct, non-generic type — never +equal to `T` for any real member's return type — it can never compete with +the `T`-conversion, for any `T`, without requiring an explicit type +argument anywhere (`T` is inferred the same way it already is today, from +the surrounding `ReturnConfigBuilder`/params-array context). Confirmed +by a real compile-and-run check across every `T` in the table above, plus +a mixed real-shaped sequence (`SequenceOutcome.Throw(ex1), +SequenceOutcome.Throw(ex2), Task.FromResult(response)`) and a `null` +reference-typed value — every case now resolves to exactly the intended +reading, with no ambiguity and no silent wrong answer. `false, false, true` +(the plain-value case this ADR's Context motivates) is unaffected — it +still reads as three bare literals, only a `throw`n entry needs the +explicit `SequenceOutcome.Throw(...)` wrapper. + +**Corrected public shape:** + +```csharp +someDouble.Configure().TrySomething().ReturnsSequence(false, false, true); + +dynamo.Configure().PutItemAsync(new PutItemRequest(), CancellationToken.None) + .ReturnsSequence( + SequenceOutcome.Throw(new ConditionalCheckFailedException("lock exists")), + SequenceOutcome.Throw(new ConditionalCheckFailedException("lock exists")), + Task.FromResult(new PutItemResponse())); +``` + +This supersedes every earlier example in this ADR and in `PLAN-0054` that +showed a bare `Exception` value passed directly to `ReturnsSequence(...)` +— those examples are wrong as written and must be corrected to +`SequenceOutcome.Throw(...)` before Phase 1 is considered done. The +already-shipped spike code (`src/Compono/SequenceOutcome.cs`, +`ReturnConfigBuilder.cs`) implements the *rejected* dual-implicit-conversion +shape and must be corrected as part of Phase 1's remaining work, not +carried forward as-is. + +## Model shape — resolved by spike: no new parallel type + +Whether sequencing needs a wholly separate `SequenceReturnConfig` type, +or whether the smaller, more natural model is for each ADR-0050 response +entry to hold **one response representation that is either a single +configured outcome or a sequence of configured outcomes**, was left open +for the implementation spike. **Resolved: no new type.** `ReturnConfig` +(`src/Compono/ReturnConfig.cs`) — the type already backing every entry, +plain-field, and closed-instantiation-bucket dispatch shape — was extended +in place with two fields (`SequenceOutcome[]? Sequence`, `int +SequenceOrdinal`) and one method (`NextSequenceOutcome()`), requiring zero +changes to `Entry`'s own shape or the ADR-0050 append/lock machinery. +`ReturnConfigBuilder.ReturnsSequence(...)` sets these three fields the +same way `Returns`/`Throws` already set the other two, and — per the +already-shipped last-configuration-wins contract — clears them too, in +both directions. Confirmed against real generated code (not just the bare +runtime type): a real `Compono.Generators.Tests` end-to-end execution test +and a real Native AOT publish-and-run both exercise a sequence on an +ADR-0050 matching-eligible entry with no additional storage type involved. + +## Scope: value-return and exception/value sequences on `Task`-returning members, evidenced; other shapes not assumed + +The evidenced need is exactly: value-returning sequences, mixed +exception/value sequences, on `Task`-returning async members (the real +AWS SDK shape this migration hit). **Void, non-generic `Task`/`ValueTask`, +and every other conceivable return shape are not assumed in scope merely +for API symmetry.** The implementation spike must determine, and record +explicitly, which shapes fall out naturally from whatever model it +adopts versus which would add meaningful complexity with no evidenced +need — and leave the latter out, recording the boundary the same way +Amendment 18 recorded overloaded-member exclusions, rather than silently +under- or over-building. + +## Links + +- `dynamodb-distributed-lock` dogfood evidence report and re-audit (this + session, 2026-08-27) — the 3 real call sites and the finding that + nearly every other apparent NSubstitute site needed nothing new. +- [ADR-0050](0050-testdoubles-multi-entry-argument-distinguished-configuration.md) — + the entry model this capability attaches to; its own explicit exclusion + of "sequential/call-count-based returns" from its original scope. +- [ADR-0053](0053-testdoubles-invocation-aware-callback-responses.md) — + the related-but-distinct invocation-aware-callback capability this ADR + is deliberately not merged into. +- [ADR-0044 Amendment 21](0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-21-2026-08-27-argument-matching-for-overloaded-members-is-now-a-pre-10-product-requirement-amendment-18s-boundary-is-superseded-not-merely-evidenced-around) — + the sibling pre-1.0 requirement from the same dogfood pass, same + "Accepted requirement, `Proposed` API" split-status precedent. +- [ADR-0042 Amendment 2](0042-compono-owned-source-generated-test-doubles.md#amendment-2-2026-08-18-full-compononsubstitute-substitutability-is-a-goal-not-an-aspiration) — + the classification policy this finding falls under. +- `src/Compono/ReturnConfig.cs` — the existing single-slot storage/ + `Interlocked`-based call-recording model this capability's concurrency + design reuses rather than reinventing. +- Real compiler spikes (this session, 2026-08-27) proving the dual-implicit- + conversion `SequenceOutcome` shape silently resolves to the wrong + reading for `T = InvalidOperationException`/`object`/`Exception`, and + that a distinct, non-generic `SequenceOutcome.Throw(...)` marker type + eliminates the ambiguity for every `T` tested, including `null`. +- [PLAN-0054](../plans/0054-testdoubles-overload-safe-matching-and-sequential-responses-impl-plan.md) — + the implementation plan this ADR's corrected shape must be reflected in + before Phase 1 is considered done. diff --git a/docs/adr/README.md b/docs/adr/README.md index 98f2c25e..d5e00495 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -115,3 +115,4 @@ the mechanics: numbering, status, and the index. | [0051](0051-compono-http-handler-based-testing-package.md) | Compono.Http: Handler-Based HTTP Client Testing Package | Accepted | | [0052](0052-compile-time-composition-discovery-boundary-for-registered-and-nested-resolved-types.md) | Compile-Time Composition-Discovery Boundary for Registered and Nested-Resolved Types | Partially Accepted (Part B shipped; Part A Proposed) | | [0053](0053-testdoubles-invocation-aware-callback-responses.md) | Compono.TestDoubles: Invocation-Aware Callback Responses | Proposed | +| [0054](0054-testdoubles-sequential-call-count-based-responses.md) | Compono.TestDoubles: Sequential/Call-Count-Based Responses | Accepted | diff --git a/docs/packages/compono-testdoubles.md b/docs/packages/compono-testdoubles.md index cf5e0cef..5daea18c 100644 --- a/docs/packages/compono-testdoubles.md +++ b/docs/packages/compono-testdoubles.md @@ -141,6 +141,63 @@ Two edge cases stay narrower than full per-overload support: and rejects the whole interface, same as the non-overloaded case (`CMP0026`). +### Overload-safe argument matching + +The discriminator-only surface above still selects an overload by real +argument *type*, not by argument *content*. When a test needs to +distinguish calls to the **same overload** by their actual argument values +(v2, [ADR-0044 Amendment 21](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-21-2026-08-27-argument-matching-for-overloaded-members-is-now-a-pre-10-product-requirement-amendment-18s-boundary-is-superseded-not-merely-evidenced-around)), +an eligible overload (real parameters, no `ref`/`out`/`in`, not a +self-referencing generic parameter — the same eligibility conditions as +the non-overloaded matching surface below) also gets a second, +matching-specific member name, `Matching`, taking real `Match` +parameters directly: + +```csharp +public interface IAmazonDynamoDB +{ + Task DeleteItemAsync(DeleteItemRequest request, CancellationToken cancellationToken); + Task DeleteItemAsync(string tableName, CancellationToken cancellationToken); +} + +client.Configure() + .DeleteItemAsync(fallbackRequest, CancellationToken.None) + .Returns(Task.FromResult(fallbackResponse)); +client.Configure() + .DeleteItemAsyncMatching(Match.Is(x => x.TableName == "special"), Match.Any()) + .Returns(Task.FromResult(specialResponse)); + +client.Verify() + .DeleteItemAsyncMatching(Match.Is(x => x.TableName == "special"), Match.Any()) + .Once(); +``` + +`DeleteItemAsyncMatching(...)` is a **configuration/verification-side +alias only** — the SUT never calls it; it's never itself an +independently-dispatched method. Both it and the unchanged +`DeleteItemAsync(realArgs, ...)` discriminator surface attach to the +**same real overload**'s entries/call log, so a call the SUT actually +makes through the real overload is visible to both surfaces consistently. +Registration order gives precedence exactly like "Multiple response +configurations per member" below — a broad discriminator-only response +registered first and a narrower `.Matching(...)` override registered after +it compose the same way two entries on a non-overloaded member would. +`Verify().DeleteItemAsync(realArgs, ...)` still reports the overload's +total real call count, now backed by the same call log. + +A literal argument on the `Matching`-named surface converts to `Match` +exactly like it does everywhere else (Amendment 18's implicit conversion) +— it's rejected only when two sibling overloads share the same +`Matching` name **and** the literal is ambiguously convertible to +both of their `Match` types (e.g. `Get(int)`/`Get(long)` called as +`GetMatching(5)`, a real `CS0121`), not as a blanket rule. In the rare +case a real interface member is literally named `Matching` and +its own generated `Configure()` extension signature would otherwise +collide with the alias's, Compono disambiguates automatically with a +deterministic fallback name, the same way it already does for other +generated names that collide — no diagnostic, no dropped capability, both +surfaces stay independently reachable. + ## Default interface members A base interface's abstract declaration resolved by a more-derived @@ -385,10 +442,11 @@ 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 for -one specific class of member - see "Argument matching and argument-filtered -verification" below. If a test needs anything else this page doesn't cover -(call-order verification, an overloaded member's own argument matching, +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). @@ -429,15 +487,23 @@ through to a computed default, or to [Configuration-required members](#configuration-required-members)' throwing behavior below) - not a distinct failure mode. -**Why this doesn't apply to an overloaded member.** A real compiler spike -(ADR-0048's Decision Outcome) proved that wrapping every overload's -parameters in a matcher type breaks C#'s own overload resolution -unpredictably for several realistic parameter-type families (base/derived -class hierarchies, `string[]` vs. `IEnumerable`, even plain `int` -vs. `long` widening) - there's no reliable per-family fix, so argument -matching is scoped out entirely for any member with more than one overload. -An overloaded member's `Configure()`/`Verify()` stay exactly the -[per-overload discriminator shape](#overloaded-members) above, unchanged. +**Why this exact surface doesn't apply to an overloaded member.** A real +compiler spike (ADR-0048's Decision Outcome) proved that wrapping *every +overload's own real parameters* in a matcher type, on the *same* call +site/member name, breaks C#'s own overload resolution unpredictably for +several realistic parameter-type families (base/derived class hierarchies, +`string[]` vs. `IEnumerable`, even plain `int` vs. `long` +widening) - there's no reliable per-family fix, so *this specific +same-name shape* stays scoped out entirely. That finding still holds and +still shapes the design below. It does **not** mean overloaded members +have no argument-matching story at all, though — see "Overload-safe +argument matching" above ([ADR-0044 Amendment 21](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-21-2026-08-27-argument-matching-for-overloaded-members-is-now-a-pre-10-product-requirement-amendment-18s-boundary-is-superseded-not-merely-evidenced-around)): +a separate `Matching` member name, taking real `Match` +parameters directly, sidesteps the exact ambiguity this spike found (a +different call site than the discriminator-only one, so there's no +overload set for the matcher-wrapped parameters to collide with) while the +unchanged, real-parameter-typed discriminator surface described here still +selects the overload the same way it always has. The same reasoning excludes a generic method whose real parameters reference its own type parameter (an `ILogger.Log`-shaped member) - a per-member call log can't hold an open type parameter's value, @@ -511,11 +577,47 @@ as guaranteed, and every existing single-`Configure()`-call usage keeps its exact same observable behavior. **What this deliberately doesn't do.** No matcher-specificity ranking (see -above). No sequential/call-count-based responses (`Configure()` doesn't -support "return X on the first call, Y on the second"). No -`Returns(Func<...>)` callback responses. Verification (`Verify()`) is -completely unaffected - it stays a count over the member's shared call -log, independent of how many response configurations exist. +above). No `Returns(Func<...>)` callback responses. Verification +(`Verify()`) is completely unaffected - it stays a count over the member's +shared call log, independent of how many response configurations exist. +"Return X on the first call, Y on the second" *is* supported - see +"Sequential/call-count-based responses" below, a distinct capability from +multi-entry argument matching. + +## Sequential/call-count-based responses + +`ReturnConfigBuilder.ReturnsSequence(...)` +([ADR-0054](../adr/0054-testdoubles-sequential-call-count-based-responses.md)) +configures a different outcome per call, consumed in order; the final +outcome repeats once the sequence is exhausted. It coexists with the +argument-matching surface above - sequence state belongs to whichever +entry the call matched, so two argument-distinguished entries on the same +member each own an independent ordinal: + +```csharp +repository.Configure().CountAsync() + .ReturnsSequence( + SequenceOutcome.Throw(new TimeoutException("attempt 1 fails")), + SequenceOutcome.Throw(new TimeoutException("attempt 2 fails")), + Task.FromResult(42)); + +await repository.CountAsync(); // throws TimeoutException("attempt 1 fails") +await repository.CountAsync(); // throws TimeoutException("attempt 2 fails") +await repository.CountAsync(); // 42 +await repository.CountAsync(); // 42 (exhausted - repeats the final outcome) +``` + +Each element is a `SequenceOutcome`: an ordinary `T` value converts to +it implicitly (`1`, `Task.FromResult(42)`, `false`), and an exception +outcome is spelled explicitly with `SequenceOutcome.Throw(exception)` - +there is no implicit conversion from `Exception`, since that's silently +wrong for a `T` that's itself `Exception` or a base/derived type of it (a +real compiler spike proved the dual-conversion design ambiguous - see +ADR-0054). Call recording (`Verify().Member(...).Exactly(n)`) is +independent of response consumption - a throwing call still counts. +Reconfiguring the same entry (`Configure()` again) replaces the sequence +and resets its ordinal; `Returns(...)`/`Throws(...)` on the same builder +clear any configured sequence, and vice versa. ## Configuration-required members @@ -634,10 +736,13 @@ configurations per member are supported for those same eligible members response configurations per member" above and [ADR-0050](../adr/0050-testdoubles-multi-entry-argument-distinguished-configuration.md) — but strictly last-matching-registration-wins, with no matcher-specificity -ranking, no sequential/call-count-based responses, and no -`Returns(Func<...>)` callbacks. Still no argument -matching on an overloaded member (a real compiler -spike proved it, see above), no call-order verification, no +ranking, and no `Returns(Func<...>)` callbacks. +Sequential/call-count-based responses (`ReturnsSequence(...)`, +[ADR-0054](../adr/0054-testdoubles-sequential-call-count-based-responses.md)) +and overload-safe argument matching (`Matching`, ADR-0044 +Amendment 21) are both now supported — see "Sequential/call-count-based +responses" and "Overload-safe argument matching" above. Still no +call-order verification, no `ReturnsForAnyArgs`/`When().Do(...)`/strict or partial substitutes/ recursive auto-configuration, and no support for classes, delegates, indexers, events, or a generic method whose return type references its own diff --git a/docs/plans/0054-testdoubles-overload-safe-matching-and-sequential-responses-impl-plan.md b/docs/plans/0054-testdoubles-overload-safe-matching-and-sequential-responses-impl-plan.md new file mode 100644 index 00000000..ac6755b2 --- /dev/null +++ b/docs/plans/0054-testdoubles-overload-safe-matching-and-sequential-responses-impl-plan.md @@ -0,0 +1,292 @@ +# [PLAN-0054] Compono.TestDoubles: Overload-Safe Argument Matching and Sequential Responses + +**Status:** Done. Phase 1 (sequential/call-count-based responses) and Phase 2 (overload-safe argument matching) are both fully implemented and validated. Full solution sweep: 820/820 on net10.0 (`Compono.Generators.Tests` 274/274 on both net10.0 and net11.0). A real Native AOT publish-and-run covering both phases' scenarios passed. Not yet done: the `dynamodb-distributed-lock` dogfood gate via `scripts/dogfood-validate.sh` - a separate, later gate per explicit instruction, not part of this plan's own scope. + +**Implements:** [ADR-0044 Amendment 21](../adr/0044-compono-testdoubles-v2-overloads-generics-verification.md#amendment-21-2026-08-27-argument-matching-for-overloaded-members-is-now-a-pre-10-product-requirement-amendment-18s-boundary-is-superseded-not-merely-evidenced-around), [ADR-0054](../adr/0054-testdoubles-sequential-call-count-based-responses.md) + +## Context + +`dynamodb-distributed-lock` dogfooding surfaced two real `Compono.TestDoubles` capability gaps against `Amazon.DynamoDBv2.IAmazonDynamoDB` — an overloaded member needing argument-content matching, and three retry-loop tests needing sequential (fail, fail, succeed) responses the SUT consumes internally, with no chance for the test to reconfigure between calls. Both are recorded as **Accepted pre-1.0 requirements** (API still `Proposed`) in the two ADRs above, per explicit product direction — this is not a "should we build this" plan, only a "how." + +This plan spans both ADRs in one document (per `docs/plans/README.md`'s multi-ADR rule) because they were evidenced together, reviewed together, and their generated-code paths intersect at exactly one place (an overloaded member with a sequenced entry), which is easier to reason about as one plan than two that have to cross-reference each other's assumptions. + +**Out of scope for this plan**, per explicit instruction: +- ADR-0053 (invocation-aware callbacks) — a different, unrelated capability. +- The final consumer dogfooding pass against `dynamodb-distributed-lock` — a separate, later gate using `scripts/dogfood-validate.sh`, run only once both phases below are merged and released to the local dogfood feed. + +## Spike findings + +Real investigation/spike work already happened across this session (uncommitted, not on a branch) before and after this plan was first written, per the instruction to preserve it as evidence. Distinguishing what it proved from what it changed. **Two rounds of API-safety spikes have now happened**; this section reflects the second (corrected) round — see each ADR's own "Links"/revision history for the first round's now-superseded findings, kept there rather than deleted. + +### Confirms ADR assumptions + +- **ADR-0054's "entry owns one response representation, no new parallel type" question is answered: no new type needed.** `ReturnConfig` (`src/Compono/ReturnConfig.cs`) already IS the entry-owned response representation for both the plain single-field dispatch shape and each ADR-0050 `Entry.Config` field — extending it in place (two new fields: `SequenceOutcome[]? Sequence`, `int SequenceOrdinal`; one new method: `NextSequenceOutcome()`) required zero changes to `Entry`'s own shape, `TestDoubleEmitter.cs`, or the ADR-0050 entry-append/lock machinery. A parallel `SequenceReturnConfig` was never actually necessary. +- **Ordinal claiming is thread-safe with no lock**, using the exact same `Interlocked.Increment` primitive `RecordCall()`/`CallCount` already establishes as this codebase's chosen concurrency approach for this class of state — no new pattern introduced. Verified with a 500-iteration `Parallel.For` unit test and, separately, a real end-to-end generator test. +- **Call recording is independent of response consumption by construction**, not by new design — `RecordCall()` already runs unconditionally before any `HasConfigured*`/dispatch branching in every dispatch shape, sequence-aware or not. +- **Reconfiguration-resets-ordinal composes with the existing `Returns`/`Throws` last-configuration-wins contract** — extended `Returns`/`Throws` to also clear `Sequence`/`SequenceOrdinal`, and `ReturnsSequence` to clear `Value`/`Exception`/reset the ordinal, so exactly one of the three states is ever live, matching `ReturnConfigBuilder`'s pre-existing documented contract. +- **The declared-return-type contract is preserved, not specially unwrapped for sequences** — per explicit product direction, `ReturnsSequence(params SequenceOutcome[])` uses the same `T` `Returns(T value)` already uses (a `Task` value for a `Task`-returning member, not a bare `PutItemResponse`). No second mental model. +- **AOT/trimming: no reflection introduced anywhere**, in either phase's design — every mechanism spiked (implicit conversions, `Interlocked` ordinal claiming, ordinary generic method overloading) is plain compile-time-resolved code. A real `dotnet publish -p:PublishAot=true` + run against `Compono.TestDoubles.AotSmokeTest` (extended with a sequenced scenario) succeeded for Phase 1's mechanism (modulo the `SequenceOutcome` correction below, not yet re-run against the corrected shape). + +### Requires an ADR amendment / another design decision — resolved this round, recorded in both ADRs + +**Capability 1 (overload-safe matching) — the API shape changed twice, now settled on real compiler evidence:** + +1. *(First round, superseded)* A nested `.For()` call under a nullary member-name property — rejected, `CS0102` (property/method name collision with the existing discriminator-only method). +2. *(Second round, this session, rejected)* A flattened, purely-generic overload of the member name (`Configure().PutItemAsync()`, zero real parameters). Compiles and avoids (1)'s collision, but **is a real type-safety hole, confirmed by compile-and-run**: `self.PutItemAsync()` compiles cleanly and silently selects the real `(PutItemRequest, CancellationToken)` overload — the type parameters are pure, completely unenforced arity witnesses. Rejected as misleading regardless of whether a careful caller would ever actually pass the wrong types. +3. **(Recommended, this session, confirmed safe by compile-and-run against every family that made Amendment 18's original shape ambiguous):** a **separate, matching-specific member name taking real `Match` parameters directly** — e.g. `Configure().DeleteItemAsyncMatching(Match request, Match ct)`. Verified against numeric widening, base/derived, array-vs-`IEnumerable`, a 3-member same-arity overload set with an overlapping type hierarchy, and the real `IAmazonDynamoDB`-shaped 2/3/4-arity family — every case resolves correctly when the caller supplies an already-`Match`-typed argument (`Match.Any()`/`Match.Is(predicate)`). **Same-arity overloads are fully supported by this shape** — a materially better outcome than shape (2) or the original `.For()` sketch, both of which would have needed a same-arity exclusion/diagnostic. **Literal shorthand does not carry over when it's genuinely ambiguous** — confirmed by a real `CS0121`, and **corrected during Phase 2 implementation** (this claim was originally stated too broadly): a bare literal only reproduces the original ambiguity when two sibling overloads share the same `Matching` alias name AND the literal implicitly converts to both of their `Match` parameter types (numeric widening - `Get(int)`/`Get(long)` sharing `GetMatching`, called as `GetMatching(5)`). When the alias group's sibling overloads have unrelated parameter types (no shared implicit conversion target - e.g. `DeleteItemAsync(DeleteItemRequest, CancellationToken)` vs `DeleteItemAsync(string, CancellationToken)`, whose aliases take `Match` vs `Match`), a bare literal compiles fine and becomes an ordinary equality matcher via `Match`'s own implicit conversion (Amendment 18), exactly like the pre-existing non-overloaded matching-eligible surface - both shapes proven by real `GeneratorTestHelpers.CompileAndExecute` evidence in `TestDoubleOverloadMatchingExecutionTests.cs`. + - **No `CMP0038` diagnostic is needed** — the same-arity exclusion it would have documented doesn't exist under this shape. This removes an entire diagnostic + its test coverage from Phase 2's scope relative to the first-round plan. + - **Action taken:** ADR-0044 Amendment 21 has been corrected in place to this shape (superseded text preserved via the Amendment mechanic, not deleted) — see that Amendment's own body for the full reasoning and compiler-evidence table. +- **Overloaded members currently bypass ADR-0050's entries/matching machinery entirely** (confirmed by reading `TestDoubleAnalyzer.cs`'s `isEligibleForMatching` computation directly: `hasConfigurationSurface && !isOverloaded && ...` — a single unconditional exclusion) and the template (`TestDouble.scriban`'s `is_overloaded` branch: one plain `ReturnConfig` field, no `Entry`/`Entries` class, no lock, no call log). Implementing the new matching-specific member name requires *adding* an entries-list dispatch path attached to that real overload — traced to the exact two call sites (`TestDoubleAnalyzer.cs`'s eligibility computation, `TestDoubleEmitter.cs`/`TestDouble.scriban`'s emission) that need to change; see Phase 2 below. + +- **Architectural correction (this round, found in review, not by compiler spike): the "independent surface, no fallthrough needed" framing above was wrong.** `DeleteItemAsyncMatching(...)` is a name that exists **only** on the generated `Configure()`/`Verify()` API — the SUT never calls it. Production code still calls the real interface member, + `IAmazonDynamoDB.DeleteItemAsync(DeleteItemRequest, CancellationToken)`. If `DeleteItemAsyncMatching` backed its *own*, separately-dispatched generated method, a `Configure().DeleteItemAsyncMatching(...)` call would never be consulted by the real dispatch at all — dead configuration. **The matching-specific member name must be a pure `Configure()`/`Verify()`-side alias that attaches its state to the *same* real overload's existing entries/call-recording state, not a second runtime-dispatched member.** + - **Corrected shape: unify, don't parallel.** An overload that meets the (structural, arity-independent) eligibility conditions gets promoted, for that overload specifically, to the *exact* `Entry`/`Entries`/call-log/lock shape ADR-0050 already emits for a non-overloaded matching-eligible member — reusing `EntryClassName`/`EntriesFieldName` keyed off that overload's own `FieldName`, no new naming scheme. The **existing discriminator-only `Configure(realArgs)` method's implementation changes** (not its signature, not its observable behavior) from "write directly to a single `ReturnConfig` field" to "append an always-matching `Entry`" — this is not a new idea, it is the *exact* migration ADR-0050 already performed for a non-overloaded member's own "compatibility" zero-argument `Configure()` (see `TestDouble.scriban`'s own comment on that overload: "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"). The new `DeleteItemAsyncMatching(Match, ...)` method appends a *real*-matcher `Entry` to the **same** list. Dispatch for the real `DeleteItemAsync(DeleteItemRequest, CancellationToken)` explicit interface implementation becomes the *same* reverse-scan-under-lock ADR-0050 already uses — the discriminator-registered (always-matching) entry and any `.Matching()`-registered (real-matcher) entries live in one ordered list, so "a more specific match wins if registered after the broad default, otherwise fall through to the broad default" is the **existing** last-registered-wins scan, not new fallback logic. Discriminator-only `Verify()` also migrates the same way ADR-0050's own compatibility-`Verify()` already did: from `field.ConfiguredCallCount` to the call log's `Count` (an unfiltered read over the same log the new filtered `Verify().DeleteItemAsyncMatching(...)` scans) — both surfaces read from one shared call log, so a call is counted by *both* consistently, automatically. + - **Consequence for the "byte-identical snapshot" acceptance criterion from the first-round plan: wrong, corrected below.** Every overload meeting the (now arity-independent) eligibility conditions gets *regenerated* to the entries/call-log shape **unconditionally** — structurally, the same way a non-overloaded member already does, whether or not any test in the compilation ever actually calls `.Matching()` against it. Generated code for such an overload *will* change shape. What stays true, and is the real invariant to test: **observable behavior for a consumer who only ever calls the existing discriminator-only `Configure(realArgs)`/`Verify(realArgs)` is unchanged** — same inputs, same outputs, same call counts — proven by execution tests, not by snapshot diffing. + - **The invariant to hold onto, stated explicitly (per review):** the matching-specific member **name** is only a `Configure()`/`Verify()`-side API disambiguator (needed because C# can't otherwise name "the real-argument overload-selecting call" and "the real-`Match`-parameter call" the same thing without recreating Amendment 18's own ambiguity) — the matching **state** (entries, call log, lock) belongs to, and is keyed by, the real interface overload, never the API name itself. Overload identity (today's existing `FieldName`/discriminator-suffix mechanism, unchanged) remains part of that key, so sibling overloads of the same member name never share entries/call-log state. + +**Capability 2 (sequential responses) — `SequenceOutcome`'s conversion design is corrected:** + +- **The dual-implicit-conversion shape (`SequenceOutcome` converting implicitly from both `T` and `Exception`) is rejected, confirmed unsafe by real compile-and-run, not by inspection.** For `T = InvalidOperationException`, `new InvalidOperationException(...)` compiles unambiguously but resolves to the *value* conversion (`IsException = false`) — silently the opposite of what a reader would likely expect. For `T = object`, it resolves to the *exception* conversion, and there is no way to express "return this exception as boxed data" for such a `T` at all. Both are real, legitimate return shapes a real interface member could have; an API whose meaning depends on which of two equally-plausible readings C#'s betterness rules silently prefer is unacceptable, independent of whether it happens to compile. +- **Fix, confirmed safe by real compile-and-run across every problematic `T` (`Exception`, `InvalidOperationException`, `object`, `Exception?`) plus `null`:** drop the implicit `Exception → SequenceOutcome` conversion. Keep the single implicit `T → SequenceOutcome` conversion (safe by construction, nothing competes with it). Add an explicit factory, `Compono.SequenceOutcome.Throw(Exception exception)`, returning a small non-generic marker type (`SequenceOutcome.ThrownOutcome`) with its own implicit conversion to `SequenceOutcome` for every `T` — a distinct, non-generic source type can never collide with the `T`-conversion regardless of what `T` is. No explicit type argument is needed anywhere; `T` is still inferred from the surrounding `ReturnConfigBuilder`/params-array context exactly as before. + - **Action taken:** ADR-0054 has been corrected in place with the full compiler-evidence table and the corrected type sketch. + - **Action needed in code (Phase 1, not yet done):** `src/Compono/SequenceOutcome.cs`/`ReturnConfigBuilder.cs` currently implement the *rejected* dual-conversion shape (shipped as spike code before this round of investigation) — must be corrected before Phase 1 is considered done. Every existing example (in this plan, in the AOT smoke test, in the generator/unit tests) that passes a bare `Exception` directly to `ReturnsSequence(...)` must be updated to `SequenceOutcome.Throw(...)`. +- **A default-struct hazard in `SequenceOutcome.ThrownOutcome`, flagged in review, not yet spiked — must be closed before the marker type ships.** `ThrownOutcome` is a public `struct`; `default(SequenceOutcome.ThrownOutcome)` (or `default(SequenceOutcome.SomeField)` wherever one is stored) is always constructible regardless of constructor accessibility, and bypasses `SequenceOutcome.Throw(...)`'s own `ArgumentNullException.ThrowIfNull` — producing a `ThrownOutcome` whose internal `Exception` field is `null`. If that default value is ever implicitly converted to `SequenceOutcome`, the result is a "configured to throw `null`" outcome that only fails, confusingly, whenever `NextSequenceOutcome()`/dispatch eventually reaches that array index — a deferred, hard-to-trace failure, not a clear one at the point the mistake was actually made. **Required fix:** `SequenceOutcome`'s implicit conversion *from* `ThrownOutcome` must itself null-check `thrown.Exception` and throw immediately (an `ArgumentException`, matching this codebase's existing `ReturnsSequence`'s own empty-array guard) at the conversion call site — the smallest guard that turns a deferred, confusing failure into an immediate, clear one. Needs a unit test (`SequenceOutcome x = default(SequenceOutcome.ThrownOutcome);` throws immediately) before Phase 1 is done. + +### Implementation details that don't affect the public design + +- The AOT smoke-test scenario I added initially had a test-authoring bug (reused a `gateway` double instance whose `Send(string)` overload had already recorded an unrelated call earlier in the same file — `Verify().Send("sequenced").Exactly(3)` failed with `4`, since overloaded-member `Verify()` is an unfiltered per-overload count, not filtered by the literal argument's value). Not a generator/runtime bug — a fresh `composer.Create()` instance per independent scenario fixes it. Worth remembering as a real trap for Phase 2's own AOT scenario too (multiple independent test scenarios must not share one double instance if they assert on `Verify()` counts). +- `ReturnConfigSequenceTests.cs`'s empty-sequence and `ReturnConfigBuilder` tests needed to avoid capturing a `ref struct`/`ref`-parameter local inside a lambda (`CS8175`/`CS1628`) — a plain try/catch, not `Should().Throw` on a captured builder, is the working pattern for a `ref struct` API under this test style. + +## Public API delta + +Representative C# after both phases ship, corrected per this round's spikes: + +```csharp +// 1. Overload-safe argument matching (Phase 2) - the real evidenced IAmazonDynamoDB case. A +// separate, matching-specific member name - NOT the existing discriminator-only DeleteItemAsync. +dynamoDb.Configure().DeleteItemAsyncMatching( + Match.Is(r => r.ConditionExpression == expected), + Match.Any()) + .Returns(Task.FromResult(response)); + +dynamoDb.Verify().DeleteItemAsyncMatching( + Match.Is(r => r.ConditionExpression == expected), + Match.Any()) + .Once(); + +// The existing discriminator-only surface is untouched and still works, unfiltered: +dynamoDb.Configure().DeleteItemAsync(new DeleteItemRequest(), CancellationToken.None) + .Returns(Task.FromResult(response)); + +// 2. A normal (value-only) sequence (Phase 1) - unaffected by the SequenceOutcome correction. +someDouble.Configure().TrySomething().ReturnsSequence(false, false, true); + +// 3. Exception -> value. +dynamo.Configure().PutItemAsync(new PutItemRequest(), CancellationToken.None) + .ReturnsSequence( + SequenceOutcome.Throw(new ConditionalCheckFailedException("lock exists")), + Task.FromResult(new PutItemResponse())); + +// 4. Exception -> exception -> value (the real evidenced retry shape). +dynamo.Configure().PutItemAsync(new PutItemRequest(), CancellationToken.None) + .ReturnsSequence( + SequenceOutcome.Throw(new ConditionalCheckFailedException("lock exists")), + SequenceOutcome.Throw(new ConditionalCheckFailedException("lock exists")), + Task.FromResult(new PutItemResponse())); + +// 5. Sequencing combined with ADR-0050 argument-distinguished entries - each entry owns its +// own independent sequence/ordinal. +repository.Configure().Withdraw(Match.Is(id => id == "acct-1"), Match.Any(), Match.Any()) + .ReturnsSequence(false, true); +repository.Configure().Withdraw(Match.Is(id => id == "acct-2"), Match.Any(), Match.Any()) + .ReturnsSequence(true, false); +``` + +New public surface introduced across both phases (corrected from the first-round sketch): +- `Compono.SequenceOutcome` — **corrected**: a single implicit conversion, from `T` only. No public constructor. +- `Compono.SequenceOutcome` (new, non-generic static class) — `Throw(Exception exception)`, returning `Compono.SequenceOutcome.ThrownOutcome` (a small marker type with its own implicit conversion to `SequenceOutcome` for any `T`). +- `Compono.ReturnConfigBuilder.ReturnsSequence(params SequenceOutcome[])` (shipped in spike form, unaffected by the `SequenceOutcome` correction). +- `Compono.ReturnConfig.HasConfiguredSequence` / `NextSequenceOutcome()` (shipped in spike form — public because generated dispatch code in the consumer's own assembly reads it, same reasoning as every other `ReturnConfig` accessor). +- A new generated (not core-library) `Configure()`/`Verify()` member name per overloaded member needing matching (illustrative convention: a `Matching` suffix, e.g. `DeleteItemAsyncMatching` — exact convention TBD at implementation time), taking real `Match` parameters directly. **This name is a configuration/verification-side alias only** — it attaches its entries/call-log state to the *same real overload* the existing discriminator-only `Configure()`/`Verify()` methods already dispatch through; it is never itself an independently-invoked generated method. Phase 2, generated code only, no new core `Compono`/`Compono.TestDoubles` public type. +- **No new diagnostic code** — `CMP0038` is no longer needed (see "Spike findings" above); this is a reduction from the first-round plan, not an addition. + +## Scope + +**In scope:** +- Phase 1: sequential/call-count-based responses (`ReturnConfig` extension, corrected `SequenceOutcome`/`SequenceOutcome.Throw(...)` shape, template dispatch wiring, generator/runtime tests, AOT proof). +- Phase 2: overload-safe argument matching via a new matching-specific member name, for every overloaded member needing it (no arity restriction — same-arity overloads are fully supported, see "Spike findings") — discovery/eligibility, emitter/template, generator/runtime tests, AOT proof. + +**Explicitly deferred** (per ADR-0054's own scope note and this session's instructions): +- Void/non-generic-`Task`/`ValueTask` sequence shapes beyond what Phase 1's implementation naturally covers (see Phase 1 Tasks — the AOT smoke test already exercises exception-only sequencing on a `void` member, which fell out for free; no further void-specific work is planned unless Phase 1 testing finds a real gap). +- A member that is both generic *and* overloaded, needing the new matching-specific surface (real boundary, not yet spiked — see Phase 2 Tasks). +- `ref`/`out` parameters interacting with the new matching-specific surface (real boundary, not yet spiked — see Phase 2 Tasks; `in` is already known-fine, no new behavior needed). +- ADR-0053 invocation-aware callbacks. +- The `dynamodb-distributed-lock` consumer dogfooding pass — separate gate, `scripts/dogfood-validate.sh`, after both phases are merged. + +## Phase 1: Sequential/call-count-based responses + +**Goal:** `ReturnConfigBuilder.ReturnsSequence(...)` works end-to-end through the real generator, for both dispatch shapes it can reach (plain single-field member, ADR-0050 entries-list member), using the **corrected** `SequenceOutcome`/`SequenceOutcome.Throw(...)` shape, with real generator, unit, and Native-AOT proof. + +**Status:** Done. `SequenceOutcome` rewritten to the corrected shape (single implicit conversion from `T`; `SequenceOutcome.Throw(Exception)` → `ThrownOutcome`, with a null-guard on the `ThrownOutcome → SequenceOutcome` conversion against `default(ThrownOutcome)`). All acceptance criteria below met. + +### Production changes + +- `src/Compono/SequenceOutcome.cs` — **rewrite**, not just extend: `SequenceOutcome` keeps only the implicit conversion from `T`; add the non-generic `SequenceOutcome` static class with `Throw(Exception)` → `ThrownOutcome`, and `SequenceOutcome`'s second implicit conversion is from `ThrownOutcome`, not `Exception`. +- `src/Compono/ReturnConfig.cs` — unaffected by the `SequenceOutcome` correction (already spiked: `Sequence`/`SequenceOrdinal` fields, `HasConfiguredSequence`, `NextSequenceOutcome()`). +- `src/Compono/ReturnConfigBuilder.cs` — unaffected by the correction (`ReturnsSequence(params SequenceOutcome[])`; `Returns`/`Throws` also clear sequence state — already spiked). +- `src/Compono.Generators/Templates/TestDouble.scriban` — unaffected by the correction (already spiked: a `HasConfiguredSequence` check ahead of the existing `HasConfiguredException`/`HasConfiguredValue` checks at every `ReturnConfig`-consuming dispatch site — plain void member, plain non-void member, property getter, ADR-0050 entries-list void loop, ADR-0050 entries-list non-void loop, closed-instantiation-eligible entries loop, closed-instantiation-eligible no-params ternary — 7 sites total). + +### Tasks + +- [x] Rewrite `src/Compono/SequenceOutcome.cs` to the corrected shape (`SequenceOutcome` single implicit conversion from `T`; new `SequenceOutcome.Throw(Exception)`/`ThrownOutcome`). +- [x] Confirm the 7 template dispatch sites are exactly and only the sites touched (`grep -n "HasConfiguredException" TestDouble.scriban` before/after should show one new `HasConfiguredSequence` line per existing one) — confirmed unchanged (7 pairs, no new/missing sites); the `SequenceOutcome` rewrite touched only `SequenceOutcome.cs`, invisible to the template. +- [x] `test/Compono.Tests/ReturnConfigSequenceTests.cs` — updated every existing case that passed a bare `Exception` to `ReturnsSequence(...)` to `SequenceOutcome.Throw(...)`; added new unit coverage for the corrected shape: `T = Exception` (value-conversion vs. `Throw` both resolve unambiguously), `T = InvalidOperationException` (value-conversion resolves as value, not throw), `T = object` (`Throw` still resolves as throw; value-conversion still works), `T = Exception?` and `T = string?` (`null` via the `T`-conversion resolves as a value, not a throw), and `default(SequenceOutcome.ThrownOutcome)` converted to `SequenceOutcome` throws `ArgumentException` immediately at the conversion site. All prior coverage retained. 17/17 passing. +- [x] `test/Compono.Generators.Tests/TestDoubleSequentialResponseExecutionTests.cs` — updated to `SequenceOutcome.Throw(...)`; all 6 real end-to-end generator-execution tests passing (zero-parameter value sequence, zero-parameter mixed exception/value sequence, calls-still-count-toward-verification, matching-eligible member value sequence, two independent argument-matched entries with independent ordinals, reconfiguring an entry resets its ordinal). +- [x] Snapshot review: reran `test/Compono.Generators.Tests` (262/262 passing) — zero `.received.cs` files produced, confirming the `SequenceOutcome` rewrite produced zero snapshot diffs as expected. +- [x] `test/Compono.TestDoubles.AotSmokeTest/Program.cs` — updated to `SequenceOutcome.Throw(...)`; real `pack-compono.sh` + `dotnet publish -c Release -f net10.0 -p:PublishAot=true` + direct run of the published binary printed `PASS` and exited 0 — confirms the mixed exception/value `Task` sequence, exhaustion-repeats-final, `Verify().Exactly(n)` correctness across throwing calls, two independent ADR-0050 entries with independent ordinals, and the fresh-double-instance `void`-member exception-only sequence (the earlier interrupted re-verification is now complete) all survive Native AOT. +- [x] Full solution test sweep: `dotnet test -f net10.0` at the repo root — 808/808 passing across every `test/*.Tests` project (`Compono.Tests` 274, `Compono.Generators.Tests` 262, `Compono.TestDoubles.Tests` 6, `Compono.DependencyInjection.Tests` 17, `Compono.TUnit.Tests` 52, `Compono.Http.Tests` 29, `Compono.Bogus.Tests` 63, `Compono.NSubstitute.Tests` 23, `Compono.XunitV3.Tests` 70, plus the two sample test projects), zero failures. +- [x] `skills/compono/references/testdoubles.md` — replaced the "no sequential/call-count-based responses" language with a new "Sequential/call-count-based responses" section documenting `.ReturnsSequence(...)`/`SequenceOutcome.Throw(...)`, and removed the item from the unsupported-capabilities list. + +### Test plan + +Covered by the Tasks checklist above — unit (`Compono.Tests`), real generated-code execution (`Compono.Generators.Tests`), and real Native AOT publish-and-run (`Compono.TestDoubles.AotSmokeTest`), matching this repo's three-tier verification convention for a `Compono.TestDoubles` capability (see `TestDoubleVerificationExecutionTests.cs`'s own doc comment for why the generator-execution tier exists separately from unit tests of the runtime type alone). + +### Acceptance criteria + +- Every Tasks checkbox above is checked. +- `dotnet test` green across every `test/*.Tests` project on at least one TFM. +- The AOT smoke test's real `dotnet publish -p:PublishAot=true` binary runs and prints `PASS`. +- No `.received.cs` files left in `test/Compono.Generators.Tests/Snapshots/` after snapshot review. +- No production or test code anywhere passes a bare `Exception` directly to `ReturnsSequence(...)` — every exception outcome goes through `SequenceOutcome.Throw(...)`. + +## Phase 2: Overload-safe argument matching + +**Goal:** every overloaded member needing content-based argument matching (no arity restriction — same-arity overloads are fully supported, per "Spike findings") gets a new, matching-specific `Configure()`/`Verify()` **member name** taking real `Match` parameters directly — but that name is purely a configuration-side alias: its state (entries, call log, lock) attaches to the **same real overload** the existing discriminator-only surface already dispatches through, so a call the SUT actually makes is visible to both surfaces consistently. The existing discriminator-only surface's *signature and observable behavior* are unaffected; its *generated implementation* is not, for any overload that becomes matching-eligible (see "Spike findings" — this replaces the first-round "byte-identical snapshot" claim, which was wrong). + +**Status:** Done. Implemented exactly per the "Architecture (revised)" design below: `IsOverloadMatchingEligible`/`MatchingMemberName` in `TestDoubleAnalyzer.cs`, unified entries/call-log/lock state reused from ADR-0050 in `TestDouble.scriban`, real generator-execution and snapshot coverage, a real Native AOT publish-and-run, and the skill reference doc updated. All acceptance criteria below met; two corrections to the plan's own text found and recorded during implementation (the generic+overloaded extension needing `generic_suffix`, and the literal-shorthand claim being narrower than originally stated) - see "Spike findings" and the Tasks checklist above. + +### Architecture (revised) + +**State ownership — per real overload, not per API name:** + +```csharp +// One Entry/Entries/call-log/lock set per matching-eligible OVERLOAD (keyed off that overload's +// own existing FieldName/discriminator suffix - unchanged identity mechanism). +internal sealed class __DeleteItemAsync__Entry +{ + internal Match? Matcher_request; // set only by DeleteItemAsyncMatching(...) + internal Match? Matcher_cancellationToken; + internal ReturnConfig> Config; // ADR-0054-capable: value, exception, or sequence +} +internal readonly List<__DeleteItemAsync__Entry> __DeleteItemAsync__entries = []; +internal readonly List<(DeleteItemRequest, CancellationToken)> __DeleteItemAsync__calls = []; +internal readonly object __DeleteItemAsync__lock = new(); +``` + +- **`Configure().DeleteItemAsync(realRequest, realToken)`** (existing, signature unchanged) now appends an **always-matching** entry to this list — the exact migration ADR-0050 already performed for a non-overloaded member's own zero-argument "compatibility" `Configure()`. +- **`Configure().DeleteItemAsyncMatching(Match, Match)`** (new) appends a **real-matcher** entry to the *same* list. +- **`Verify().DeleteItemAsync(realRequest, realToken)`** (existing, signature unchanged) reads the call log's unfiltered `Count` — the same migration ADR-0050's own compatibility-`Verify()` already performed. +- **`Verify().DeleteItemAsyncMatching(Match, Match)`** (new) reads the call log filtered by the supplied matchers — the same filtered-scan ADR-0050 already implements for a non-overloaded matching-eligible member. + +**Dispatch (the real `IAmazonDynamoDB.DeleteItemAsync(...)` explicit interface implementation) — the existing ADR-0050 reverse-scan, unchanged in shape:** + +1. Under the shared lock: append the actual `(request, cancellationToken)` to the call log (so *both* `Verify()` surfaces see every real call, regardless of which entry — or none — ends up answering it). +2. Reverse-scan `entries` newest-to-oldest. For the first entry whose matchers all match the real arguments (an always-matching discriminator-entry matches unconditionally, exactly like today): + - `HasConfiguredSequence` → `NextSequenceOutcome()` (ADR-0054, already wired into `ReturnConfig`/the template — no new interaction code needed here, it's the same `Config` type every other entry already uses). + - else `HasConfiguredException` → throw. + - else `HasConfiguredValue` → return. + - else (matched but not yet configured) continue the scan, exactly like ADR-0050's own "no `break`" rule. +3. If nothing configured is found anywhere in the list: existing default/configuration-required fallback, unchanged. + +**Why no separate "fall back to the old field" step is needed:** there is no separate old field once an overload is promoted — the discriminator-only `Configure()` call *is* an entry in the same list. Registration order gives the correct precedence for free: register the broad discriminator response first, a specific `.Matching()` override second, and the reverse-scan finds the specific one first, falling through to the broad one for anything it doesn't match — the same idiom the AOT smoke test's own `Withdraw` scenario already demonstrates for a non-overloaded member. + +**Overload identity stays part of the key:** each overload keeps its own independent `Entry`/`Entries`/call-log/lock set (already-existing per-overload `FieldName`-derived naming) — a call to a sibling overload of the same member name can never be recorded into, or matched against, another overload's state. + +### Naming/collision policy for the matching-specific member name + +The convention `Matching` was illustrative through this ADR/plan's earlier revisions, with the exact convention left "TBD at implementation time." Before committing to it, real compiler spikes checked what happens when the interface's own closure already contains a real member whose literal name equals the candidate alias name — the case the review raised: + +```csharp +interface IFoo +{ + Result Get(Request request); + Result Get(string id); // overloaded Get -> wants an alias named "GetMatching" + + Result GetMatching(SomeOtherType value); // a REAL, unrelated member with that exact name +} +``` + +**Findings (real `dotnet build`/`dotnet run`, not predicted):** + +1. **No real collision (the common case):** when the real `GetMatching` member's own generated `Configure()` extension has a genuinely different parameter-type signature than either of the alias's own generated overloads (`GetMatching(Match)` vs. `GetMatching(Match)`/`GetMatching(Match)`), all three coexist as ordinary C# overloads, resolved correctly by argument type — confirmed by compile-and-run. +2. **Real collision:** when the real member's own generated extension signature is *identical* to one of the alias's own generated overloads (e.g. the real member happens to be `GetMatching(string value)`, whose own `Configure()` extension would be `GetMatching(Match value)` — exactly the same signature the alias already generates for the `Get(string)` overload) — confirmed `CS0111` ("already defines a member with the same parameter types"). A real, if narrow, risk that must be detected and handled deterministically, not left to produce broken generated code. +3. **Inheritance-hierarchy variant of (1)/(2):** doesn't introduce a new case. Every member (real or alias) a generated double implements is flattened onto *one* `_DoubleConfiguration` static class regardless of which interface in the closure originally declared it (already true architecturally — confirmed by every existing generated-code example touching a multi-interface closure, e.g. `IRepository : IClock` in the AOT smoke test) — so the collision check is naturally interface-closure-wide, not per-declaring-interface, with no separate mechanism needed for the cross-interface case. +4. **A real *generic* member sharing the candidate name** (`GetMatching(Match value)`, alongside the non-generic alias overloads) — confirmed to **compile cleanly, no `CS0111`** (generic arity is part of C# overload identity, and the alias's own overloads are never artificially generic — see the rejected flattened-selector shape). **A softer, non-blocking finding worth documenting, not solving in this phase:** an ordinary (non-explicit-type-argument) call always prefers the non-generic overload when both are applicable — confirmed by compile-and-run — so if the real generic member's own closed instantiation for some `T` happens to share a signature with one of the alias's concrete overloads, that specific instantiation becomes reachable only via an explicit type argument on the real member's own name, never implicitly. Since generic-and-overloaded matching is already out of this phase's scope, this is recorded as a documented caveat for `references/testdoubles.md`, not new template logic. + +**Policy, in the priority order requested:** + +1. **Default:** generate the natural `Matching` name. This is safe and predictable for every case that doesn't hit finding (2) above — the overwhelming majority. +2. **On a detected real collision (finding 2):** fall back to a **deterministic alternate name**, not a diagnostic and not silently dropping matching support. This repo already has a proven, shipped mechanism for exactly this shape of problem — `TestDoubleAnalyzer`'s existing derived-name-collision detection (`derivedNameCollisionMembers`) and discriminator-suffix pre-pass, covered by the existing `OverloadSuffixCollidesWithDifferentlyNamedRealMember_GeneratesDoubleWithDistinctFieldNames` test: when a generated name collides with a real member's own literal name, lengthen/rehash the generated name until it's globally unique. Extend that **same** mechanism's reserved-name pool to include the candidate `Matching` name, with the same fallback shape (e.g. `Matching_`, using the existing `TestDoubleOverloadIdentity.StableHash`/`DiscriminatorSuffix` convention) — not a new mechanism, an extension of an existing one. Per the instruction to avoid hash-looking names "in the normal case": this fallback is exactly that — a rare, deterministic escape hatch for a proven real collision, not the default shape. +3. **No new diagnostic is needed.** Because the deterministic fallback in (2) always succeeds (the existing discriminator-suffix mechanism already has its own collision-handling for the astronomically rarer case of the *hash itself* colliding), there is no case where "no clean generated surface" actually occurs — so priority 3 ("an actionable diagnostic only if there is no clean generated surface") is never reached for this specific problem, consistent with Phase 2 needing no new `CMP0xxx` code anywhere. + +**Documentation:** `references/testdoubles.md` can describe the rule in one sentence — "the matching-specific member name is `Matching`; in the rare case that collides with a real member of that exact name, Compono disambiguates automatically, the same way it already does for other generated names" — without needing to explain the mechanism's internals to a consumer, since the fallback is deterministic and (per finding 2) provably rare. + +### Production changes + +- `src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs`: + - New `TestDoubleMemberInfo` flag (name TBD, e.g. `IsOverloadMatchingEligible`) = `hasConfigurationSurface && isOverloaded && 0, no ref-like parameter, no derived-name collision, not the `Equals`-collision shape, no open-type-parameter reference for a generic method>` — same condition list as `isEligibleForMatching`, minus the `!isOverloaded` guard. **No arity-uniqueness check** (same-arity is fully supported). + - Matching-specific member name derivation: `Matching` by default; on a detected real-member-name collision (see "Naming/collision policy" above), fall back to the existing discriminator-suffix mechanism's hash-suffixed convention — extend `derivedNameCollisionMembers`'s existing reserved-name pool, don't invent a parallel check. +- `src/Compono.Generators/Emitters/TestDoubleEmitter.cs` / `Templates/TestDouble.scriban`: + - For each `IsOverloadMatchingEligible` overload: **replace** its existing plain-`ReturnConfig`-field emission with the `Entry`/`Entries`/call-log/lock shape above (reusing `EntryClassName`/`EntriesFieldName` keyed off that overload's own `FieldName`). + - The real interface member's explicit-implementation dispatch body for that overload changes to the reverse-scan shape above (structurally identical to today's non-overloaded matching-eligible dispatch, not new logic). + - `Configure()`/`Verify()` extensions for that overload: the **existing** discriminator-only method's body changes to "append an always-matching entry" / "read the call log's `Count`"; a **new** `Matching`-suffixed method is added, taking `Match, Match, ...` directly, appending a real-matcher entry / performing a filtered scan — mirroring the existing non-overloaded matching-eligible member's own two-method shape (zero-arg compatibility + real-matcher) as closely as possible, just under two *different* names instead of one overloaded name (forced by C#, since the real-argument and `Match`-argument versions can't share a name without reopening Amendment 18's ambiguity). + - An overloaded member that does **not** meet `IsOverloadMatchingEligible` (fails an eligibility condition — e.g. has a `ref`/`out` parameter, per Amendment 18's existing carve-out) keeps today's plain single-field shape, completely unaffected. +- `skills/compono/references/testdoubles.md` — replace the "overloaded members are discriminator-only, never matchers" language with the corrected boundary, and document that the discriminator-only surface's *underlying storage* changes for a matching-eligible overload even though its own call signature/behavior doesn't. + +### Tasks + +- [x] **Before any template work**: proven not via a separate standalone spike but by the fact that `GeneratorTestHelpers.Verify`'s own `outputCompilation.GetDiagnostics()` assertion (zero errors) passes for every one of the 265+ fixtures below — the discriminator-only and `Matching`-named methods coexist cleanly on the same `{{safe_identifier}}_DoubleConfiguration`/`_DoubleVerification` static classes in every case, including the dedicated `OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState` snapshot test. +- [x] `TestDoubleAnalyzer.cs`: new `IsOverloadMatchingEligible` flag (condition list mirrors `IsEligibleForMatching` minus the `!IsOverloaded` guard, plus `!IsClosedInstantiationEligible`; no arity-uniqueness computation) + `MatchingMemberName` derivation with the hash-suffixed collision fallback (signature-based, per "Naming/collision policy"). +- [x] `TestDoubleEmitter.cs`/`TestDouble.scriban`: each `IsOverloadMatchingEligible` overload promoted to the unified `Entry`/`Entries`/call-log/lock shape (reusing the existing `IsEligibleForMatching` state-declaration/dispatch blocks, gated on `is_eligible_for_matching || is_overload_matching_eligible`); the discriminator-only `Configure()`/`Verify()` bodies now append/scan the shared entries list instead of a removed single field; the new `Matching`-named `Configure()`/`Verify()` methods added; the real overload's dispatch reuses the existing reverse-scan shape unchanged. +- [x] Spike (real, kept as regression coverage - `GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions`): **yes** - a member that is both generic and overloaded (with a real parameter not referencing its own type parameter, so still matching-eligible-shaped) needs the SAME "extension becomes generic" treatment Amendment 1 already gives the discriminator-only surface, applied identically to the new `Matching`-named method (`{{ member.generic_suffix }}`/`{{ member.constraint_clauses_text }}` added to both). Found by a real snapshot regression sweep after the initial (ungeneric) template draft: a same-parameter-types generic/non-generic overload pair (e.g. `Process(int, string)` / `Process(int, string)`) collided (`CS0111`) with a fixed, non-generic `ProcessMatching` signature until fixed - confirmed compiling cleanly (zero diagnostics) after the fix. +- [x] Spike (real, kept as regression coverage - `RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected`): a `ref`/`out` overload needs **no new rule** - `IsOverloadMatchingEligible` already requires `WouldGetConfigurationSurface`, which already excludes a `ref`/`out`/`in` parameter unconditionally (Amendment 5). Confirmed with a real three-way overload set (one `ref`/`out` sibling with no surface at all, two real-parameter siblings each independently promoted) - the `ref`/`out` sibling still reports `CMP0030` and is otherwise untouched; its two siblings each get their own `Matching`-named surface. +- [x] `test/Compono.Generators.Tests/TestDoubleVerifyTests.cs`-style snapshot coverage: `OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState` (a matching-eligible overload generating both the unchanged-signature discriminator method and the new `Matching`-named method, both attached to one shared entries/call-log state); `OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName` and `OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName` (the naming-collision pair below); `RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected` (a non-matching-eligible overloaded member unaffected). Every one of the 77 Phase-1-touched snapshots was also reviewed and confirmed to change ONLY where the member is actually `IsOverloadMatchingEligible` - see "Meaningful generated-code changes" in the completion report. +- [x] Real end-to-end generator execution tests (`test/Compono.Generators.Tests/TestDoubleOverloadMatchingExecutionTests.cs`, mirroring Phase 1's `TestDoubleSequentialResponseExecutionTests.cs` pattern) — **the invariant proved throughout is that the matching-specific name only configures/observes; the SUT-visible dispatch is always through the real overload**: + - **Coexistence/precedence** (user-specified example): proved by `CoexistencePrecedence_MatchingEntryOverridesDiscriminatorFallback_ForMatchingCallsOnly`. + - **Sibling-overload independence**: proved by `SiblingOverloadIndependence_ConfiguringOneOverloadNeverAffectsTheOther`. + - **Filtered verification**: proved by `FilteredVerification_CountsOnlyRealCallsMatchingThePredicate`. + - **Discriminator verification unchanged**: proved by `DiscriminatorVerification_StillReportsTotalRealCallCount_BackedByTheCallLogNow`. + - **Sequencing on a matching-eligible entry**: proved by `SequencingOnAMatchingEligibleEntry_EveryRealCallStillRecordedInTheSharedCallLog`. + - **Literal shorthand — corrected finding, see "Spike findings"**: `LiteralShorthandOnTheMatchingNamedSurface_CompilesAndMatchesByEquality` proves a literal compiles and matches by equality when the sibling overloads' `Match` types are unrelated (no shared implicit-conversion target); `LiteralShorthandAmbiguousAcrossSiblingOverloads_FailsToCompile` proves the real `CS0121` only when two siblings' `Match` types are ambiguously literal-convertible (numeric widening). The plan's original text ("literal shorthand does not carry over") overstated this as a blanket rule; both shapes are now real, evidenced tests. + - **Naming collision**: proved at the snapshot layer (`TestDoubleVerifyTests.cs`, above) rather than execution, since the fallback name itself is opaque/hash-derived and not meant to be called directly from a consumer's test. +- [x] `Compono.TestDoubles.AotSmokeTest`: extended `IGateway`'s existing overloaded-member coverage with coexistence/precedence (`SendMatching` narrower override vs. the broad `Send(...)` discriminator entry) and sibling-independence (`Send(string)` vs `Send(int, string)`) - proven via a real `pack-compono.sh` + `dotnet publish -c Release -f net10.0 -p:PublishAot=true` + run of the published binary, printed `PASS`, exit 0. +- [x] `skills/compono/references/testdoubles.md` update — new "Overload-safe argument matching (ADR-0044 Amendment 21)" section added under "Overloaded members (v2)": the `Matching` naming rule, the corrected (non-blanket) literal-shorthand behavior, the rare-collision fallback, and the documented generic-member soft-shadowing caveat. +- [x] Full solution test sweep green (see validation results in the completion report). + +### Test plan + +Same three-tier shape as Phase 1 (snapshot/compile-only coverage in `TestDoubleVerifyTests.cs`, real generated-code execution in `TestDoubleOverloadMatchingExecutionTests.cs`, real Native AOT proof) plus the coexistence/precedence, sibling-independence, filtered-verification, and sequence-interaction scenarios listed above — these are the tests that actually prove the corrected architecture (state attaches to the real overload) rather than the rejected one (an independently-dispatched alias member). + +### Acceptance criteria + +- Every Tasks checkbox above is checked. +- The real `IAmazonDynamoDB.DeleteItemAsync(Match.Is(x => x.ConditionExpression == expected), Match.Any())` shape from the original dogfood investigation is provably expressible (a generator test using that exact real AWS SDK shape, or as close a stand-in as `Compono.Generators.Tests`' existing fixtures allow without taking a new external package dependency), via the new matching-specific member name, **and dispatches through the real overload** (proven by the coexistence test, not assumed). +- `dotnet test` green across every `test/*.Tests` project on at least one TFM. +- The AOT smoke test's real published binary runs and prints `PASS`. +- **Corrected criterion** (the first-round plan's "byte-identical snapshot" claim was wrong — see "Spike findings"): every matching-eligible overload's generated code changes shape (new entries/call-log emission); every **non**-matching-eligible overloaded member, and every non-overloaded member, is unaffected — verified by diffing the full snapshot set against Phase 1's already-clean baseline and confirming only matching-eligible-overload fixtures changed. Discriminator-only `Configure()`/`Verify()` **observable behavior** (not generated code shape) for a matching-eligible overload is unchanged — proven by execution tests, not snapshot diffing. + +## Critical files + +- `src/Compono/SequenceOutcome.cs` — rewritten to the corrected shape, including the `ThrownOutcome`-from-`default` guard (Phase 1). +- `src/Compono/ReturnConfig.cs`, `src/Compono/ReturnConfigBuilder.cs` — extended (Phase 1, unaffected by the `SequenceOutcome` correction). +- `src/Compono.Generators/Templates/TestDouble.scriban` — extended in both phases (7 sites in Phase 1; Phase 2 **replaces**, not adds alongside, each matching-eligible overload's dispatch/`Configure()`/`Verify()` emission with the unified entries/call-log shape). +- `src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs` — extended (Phase 2 only: new eligibility flag + matching-specific member name derivation). +- `test/Compono.Tests/ReturnConfigSequenceTests.cs`, `test/Compono.Generators.Tests/TestDoubleSequentialResponseExecutionTests.cs` — new/updated (Phase 1) — the former gains the `ThrownOutcome`-default-guard test. +- `test/Compono.Generators.Tests/TestDoubleOverloadMatchingExecutionTests.cs` — new (Phase 2) — coexistence/precedence, sibling-independence, filtered-verification, and sequence-interaction tests (see Phase 2 Tasks). +- `test/Compono.TestDoubles.AotSmokeTest/Program.cs` — extended/updated in both phases. +- `test/Compono.Generators.Tests/Snapshots/*.verified.cs` — ~77 files touched by Phase 1 alone (mechanical, template-wide change; zero further diffs expected from the `SequenceOutcome` correction, since it's invisible to generated code). Phase 2 touches every matching-eligible-overload fixture's snapshot (a real, not byte-identical, change — see Phase 2's corrected acceptance criterion) and leaves every non-matching-eligible overloaded member and every non-overloaded member untouched. +- `docs/adr/0044-...md`, `docs/adr/0054-...md`, `skills/compono/references/testdoubles.md` — documentation/ADR touches per phase (both ADRs already corrected in place for the API-shape questions; this round's dispatch-architecture correction lives in this plan only, per `tasks/design.md`'s ADR-records-what/why-plan-records-how split — no further ADR edit needed for it). + +## Notes + +(Empty at plan-writing time — this section fills in as implementation reveals divergence from what's scoped here, per this repo's own plan-maintenance convention.) diff --git a/docs/plans/README.md b/docs/plans/README.md index 2908db17..02dc56b6 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -68,3 +68,4 @@ one. This file is just the mechanics: numbering, status, and the index. | [0050](0050-testdoubles-multi-entry-argument-distinguished-configuration-impl-plan.md) | Compono.TestDoubles: Multi-Entry, Argument-Distinguished Response Configuration | Done | | [0051](0051-compono-http-handler-based-testing-package-impl-plan.md) | Compono.Http: Handler-Based HTTP Client Testing Package | Done | | [0053](0053-testdoubles-default-interface-member-fallback-fix-impl-plan.md) | Compono.TestDoubles: Default-Interface-Member Fallback Fix | In Progress | +| [0054](0054-testdoubles-overload-safe-matching-and-sequential-responses-impl-plan.md) | Compono.TestDoubles: Overload-Safe Argument Matching and Sequential Responses | Done | diff --git a/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.Returns(T).md b/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.Returns(T).md index 716911aa..58720660 100644 --- a/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.Returns(T).md +++ b/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.Returns(T).md @@ -3,7 +3,7 @@ ## ReturnConfigBuilder\\.Returns\(T\) Method -Configures the member to return [value](Compono.ReturnConfigBuilder_T_.Returns(T).md#Compono.ReturnConfigBuilder_T_.Returns(T).value 'Compono\.ReturnConfigBuilder\\.Returns\(T\)\.value'), clearing any prior [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)')\. +Configures the member to return [value](Compono.ReturnConfigBuilder_T_.Returns(T).md#Compono.ReturnConfigBuilder_T_.Returns(T).value 'Compono\.ReturnConfigBuilder\\.Returns\(T\)\.value'), clearing any prior [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)')/[ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)')\. ```csharp public void Returns(T value); diff --git a/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md b/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md new file mode 100644 index 00000000..4c9d82ea --- /dev/null +++ b/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md @@ -0,0 +1,31 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[ReturnConfigBuilder<T>](Compono.ReturnConfigBuilder_T_.md 'Compono\.ReturnConfigBuilder\') + +## ReturnConfigBuilder\\.ReturnsSequence\(SequenceOutcome\\[\]\) Method + +Configures the member to return \(or throw\) each [outcomes](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md#Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).outcomes 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)\.outcomes') entry in order, one +per invocation, by ordinal \- the first call gets `outcomes[0]`, the second +`outcomes[1]`, and so on; once exhausted, every further call repeats the final entry +\(ADR\-0054\)\. Clears any prior [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')/[Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)')/[ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)') +state and resets the ordinal to 0, the same last\-configuration\-wins contract [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')/ +[Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)') already document\. An ordinary [T](Compono.ReturnConfigBuilder_T_.md#Compono.ReturnConfigBuilder_T_.T 'Compono\.ReturnConfigBuilder\\.T') value implicitly +converts to [SequenceOutcome<T>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\'), so a pure\-value sequence reads as plain values +\(`.ReturnsSequence(false, false, true)`\); an exception outcome is spelled explicitly with +[Throw\(Exception\)](Compono.SequenceOutcome.Throw(System.Exception).md 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)') \- there is no implicit conversion from +[System\.Exception](https://learn.microsoft.com/en-us/dotnet/api/system.exception 'System\.Exception'), since that would be silently wrong for a [T](Compono.ReturnConfigBuilder_T_.md#Compono.ReturnConfigBuilder_T_.T 'Compono\.ReturnConfigBuilder\\.T') that +is itself [System\.Exception](https://learn.microsoft.com/en-us/dotnet/api/system.exception 'System\.Exception') or a base/derived type of it \- so a mixed sequence reads +`.ReturnsSequence(SequenceOutcome.Throw(ex1), SequenceOutcome.Throw(ex2), value)`\. + +```csharp +public void ReturnsSequence(params Compono.SequenceOutcome[] outcomes); +``` +#### Parameters + + + +`outcomes` [Compono\.SequenceOutcome<](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\')[T](Compono.ReturnConfigBuilder_T_.md#Compono.ReturnConfigBuilder_T_.T 'Compono\.ReturnConfigBuilder\\.T')[>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\')[\[\]](https://learn.microsoft.com/en-us/dotnet/api/system.array 'System\.Array') + +#### Exceptions + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +[outcomes](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md#Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).outcomes 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)\.outcomes') is empty\. \ No newline at end of file diff --git a/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md b/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md index d5ea563b..33e373a4 100644 --- a/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md +++ b/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md @@ -3,7 +3,7 @@ ## ReturnConfigBuilder\\.Throws\(Exception\) Method -Configures the member to throw [exception](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md#Compono.ReturnConfigBuilder_T_.Throws(System.Exception).exception 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)\.exception'), clearing any prior [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')\. +Configures the member to throw [exception](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md#Compono.ReturnConfigBuilder_T_.Throws(System.Exception).exception 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)\.exception'), clearing any prior [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')/[ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)')\. ```csharp public void Throws(System.Exception exception); diff --git a/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.md b/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.md index 6d5c9ffe..fa5df3e4 100644 --- a/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.md +++ b/docs/reference/api/Compono/Compono.ReturnConfigBuilder_T_.md @@ -19,9 +19,10 @@ public readonly ref struct ReturnConfigBuilder `T` ### Remarks -[Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')/[Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)') are last\-configuration\-wins: each clears the other's - state, so configuring a return after an earlier [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)') \(or vice versa\) doesn't - leave stale state behind\. See ADR\-0043 Amendment 7, Finding R\. +[Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')/[Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)')/[ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)') are all + last\-configuration\-wins: each of the three clears the other two's state, so configuring any one + of them after an earlier call to a different one of them doesn't leave stale state behind\. See + ADR\-0043 Amendment 7, Finding R \(the original two\-way rule\) and ADR\-0054 \(the sequence extension\)\. | Constructors | | | :--- | :--- | @@ -29,5 +30,6 @@ public readonly ref struct ReturnConfigBuilder | Methods | | | :--- | :--- | -| [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)') | Configures the member to return [value](Compono.ReturnConfigBuilder_T_.Returns(T).md#Compono.ReturnConfigBuilder_T_.Returns(T).value 'Compono\.ReturnConfigBuilder\\.Returns\(T\)\.value'), clearing any prior [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)')\. | -| [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)') | Configures the member to throw [exception](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md#Compono.ReturnConfigBuilder_T_.Throws(System.Exception).exception 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)\.exception'), clearing any prior [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')\. | +| [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)') | Configures the member to return [value](Compono.ReturnConfigBuilder_T_.Returns(T).md#Compono.ReturnConfigBuilder_T_.Returns(T).value 'Compono\.ReturnConfigBuilder\\.Returns\(T\)\.value'), clearing any prior [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)')/[ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)')\. | +| [ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)') | Configures the member to return \(or throw\) each [outcomes](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md#Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).outcomes 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)\.outcomes') entry in order, one per invocation, by ordinal \- the first call gets `outcomes[0]`, the second `outcomes[1]`, and so on; once exhausted, every further call repeats the final entry \(ADR\-0054\)\. Clears any prior [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')/[Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)')/[ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)') state and resets the ordinal to 0, the same last\-configuration\-wins contract [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')/ [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)') already document\. An ordinary [T](Compono.ReturnConfigBuilder_T_.md#Compono.ReturnConfigBuilder_T_.T 'Compono\.ReturnConfigBuilder\\.T') value implicitly converts to [SequenceOutcome<T>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\'), so a pure\-value sequence reads as plain values \(`.ReturnsSequence(false, false, true)`\); an exception outcome is spelled explicitly with [Throw\(Exception\)](Compono.SequenceOutcome.Throw(System.Exception).md 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)') \- there is no implicit conversion from [System\.Exception](https://learn.microsoft.com/en-us/dotnet/api/system.exception 'System\.Exception'), since that would be silently wrong for a [T](Compono.ReturnConfigBuilder_T_.md#Compono.ReturnConfigBuilder_T_.T 'Compono\.ReturnConfigBuilder\\.T') that is itself [System\.Exception](https://learn.microsoft.com/en-us/dotnet/api/system.exception 'System\.Exception') or a base/derived type of it \- so a mixed sequence reads `.ReturnsSequence(SequenceOutcome.Throw(ex1), SequenceOutcome.Throw(ex2), value)`\. | +| [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)') | Configures the member to throw [exception](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md#Compono.ReturnConfigBuilder_T_.Throws(System.Exception).exception 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)\.exception'), clearing any prior [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')/[ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)')\. | diff --git a/docs/reference/api/Compono/Compono.ReturnConfig_T_.HasConfiguredSequence.md b/docs/reference/api/Compono/Compono.ReturnConfig_T_.HasConfiguredSequence.md new file mode 100644 index 00000000..2e3fba6e --- /dev/null +++ b/docs/reference/api/Compono/Compono.ReturnConfig_T_.HasConfiguredSequence.md @@ -0,0 +1,13 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[ReturnConfig<T>](Compono.ReturnConfig_T_.md 'Compono\.ReturnConfig\') + +## ReturnConfig\\.HasConfiguredSequence Property + +Whether a response sequence was set via [ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)')\. + +```csharp +public readonly bool HasConfiguredSequence { get; } +``` + +#### Property Value +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') \ No newline at end of file diff --git a/docs/reference/api/Compono/Compono.ReturnConfig_T_.NextSequenceOutcome().md b/docs/reference/api/Compono/Compono.ReturnConfig_T_.NextSequenceOutcome().md new file mode 100644 index 00000000..9130b02a --- /dev/null +++ b/docs/reference/api/Compono/Compono.ReturnConfig_T_.NextSequenceOutcome().md @@ -0,0 +1,26 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[ReturnConfig<T>](Compono.ReturnConfig_T_.md 'Compono\.ReturnConfig\') + +## ReturnConfig\\.NextSequenceOutcome\(\) Method + +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\)\. + +```csharp +public T NextSequenceOutcome(); +``` + +#### Returns +[T](Compono.ReturnConfig_T_.md#Compono.ReturnConfig_T_.T 'Compono\.ReturnConfig\\.T') + +### Remarks +Thread\-safe with no lock: `Compono.ReturnConfig<>.Sequence` is never mutated after +[ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)') sets it \(a reconfiguration replaces the +whole array reference, never edits an element in place\), so the only shared mutable state is +the ordinal itself \- claimed via [System\.Threading\.Interlocked\.Increment\(System\.Int32@\)](https://learn.microsoft.com/en-us/dotnet/api/system.threading.interlocked.increment#system-threading-interlocked-increment(system-int32@) 'System\.Threading\.Interlocked\.Increment\(System\.Int32@\)'), +the same primitive [RecordCall\(\)](Compono.ReturnConfig_T_.RecordCall().md 'Compono\.ReturnConfig\\.RecordCall\(\)') already uses, so two concurrent callers always +claim two distinct, strictly\-increasing ordinals and never observe or corrupt each other's +index\. \ 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 83f6a305..e1cc4439 100644 --- a/docs/reference/api/Compono/Compono.ReturnConfig_T_.md +++ b/docs/reference/api/Compono/Compono.ReturnConfig_T_.md @@ -24,8 +24,10 @@ public struct ReturnConfig | [ConfiguredException](Compono.ReturnConfig_T_.ConfiguredException.md 'Compono\.ReturnConfig\\.ConfiguredException') | The exception configured via [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)')\. Only meaningful when [HasConfiguredException](Compono.ReturnConfig_T_.HasConfiguredException.md 'Compono\.ReturnConfig\\.HasConfiguredException') 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')\. | | [ConfiguredValue](Compono.ReturnConfig_T_.ConfiguredValue.md 'Compono\.ReturnConfig\\.ConfiguredValue') | The value configured via [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')\. Only meaningful when [HasConfiguredValue](Compono.ReturnConfig_T_.HasConfiguredValue.md 'Compono\.ReturnConfig\\.HasConfiguredValue') 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')\. | | [HasConfiguredException](Compono.ReturnConfig_T_.HasConfiguredException.md 'Compono\.ReturnConfig\\.HasConfiguredException') | Whether [ConfiguredException](Compono.ReturnConfig_T_.ConfiguredException.md 'Compono\.ReturnConfig\\.ConfiguredException') was set via [Throws\(Exception\)](Compono.ReturnConfigBuilder_T_.Throws(System.Exception).md 'Compono\.ReturnConfigBuilder\\.Throws\(System\.Exception\)')\. | +| [HasConfiguredSequence](Compono.ReturnConfig_T_.HasConfiguredSequence.md 'Compono\.ReturnConfig\\.HasConfiguredSequence') | Whether a response sequence was set via [ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)')\. | | [HasConfiguredValue](Compono.ReturnConfig_T_.HasConfiguredValue.md 'Compono\.ReturnConfig\\.HasConfiguredValue') | Whether [ConfiguredValue](Compono.ReturnConfig_T_.ConfiguredValue.md 'Compono\.ReturnConfig\\.ConfiguredValue') was set via [Returns\(T\)](Compono.ReturnConfigBuilder_T_.Returns(T).md 'Compono\.ReturnConfigBuilder\\.Returns\(T\)')\. | | Methods | | | :--- | :--- | +| [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/reference/api/Compono/Compono.SequenceOutcome.Throw(System.Exception).md b/docs/reference/api/Compono/Compono.SequenceOutcome.Throw(System.Exception).md new file mode 100644 index 00000000..711b830b --- /dev/null +++ b/docs/reference/api/Compono/Compono.SequenceOutcome.Throw(System.Exception).md @@ -0,0 +1,18 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[SequenceOutcome](Compono.SequenceOutcome.md 'Compono\.SequenceOutcome') + +## SequenceOutcome\.Throw\(Exception\) Method + +A sequence entry that throws [exception](Compono.SequenceOutcome.Throw(System.Exception).md#Compono.SequenceOutcome.Throw(System.Exception).exception 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)\.exception') when consumed\. + +```csharp +public static Compono.SequenceOutcome.ThrownOutcome Throw(System.Exception exception); +``` +#### Parameters + + + +`exception` [System\.Exception](https://learn.microsoft.com/en-us/dotnet/api/system.exception 'System\.Exception') + +#### Returns +[ThrownOutcome](Compono.SequenceOutcome.ThrownOutcome.md 'Compono\.SequenceOutcome\.ThrownOutcome') \ No newline at end of file diff --git a/docs/reference/api/Compono/Compono.SequenceOutcome.ThrownOutcome.md b/docs/reference/api/Compono/Compono.SequenceOutcome.ThrownOutcome.md new file mode 100644 index 00000000..c4d49373 --- /dev/null +++ b/docs/reference/api/Compono/Compono.SequenceOutcome.ThrownOutcome.md @@ -0,0 +1,13 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[SequenceOutcome](Compono.SequenceOutcome.md 'Compono\.SequenceOutcome') + +## SequenceOutcome\.ThrownOutcome Struct + +Marker carrying the exception for a thrown sequence entry, implicitly convertible to +[SequenceOutcome<T>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\') for any `T`\. Only ever produced by [Throw\(Exception\)](Compono.SequenceOutcome.Throw(System.Exception).md 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)') \- +its own conversion guards against the struct's `default` value, which would otherwise +carry a null exception\. + +```csharp +public readonly struct SequenceOutcome.ThrownOutcome +``` \ No newline at end of file diff --git a/docs/reference/api/Compono/Compono.SequenceOutcome.md b/docs/reference/api/Compono/Compono.SequenceOutcome.md new file mode 100644 index 00000000..58277d3f --- /dev/null +++ b/docs/reference/api/Compono/Compono.SequenceOutcome.md @@ -0,0 +1,16 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono') + +## SequenceOutcome Class + +Factory for the exception side of a [ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)') entry\. + +```csharp +public static class SequenceOutcome +``` + +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') → SequenceOutcome + +| Methods | | +| :--- | :--- | +| [Throw\(Exception\)](Compono.SequenceOutcome.Throw(System.Exception).md 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)') | A sequence entry that throws [exception](Compono.SequenceOutcome.Throw(System.Exception).md#Compono.SequenceOutcome.Throw(System.Exception).exception 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)\.exception') when consumed\. | diff --git a/docs/reference/api/Compono/Compono.SequenceOutcome_T_.md b/docs/reference/api/Compono/Compono.SequenceOutcome_T_.md new file mode 100644 index 00000000..775dffd8 --- /dev/null +++ b/docs/reference/api/Compono/Compono.SequenceOutcome_T_.md @@ -0,0 +1,34 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono') + +## SequenceOutcome\ Struct + +One outcome in a [ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)') sequence \- either a +configured return value \(implicit conversion from [T](Compono.SequenceOutcome_T_.md#Compono.SequenceOutcome_T_.T 'Compono\.SequenceOutcome\\.T')\) or a configured +exception \([Throw\(Exception\)](Compono.SequenceOutcome.Throw(System.Exception).md 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)')\), target\-typed so a consumer never spells +`SequenceOutcome` directly \(ADR\-0054\)\. Mirrors [Match<T>](Compono.Match_T_.md 'Compono\.Match\')'s own "implicit +conversion from a literal, no public constructor" shape\. + +```csharp +public readonly struct SequenceOutcome +``` +#### Type parameters + + + +`T` + +### Remarks +Only a single implicit conversion exists \(from [T](Compono.SequenceOutcome_T_.md#Compono.SequenceOutcome_T_.T 'Compono\.SequenceOutcome\\.T')\) \- a second implicit +conversion from [System\.Exception](https://learn.microsoft.com/en-us/dotnet/api/system.exception 'System\.Exception') was rejected because it is silently ambiguous/wrong for +[T](Compono.SequenceOutcome_T_.md#Compono.SequenceOutcome_T_.T 'Compono\.SequenceOutcome\\.T') values that are themselves [System\.Exception](https://learn.microsoft.com/en-us/dotnet/api/system.exception 'System\.Exception') or a base/derived type +of it \(e\.g\. `T = object` resolves to "throw" with no way left to express "value"; `T = + InvalidOperationException` silently resolves to "value" instead of "throw" \- both confirmed by +real compiler/runtime evidence, not assumed\)\. [Throw\(Exception\)](Compono.SequenceOutcome.Throw(System.Exception).md 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)') plus the second +implicit conversion from [ThrownOutcome](Compono.SequenceOutcome.ThrownOutcome.md 'Compono\.SequenceOutcome\.ThrownOutcome') is unambiguous for every +[T](Compono.SequenceOutcome_T_.md#Compono.SequenceOutcome_T_.T 'Compono\.SequenceOutcome\\.T')\. + +| Operators | | +| :--- | :--- | +| [implicit operator SequenceOutcome<T>\(ThrownOutcome\)](Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(Compono.SequenceOutcome.ThrownOutcome).md 'Compono\.SequenceOutcome\\.op\_Implicit Compono\.SequenceOutcome\\(Compono\.SequenceOutcome\.ThrownOutcome\)') | A sequence entry that throws the exception carried by [thrown](Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(Compono.SequenceOutcome.ThrownOutcome).md#Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(Compono.SequenceOutcome.ThrownOutcome).thrown 'Compono\.SequenceOutcome\\.op\_Implicit Compono\.SequenceOutcome\\(Compono\.SequenceOutcome\.ThrownOutcome\)\.thrown') when consumed\. | +| [implicit operator SequenceOutcome<T>\(T\)](Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(T).md 'Compono\.SequenceOutcome\\.op\_Implicit Compono\.SequenceOutcome\\(T\)') | A sequence entry that returns [value](Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(T).md#Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(T).value 'Compono\.SequenceOutcome\\.op\_Implicit Compono\.SequenceOutcome\\(T\)\.value') when consumed\. | diff --git a/docs/reference/api/Compono/Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(Compono.SequenceOutcome.ThrownOutcome).md b/docs/reference/api/Compono/Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(Compono.SequenceOutcome.ThrownOutcome).md new file mode 100644 index 00000000..8e859856 --- /dev/null +++ b/docs/reference/api/Compono/Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(Compono.SequenceOutcome.ThrownOutcome).md @@ -0,0 +1,18 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[SequenceOutcome<T>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\') + +## SequenceOutcome\\.implicit operator SequenceOutcome\\(ThrownOutcome\) Operator + +A sequence entry that throws the exception carried by [thrown](Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(Compono.SequenceOutcome.ThrownOutcome).md#Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(Compono.SequenceOutcome.ThrownOutcome).thrown 'Compono\.SequenceOutcome\\.op\_Implicit Compono\.SequenceOutcome\\(Compono\.SequenceOutcome\.ThrownOutcome\)\.thrown') when consumed\. + +```csharp +public static Compono.SequenceOutcome implicit operator Compono.SequenceOutcome(Compono.SequenceOutcome.ThrownOutcome thrown); +``` +#### Parameters + + + +`thrown` [ThrownOutcome](Compono.SequenceOutcome.ThrownOutcome.md 'Compono\.SequenceOutcome\.ThrownOutcome') + +#### Returns +[Compono\.SequenceOutcome<](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\')[T](Compono.SequenceOutcome_T_.md#Compono.SequenceOutcome_T_.T 'Compono\.SequenceOutcome\\.T')[>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\') \ No newline at end of file diff --git a/docs/reference/api/Compono/Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(T).md b/docs/reference/api/Compono/Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(T).md new file mode 100644 index 00000000..fea50a60 --- /dev/null +++ b/docs/reference/api/Compono/Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(T).md @@ -0,0 +1,18 @@ +#### [Compono](index.md 'index') +### [Compono](Compono.md 'Compono').[SequenceOutcome<T>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\') + +## SequenceOutcome\\.implicit operator SequenceOutcome\\(T\) Operator + +A sequence entry that returns [value](Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(T).md#Compono.SequenceOutcome_T_.op_ImplicitCompono.SequenceOutcome_T_(T).value 'Compono\.SequenceOutcome\\.op\_Implicit Compono\.SequenceOutcome\\(T\)\.value') when consumed\. + +```csharp +public static Compono.SequenceOutcome implicit operator Compono.SequenceOutcome(T value); +``` +#### Parameters + + + +`value` [T](Compono.SequenceOutcome_T_.md#Compono.SequenceOutcome_T_.T 'Compono\.SequenceOutcome\\.T') + +#### Returns +[Compono\.SequenceOutcome<](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\')[T](Compono.SequenceOutcome_T_.md#Compono.SequenceOutcome_T_.T 'Compono\.SequenceOutcome\\.T')[>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\') \ No newline at end of file diff --git a/docs/reference/api/Compono/Compono.md b/docs/reference/api/Compono/Compono.md index 96ab3407..f25fc8ab 100644 --- a/docs/reference/api/Compono/Compono.md +++ b/docs/reference/api/Compono/Compono.md @@ -26,6 +26,7 @@ | [Match](Compono.Match.md 'Compono\.Match') | Factory methods for [Match<T>](Compono.Match_T_.md 'Compono\.Match\') \- `Match.Any()`/`Match.Is(predicate)`\. | | [PlanCache<T>](Compono.PlanCache_T_.md 'Compono\.PlanCache\') | Holds the generated [ICompositionPlan<T>](Compono.ICompositionPlan_T_.md 'Compono\.ICompositionPlan\') for [T](Compono.PlanCache_T_.md#Compono.PlanCache_T_.T 'Compono\.PlanCache\\.T'), per `docs/adr/0004-composition-plan-discovery-and-dispatch.md`'s dispatch mechanism\. | | [RowInvokerRegistry](Compono.RowInvokerRegistry.md 'Compono\.RowInvokerRegistry') | A non\-generic, [System\.Type](https://learn.microsoft.com/en-us/dotnet/api/system.type 'System\.Type')\-keyed registry of [Resolve<TValue>\(CompositionRequestDescriptor\)](Compono.CompositionRow.Resolve.md#Compono.CompositionRow.Resolve_TValue_(Compono.CompositionRequestDescriptor) 'Compono\.CompositionRow\.Resolve\\(Compono\.CompositionRequestDescriptor\)')/ [ResolveShared<TValue>\(CompositionRequestDescriptor\)](Compono.CompositionRow.ResolveShared_TValue_(Compono.CompositionRequestDescriptor).md 'Compono\.CompositionRow\.ResolveShared\\(Compono\.CompositionRequestDescriptor\)')/ [ShareExplicit<TValue>\(CompositionRequestDescriptor, TValue\)](Compono.CompositionRow.ShareExplicit_TValue_(Compono.CompositionRequestDescriptor,TValue).md 'Compono\.CompositionRow\.ShareExplicit\\(Compono\.CompositionRequestDescriptor, TValue\)') dispatch delegates, per ADR\-0041 Amendment 2\. | +| [SequenceOutcome](Compono.SequenceOutcome.md 'Compono\.SequenceOutcome') | Factory for the exception side of a [ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)') entry\. | | [TestDoubleNotConfiguredException](Compono.TestDoubleNotConfiguredException.md 'Compono\.TestDoubleNotConfiguredException') | Thrown by a generated test double's configuration\-required member when it's invoked before `Configure().Member(...).Returns(...)`/`.Throws(...)` configures it \- see ADR\-0045\. | | [TestDoubleVerificationException](Compono.TestDoubleVerificationException.md 'Compono\.TestDoubleVerificationException') | Thrown by [CallVerifier](Compono.CallVerifier.md 'Compono\.CallVerifier') when a generated test double's member was called a different number of times than expected\. | | [UniqueValueResolver](Compono.UniqueValueResolver.md 'Compono\.UniqueValueResolver') | The bounded, deterministic duplicate\-value retry helper a generated `HashSet`/ `Dictionary` collection plan calls once per element/key position, per `docs/adr/0013-collection-generation-semantics.md` \(bounded retry, then diagnosable failure\) and `docs/adr/0014-generator-emitted-collection-plans.md` \(generated code, not a runtime provider, builds collections\)\. | @@ -40,6 +41,8 @@ | [ProviderAttempt](Compono.ProviderAttempt.md 'Compono\.ProviderAttempt') | One resolution\-pipeline stage tried for one composition request, and what it resulted in\. | | [ReturnConfig<T>](Compono.ReturnConfig_T_.md 'Compono\.ReturnConfig\') | Per\-member configured\-return state for a generator\-emitted test double, one instance per double member\. Backing fields are [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') \- only [ReturnConfigBuilder<T>](Compono.ReturnConfigBuilder_T_.md 'Compono\.ReturnConfigBuilder\'), same assembly, ever writes them \- but the read side is [public](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/public 'https://docs\.microsoft\.com/en\-us/dotnet/csharp/language\-reference/keywords/public') because the generated dispatch code reading a slot's configured state lives in a different \(consumer\) assembly\. See ADR\-0043 Amendment 3, Finding A\. | | [ReturnConfigBuilder<T>](Compono.ReturnConfigBuilder_T_.md 'Compono\.ReturnConfigBuilder\') | Public write surface over a single [ReturnConfig<T>](Compono.ReturnConfig_T_.md 'Compono\.ReturnConfig\') slot \- constructed by generator\-emitted configuration extensions \(`Configure().Member()`\) in the consumer's own assembly, per ADR\-0043\. A [ref struct](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref struct 'https://docs\.microsoft\.com/en\-us/dotnet/csharp/language\-reference/keywords/ref struct') because it only ever wraps a [ref](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref 'https://docs\.microsoft\.com/en\-us/dotnet/csharp/language\-reference/keywords/ref') to a field already living on the generated double instance; it's never stored, only used inline at the call site\. | +| [SequenceOutcome\.ThrownOutcome](Compono.SequenceOutcome.ThrownOutcome.md 'Compono\.SequenceOutcome\.ThrownOutcome') | Marker carrying the exception for a thrown sequence entry, implicitly convertible to [SequenceOutcome<T>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\') for any `T`\. Only ever produced by [Throw\(Exception\)](Compono.SequenceOutcome.Throw(System.Exception).md 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)') \- its own conversion guards against the struct's `default` value, which would otherwise carry a null exception\. | +| [SequenceOutcome<T>](Compono.SequenceOutcome_T_.md 'Compono\.SequenceOutcome\') | One outcome in a [ReturnsSequence\(SequenceOutcome<T>\[\]\)](Compono.ReturnConfigBuilder_T_.ReturnsSequence(Compono.SequenceOutcome_T_[]).md 'Compono\.ReturnConfigBuilder\\.ReturnsSequence\(Compono\.SequenceOutcome\\[\]\)') sequence \- either a configured return value \(implicit conversion from [T](Compono.SequenceOutcome_T_.md#Compono.SequenceOutcome_T_.T 'Compono\.SequenceOutcome\\.T')\) or a configured exception \([Throw\(Exception\)](Compono.SequenceOutcome.Throw(System.Exception).md 'Compono\.SequenceOutcome\.Throw\(System\.Exception\)')\), target\-typed so a consumer never spells `SequenceOutcome` directly \(ADR\-0054\)\. Mirrors [Match<T>](Compono.Match_T_.md 'Compono\.Match\')'s own "implicit conversion from a literal, no public constructor" shape\. | | [Unit](Compono.Unit.md 'Compono\.Unit') | Void\-marker type for a generated test double's `void`/[Task](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/Task 'https://docs\.microsoft\.com/en\-us/dotnet/csharp/language\-reference/keywords/Task')\-returning members, so [ReturnConfig<T>](Compono.ReturnConfig_T_.md 'Compono\.ReturnConfig\') has a closeable type argument even when the member itself returns nothing\. [public](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/public 'https://docs\.microsoft\.com/en\-us/dotnet/csharp/language\-reference/keywords/public') from the start \(not [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')\) \- the same cross\-assembly\-accessibility lesson ADR\-0043 Amendment 3 already applied to [ReturnConfig<T>](Compono.ReturnConfig_T_.md 'Compono\.ReturnConfig\')/[ReturnConfigBuilder<T>](Compono.ReturnConfigBuilder_T_.md 'Compono\.ReturnConfigBuilder\') applies here too: a generated double lives in the consumer's own assembly, not core `Compono`\. See ADR\-0043 Amendment 4\. | | Interfaces | | diff --git a/skills/compono/references/testdoubles.md b/skills/compono/references/testdoubles.md index a3e84034..fce9b668 100644 --- a/skills/compono/references/testdoubles.md +++ b/skills/compono/references/testdoubles.md @@ -137,8 +137,38 @@ repository.Configure() There is no matcher-specificity ranking — if two entries both match, the one configured later wins. -This is still not sequential/call-count-based responses. There is no -"return X on the first call, Y on the second" API. +## Sequential/call-count-based responses + +`ReturnConfigBuilder.ReturnsSequence(...)` (ADR-0054) configures a +different outcome per call, consumed in order; the final outcome repeats +once the sequence is exhausted. It coexists with the argument-matching +surface above — sequence state belongs to whichever entry the call +matched, so two argument-distinguished entries on the same member each own +an independent ordinal: + +```csharp +repository.Configure().CountAsync() + .ReturnsSequence( + SequenceOutcome.Throw(new TimeoutException("attempt 1 fails")), + SequenceOutcome.Throw(new TimeoutException("attempt 2 fails")), + Task.FromResult(42)); + +await repository.CountAsync(); // throws TimeoutException("attempt 1 fails") +await repository.CountAsync(); // throws TimeoutException("attempt 2 fails") +await repository.CountAsync(); // 42 +await repository.CountAsync(); // 42 (exhausted - repeats the final outcome) +``` + +Each element is a `SequenceOutcome`: an ordinary `T` value converts to +it implicitly (`1`, `Task.FromResult(42)`, `false`), and an exception +outcome is spelled explicitly with `SequenceOutcome.Throw(exception)` — +there is no implicit conversion from `Exception`, since that is silently +wrong for a `T` that is itself `Exception` or a base/derived type of it. +Call recording (`Verify().Member(...).Exactly(n)`) is independent of +response consumption — a throwing call still counts. Reconfiguring the +same entry (`Configure()` again) replaces the sequence and resets its +ordinal; `Returns(...)`/`Throws(...)` on the same builder clear any +configured sequence, and vice versa. ## Overloaded members (v2) @@ -184,6 +214,62 @@ both interface views share one call-recording state. See `docs/packages/compono-testdoubles.md`'s "Default interface members" section for the full example. +### Overload-safe argument matching (ADR-0044 Amendment 21) + +The discriminator-only surface above still selects an overload by real +argument *type*, not by argument *content*. When a test needs to distinguish +calls to the **same overload** by their actual argument values, an eligible +overload (real parameters, no `ref`/`out`/`in`, not a self-referencing +generic parameter - the same eligibility conditions as the non-overloaded +matching surface above) also gets a second, matching-specific member name - +`Matching` - taking real `Match` parameters directly: + +```csharp +public interface IAmazonDynamoDB +{ + Task DeleteItemAsync(DeleteItemRequest request, CancellationToken cancellationToken); + Task DeleteItemAsync(string tableName, CancellationToken cancellationToken); +} + +client.Configure() + .DeleteItemAsync(fallbackRequest, CancellationToken.None) + .Returns(Task.FromResult(fallbackResponse)); +client.Configure() + .DeleteItemAsyncMatching(Match.Is(x => x.TableName == "special"), Match.Any()) + .Returns(Task.FromResult(specialResponse)); + +client.Verify() + .DeleteItemAsyncMatching(Match.Is(x => x.TableName == "special"), Match.Any()) + .Once(); +``` + +`DeleteItemAsyncMatching(...)` is a **configuration/verification-side alias +only** - it is never itself an independently-dispatched method the SUT can +call. Both it and the unchanged `DeleteItemAsync(realArgs, ...)` +discriminator surface attach to the **same real overload**'s entries/call +log: registration order gives precedence (last-matching-registration-wins, +same reverse-scan rule as "Multiple response configurations per member" +above), so a broad discriminator-only response registered first and a +narrower `.Matching(...)` override registered after it compose exactly like +two entries on a non-overloaded member would. `Verify().DeleteItemAsync(realArgs, ...)` +still reports the overload's total real call count, now backed by the same +call log. A literal argument on the `Matching`-named surface converts to +`Match` exactly like it does everywhere else (Amendment 18) - it's +rejected only when two sibling overloads share the same `Matching` +name AND the literal is ambiguously convertible to both of their `Match` +types (e.g. `Get(int)`/`Get(long)` called as `GetMatching(5)`, a real +`CS0121`), not as a blanket rule. + +In the rare case a real interface member is literally named +`Matching` and its own generated `Configure()` extension +signature would otherwise collide with the alias's, Compono disambiguates +automatically with a deterministic fallback name, the same way it already +does for other generated names that collide - no diagnostic, no dropped +capability, both surfaces stay independently reachable. A real *generic* +member sharing a closed-instantiation signature with the alias is a softer, +non-blocking case: that specific closed instantiation is reachable only via +an explicit type argument on the real member's own name, never implicitly. + ## Generic methods (v2) A generic method is supported when its return type doesn't reference its @@ -293,7 +379,6 @@ that needs access to the actual invocation as a first-class value: its result"); - callback side effects based on the actual invocation; - call-order verification; -- sequential/call-count-based responses; - strict mode, partial substitutes, recursive auto-configuration; - classes, delegates, indexers, events, and other unsupported shapes listed below. diff --git a/src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs b/src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs index 437196bb..127aca65 100644 --- a/src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs +++ b/src/Compono.Generators/Discovery/TestDoubleAnalyzer.cs @@ -593,6 +593,45 @@ public static DiscoveredTestDoubleInfo Analyze(INamedTypeSymbol interfaceType, C } } + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: an overloaded, matching-eligible-SHAPED + // candidate (real parameters, no ref-like parameter, no self-referencing generic parameter, + // not Equals(object)) gets the SAME Entry/Entries/_calls/_lock layout ADR-0050 already gives + // a non-overloaded matching-eligible member - keyed off its own FieldName (which already + // carries a discriminator suffix from discriminatorSuffixByIdentity above, so it's unique per + // overload without any new naming scheme), reserved into the SAME derivedAuxiliaryNameOwners + // pool so a genuine collision with any other member's own derived names is caught by the same + // collision-resolution pass just below, not a separate mechanism. + var matchingEligibleShapedOverloadedCandidates = new HashSet(SymbolEqualityComparer.Default); + + foreach (var candidate in eligibleCandidates) + { + if (candidate is not IMethodSymbol candidateMethod || !overloadedNames.Contains(candidateMethod.Name) || + candidateMethod.Parameters.Length == 0 || + IsClosedInstantiationEligibleCandidate(candidateMethod, compilation) || + (candidateMethod.IsGenericMethod && candidateMethod.Parameters.Any(p => TypeReferencesOwnTypeParameter(p.Type, candidateMethod))) || + candidateMethod.Parameters.Any(p => p.Type.IsRefLikeType) || + (candidateMethod.Name == "Equals" && candidateMethod.Parameters.Length == 1) || + !WouldGetConfigurationSurface(candidateMethod, diamondCollisionIdentities)) + continue; + + matchingEligibleShapedOverloadedCandidates.Add(candidateMethod); + + var overloadFieldName = $"__{candidateMethod.Name}{discriminatorSuffixByIdentity[(candidateMethod.Name, Canonical: IdentityFor(candidateMethod))]}"; + var derivedOverloadNames = new[] + { + $"{overloadFieldName}_calls", $"{overloadFieldName}_lock", + $"{overloadFieldName}_Entry", $"{overloadFieldName}_entries", + }; + + foreach (var name in derivedOverloadNames) + { + if (!derivedAuxiliaryNameOwners.TryGetValue(name, out var owners)) + derivedAuxiliaryNameOwners[name] = owners = new List(); + + owners.Add(candidateMethod); + } + } + foreach (var (name, owners) in derivedAuxiliaryNameOwners) { if (owners.Count <= 1 && !usedFieldNames.Contains(name)) @@ -602,6 +641,119 @@ public static DiscoveredTestDoubleInfo Analyze(INamedTypeSymbol interfaceType, C derivedNameCollisionMembers.Add(owner); } + // ADR-0044 Amendment 21: an overloaded, matching-eligible-shaped candidate whose own derived + // names survived the collision pass above is eligible for the new matching-specific + // Configure()/Verify() member name. Its alias name defaults to "Matching"; on a real + // signature collision with an already-matching-eligible sibling of that exact literal name + // (a genuine CS0111 risk, confirmed by compiler spike - see PLAN-0054's "Naming/collision + // policy"), it falls back to a deterministic hash-suffixed name, reusing + // TestDoubleOverloadIdentity.StableHash exactly like discriminatorSuffixByIdentity above. + var overloadMatchingEligibleCandidates = new HashSet( + matchingEligibleShapedOverloadedCandidates.Where(m => !derivedNameCollisionMembers.Contains(m)), + SymbolEqualityComparer.Default); + + // Codex review, PR #115 (round 1): the alias-collision check below must compare REAL C# + // signature identity - which, per TestDoubleOverloadIdentity's own established precedent, + // never considers nullable-reference annotations - not nullable-aware display-string text. + // Comparing ITypeSymbol via SymbolEqualityComparer.Default (nullability-insensitive by + // default) instead of formatted strings fixes this directly: `string` and `string?` compare + // equal, exactly matching what the real compiler does when it decides CS0111. + var matchTypeDefinition = compilation.GetTypeByMetadataName("Compono.Match`1"); + + // Codex review, PR #115 (round 2): every real member sharing the alias's literal name is a + // potential collision, not just a non-overloaded matching-eligible one - an ordinary + // overloaded member (or a ref-like/self-referencing-generic/Equals(object)-arity one) still + // emits its own real-parameter-typed Configure() extension, which can collide with our + // alias's Match-wrapped one exactly as easily as a matching-eligible member's Match + // extension can. Build the ACTUAL generated (arity, parameter-type list) for every real + // candidate sharing a literal name with some alias - Match-constructed when that + // candidate is itself genuinely matching-eligible (its own extension is Match-wrapped, + // including a non-overloaded ADR-0049 closed-instantiation-eligible member with real + // matched parameters - Codex review, PR #115 round 3: excluding closed-instantiation- + // eligible candidates here entirely missed exactly this real collision shape), the real + // declared type otherwise (its own extension - ordinary overloaded, an overloaded closed- + // instantiation-eligible member, ref-like, self-referencing-generic, Equals(object)-arity, + // or derived-name-collision-fallback - always uses the real type as declared, never + // wrapped). Generic arity is tracked alongside the parameter types (Codex review, PR #115 + // round 3) - part of real C# signature identity same as the parameter types themselves, per + // PLAN-0054's own "Naming/collision policy" Finding 4 (a generic member sharing a candidate + // name never collides via CS0111 purely on arity grounds, only ever soft-shadows). + var realGeneratedSignaturesByName = new Dictionary>(StringComparer.Ordinal); + + if (matchTypeDefinition is not null) + { + foreach (var candidate in eligibleCandidates) + { + if (candidate is not IMethodSymbol candidateMethod || + candidateMethod.Parameters.Length == 0 || + !WouldGetConfigurationSurface(candidateMethod, diamondCollisionIdentities)) + continue; + + var isMatchingEligible = !overloadedNames.Contains(candidateMethod.Name) && + !(candidateMethod.IsGenericMethod && + candidateMethod.Parameters.Any(p => TypeReferencesOwnTypeParameter(p.Type, candidateMethod))) && + !candidateMethod.Parameters.Any(p => p.Type.IsRefLikeType) && + !(candidateMethod.Name == "Equals" && candidateMethod.Parameters.Length == 1) && + !derivedNameCollisionMembers.Contains(candidateMethod); + + var parameterTypes = isMatchingEligible + ? candidateMethod.Parameters.Select(p => (ISymbol)matchTypeDefinition.Construct(p.Type)).ToArray() + : candidateMethod.Parameters.Select(p => (ISymbol)p.Type).ToArray(); + + // Codex review, PR #115 (round 4): a candidate's own TypeParameters.Length is NOT + // always its generated extension's actual arity - TestDoubleMemberInfo.ExtensionIsGeneric + // (the template's own governing rule) says a SOLO generic member's extension only + // stays generic when it's also overloaded or closed-instantiation-eligible; a solo + // (non-overloaded, non-closed-instantiation) generic member's Configure()/Verify() + // extension is emitted non-generic regardless of TypeParameters.Length (Requirement + // 2's "one backing slot covers every closed instantiation" rule, extended identically + // to a matching-eligible member's own extension). Mirror that exact rule here instead + // of assuming generic arity always survives into the emitted signature. + var effectiveArity = candidateMethod.IsGenericMethod && + (overloadedNames.Contains(candidateMethod.Name) || IsClosedInstantiationEligibleCandidate(candidateMethod, compilation)) + ? candidateMethod.TypeParameters.Length + : 0; + + if (!realGeneratedSignaturesByName.TryGetValue(candidateMethod.Name, out var signatures)) + realGeneratedSignaturesByName[candidateMethod.Name] = signatures = new List<(int, ISymbol[])>(); + + signatures.Add((effectiveArity, parameterTypes)); + } + } + + var allCandidateNames = new HashSet(eligibleCandidates.Select(m => m.Name), StringComparer.Ordinal); + var matchingAliasNameByMemberName = new Dictionary(StringComparer.Ordinal); + + foreach (var group in overloadMatchingEligibleCandidates.GroupBy(m => m.Name)) + { + var aliasBase = $"{group.Key}Matching"; + var hasSignatureCollision = matchTypeDefinition is not null && + realGeneratedSignaturesByName.TryGetValue(aliasBase, out var signatures) && + group.Any(overload => + { + var aliasArity = overload.TypeParameters.Length; + var aliasSignature = overload.Parameters.Select(p => (ISymbol)matchTypeDefinition.Construct(p.Type)).ToArray(); + return signatures!.Any(signature => + signature.Arity == aliasArity && + signature.ParameterTypes.SequenceEqual(aliasSignature, SymbolEqualityComparer.Default)); + }); + + if (!hasSignatureCollision) + { + matchingAliasNameByMemberName[group.Key] = aliasBase; + continue; + } + + var baseHash = TestDoubleOverloadIdentity.StableHash(aliasBase); + var aliasName = $"{aliasBase}_{baseHash}"; + var disambiguator = 2; + + while (allCandidateNames.Contains(aliasName) || matchingAliasNameByMemberName.ContainsValue(aliasName)) + aliasName = $"{aliasBase}_{baseHash}_{disambiguator++}"; + + matchingAliasNameByMemberName[group.Key] = aliasName; + } + // Codex review, PR #108 (round 8): a matching-eligible-SHAPED candidate deferred above can // have just been added to `derivedNameCollisionMembers` by the pass immediately above (via // its OWN `_calls`/`_lock`/etc. auxiliary names colliding with something else) - meaning it @@ -1288,6 +1440,16 @@ public static DiscoveredTestDoubleInfo Analyze(INamedTypeSymbol interfaceType, C !derivedNameCollisionMembers.Contains(method) && !(method.Name == "Equals" && parameters.Count == 1); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: whether this overload gets the + // new matching-specific Configure()/Verify() member name - computed by the + // pre-pass above (matchingEligibleShapedOverloadedCandidates, minus any + // derived-name collision), mutually exclusive with isEligibleForMatching (that + // one explicitly excludes isOverloaded). + var isOverloadMatchingEligible = overloadMatchingEligibleCandidates.Contains(method); + var matchingMemberName = isOverloadMatchingEligible + ? RequiredMemberCollector.EscapeIdentifier(matchingAliasNameByMemberName[method.Name]) + : ""; + var extensionReceiverName = hasConfigurationSurface && (isOverloaded || isEligibleForMatching || isClosedInstantiationEligible) ? SafeReceiverName(parameters.Select(p => p.EscapedName).Concat(typeParameterNames)) : "self"; @@ -1322,6 +1484,8 @@ public static DiscoveredTestDoubleInfo Analyze(INamedTypeSymbol interfaceType, C constraintClauses, IsConfigurationRequired: isConfigurationRequired, IsEligibleForMatching: isEligibleForMatching, + IsOverloadMatchingEligible: isOverloadMatchingEligible, + MatchingMemberName: matchingMemberName, IsClosedInstantiationEligible: isClosedInstantiationEligible, IsClosedInstantiationEligibleShape: isClosedInstantiationEligibleShape, IsDimFallbackTarget: isDimFallbackTarget, diff --git a/src/Compono.Generators/Emitters/TestDoubleEmitter.cs b/src/Compono.Generators/Emitters/TestDoubleEmitter.cs index 46ecb351..f698fc9b 100644 --- a/src/Compono.Generators/Emitters/TestDoubleEmitter.cs +++ b/src/Compono.Generators/Emitters/TestDoubleEmitter.cs @@ -105,6 +105,8 @@ public static void Generate(SourceProductionContext context, DiscoveredTestDoubl m.IsConfigurationRequired, m.IsOverloaded, m.IsEligibleForMatching, + m.IsOverloadMatchingEligible, + m.MatchingMemberName, m.IsClosedInstantiationEligible, m.ExtensionReceiverName, m.GenericSuffix, diff --git a/src/Compono.Generators/Models/TestDoubleMemberInfo.cs b/src/Compono.Generators/Models/TestDoubleMemberInfo.cs index a28ebfc0..8b15f5ba 100644 --- a/src/Compono.Generators/Models/TestDoubleMemberInfo.cs +++ b/src/Compono.Generators/Models/TestDoubleMemberInfo.cs @@ -125,6 +125,20 @@ namespace Compono.Generators.Models; /// ILogger<TState>.Log shape). An ineligible member generates its existing v1/v2/ADR-0044 /// shape, byte-for-byte unchanged. /// +/// +/// ADR-0044 Amendment 21 / PLAN-0054 Phase 2: whether this overloaded member gets a second, +/// matching-specific Configure()/Verify() member name () +/// taking real Compono.Match<T> parameters directly, in addition to its unchanged +/// discriminator-only surface - both attach to the SAME real overload's entries/call-log/lock state +/// (the same condition list uses, minus the !IsOverloaded +/// guard - see that parameter's own doc for the individual exclusions). +/// +/// +/// The matching-specific member name - "<Name>Matching" by default, or a deterministic +/// hash-suffixed fallback on a genuine signature collision with an already-matching-eligible sibling +/// of that exact literal name (a real CS0111 risk, confirmed by compiler spike). Empty when +/// is . +/// /// /// Whether this member gets ADR-0049's per-closed-T Configure<T>()/Verify<T>() /// surface - a generic method whose return type is exactly its own sole type parameter T, or the @@ -208,6 +222,8 @@ internal sealed record TestDoubleMemberInfo( EquatableArray ConstraintClauses = default, bool IsConfigurationRequired = false, bool IsEligibleForMatching = false, + bool IsOverloadMatchingEligible = false, + string MatchingMemberName = "", bool IsClosedInstantiationEligible = false, bool IsClosedInstantiationEligibleShape = false, bool IsForwarding = false, diff --git a/src/Compono.Generators/Templates/TestDouble.scriban b/src/Compono.Generators/Templates/TestDouble.scriban index 3f9a9260..a86abb34 100644 --- a/src/Compono.Generators/Templates/TestDouble.scriban +++ b/src/Compono.Generators/Templates/TestDouble.scriban @@ -53,12 +53,12 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie return ({{ member.closed_instantiation_state_class_name }}<{{ member.closed_instantiation_type_parameter_name }}>){{ member.boxed_local_name }}; } } -{{~ else if member.has_configuration_surface && !member.is_eligible_for_matching ~}} +{{~ else if member.has_configuration_surface && !member.is_eligible_for_matching && !member.is_overload_matching_eligible ~}} internal global::Compono.ReturnConfig<{{ member.slot_type_fully_qualified_name }}> {{ member.field_name }}; {{~ end ~}} {{~ end ~}} {{~ for member in members ~}} -{{~ if member.is_eligible_for_matching ~}} +{{~ if member.is_eligible_for_matching || member.is_overload_matching_eligible ~}} // ADR-0050: multi-entry response configuration - replaces the single // {{ member.field_name }}/{{ member.field_name }}_m_{param} shape with an ordered, append-only // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). @@ -164,6 +164,10 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie // 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 ({{ member.entry_local_name }}.Config.HasConfiguredSequence) return {{ member.entry_local_name }}.Config.NextSequenceOutcome(); if ({{ member.entry_local_name }}.Config.HasConfiguredException) throw {{ member.entry_local_name }}.Config.ConfiguredException; if ({{ member.entry_local_name }}.Config.HasConfiguredValue) return {{ member.entry_local_name }}.Config.ConfiguredValue; } @@ -179,7 +183,8 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie {{~ end ~}} {{~ else ~}} {{ member.bucket_local_name }}.Config.RecordCall(); - return {{ member.bucket_local_name }}.Config.HasConfiguredException ? throw {{ member.bucket_local_name }}.Config.ConfiguredException + return {{ member.bucket_local_name }}.Config.HasConfiguredSequence ? {{ member.bucket_local_name }}.Config.NextSequenceOutcome() + : {{ member.bucket_local_name }}.Config.HasConfiguredException ? throw {{ member.bucket_local_name }}.Config.ConfiguredException : {{ member.bucket_local_name }}.Config.HasConfiguredValue ? {{ member.bucket_local_name }}.Config.ConfiguredValue {{~ if member.is_configuration_required ~}} : throw new global::Compono.TestDoubleNotConfiguredException( @@ -194,15 +199,16 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie {{~ if member.closed_instantiation_needs_nullable_suppression ~}} #pragma warning restore CS8603, CS8616, CS8619 {{~ end ~}} -{{~ else if member.is_eligible_for_matching ~}} +{{~ else if member.is_eligible_for_matching || member.is_overload_matching_eligible ~}} {{~ if member.is_void ~}} void {{ member.declaring_interface_fully_qualified_name }}.{{ member.escaped_name }}{{ member.generic_suffix }}({{ for p in member.parameters }}{{ p.ref_kind_prefix }}{{ p.fully_qualified_type_name }} {{ p.escaped_name }}{{ if !for.last }}, {{ end }}{{ end }}) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock ({{ member.field_name }}_lock) { {{ member.field_name }}_calls.Add({{ member.call_log_construct_expression }}); @@ -221,6 +227,10 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if ({{ member.entry_local_name }}.Config.HasConfiguredSequence) { {{ member.entry_local_name }}.Config.NextSequenceOutcome(); return; } if ({{ member.entry_local_name }}.Config.HasConfiguredException) throw {{ member.entry_local_name }}.Config.ConfiguredException; if ({{ member.entry_local_name }}.Config.HasConfiguredValue) return; } @@ -252,6 +262,10 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie // 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 ({{ member.entry_local_name }}.Config.HasConfiguredSequence) return {{ member.entry_local_name }}.Config.NextSequenceOutcome(); if ({{ member.entry_local_name }}.Config.HasConfiguredException) throw {{ member.entry_local_name }}.Config.ConfiguredException; if ({{ member.entry_local_name }}.Config.HasConfiguredValue) return {{ member.entry_local_name }}.Config.ConfiguredValue; } @@ -271,7 +285,9 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie void {{ member.declaring_interface_fully_qualified_name }}.{{ member.escaped_name }}{{ member.generic_suffix }}({{ for p in member.parameters }}{{ p.ref_kind_prefix }}{{ p.fully_qualified_type_name }} {{ p.escaped_name }}{{ if !for.last }}, {{ end }}{{ end }}) { {{ member.field_name }}.RecordCall(); - if ({{ member.field_name }}.HasConfiguredException) + if ({{ member.field_name }}.HasConfiguredSequence) + {{ member.field_name }}.NextSequenceOutcome(); + else if ({{ member.field_name }}.HasConfiguredException) throw {{ member.field_name }}.ConfiguredException; {{~ if member.is_dim_fallback_target ~}} else if (!{{ member.field_name }}.HasConfiguredValue) @@ -282,7 +298,8 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie {{ member.return_type_fully_qualified_name }} {{ member.declaring_interface_fully_qualified_name }}.{{ member.escaped_name }}{{ member.generic_suffix }}({{ for p in member.parameters }}{{ p.ref_kind_prefix }}{{ p.fully_qualified_type_name }} {{ p.escaped_name }}{{ if !for.last }}, {{ end }}{{ end }}) { {{ member.field_name }}.RecordCall(); - return {{ member.field_name }}.HasConfiguredException ? throw {{ member.field_name }}.ConfiguredException + return {{ member.field_name }}.HasConfiguredSequence ? {{ member.field_name }}.NextSequenceOutcome() + : {{ member.field_name }}.HasConfiguredException ? throw {{ member.field_name }}.ConfiguredException : {{ member.field_name }}.HasConfiguredValue ? {{ member.field_name }}.ConfiguredValue {{~ if member.is_configuration_required ~}} : throw new global::Compono.TestDoubleNotConfiguredException( @@ -320,7 +337,8 @@ internal sealed class {{ safe_identifier }}_Double : {{ interface_fully_qualifie get { {{ member.field_name }}.RecordCall(); - return {{ member.field_name }}.HasConfiguredException ? throw {{ member.field_name }}.ConfiguredException + return {{ member.field_name }}.HasConfiguredSequence ? {{ member.field_name }}.NextSequenceOutcome() + : {{ member.field_name }}.HasConfiguredException ? throw {{ member.field_name }}.ConfiguredException : {{ member.field_name }}.HasConfiguredValue ? {{ member.field_name }}.ConfiguredValue {{~ if member.is_configuration_required ~}} : throw new global::Compono.TestDoubleNotConfiguredException( @@ -424,6 +442,32 @@ internal static class {{ safe_identifier }}_DoubleConfiguration lock (self.{{ member.field_name }}_lock) { self.{{ member.entries_field_name }}.Add({{ member.entry_local_name }}); } return new global::Compono.ReturnConfigBuilder<{{ member.slot_type_fully_qualified_name }}>(ref {{ member.entry_local_name }}.Config); } +{{~ else if member.is_overload_matching_eligible ~}} + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder<{{ member.slot_type_fully_qualified_name }}> {{ member.escaped_name }}{{ member.generic_suffix }}(this global::{{ safe_identifier }}_Double {{ member.extension_receiver_name }}{{ for p in member.parameters }}, {{ if p.is_params }}params {{ end }}{{ p.fully_qualified_type_name }} {{ p.escaped_name }}{{ if p.default_value_expression != "" }} = {{ p.default_value_expression }}{{ end }}{{ end }}){{ member.constraint_clauses_text }} + { + var {{ member.entry_local_name }} = new global::{{ safe_identifier }}_Double.{{ member.entry_class_name }}(); + lock ({{ member.extension_receiver_name }}.{{ member.field_name }}_lock) { {{ member.extension_receiver_name }}.{{ member.entries_field_name }}.Add({{ member.entry_local_name }}); } + return new global::Compono.ReturnConfigBuilder<{{ member.slot_type_fully_qualified_name }}>(ref {{ member.entry_local_name }}.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder<{{ member.slot_type_fully_qualified_name }}> {{ member.matching_member_name }}{{ member.generic_suffix }}(this global::{{ safe_identifier }}_Double {{ member.extension_receiver_name }}{{ for p in member.parameters }}, global::Compono.Match<{{ p.fully_qualified_type_name }}> {{ p.escaped_name }}{{ end }}){{ member.constraint_clauses_text }} + { + var {{ member.entry_local_name }} = new global::{{ safe_identifier }}_Double.{{ member.entry_class_name }}(); +{{~ for p in member.parameters ~}} + {{ member.entry_local_name }}.Matcher_{{ p.original_name }} = {{ p.escaped_name }}; +{{~ end ~}} + lock ({{ member.extension_receiver_name }}.{{ member.field_name }}_lock) { {{ member.extension_receiver_name }}.{{ member.entries_field_name }}.Add({{ member.entry_local_name }}); } + return new global::Compono.ReturnConfigBuilder<{{ member.slot_type_fully_qualified_name }}>(ref {{ member.entry_local_name }}.Config); + } {{~ else if member.is_overloaded ~}} public static global::Compono.ReturnConfigBuilder<{{ member.slot_type_fully_qualified_name }}> {{ member.escaped_name }}{{ member.generic_suffix }}(this global::{{ safe_identifier }}_Double {{ member.extension_receiver_name }}{{ for p in member.parameters }}, {{ if p.is_params }}params {{ end }}{{ p.fully_qualified_type_name }} {{ p.escaped_name }}{{ if p.default_value_expression != "" }} = {{ p.default_value_expression }}{{ end }}{{ end }}){{ member.constraint_clauses_text }} => new global::Compono.ReturnConfigBuilder<{{ member.slot_type_fully_qualified_name }}>(ref {{ member.extension_receiver_name }}.{{ member.field_name }}); @@ -508,6 +552,34 @@ internal static class {{ safe_identifier }}_DoubleVerification lock (self.Instance.{{ member.field_name }}_lock) { {{ member.count_local_name }} = self.Instance.{{ member.field_name }}_calls.Count; } return new({{ member.count_local_name }}, "{{ member.member_description }}"); } +{{~ else if member.is_overload_matching_eligible ~}} + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier {{ member.escaped_name }}{{ member.generic_suffix }}(this global::{{ safe_identifier }}_DoubleVerifier {{ member.extension_receiver_name }}{{ for p in member.parameters }}, {{ if p.is_params }}params {{ end }}{{ p.fully_qualified_type_name }} {{ p.escaped_name }}{{ if p.default_value_expression != "" }} = {{ p.default_value_expression }}{{ end }}{{ end }}){{ member.constraint_clauses_text }} + { + int {{ member.count_local_name }}; + lock ({{ member.extension_receiver_name }}.Instance.{{ member.field_name }}_lock) { {{ member.count_local_name }} = {{ member.extension_receiver_name }}.Instance.{{ member.field_name }}_calls.Count; } + return new({{ member.count_local_name }}, "{{ member.member_description }}"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier {{ member.matching_member_name }}{{ member.generic_suffix }}(this global::{{ safe_identifier }}_DoubleVerifier {{ member.extension_receiver_name }}{{ for p in member.parameters }}, global::Compono.Match<{{ p.fully_qualified_type_name }}> {{ p.escaped_name }}{{ end }}){{ member.constraint_clauses_text }} + { + int {{ member.count_local_name }}; + lock ({{ member.extension_receiver_name }}.Instance.{{ member.field_name }}_lock) + { + {{ member.count_local_name }} = 0; + foreach (var {{ member.call_loop_variable_name }} in {{ member.extension_receiver_name }}.Instance.{{ member.field_name }}_calls) + { + if ({{ for p in member.parameters }}{{ p.escaped_name }}.Matches({{ p.call_log_access_expression }}){{ if !for.last }} && {{ end }}{{ end }}) + {{ member.count_local_name }}++; + } + } + return new({{ member.count_local_name }}, "{{ member.member_description }}"); + } {{~ else if member.is_overloaded ~}} public static global::Compono.CallVerifier {{ member.escaped_name }}{{ member.generic_suffix }}(this global::{{ safe_identifier }}_DoubleVerifier {{ member.extension_receiver_name }}{{ for p in member.parameters }}, {{ if p.is_params }}params {{ end }}{{ p.fully_qualified_type_name }} {{ p.escaped_name }}{{ if p.default_value_expression != "" }} = {{ p.default_value_expression }}{{ end }}{{ end }}){{ member.constraint_clauses_text }} => new({{ member.extension_receiver_name }}.Instance.{{ member.field_name }}.ConfiguredCallCount, "{{ member.member_description }}"); diff --git a/src/Compono/ReturnConfig.cs b/src/Compono/ReturnConfig.cs index 1cd7a934..055bc369 100644 --- a/src/Compono/ReturnConfig.cs +++ b/src/Compono/ReturnConfig.cs @@ -14,12 +14,26 @@ public struct ReturnConfig internal Exception? Exception; internal int CallCount; + // ADR-0054: sequential/call-count-based responses. Null except after + // ReturnConfigBuilder.ReturnsSequence - the same slot Returns/Throws already use, extended + // with a third, mutually-exclusive state rather than a separate parallel type (see that ADR's + // "one remaining open design question" section, resolved by this shape composing cleanly with + // the existing Value/Exception fields and this struct's own already-Interlocked-based + // RecordCall() precedent). Immutable once set (ReturnsSequence always replaces the whole array, + // never mutates an element) - safe to read from multiple threads with no lock, only the ordinal + // claim below needs synchronization. + internal SequenceOutcome[]? Sequence; + internal int SequenceOrdinal; + /// Whether was set via . public readonly bool HasConfiguredValue => HasValue; /// Whether was set via . public readonly bool HasConfiguredException => Exception is not null; + /// Whether a response sequence was set via . + public readonly bool HasConfiguredSequence => Sequence is not null; + /// /// The value configured via . Only meaningful when /// is . @@ -45,4 +59,28 @@ public struct ReturnConfig /// Amendment 2, Finding 1. /// public void RecordCall() => System.Threading.Interlocked.Increment(ref CallCount); + + /// + /// 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 + /// is . 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). + /// + /// + /// Thread-safe with no lock: is never mutated after + /// sets it (a reconfiguration replaces the + /// whole array reference, never edits an element in place), so the only shared mutable state is + /// the ordinal itself - claimed via , + /// the same primitive already uses, so two concurrent callers always + /// claim two distinct, strictly-increasing ordinals and never observe or corrupt each other's + /// index. + /// + public T NextSequenceOutcome() + { + var outcomes = Sequence!; + var ordinal = System.Threading.Interlocked.Increment(ref SequenceOrdinal) - 1; + var index = ordinal >= outcomes.Length ? outcomes.Length - 1 : ordinal; + return outcomes[index].Resolve(); + } } diff --git a/src/Compono/ReturnConfigBuilder.cs b/src/Compono/ReturnConfigBuilder.cs index e933d4cd..61c8058e 100644 --- a/src/Compono/ReturnConfigBuilder.cs +++ b/src/Compono/ReturnConfigBuilder.cs @@ -8,9 +8,10 @@ namespace Compono; /// stored, only used inline at the call site. /// /// -/// / are last-configuration-wins: each clears the other's -/// state, so configuring a return after an earlier (or vice versa) doesn't -/// leave stale state behind. See ADR-0043 Amendment 7, Finding R. +/// // are all +/// last-configuration-wins: each of the three clears the other two's state, so configuring any one +/// of them after an earlier call to a different one of them doesn't leave stale state behind. See +/// ADR-0043 Amendment 7, Finding R (the original two-way rule) and ADR-0054 (the sequence extension). /// public readonly ref struct ReturnConfigBuilder { @@ -19,19 +20,55 @@ public readonly ref struct ReturnConfigBuilder /// Wraps , the generated double's own backing field for this member. public ReturnConfigBuilder(ref ReturnConfig slot) => _slot = ref slot; - /// Configures the member to return , clearing any prior . + /// Configures the member to return , clearing any prior /. public void Returns(T value) { _slot.Value = value; _slot.HasValue = true; _slot.Exception = null; + _slot.Sequence = null; + _slot.SequenceOrdinal = 0; } - /// Configures the member to throw , clearing any prior . + /// Configures the member to throw , clearing any prior /. public void Throws(Exception exception) { _slot.Exception = exception; _slot.HasValue = false; _slot.Value = default; + _slot.Sequence = null; + _slot.SequenceOrdinal = 0; + } + + /// + /// Configures the member to return (or throw) each entry in order, one + /// per invocation, by ordinal - the first call gets outcomes[0], the second + /// outcomes[1], and so on; once exhausted, every further call repeats the final entry + /// (ADR-0054). Clears any prior // + /// state and resets the ordinal to 0, the same last-configuration-wins contract / + /// already document. An ordinary value implicitly + /// converts to , so a pure-value sequence reads as plain values + /// (.ReturnsSequence(false, false, true)); an exception outcome is spelled explicitly with + /// - there is no implicit conversion from + /// , since that would be silently wrong for a that + /// is itself or a base/derived type of it - so a mixed sequence reads + /// .ReturnsSequence(SequenceOutcome.Throw(ex1), SequenceOutcome.Throw(ex2), value). + /// + /// is empty. + public void ReturnsSequence(params SequenceOutcome[] outcomes) + { + if (outcomes.Length == 0) + throw new ArgumentException("A response sequence needs at least one outcome.", nameof(outcomes)); + + // Codex review, PR #115: `outcomes` is not guaranteed to be a fresh array - a caller can pass + // an existing named array through the `params` parameter and mutate an element afterward, + // which would silently change an already-configured response and violate + // ReturnConfig.NextSequenceOutcome()'s lock-free-safety premise that the sequence is + // immutable once configured. Snapshot it. + _slot.Sequence = (SequenceOutcome[])outcomes.Clone(); + _slot.SequenceOrdinal = 0; + _slot.HasValue = false; + _slot.Value = default; + _slot.Exception = null; } } diff --git a/src/Compono/SequenceOutcome.cs b/src/Compono/SequenceOutcome.cs new file mode 100644 index 00000000..680c3b62 --- /dev/null +++ b/src/Compono/SequenceOutcome.cs @@ -0,0 +1,78 @@ +namespace Compono; + +/// +/// One outcome in a sequence - either a +/// configured return value (implicit conversion from ) or a configured +/// exception (), target-typed so a consumer never spells +/// SequenceOutcome<T> directly (ADR-0054). Mirrors 's own "implicit +/// conversion from a literal, no public constructor" shape. +/// +/// +/// Only a single implicit conversion exists (from ) - a second implicit +/// conversion from was rejected because it is silently ambiguous/wrong for +/// values that are themselves or a base/derived type +/// of it (e.g. T = object resolves to "throw" with no way left to express "value"; T = +/// InvalidOperationException silently resolves to "value" instead of "throw" - both confirmed by +/// real compiler/runtime evidence, not assumed). plus the second +/// implicit conversion from is unambiguous for every +/// . +/// +public readonly struct SequenceOutcome +{ + private readonly bool _isException; + private readonly T? _value; + private readonly Exception? _exception; + + private SequenceOutcome(bool isException, T? value, Exception? exception) + { + _isException = isException; + _value = value; + _exception = exception; + } + + /// A sequence entry that returns when consumed. + public static implicit operator SequenceOutcome(T value) => new(false, value, null); + + /// A sequence entry that throws the exception carried by when consumed. + public static implicit operator SequenceOutcome(SequenceOutcome.ThrownOutcome thrown) + { + // Guards against `default(ThrownOutcome)` - a public struct's default bypasses + // SequenceOutcome.Throw's own null-check, so this conversion must re-check. + if (thrown.Exception is null) + throw new ArgumentException("A thrown sequence outcome must carry an exception - use SequenceOutcome.Throw(exception), not default(SequenceOutcome.ThrownOutcome).", nameof(thrown)); + + return new SequenceOutcome(true, default, thrown.Exception); + } + + /// Returns the configured value, or throws the configured exception. + internal T Resolve() => _isException ? throw _exception! : _value!; +} + +/// +/// Factory for the exception side of a entry. +/// +public static class SequenceOutcome +{ + /// A sequence entry that throws when consumed. + public static ThrownOutcome Throw(Exception exception) + { + ArgumentNullException.ThrowIfNull(exception); + return new ThrownOutcome(exception); + } + + /// + /// Marker carrying the exception for a thrown sequence entry, implicitly convertible to + /// for any T. Only ever produced by - + /// its own conversion guards against the struct's default value, which would otherwise + /// carry a null exception. + /// + public readonly struct ThrownOutcome + { + internal readonly Exception? Exception; + + internal ThrownOutcome(Exception exception) + { + Exception = exception; + } + } +} 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 30a52f10..35568ec7 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 @@ -39,6 +39,10 @@ internal sealed class __CanHandle_Entry // 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; } 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 4831f77e..ac5b37a9 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 @@ -11,7 +11,8 @@ internal sealed class TestNamespace_ICollision_e0157ffc_Double : global::TestNam bool global::TestNamespace.ICollision.Foo() { __Foo.RecordCall(); - return __Foo.HasConfiguredException ? throw __Foo.ConfiguredException + return __Foo.HasConfiguredSequence ? __Foo.NextSequenceOutcome() + : __Foo.HasConfiguredException ? throw __Foo.ConfiguredException : __Foo.HasConfiguredValue ? __Foo.ConfiguredValue : default; } @@ -19,7 +20,9 @@ internal sealed class TestNamespace_ICollision_e0157ffc_Double : global::TestNam void global::TestNamespace.ICollision.Foo_dimHelper() { __Foo_dimHelper.RecordCall(); - if (__Foo_dimHelper.HasConfiguredException) + if (__Foo_dimHelper.HasConfiguredSequence) + __Foo_dimHelper.NextSequenceOutcome(); + else if (__Foo_dimHelper.HasConfiguredException) throw __Foo_dimHelper.ConfiguredException; } } 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 f2d215df..a4695b60 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 @@ -39,6 +39,10 @@ internal sealed class __CanHandle_Entry // 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; } 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 82163bce..b3570f1b 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 @@ -30,7 +30,8 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase12 bool global::TestNamespace.IBase12.Flag() { __Flag.RecordCall(); - return __Flag.HasConfiguredException ? throw __Flag.ConfiguredException + return __Flag.HasConfiguredSequence ? __Flag.NextSequenceOutcome() + : __Flag.HasConfiguredException ? throw __Flag.ConfiguredException : __Flag.HasConfiguredValue ? __Flag.ConfiguredValue : ((global::TestNamespace.IBase12)(this.__Flag_dimHelper ??= new __Flag_DimFallback(this))).Flag(); } 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 21b3a36d..10a4f145 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_ILeaf10_232473a5_Double : global::TestNamesp bool global::TestNamespace.IBase10.Flag() { __Flag.RecordCall(); - return __Flag.HasConfiguredException ? throw __Flag.ConfiguredException + return __Flag.HasConfiguredSequence ? __Flag.NextSequenceOutcome() + : __Flag.HasConfiguredException ? throw __Flag.ConfiguredException : __Flag.HasConfiguredValue ? __Flag.ConfiguredValue : default; } 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 5714ab0c..0310c68d 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 @@ -32,7 +32,8 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase13 get { __Flag.RecordCall(); - return __Flag.HasConfiguredException ? throw __Flag.ConfiguredException + return __Flag.HasConfiguredSequence ? __Flag.NextSequenceOutcome() + : __Flag.HasConfiguredException ? throw __Flag.ConfiguredException : __Flag.HasConfiguredValue ? __Flag.ConfiguredValue : ((global::TestNamespace.IBase13)(this.__Flag_dimHelper ??= new __Flag_DimFallback(this))).Flag; } 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 89913c36..be14e83a 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 @@ -12,7 +12,8 @@ internal sealed class TestNamespace_ILeaf11_22247212_Double : global::TestNamesp get { __Flag.RecordCall(); - return __Flag.HasConfiguredException ? throw __Flag.ConfiguredException + return __Flag.HasConfiguredSequence ? __Flag.NextSequenceOutcome() + : __Flag.HasConfiguredException ? throw __Flag.ConfiguredException : __Flag.HasConfiguredValue ? __Flag.ConfiguredValue : default; } 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 f51cc4e8..2116532f 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 @@ -46,7 +46,8 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase8 bool global::TestNamespace.IBase8.Flag() { __Flag.RecordCall(); - return __Flag.HasConfiguredException ? throw __Flag.ConfiguredException + return __Flag.HasConfiguredSequence ? __Flag.NextSequenceOutcome() + : __Flag.HasConfiguredException ? throw __Flag.ConfiguredException : __Flag.HasConfiguredValue ? __Flag.ConfiguredValue : ((global::TestNamespace.IBase8)(this.__Flag_dimHelper ??= new __Flag_DimFallback(this))).Flag(); } @@ -57,11 +58,12 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase8 void global::TestNamespace.IBase8.Visit(string label) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock (__Visit_lock) { __Visit_calls.Add(label); @@ -80,6 +82,10 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase8 // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; if (__entry.Config.HasConfiguredValue) return; } 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 63f1fb40..3b986209 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 @@ -46,7 +46,8 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase5 bool global::TestNamespace.IBase5.Flag() { __Flag.RecordCall(); - return __Flag.HasConfiguredException ? throw __Flag.ConfiguredException + return __Flag.HasConfiguredSequence ? __Flag.NextSequenceOutcome() + : __Flag.HasConfiguredException ? throw __Flag.ConfiguredException : __Flag.HasConfiguredValue ? __Flag.ConfiguredValue : ((global::TestNamespace.IBase5)(this.__Flag_dimHelper ??= new __Flag_DimFallback(this))).Flag(); } @@ -57,11 +58,12 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase5 void global::TestNamespace.IBase5.Visit(string label) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock (__Visit_lock) { __Visit_calls.Add(label); @@ -80,6 +82,10 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase5 // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; if (__entry.Config.HasConfiguredValue) return; } 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 d4d91bb9..253833ec 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 @@ -46,7 +46,8 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase6 bool global::TestNamespace.IBase6.Flag() { __Flag.RecordCall(); - return __Flag.HasConfiguredException ? throw __Flag.ConfiguredException + return __Flag.HasConfiguredSequence ? __Flag.NextSequenceOutcome() + : __Flag.HasConfiguredException ? throw __Flag.ConfiguredException : __Flag.HasConfiguredValue ? __Flag.ConfiguredValue : ((global::TestNamespace.IBase6)(this.__Flag_dimHelper ??= new __Flag_DimFallback(this))).Flag(); } @@ -57,11 +58,12 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase6 void global::TestNamespace.IBase6.Visit(string label) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock (__Visit_lock) { __Visit_calls.Add(label); @@ -80,6 +82,10 @@ internal sealed class __Flag_DimFallback : global::TestNamespace.IBase6 // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; if (__entry.Config.HasConfiguredValue) return; } 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 6cf8790f..a0ed130e 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_ICollisionM_72d803a3_Double : global::TestNa bool global::TestNamespace.ICollisionM.M() { __M.RecordCall(); - return __M.HasConfiguredException ? throw __M.ConfiguredException + return __M.HasConfiguredSequence ? __M.NextSequenceOutcome() + : __M.HasConfiguredException ? throw __M.ConfiguredException : __M.HasConfiguredValue ? __M.ConfiguredValue : default; } 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 520ef152..3c690e1a 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 @@ -30,7 +30,8 @@ internal __ToString_State __ToString_Bucket() where T : class { var __bucket = __ToString_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactory.ToString' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().ToString(...).Returns(...) or .Throws(...) before invoking it."); 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 e7184e6d..ee7e325c 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 @@ -31,7 +31,8 @@ internal __Get_State __Get_Bucket() where T : class { var __bucket = __Get_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : global::System.Threading.Tasks.ValueTask.FromResult(default); } 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 aa275cf1..21b04586 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 @@ -30,7 +30,8 @@ internal __Get_State __Get_Bucket() { var __bucket = __Get_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactory.Get' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().Get(...).Returns(...) or .Throws(...) before invoking it."); 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 47f6c2cd..f5f8c578 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 @@ -31,7 +31,8 @@ internal __Get_State __Get_Bucket() where T : class { var __bucket = __Get_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : default; } 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 638b8d1e..3ea2b532 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 @@ -62,6 +62,10 @@ internal __Get_State __Get_Bucket() where T : class // 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; } @@ -76,7 +80,8 @@ internal __Get_State __Get_Bucket() where T : class get { __Get_calls.RecordCall(); - return __Get_calls.HasConfiguredException ? throw __Get_calls.ConfiguredException + return __Get_calls.HasConfiguredSequence ? __Get_calls.NextSequenceOutcome() + : __Get_calls.HasConfiguredException ? throw __Get_calls.ConfiguredException : __Get_calls.HasConfiguredValue ? __Get_calls.ConfiguredValue : default; } 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 acf63f55..fc3d7345 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 @@ -31,7 +31,8 @@ internal __M_m_x_State __M_m_x_Bucket() { var __bucket = __M_m_x_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactory.M_m_x' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().M_m_x(...).Returns(...) or .Throws(...) before invoking it."); @@ -40,7 +41,9 @@ internal __M_m_x_State __M_m_x_Bucket() void global::TestNamespace.IFactory.M(T x_State) { __M.RecordCall(); - if (__M.HasConfiguredException) + if (__M.HasConfiguredSequence) + __M.NextSequenceOutcome(); + else if (__M.HasConfiguredException) throw __M.ConfiguredException; } } 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 ef93359b..07f15931 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 @@ -30,7 +30,8 @@ internal __Create_State __Create_Bucket() { var __bucket = __Create_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactory.Create' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().Create(...).Returns(...) or .Throws(...) before invoking it."); 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 2dd0be99..d7bad1ea 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 @@ -50,7 +50,8 @@ internal __B_State_State __B_State_Bucket() { var __bucket = __B_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactory.B' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().B(...).Returns(...) or .Throws(...) before invoking it."); @@ -60,7 +61,8 @@ internal __B_State_State __B_State_Bucket() { var __bucket = __B_State_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactory.B_State' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().B_State(...).Returns(...) or .Throws(...) before invoking it."); 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 bceabd18..d80828c8 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 @@ -50,7 +50,8 @@ internal __Other_State<__Get_State> __Other_Bucket<__Get_State>() { var __bucket = __Get_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactory.Get' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().Get(...).Returns(...) or .Throws(...) before invoking it."); @@ -60,7 +61,8 @@ internal __Other_State<__Get_State> __Other_Bucket<__Get_State>() { var __bucket = __Other_Bucket<__Get_State>(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactory.Other' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().Other<__Get_State>(...).Returns(...) or .Throws(...) before invoking it."); 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 1a5fada9..98bee4cb 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 @@ -61,6 +61,10 @@ internal __GetContextDataAsync_State __GetContextDataAsync_Bucket() where // 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; } 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 320f84ae..8b1bfa8a 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 @@ -31,7 +31,8 @@ internal __Get_467634cd_State __Get_467634cd_Bucket() { var __bucket = __Get_467634cd_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactoryA.Get' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().Get(...).Returns(...) or .Throws(...) before invoking it."); @@ -42,7 +43,8 @@ internal __Get_467634cd_State __Get_467634cd_Bucket() get { __Get.RecordCall(); - return __Get.HasConfiguredException ? throw __Get.ConfiguredException + return __Get.HasConfiguredSequence ? __Get.NextSequenceOutcome() + : __Get.HasConfiguredException ? throw __Get.ConfiguredException : __Get.HasConfiguredValue ? __Get.ConfiguredValue : default; } 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 70a62d5d..4273e81d 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 @@ -30,7 +30,8 @@ internal __Create_State __Create_Bucket() { var __bucket = __Create_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IFactory.Create' was invoked without being configured (or without a matching argument configuration) for this closed type argument - call Configure().Create(...).Returns(...) or .Throws(...) before invoking it."); 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 9f31b970..5a92e944 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 @@ -60,6 +60,10 @@ internal __Create_State __Create_Bucket() // 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; } 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 61bbcf7f..ff445578 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 @@ -68,6 +68,10 @@ internal __Get_State __Get_Bucket() where T : class // 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; } 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 4c7d3569..9f46a239 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 @@ -21,11 +21,12 @@ internal sealed class __Configure_Entry void global::TestNamespace.IRepository.Configure(int mode) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock (__Configure_lock) { __Configure_calls.Add(mode); @@ -44,6 +45,10 @@ internal sealed class __Configure_Entry // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; if (__entry.Config.HasConfiguredValue) return; } @@ -54,7 +59,8 @@ internal sealed class __Configure_Entry string? global::TestNamespace.IRepository.GetName() { __GetName.RecordCall(); - return __GetName.HasConfiguredException ? throw __GetName.ConfiguredException + return __GetName.HasConfiguredSequence ? __GetName.NextSequenceOutcome() + : __GetName.HasConfiguredException ? throw __GetName.ConfiguredException : __GetName.HasConfiguredValue ? __GetName.ConfiguredValue : default; } 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 394483a1..b8a64c62 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa global::System.Collections.Generic.Dictionary global::TestNamespace.IRepository.GetCounts() { __GetCounts.RecordCall(); - return __GetCounts.HasConfiguredException ? throw __GetCounts.ConfiguredException + return __GetCounts.HasConfiguredSequence ? __GetCounts.NextSequenceOutcome() + : __GetCounts.HasConfiguredException ? throw __GetCounts.ConfiguredException : __GetCounts.HasConfiguredValue ? __GetCounts.ConfiguredValue : []; } 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 new file mode 100644 index 00000000..65d99d1b --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,259 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + // ADR-0050: multi-entry response configuration - replaces the single + // __Process_6729db8e/__Process_6729db8e_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Process_6729db8e_Entry + { + internal global::Compono.Match? Matcher_index; + internal global::Compono.Match? Matcher_label; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Process_6729db8e_Entry> __Process_6729db8e_entries = []; + internal readonly global::System.Collections.Generic.List<(int, string)> __Process_6729db8e_calls = []; + internal readonly object __Process_6729db8e_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Process_ac263cc1/__Process_ac263cc1_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Process_ac263cc1_Entry + { + internal global::Compono.Match? Matcher_index; + internal global::Compono.Match? Matcher_label; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Process_ac263cc1_Entry> __Process_ac263cc1_entries = []; + internal readonly global::System.Collections.Generic.List<(int, string)> __Process_ac263cc1_calls = []; + internal readonly object __Process_ac263cc1_lock = new(); + + void global::TestNamespace.IRepository.Process(int index, string label) + { + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Process_6729db8e_lock) + { + __Process_6729db8e_calls.Add((index, label)); + for (var __i = __Process_6729db8e_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Process_6729db8e_entries[__i]; + if ((__entry.Matcher_index is not { } __m_index || __m_index.Matches(index)) && (__entry.Matcher_label is not { } __m_label || __m_label.Matches(label))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } + } + + void global::TestNamespace.IRepository.Process(int index, string label) + { + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Process_ac263cc1_lock) + { + __Process_ac263cc1_calls.Add((index, label)); + for (var __i = __Process_ac263cc1_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Process_ac263cc1_entries[__i]; + if ((__entry.Matcher_index is not { } __m_index || __m_index.Matches(index)) && (__entry.Matcher_label is not { } __m_label || __m_label.Matches(label))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Process(this global::TestNamespace_IRepository_e3198068_Double __self, int index, string label) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Process_6729db8e_Entry(); + lock (__self.__Process_6729db8e_lock) { __self.__Process_6729db8e_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder ProcessMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match index, global::Compono.Match label) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Process_6729db8e_Entry(); + __entry.Matcher_index = index; + __entry.Matcher_label = label; + lock (__self.__Process_6729db8e_lock) { __self.__Process_6729db8e_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Process(this global::TestNamespace_IRepository_e3198068_Double __self, int index, string label) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Process_ac263cc1_Entry(); + lock (__self.__Process_ac263cc1_lock) { __self.__Process_ac263cc1_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder ProcessMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match index, global::Compono.Match label) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Process_ac263cc1_Entry(); + __entry.Matcher_index = index; + __entry.Matcher_label = label; + lock (__self.__Process_ac263cc1_lock) { __self.__Process_ac263cc1_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Process(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int index, string label) + { + int __count; + lock (__self.Instance.__Process_6729db8e_lock) { __count = __self.Instance.__Process_6729db8e_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Process"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier ProcessMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match index, global::Compono.Match label) + { + int __count; + lock (__self.Instance.__Process_6729db8e_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Process_6729db8e_calls) + { + if (index.Matches(call.Item1) && label.Matches(call.Item2)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Process"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Process(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int index, string label) + { + int __count; + lock (__self.Instance.__Process_ac263cc1_lock) { __count = __self.Instance.__Process_ac263cc1_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Process"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier ProcessMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match index, global::Compono.Match label) + { + int __count; + lock (__self.Instance.__Process_ac263cc1_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Process_ac263cc1_calls) + { + if (index.Matches(call.Item1) && label.Matches(call.Item2)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Process"); + } + +} + +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.GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions#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.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 32d432fa..d392004f 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 @@ -11,14 +11,17 @@ internal sealed class TestNamespace_ILoggerLike_a9dd3ec3_Double : global::TestNa void global::TestNamespace.ILoggerLike.Log(int logLevel, TState state, global::System.Exception? exception) { __Log.RecordCall(); - if (__Log.HasConfiguredException) + if (__Log.HasConfiguredSequence) + __Log.NextSequenceOutcome(); + else if (__Log.HasConfiguredException) throw __Log.ConfiguredException; } global::System.IDisposable? global::TestNamespace.ILoggerLike.BeginScope(TState state) { __BeginScope.RecordCall(); - return __BeginScope.HasConfiguredException ? throw __BeginScope.ConfiguredException + return __BeginScope.HasConfiguredSequence ? __BeginScope.NextSequenceOutcome() + : __BeginScope.HasConfiguredException ? throw __BeginScope.ConfiguredException : __BeginScope.HasConfiguredValue ? __BeginScope.ConfiguredValue : default; } 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 3c70b8e6..85901b8c 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa global::System.Collections.Generic.HashSet global::TestNamespace.IRepository.GetIds() { __GetIds.RecordCall(); - return __GetIds.HasConfiguredException ? throw __GetIds.ConfiguredException + return __GetIds.HasConfiguredSequence ? __GetIds.NextSequenceOutcome() + : __GetIds.HasConfiguredException ? throw __GetIds.ConfiguredException : __GetIds.HasConfiguredValue ? __GetIds.ConfiguredValue : []; } 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 eb3ff72a..76f278db 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 @@ -11,7 +11,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa global::System.Collections.Generic.IDictionary global::TestNamespace.IRepository.GetCounts() { __GetCounts.RecordCall(); - return __GetCounts.HasConfiguredException ? throw __GetCounts.ConfiguredException + return __GetCounts.HasConfiguredSequence ? __GetCounts.NextSequenceOutcome() + : __GetCounts.HasConfiguredException ? throw __GetCounts.ConfiguredException : __GetCounts.HasConfiguredValue ? __GetCounts.ConfiguredValue : new global::System.Collections.Generic.Dictionary(); } @@ -19,7 +20,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa global::System.Collections.Generic.IReadOnlyDictionary global::TestNamespace.IRepository.GetReadOnlyCounts() { __GetReadOnlyCounts.RecordCall(); - return __GetReadOnlyCounts.HasConfiguredException ? throw __GetReadOnlyCounts.ConfiguredException + return __GetReadOnlyCounts.HasConfiguredSequence ? __GetReadOnlyCounts.NextSequenceOutcome() + : __GetReadOnlyCounts.HasConfiguredException ? throw __GetReadOnlyCounts.ConfiguredException : __GetReadOnlyCounts.HasConfiguredValue ? __GetReadOnlyCounts.ConfiguredValue : new global::System.Collections.Generic.Dictionary(); } 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 894c4ce3..57778eae 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 @@ -10,7 +10,9 @@ internal sealed class TestNamespace_IMultiMapper_d3a016dc_Double : global::TestN void global::TestNamespace.IMultiMapper.Map(TKey key, TValue value) { __Map.RecordCall(); - if (__Map.HasConfiguredException) + if (__Map.HasConfiguredSequence) + __Map.NextSequenceOutcome(); + else if (__Map.HasConfiguredException) throw __Map.ConfiguredException; } } 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 ba307000..f345e1db 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa int[,] global::TestNamespace.IRepository.GetGrid() { __GetGrid.RecordCall(); - return __GetGrid.HasConfiguredException ? throw __GetGrid.ConfiguredException + return __GetGrid.HasConfiguredSequence ? __GetGrid.NextSequenceOutcome() + : __GetGrid.HasConfiguredException ? throw __GetGrid.ConfiguredException : __GetGrid.HasConfiguredValue ? __GetGrid.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IRepository.GetGrid' was invoked without being configured - call Configure().GetGrid().Returns(...) or .Throws(...) before invoking it."); 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 61816fd2..4abe7bc2 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 @@ -12,7 +12,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa string global::TestNamespace.IRepository.GetName() { __GetName.RecordCall(); - return __GetName.HasConfiguredException ? throw __GetName.ConfiguredException + return __GetName.HasConfiguredSequence ? __GetName.NextSequenceOutcome() + : __GetName.HasConfiguredException ? throw __GetName.ConfiguredException : __GetName.HasConfiguredValue ? __GetName.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IRepository.GetName' was invoked without being configured - call Configure().GetName().Returns(...) or .Throws(...) before invoking it."); @@ -23,7 +24,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa get { __Description.RecordCall(); - return __Description.HasConfiguredException ? throw __Description.ConfiguredException + return __Description.HasConfiguredSequence ? __Description.NextSequenceOutcome() + : __Description.HasConfiguredException ? throw __Description.ConfiguredException : __Description.HasConfiguredValue ? __Description.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IRepository.Description' was invoked without being configured - call Configure().Description().Returns(...) or .Throws(...) before invoking it."); @@ -33,7 +35,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa int global::TestNamespace.IRepository.GetCount() { __GetCount.RecordCall(); - return __GetCount.HasConfiguredException ? throw __GetCount.ConfiguredException + return __GetCount.HasConfiguredSequence ? __GetCount.NextSequenceOutcome() + : __GetCount.HasConfiguredException ? throw __GetCount.ConfiguredException : __GetCount.HasConfiguredValue ? __GetCount.ConfiguredValue : default; } 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 f8fad45e..1a37b6b7 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 @@ -12,7 +12,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa get { __Name.RecordCall(); - return __Name.HasConfiguredException ? throw __Name.ConfiguredException + return __Name.HasConfiguredSequence ? __Name.NextSequenceOutcome() + : __Name.HasConfiguredException ? throw __Name.ConfiguredException : __Name.HasConfiguredValue ? __Name.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IRepository.Name' was invoked without being configured - call Configure().Name().Returns(...) or .Throws(...) before invoking it."); 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 6aa12fa8..92596b58 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa string global::TestNamespace.IRepository.GetName() { __GetName.RecordCall(); - return __GetName.HasConfiguredException ? throw __GetName.ConfiguredException + return __GetName.HasConfiguredSequence ? __GetName.NextSequenceOutcome() + : __GetName.HasConfiguredException ? throw __GetName.ConfiguredException : __GetName.HasConfiguredValue ? __GetName.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IRepository.GetName' was invoked without being configured - call Configure().GetName().Returns(...) or .Throws(...) before invoking it."); 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 e9f8eb49..2112eb72 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa global::System.Threading.Tasks.ValueTask global::TestNamespace.IRepository.GetNameAsync() { __GetNameAsync.RecordCall(); - return __GetNameAsync.HasConfiguredException ? throw __GetNameAsync.ConfiguredException + return __GetNameAsync.HasConfiguredSequence ? __GetNameAsync.NextSequenceOutcome() + : __GetNameAsync.HasConfiguredException ? throw __GetNameAsync.ConfiguredException : __GetNameAsync.HasConfiguredValue ? __GetNameAsync.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IRepository.GetNameAsync' was invoked without being configured - call Configure().GetNameAsync().Returns(...) or .Throws(...) before invoking it."); 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 885ae3f1..7dadc178 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa global::System.Collections.Generic.List? global::TestNamespace.IRepository.GetValues() { __GetValues.RecordCall(); - return __GetValues.HasConfiguredException ? throw __GetValues.ConfiguredException + return __GetValues.HasConfiguredSequence ? __GetValues.NextSequenceOutcome() + : __GetValues.HasConfiguredException ? throw __GetValues.ConfiguredException : __GetValues.HasConfiguredValue ? __GetValues.ConfiguredValue : []; } 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 92b1b843..45a26a71 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 @@ -51,6 +51,10 @@ internal sealed class __Save_Entry // 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; } @@ -61,11 +65,12 @@ internal sealed class __Save_Entry void global::TestNamespace.IRepository.Save(string? name) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock (__Save_lock) { __Save_calls.Add(name); @@ -84,6 +89,10 @@ internal sealed class __Save_Entry // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; if (__entry.Config.HasConfiguredValue) return; } 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 8c35b26a..b612712d 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa global::System.Threading.Tasks.ValueTask global::TestNamespace.IRepository.GetNameAsync() { __GetNameAsync.RecordCall(); - return __GetNameAsync.HasConfiguredException ? throw __GetNameAsync.ConfiguredException + return __GetNameAsync.HasConfiguredSequence ? __GetNameAsync.NextSequenceOutcome() + : __GetNameAsync.HasConfiguredException ? throw __GetNameAsync.ConfiguredException : __GetNameAsync.HasConfiguredValue ? __GetNameAsync.ConfiguredValue : default; } 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 new file mode 100644 index 00000000..5f4d4ee1 --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasArityDiffersFromRealMemberOfSameName_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,343 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo_e5607478/__Foo_e5607478_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_e5607478_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Foo_e5607478_Entry> __Foo_e5607478_entries = []; + internal readonly global::System.Collections.Generic.List __Foo_e5607478_calls = []; + internal readonly object __Foo_e5607478_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo_1a56931a/__Foo_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_1a56931a_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Foo_1a56931a_Entry> __Foo_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Foo_1a56931a_calls = []; + internal readonly object __Foo_1a56931a_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __FooMatching/__FooMatching_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __FooMatching_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__FooMatching_Entry> __FooMatching_entries = []; + internal readonly global::System.Collections.Generic.List __FooMatching_calls = []; + internal readonly object __FooMatching_lock = new(); + + bool global::TestNamespace.IRepository.Foo(int id) + { + // 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_e5607478_lock) + { + __Foo_e5607478_calls.Add(id); + for (var __i = __Foo_e5607478_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_e5607478_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.Foo(string id) + { + // 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_1a56931a_lock) + { + __Foo_1a56931a_calls.Add(id); + for (var __i = __Foo_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_1a56931a_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.FooMatching(int value) + { + // 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 (__FooMatching_lock) + { + __FooMatching_calls.Add(value); + for (var __i = __FooMatching_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __FooMatching_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // 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; + } + } + } + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, int id) where T : class + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_e5607478_Entry(); + lock (__self.__Foo_e5607478_lock) { __self.__Foo_e5607478_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) where T : class + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_e5607478_Entry(); + __entry.Matcher_id = id; + lock (__self.__Foo_e5607478_lock) { __self.__Foo_e5607478_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, string id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_1a56931a_Entry(); + lock (__self.__Foo_1a56931a_lock) { __self.__Foo_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_1a56931a_Entry(); + __entry.Matcher_id = id; + lock (__self.__Foo_1a56931a_lock) { __self.__Foo_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + public static global::Compono.ReturnConfigBuilder FooMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match 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.__FooMatching_Entry(); + __entry.Matcher_value = value; + lock (__self.__FooMatching_lock) { __self.__FooMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // 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::Compono.ReturnConfigBuilder FooMatching(this global::TestNamespace_IRepository_e3198068_Double self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__FooMatching_Entry(); + lock (self.__FooMatching_lock) { self.__FooMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) where T : class + { + int __count; + lock (__self.Instance.__Foo_e5607478_lock) { __count = __self.Instance.__Foo_e5607478_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) where T : class + { + int __count; + lock (__self.Instance.__Foo_e5607478_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_e5607478_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) + { + int __count; + lock (__self.Instance.__Foo_1a56931a_lock) { __count = __self.Instance.__Foo_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Foo_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_1a56931a_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + public static global::Compono.CallVerifier FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__FooMatching_lock) + { + __count = 0; + foreach (var call in __self.Instance.__FooMatching_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.FooMatching"); + } + + // 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 FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) + { + int __count; + lock (self.Instance.__FooMatching_lock) { __count = self.Instance.__FooMatching_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.FooMatching"); + } + +} + +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.OverloadMatchingAliasArityDiffersFromRealMemberOfSameName_KeepsNaturalName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasArityDiffersFromRealMemberOfSameName_KeepsNaturalName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasArityDiffersFromRealMemberOfSameName_KeepsNaturalName#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.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 new file mode 100644 index 00000000..5a91f8eb --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesDespiteNullableAnnotationMismatch_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,343 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + // ADR-0050: multi-entry response configuration - replaces the single + // __Get_b9dfaa09/__Get_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Get_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Get_b9dfaa09_Entry> __Get_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Get_b9dfaa09_calls = []; + internal readonly object __Get_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Get_1a56931a/__Get_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Get_1a56931a_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Get_1a56931a_Entry> __Get_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Get_1a56931a_calls = []; + internal readonly object __Get_1a56931a_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __GetMatching/__GetMatching_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __GetMatching_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__GetMatching_Entry> __GetMatching_entries = []; + internal readonly global::System.Collections.Generic.List __GetMatching_calls = []; + internal readonly object __GetMatching_lock = new(); + + bool global::TestNamespace.IRepository.Get(int id) + { + // 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 (__Get_b9dfaa09_lock) + { + __Get_b9dfaa09_calls.Add(id); + for (var __i = __Get_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Get_b9dfaa09_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.Get(string id) + { + // 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 (__Get_1a56931a_lock) + { + __Get_1a56931a_calls.Add(id); + for (var __i = __Get_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Get_1a56931a_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.GetMatching(string? value) + { + // 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 (__GetMatching_lock) + { + __GetMatching_calls.Add(value); + for (var __i = __GetMatching_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __GetMatching_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // 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; + } + } + } + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, int id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_b9dfaa09_Entry(); + lock (__self.__Get_b9dfaa09_lock) { __self.__Get_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder GetMatching_cda21338(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_b9dfaa09_Entry(); + __entry.Matcher_id = id; + lock (__self.__Get_b9dfaa09_lock) { __self.__Get_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, string id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_1a56931a_Entry(); + lock (__self.__Get_1a56931a_lock) { __self.__Get_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder GetMatching_cda21338(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_1a56931a_Entry(); + __entry.Matcher_id = id; + lock (__self.__Get_1a56931a_lock) { __self.__Get_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + public static global::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match 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.__GetMatching_Entry(); + __entry.Matcher_value = value; + lock (__self.__GetMatching_lock) { __self.__GetMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // 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::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__GetMatching_Entry(); + lock (self.__GetMatching_lock) { self.__GetMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) + { + int __count; + lock (__self.Instance.__Get_b9dfaa09_lock) { __count = __self.Instance.__Get_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier GetMatching_cda21338(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Get_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Get_b9dfaa09_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) + { + int __count; + lock (__self.Instance.__Get_1a56931a_lock) { __count = __self.Instance.__Get_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier GetMatching_cda21338(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Get_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Get_1a56931a_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + public static global::Compono.CallVerifier GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__GetMatching_lock) + { + __count = 0; + foreach (var call in __self.Instance.__GetMatching_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.GetMatching"); + } + + // 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 GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) + { + int __count; + lock (self.Instance.__GetMatching_lock) { __count = self.Instance.__GetMatching_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.GetMatching"); + } + +} + +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.OverloadMatchingAliasCollidesDespiteNullableAnnotationMismatch_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesDespiteNullableAnnotationMismatch_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesDespiteNullableAnnotationMismatch_FallsBackToHashSuffixedName#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.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 new file mode 100644 index 00000000..a93eefdf --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithClosedInstantiationEligibleRealMember_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,347 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + internal sealed class __FooMatching_State 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 + // nested one level deeper (per closed T instead of per member). + internal sealed class Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List Entries = []; + internal readonly global::System.Collections.Generic.List Calls = []; + internal readonly object Lock = new(); + } + + internal readonly global::System.Collections.Generic.Dictionary __FooMatching_buckets = new(); + + internal __FooMatching_State __FooMatching_Bucket() where T : class + { + lock (__FooMatching_buckets) + { + if (!__FooMatching_buckets.TryGetValue(typeof(T), out var __boxed)) + { + __boxed = new __FooMatching_State(); + __FooMatching_buckets[typeof(T)] = __boxed; + } + + return (__FooMatching_State)__boxed; + } + } + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo_e5607478/__Foo_e5607478_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_e5607478_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Foo_e5607478_Entry> __Foo_e5607478_entries = []; + internal readonly global::System.Collections.Generic.List __Foo_e5607478_calls = []; + internal readonly object __Foo_e5607478_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo_1a56931a/__Foo_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_1a56931a_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Foo_1a56931a_Entry> __Foo_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Foo_1a56931a_calls = []; + internal readonly object __Foo_1a56931a_lock = new(); + + bool global::TestNamespace.IRepository.Foo(int id) + { + // 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_e5607478_lock) + { + __Foo_e5607478_calls.Add(id); + for (var __i = __Foo_e5607478_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_e5607478_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.Foo(string id) + { + // 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_1a56931a_lock) + { + __Foo_1a56931a_calls.Add(id); + for (var __i = __Foo_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_1a56931a_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + +#pragma warning disable CS8603, CS8616, CS8619 + T global::TestNamespace.IRepository.FooMatching(int value) + { + var __bucket = __FooMatching_Bucket(); + // 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 Entries.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 (the compiler emits + // the equivalent of try/finally), so returning directly from inside the block below is safe. + lock (__bucket.Lock) + { + __bucket.Calls.Add(value); + for (var __i = __bucket.Entries.Count - 1; __i >= 0; __i--) + { + var __entry = __bucket.Entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // 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; + } + } + } + return default; + } +#pragma warning restore CS8603, CS8616, CS8619 +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, int id) where T : class + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_e5607478_Entry(); + lock (__self.__Foo_e5607478_lock) { __self.__Foo_e5607478_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) where T : class + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_e5607478_Entry(); + __entry.Matcher_id = id; + lock (__self.__Foo_e5607478_lock) { __self.__Foo_e5607478_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, string id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_1a56931a_Entry(); + lock (__self.__Foo_1a56931a_lock) { __self.__Foo_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_1a56931a_Entry(); + __entry.Matcher_id = id; + lock (__self.__Foo_1a56931a_lock) { __self.__Foo_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + public static global::Compono.ReturnConfigBuilder FooMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) where T : class + { + // ADR-0050: appends a new entry rather than overwriting the (removed) single + // slot - `ref entry.Config` stays valid regardless of later Entries.Add() reallocating the + // list's backing array, since the ref targets the Entry object itself (heap-stable), not a + // slot inside the list's array. See spike report for the reallocation-hazard proof. The + // Add() itself is under the same member lock dispatch scans under (Codex review, PR #108 + // round 5) - concurrent Configure() calls must not race each other or a concurrent + // in-progress dispatch scan while mutating the shared List. + var __bucket = __self.__FooMatching_Bucket(); + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__FooMatching_State.Entry(); + __entry.Matcher_value = value; + lock (__bucket.Lock) { __bucket.Entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) where T : class + { + int __count; + lock (__self.Instance.__Foo_e5607478_lock) { __count = __self.Instance.__Foo_e5607478_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) where T : class + { + int __count; + lock (__self.Instance.__Foo_e5607478_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_e5607478_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) + { + int __count; + lock (__self.Instance.__Foo_1a56931a_lock) { __count = __self.Instance.__Foo_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Foo_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_1a56931a_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + public static global::Compono.CallVerifier FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) where T : class + { + var __bucket = __self.Instance.__FooMatching_Bucket(); + int __count; + lock (__bucket.Lock) + { + __count = 0; + foreach (var call in __bucket.Calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.FooMatching"); + } + +} + +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.OverloadMatchingAliasCollidesWithClosedInstantiationEligibleRealMember_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithClosedInstantiationEligibleRealMember_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithClosedInstantiationEligibleRealMember_FallsBackToHashSuffixedName#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.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 new file mode 100644 index 00000000..0fafe5ce --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithOverloadedRealFamily_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,441 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo_b9dfaa09/__Foo_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Foo_b9dfaa09_Entry> __Foo_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Foo_b9dfaa09_calls = []; + internal readonly object __Foo_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo_1a56931a/__Foo_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_1a56931a_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Foo_1a56931a_Entry> __Foo_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Foo_1a56931a_calls = []; + internal readonly object __Foo_1a56931a_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __FooMatching_8ac5d184/__FooMatching_8ac5d184_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __FooMatching_8ac5d184_Entry + { + internal global::Compono.Match>? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__FooMatching_8ac5d184_Entry> __FooMatching_8ac5d184_entries = []; + internal readonly global::System.Collections.Generic.List> __FooMatching_8ac5d184_calls = []; + internal readonly object __FooMatching_8ac5d184_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __FooMatching_1a56931a/__FooMatching_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __FooMatching_1a56931a_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__FooMatching_1a56931a_Entry> __FooMatching_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __FooMatching_1a56931a_calls = []; + internal readonly object __FooMatching_1a56931a_lock = new(); + + bool global::TestNamespace.IRepository.Foo(int id) + { + // 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_b9dfaa09_lock) + { + __Foo_b9dfaa09_calls.Add(id); + for (var __i = __Foo_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_b9dfaa09_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.Foo(string id) + { + // 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_1a56931a_lock) + { + __Foo_1a56931a_calls.Add(id); + for (var __i = __Foo_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_1a56931a_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.FooMatching(global::Compono.Match value) + { + // 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 (__FooMatching_8ac5d184_lock) + { + __FooMatching_8ac5d184_calls.Add(value); + for (var __i = __FooMatching_8ac5d184_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __FooMatching_8ac5d184_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.FooMatching(string value) + { + // 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 (__FooMatching_1a56931a_lock) + { + __FooMatching_1a56931a_calls.Add(value); + for (var __i = __FooMatching_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __FooMatching_1a56931a_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // 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; + } + } + } + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, int id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_b9dfaa09_Entry(); + lock (__self.__Foo_b9dfaa09_lock) { __self.__Foo_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_b9dfaa09_Entry(); + __entry.Matcher_id = id; + lock (__self.__Foo_b9dfaa09_lock) { __self.__Foo_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, string id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_1a56931a_Entry(); + lock (__self.__Foo_1a56931a_lock) { __self.__Foo_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_1a56931a_Entry(); + __entry.Matcher_id = id; + lock (__self.__Foo_1a56931a_lock) { __self.__Foo_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder FooMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__FooMatching_8ac5d184_Entry(); + lock (__self.__FooMatching_8ac5d184_lock) { __self.__FooMatching_8ac5d184_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatchingMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match> value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__FooMatching_8ac5d184_Entry(); + __entry.Matcher_value = value; + lock (__self.__FooMatching_8ac5d184_lock) { __self.__FooMatching_8ac5d184_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder FooMatching(this global::TestNamespace_IRepository_e3198068_Double __self, string value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__FooMatching_1a56931a_Entry(); + lock (__self.__FooMatching_1a56931a_lock) { __self.__FooMatching_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatchingMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__FooMatching_1a56931a_Entry(); + __entry.Matcher_value = value; + lock (__self.__FooMatching_1a56931a_lock) { __self.__FooMatching_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) + { + int __count; + lock (__self.Instance.__Foo_b9dfaa09_lock) { __count = __self.Instance.__Foo_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Foo_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_b9dfaa09_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) + { + int __count; + lock (__self.Instance.__Foo_1a56931a_lock) { __count = __self.Instance.__Foo_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Foo_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_1a56931a_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__FooMatching_8ac5d184_lock) { __count = __self.Instance.__FooMatching_8ac5d184_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.FooMatching"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatchingMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match> value) + { + int __count; + lock (__self.Instance.__FooMatching_8ac5d184_lock) + { + __count = 0; + foreach (var call in __self.Instance.__FooMatching_8ac5d184_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.FooMatching"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) + { + int __count; + lock (__self.Instance.__FooMatching_1a56931a_lock) { __count = __self.Instance.__FooMatching_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.FooMatching"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatchingMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__FooMatching_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__FooMatching_1a56931a_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.FooMatching"); + } + +} + +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.OverloadMatchingAliasCollidesWithOverloadedRealFamily_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithOverloadedRealFamily_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithOverloadedRealFamily_FallsBackToHashSuffixedName#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.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 new file mode 100644 index 00000000..c9dc5cfd --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,343 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + // ADR-0050: multi-entry response configuration - replaces the single + // __Get_b9dfaa09/__Get_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Get_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Get_b9dfaa09_Entry> __Get_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Get_b9dfaa09_calls = []; + internal readonly object __Get_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Get_1a56931a/__Get_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Get_1a56931a_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Get_1a56931a_Entry> __Get_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Get_1a56931a_calls = []; + internal readonly object __Get_1a56931a_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __GetMatching/__GetMatching_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __GetMatching_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__GetMatching_Entry> __GetMatching_entries = []; + internal readonly global::System.Collections.Generic.List __GetMatching_calls = []; + internal readonly object __GetMatching_lock = new(); + + bool global::TestNamespace.IRepository.Get(int id) + { + // 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 (__Get_b9dfaa09_lock) + { + __Get_b9dfaa09_calls.Add(id); + for (var __i = __Get_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Get_b9dfaa09_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.Get(string id) + { + // 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 (__Get_1a56931a_lock) + { + __Get_1a56931a_calls.Add(id); + for (var __i = __Get_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Get_1a56931a_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.GetMatching(string value) + { + // 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 (__GetMatching_lock) + { + __GetMatching_calls.Add(value); + for (var __i = __GetMatching_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __GetMatching_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // 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; + } + } + } + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, int id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_b9dfaa09_Entry(); + lock (__self.__Get_b9dfaa09_lock) { __self.__Get_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder GetMatching_cda21338(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_b9dfaa09_Entry(); + __entry.Matcher_id = id; + lock (__self.__Get_b9dfaa09_lock) { __self.__Get_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, string id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_1a56931a_Entry(); + lock (__self.__Get_1a56931a_lock) { __self.__Get_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder GetMatching_cda21338(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_1a56931a_Entry(); + __entry.Matcher_id = id; + lock (__self.__Get_1a56931a_lock) { __self.__Get_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + public static global::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match 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.__GetMatching_Entry(); + __entry.Matcher_value = value; + lock (__self.__GetMatching_lock) { __self.__GetMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // 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::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__GetMatching_Entry(); + lock (self.__GetMatching_lock) { self.__GetMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) + { + int __count; + lock (__self.Instance.__Get_b9dfaa09_lock) { __count = __self.Instance.__Get_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier GetMatching_cda21338(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Get_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Get_b9dfaa09_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) + { + int __count; + lock (__self.Instance.__Get_1a56931a_lock) { __count = __self.Instance.__Get_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier GetMatching_cda21338(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Get_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Get_1a56931a_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + public static global::Compono.CallVerifier GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__GetMatching_lock) + { + __count = 0; + foreach (var call in __self.Instance.__GetMatching_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.GetMatching"); + } + + // 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 GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) + { + int __count; + lock (self.Instance.__GetMatching_lock) { __count = self.Instance.__GetMatching_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.GetMatching"); + } + +} + +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.OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName#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.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 new file mode 100644 index 00000000..d6683a07 --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithSoloGenericRealMemberEmittingNonGenericExtension_FallsBackToHashSuffixedName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,343 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo_b9dfaa09/__Foo_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Foo_b9dfaa09_Entry> __Foo_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Foo_b9dfaa09_calls = []; + internal readonly object __Foo_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Foo_97bed815/__Foo_97bed815_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Foo_97bed815_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Foo_97bed815_Entry> __Foo_97bed815_entries = []; + internal readonly global::System.Collections.Generic.List __Foo_97bed815_calls = []; + internal readonly object __Foo_97bed815_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __FooMatching/__FooMatching_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __FooMatching_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__FooMatching_Entry> __FooMatching_entries = []; + internal readonly global::System.Collections.Generic.List __FooMatching_calls = []; + internal readonly object __FooMatching_lock = new(); + + bool global::TestNamespace.IRepository.Foo(int id) + { + // 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_b9dfaa09_lock) + { + __Foo_b9dfaa09_calls.Add(id); + for (var __i = __Foo_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_b9dfaa09_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.Foo(string id) + { + // 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_97bed815_lock) + { + __Foo_97bed815_calls.Add(id); + for (var __i = __Foo_97bed815_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Foo_97bed815_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.FooMatching(int value) + { + // 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 (__FooMatching_lock) + { + __FooMatching_calls.Add(value); + for (var __i = __FooMatching_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __FooMatching_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // 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; + } + } + } + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, int id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_b9dfaa09_Entry(); + lock (__self.__Foo_b9dfaa09_lock) { __self.__Foo_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_b9dfaa09_Entry(); + __entry.Matcher_id = id; + lock (__self.__Foo_b9dfaa09_lock) { __self.__Foo_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Foo(this global::TestNamespace_IRepository_e3198068_Double __self, string id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_97bed815_Entry(); + lock (__self.__Foo_97bed815_lock) { __self.__Foo_97bed815_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Foo_97bed815_Entry(); + __entry.Matcher_id = id; + lock (__self.__Foo_97bed815_lock) { __self.__Foo_97bed815_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + public static global::Compono.ReturnConfigBuilder FooMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match 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.__FooMatching_Entry(); + __entry.Matcher_value = value; + lock (__self.__FooMatching_lock) { __self.__FooMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // 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::Compono.ReturnConfigBuilder FooMatching(this global::TestNamespace_IRepository_e3198068_Double self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__FooMatching_Entry(); + lock (self.__FooMatching_lock) { self.__FooMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) + { + int __count; + lock (__self.Instance.__Foo_b9dfaa09_lock) { __count = __self.Instance.__Foo_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Foo_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_b9dfaa09_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Foo(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) + { + int __count; + lock (__self.Instance.__Foo_97bed815_lock) { __count = __self.Instance.__Foo_97bed815_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier FooMatching_b1dc5ab8(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Foo_97bed815_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Foo_97bed815_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Foo"); + } + + public static global::Compono.CallVerifier FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__FooMatching_lock) + { + __count = 0; + foreach (var call in __self.Instance.__FooMatching_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.FooMatching"); + } + + // 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 FooMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) + { + int __count; + lock (self.Instance.__FooMatching_lock) { __count = self.Instance.__FooMatching_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.FooMatching"); + } + +} + +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.OverloadMatchingAliasCollidesWithSoloGenericRealMemberEmittingNonGenericExtension_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithSoloGenericRealMemberEmittingNonGenericExtension_FallsBackToHashSuffixedName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasCollidesWithSoloGenericRealMemberEmittingNonGenericExtension_FallsBackToHashSuffixedName#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.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 new file mode 100644 index 00000000..5e0dbe11 --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,343 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + // ADR-0050: multi-entry response configuration - replaces the single + // __Get_b9dfaa09/__Get_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Get_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Get_b9dfaa09_Entry> __Get_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Get_b9dfaa09_calls = []; + internal readonly object __Get_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Get_1a56931a/__Get_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Get_1a56931a_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Get_1a56931a_Entry> __Get_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Get_1a56931a_calls = []; + internal readonly object __Get_1a56931a_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __GetMatching/__GetMatching_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __GetMatching_Entry + { + internal global::Compono.Match? Matcher_flag; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__GetMatching_Entry> __GetMatching_entries = []; + internal readonly global::System.Collections.Generic.List __GetMatching_calls = []; + internal readonly object __GetMatching_lock = new(); + + bool global::TestNamespace.IRepository.Get(int id) + { + // 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 (__Get_b9dfaa09_lock) + { + __Get_b9dfaa09_calls.Add(id); + for (var __i = __Get_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Get_b9dfaa09_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.Get(string id) + { + // 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 (__Get_1a56931a_lock) + { + __Get_1a56931a_calls.Add(id); + for (var __i = __Get_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Get_1a56931a_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.GetMatching(bool flag) + { + // 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 (__GetMatching_lock) + { + __GetMatching_calls.Add(flag); + for (var __i = __GetMatching_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __GetMatching_entries[__i]; + if ((__entry.Matcher_flag is not { } __m_flag || __m_flag.Matches(flag))) + { + // 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; + } + } + } + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, int id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_b9dfaa09_Entry(); + lock (__self.__Get_b9dfaa09_lock) { __self.__Get_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_b9dfaa09_Entry(); + __entry.Matcher_id = id; + lock (__self.__Get_b9dfaa09_lock) { __self.__Get_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, string id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_1a56931a_Entry(); + lock (__self.__Get_1a56931a_lock) { __self.__Get_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_1a56931a_Entry(); + __entry.Matcher_id = id; + lock (__self.__Get_1a56931a_lock) { __self.__Get_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + public static global::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match flag) + { + // 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.__GetMatching_Entry(); + __entry.Matcher_flag = flag; + lock (__self.__GetMatching_lock) { __self.__GetMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // 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::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__GetMatching_Entry(); + lock (self.__GetMatching_lock) { self.__GetMatching_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) + { + int __count; + lock (__self.Instance.__Get_b9dfaa09_lock) { __count = __self.Instance.__Get_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Get_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Get_b9dfaa09_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) + { + int __count; + lock (__self.Instance.__Get_1a56931a_lock) { __count = __self.Instance.__Get_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Get_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Get_1a56931a_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + public static global::Compono.CallVerifier GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match flag) + { + int __count; + lock (__self.Instance.__GetMatching_lock) + { + __count = 0; + foreach (var call in __self.Instance.__GetMatching_calls) + { + if (flag.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.GetMatching"); + } + + // 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 GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) + { + int __count; + lock (self.Instance.__GetMatching_lock) { __count = self.Instance.__GetMatching_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.GetMatching"); + } + +} + +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.OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName#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.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 b23e251e..0705939a 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 @@ -5,39 +5,165 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __M_b9dfaa09_2; - internal global::Compono.ReturnConfig __M_1a56931a; internal global::Compono.ReturnConfig __M_b9dfaa09; + // ADR-0050: multi-entry response configuration - replaces the single + // __M_b9dfaa09_2/__M_b9dfaa09_2_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_b9dfaa09_2_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_b9dfaa09_2_Entry> __M_b9dfaa09_2_entries = []; + internal readonly global::System.Collections.Generic.List __M_b9dfaa09_2_calls = []; + internal readonly object __M_b9dfaa09_2_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __M_1a56931a/__M_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_1a56931a_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_1a56931a_Entry> __M_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __M_1a56931a_calls = []; + internal readonly object __M_1a56931a_lock = new(); void global::TestNamespace.IRepository.M(int value) { - __M_b9dfaa09_2.RecordCall(); - if (__M_b9dfaa09_2.HasConfiguredException) - throw __M_b9dfaa09_2.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_b9dfaa09_2_lock) + { + __M_b9dfaa09_2_calls.Add(value); + for (var __i = __M_b9dfaa09_2_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_b9dfaa09_2_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.M(string value) { - __M_1a56931a.RecordCall(); - if (__M_1a56931a.HasConfiguredException) - throw __M_1a56931a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_1a56931a_lock) + { + __M_1a56931a_calls.Add(value); + for (var __i = __M_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_1a56931a_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.M_b9dfaa09() { __M_b9dfaa09.RecordCall(); - if (__M_b9dfaa09.HasConfiguredException) + if (__M_b9dfaa09.HasConfiguredSequence) + __M_b9dfaa09.NextSequenceOutcome(); + else if (__M_b9dfaa09.HasConfiguredException) throw __M_b9dfaa09.ConfiguredException; } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, int value) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_b9dfaa09_2); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, int value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_b9dfaa09_2_Entry(); + lock (__self.__M_b9dfaa09_2_lock) { __self.__M_b9dfaa09_2_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_b9dfaa09_2_Entry(); + __entry.Matcher_value = value; + lock (__self.__M_b9dfaa09_2_lock) { __self.__M_b9dfaa09_2_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_1a56931a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + __entry.Matcher_value = value; + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } public static global::Compono.ReturnConfigBuilder M_b9dfaa09(this global::TestNamespace_IRepository_e3198068_Double self) => new global::Compono.ReturnConfigBuilder(ref self.__M_b9dfaa09); @@ -64,11 +190,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value) => - new(__self.Instance.__M_b9dfaa09_2.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value) + { + int __count; + lock (__self.Instance.__M_b9dfaa09_2_lock) { __count = __self.Instance.__M_b9dfaa09_2_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) => - new(__self.Instance.__M_1a56931a.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__M_b9dfaa09_2_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_b9dfaa09_2_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) { __count = __self.Instance.__M_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_1a56931a_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } public static global::Compono.CallVerifier M_b9dfaa09(this global::TestNamespace_IRepository_e3198068_DoubleVerifier self) => new(self.Instance.__M_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.M_b9dfaa09"); 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 764eca25..8fb030eb 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 @@ -45,6 +45,10 @@ internal sealed class __TryGet_Entry // 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; } 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 08e550fc..18bcd203 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 @@ -6,20 +6,63 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { internal global::Compono.ReturnConfig __Seek_37c3f22f; - internal global::Compono.ReturnConfig __Seek_b9dfaa09; + // ADR-0050: multi-entry response configuration - replaces the single + // __Seek_b9dfaa09/__Seek_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Seek_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Seek_b9dfaa09_Entry> __Seek_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Seek_b9dfaa09_calls = []; + internal readonly object __Seek_b9dfaa09_lock = new(); void global::TestNamespace.IRepository.Seek(scoped global::System.Span value) { __Seek_37c3f22f.RecordCall(); - if (__Seek_37c3f22f.HasConfiguredException) + if (__Seek_37c3f22f.HasConfiguredSequence) + __Seek_37c3f22f.NextSequenceOutcome(); + else if (__Seek_37c3f22f.HasConfiguredException) throw __Seek_37c3f22f.ConfiguredException; } void global::TestNamespace.IRepository.Seek(int value) { - __Seek_b9dfaa09.RecordCall(); - if (__Seek_b9dfaa09.HasConfiguredException) - throw __Seek_b9dfaa09.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Seek_b9dfaa09_lock) + { + __Seek_b9dfaa09_calls.Add(value); + for (var __i = __Seek_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Seek_b9dfaa09_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } @@ -28,8 +71,29 @@ internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration public static global::Compono.ReturnConfigBuilder Seek(this global::TestNamespace_IRepository_e3198068_Double __self, global::System.Span value) => new global::Compono.ReturnConfigBuilder(ref __self.__Seek_37c3f22f); - public static global::Compono.ReturnConfigBuilder Seek(this global::TestNamespace_IRepository_e3198068_Double __self, int value) => - new global::Compono.ReturnConfigBuilder(ref __self.__Seek_b9dfaa09); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Seek(this global::TestNamespace_IRepository_e3198068_Double __self, int value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Seek_b9dfaa09_Entry(); + lock (__self.__Seek_b9dfaa09_lock) { __self.__Seek_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder SeekMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Seek_b9dfaa09_Entry(); + __entry.Matcher_value = value; + lock (__self.__Seek_b9dfaa09_lock) { __self.__Seek_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -56,8 +120,33 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification public static global::Compono.CallVerifier Seek(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::System.Span value) => new(__self.Instance.__Seek_37c3f22f.ConfiguredCallCount, "global::TestNamespace.IRepository.Seek"); - public static global::Compono.CallVerifier Seek(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value) => - new(__self.Instance.__Seek_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.Seek"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Seek(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value) + { + int __count; + lock (__self.Instance.__Seek_b9dfaa09_lock) { __count = __self.Instance.__Seek_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Seek"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier SeekMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__Seek_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Seek_b9dfaa09_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Seek"); + } } 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 e4be6276..13be42e2 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 @@ -24,11 +24,12 @@ internal sealed class __Seek_Entry void global::TestNamespace.IRepository.Seek(int value) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock (__Seek_lock) { __Seek_calls.Add(value); @@ -47,6 +48,10 @@ internal sealed class __Seek_Entry // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; if (__entry.Config.HasConfiguredValue) return; } 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 35abb9c5..b714896e 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 @@ -25,11 +25,12 @@ internal sealed class __Seek_Entry void global::TestNamespace.IRepository.Seek(int value) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock (__Seek_lock) { __Seek_calls.Add(value); @@ -48,6 +49,10 @@ internal sealed class __Seek_Entry // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; if (__entry.Config.HasConfiguredValue) return; } 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 6d6c6846..935666a3 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 @@ -51,7 +51,8 @@ internal __GetDataAsync_1aae9cd0_State __GetDataAsync_1aae9cd0_Bucket() wh { var __bucket = __GetDataAsync_97bed815_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : global::System.Threading.Tasks.Task.FromResult(default); } @@ -62,7 +63,8 @@ internal __GetDataAsync_1aae9cd0_State __GetDataAsync_1aae9cd0_Bucket() wh { var __bucket = __GetDataAsync_1aae9cd0_Bucket(); __bucket.Config.RecordCall(); - return __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException + return __bucket.Config.HasConfiguredSequence ? __bucket.Config.NextSequenceOutcome() + : __bucket.Config.HasConfiguredException ? throw __bucket.Config.ConfiguredException : __bucket.Config.HasConfiguredValue ? __bucket.Config.ConfiguredValue : global::System.Threading.Tasks.Task.FromResult(default); } 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 dabaf4fd..a96e9ac6 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 @@ -6,22 +6,61 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { internal global::Compono.ReturnConfig __Equals_b9dfaa09; - internal global::Compono.ReturnConfig __Equals_07b0838c; + // ADR-0050: multi-entry response configuration - replaces the single + // __Equals_07b0838c/__Equals_07b0838c_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Equals_07b0838c_Entry + { + internal global::Compono.Match? Matcher_a; + internal global::Compono.Match? Matcher_b; + internal global::Compono.Match? Matcher_c; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Equals_07b0838c_Entry> __Equals_07b0838c_entries = []; + internal readonly global::System.Collections.Generic.List<(long, long, long)> __Equals_07b0838c_calls = []; + internal readonly object __Equals_07b0838c_lock = new(); bool global::TestNamespace.IRepository.Equals(int value) { __Equals_b9dfaa09.RecordCall(); - return __Equals_b9dfaa09.HasConfiguredException ? throw __Equals_b9dfaa09.ConfiguredException + return __Equals_b9dfaa09.HasConfiguredSequence ? __Equals_b9dfaa09.NextSequenceOutcome() + : __Equals_b9dfaa09.HasConfiguredException ? throw __Equals_b9dfaa09.ConfiguredException : __Equals_b9dfaa09.HasConfiguredValue ? __Equals_b9dfaa09.ConfiguredValue : default; } bool global::TestNamespace.IRepository.Equals(long a, long b, long c) { - __Equals_07b0838c.RecordCall(); - return __Equals_07b0838c.HasConfiguredException ? throw __Equals_07b0838c.ConfiguredException - : __Equals_07b0838c.HasConfiguredValue ? __Equals_07b0838c.ConfiguredValue - : default; + // 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 (__Equals_07b0838c_lock) + { + __Equals_07b0838c_calls.Add((a, b, c)); + for (var __i = __Equals_07b0838c_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Equals_07b0838c_entries[__i]; + if ((__entry.Matcher_a is not { } __m_a || __m_a.Matches(a)) && (__entry.Matcher_b is not { } __m_b || __m_b.Matches(b)) && (__entry.Matcher_c is not { } __m_c || __m_c.Matches(c))) + { + // 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; + } + } + } + return default; } } @@ -30,8 +69,31 @@ internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, int value = default) => new global::Compono.ReturnConfigBuilder(ref __self.__Equals_b9dfaa09); - public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, long a, long b, long c) => - new global::Compono.ReturnConfigBuilder(ref __self.__Equals_07b0838c); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, long a, long b, long c) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Equals_07b0838c_Entry(); + lock (__self.__Equals_07b0838c_lock) { __self.__Equals_07b0838c_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder EqualsMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match a, global::Compono.Match b, global::Compono.Match c) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Equals_07b0838c_Entry(); + __entry.Matcher_a = a; + __entry.Matcher_b = b; + __entry.Matcher_c = c; + lock (__self.__Equals_07b0838c_lock) { __self.__Equals_07b0838c_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -58,8 +120,33 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value = default) => new(__self.Instance.__Equals_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.Equals"); - public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, long a, long b, long c) => - new(__self.Instance.__Equals_07b0838c.ConfiguredCallCount, "global::TestNamespace.IRepository.Equals"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, long a, long b, long c) + { + int __count; + lock (__self.Instance.__Equals_07b0838c_lock) { __count = __self.Instance.__Equals_07b0838c_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Equals"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier EqualsMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match a, global::Compono.Match b, global::Compono.Match c) + { + int __count; + lock (__self.Instance.__Equals_07b0838c_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Equals_07b0838c_calls) + { + if (a.Matches(call.Item1) && b.Matches(call.Item2) && c.Matches(call.Item3)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Equals"); + } } 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 09664d9a..eeecce56 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 @@ -6,22 +6,61 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { internal global::Compono.ReturnConfig __Equals_b9dfaa09; - internal global::Compono.ReturnConfig __Equals_07b0838c; + // ADR-0050: multi-entry response configuration - replaces the single + // __Equals_07b0838c/__Equals_07b0838c_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Equals_07b0838c_Entry + { + internal global::Compono.Match? Matcher_a; + internal global::Compono.Match? Matcher_b; + internal global::Compono.Match? Matcher_c; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Equals_07b0838c_Entry> __Equals_07b0838c_entries = []; + internal readonly global::System.Collections.Generic.List<(long, long, long)> __Equals_07b0838c_calls = []; + internal readonly object __Equals_07b0838c_lock = new(); bool global::TestNamespace.IRepository.Equals(int value) { __Equals_b9dfaa09.RecordCall(); - return __Equals_b9dfaa09.HasConfiguredException ? throw __Equals_b9dfaa09.ConfiguredException + return __Equals_b9dfaa09.HasConfiguredSequence ? __Equals_b9dfaa09.NextSequenceOutcome() + : __Equals_b9dfaa09.HasConfiguredException ? throw __Equals_b9dfaa09.ConfiguredException : __Equals_b9dfaa09.HasConfiguredValue ? __Equals_b9dfaa09.ConfiguredValue : default; } bool global::TestNamespace.IRepository.Equals(long a, long b, long c) { - __Equals_07b0838c.RecordCall(); - return __Equals_07b0838c.HasConfiguredException ? throw __Equals_07b0838c.ConfiguredException - : __Equals_07b0838c.HasConfiguredValue ? __Equals_07b0838c.ConfiguredValue - : default; + // 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 (__Equals_07b0838c_lock) + { + __Equals_07b0838c_calls.Add((a, b, c)); + for (var __i = __Equals_07b0838c_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Equals_07b0838c_entries[__i]; + if ((__entry.Matcher_a is not { } __m_a || __m_a.Matches(a)) && (__entry.Matcher_b is not { } __m_b || __m_b.Matches(b)) && (__entry.Matcher_c is not { } __m_c || __m_c.Matches(c))) + { + // 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; + } + } + } + return default; } } @@ -30,8 +69,31 @@ internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, int value = 0) => new global::Compono.ReturnConfigBuilder(ref __self.__Equals_b9dfaa09); - public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, long a, long b, long c) => - new global::Compono.ReturnConfigBuilder(ref __self.__Equals_07b0838c); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, long a, long b, long c) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Equals_07b0838c_Entry(); + lock (__self.__Equals_07b0838c_lock) { __self.__Equals_07b0838c_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder EqualsMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match a, global::Compono.Match b, global::Compono.Match c) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Equals_07b0838c_Entry(); + __entry.Matcher_a = a; + __entry.Matcher_b = b; + __entry.Matcher_c = c; + lock (__self.__Equals_07b0838c_lock) { __self.__Equals_07b0838c_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -58,8 +120,33 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value = 0) => new(__self.Instance.__Equals_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.Equals"); - public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, long a, long b, long c) => - new(__self.Instance.__Equals_07b0838c.ConfiguredCallCount, "global::TestNamespace.IRepository.Equals"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, long a, long b, long c) + { + int __count; + lock (__self.Instance.__Equals_07b0838c_lock) { __count = __self.Instance.__Equals_07b0838c_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Equals"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier EqualsMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match a, global::Compono.Match b, global::Compono.Match c) + { + int __count; + lock (__self.Instance.__Equals_07b0838c_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Equals_07b0838c_calls) + { + if (a.Matches(call.Item1) && b.Matches(call.Item2) && c.Matches(call.Item3)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Equals"); + } } 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 12c639a9..ae7f5ef2 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 @@ -6,22 +6,61 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { internal global::Compono.ReturnConfig __Equals_4731af18; - internal global::Compono.ReturnConfig __Equals_07b0838c; + // ADR-0050: multi-entry response configuration - replaces the single + // __Equals_07b0838c/__Equals_07b0838c_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Equals_07b0838c_Entry + { + internal global::Compono.Match? Matcher_a; + internal global::Compono.Match? Matcher_b; + internal global::Compono.Match? Matcher_c; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Equals_07b0838c_Entry> __Equals_07b0838c_entries = []; + internal readonly global::System.Collections.Generic.List<(long, long, long)> __Equals_07b0838c_calls = []; + internal readonly object __Equals_07b0838c_lock = new(); bool global::TestNamespace.IRepository.Equals(int[] values) { __Equals_4731af18.RecordCall(); - return __Equals_4731af18.HasConfiguredException ? throw __Equals_4731af18.ConfiguredException + return __Equals_4731af18.HasConfiguredSequence ? __Equals_4731af18.NextSequenceOutcome() + : __Equals_4731af18.HasConfiguredException ? throw __Equals_4731af18.ConfiguredException : __Equals_4731af18.HasConfiguredValue ? __Equals_4731af18.ConfiguredValue : default; } bool global::TestNamespace.IRepository.Equals(long a, long b, long c) { - __Equals_07b0838c.RecordCall(); - return __Equals_07b0838c.HasConfiguredException ? throw __Equals_07b0838c.ConfiguredException - : __Equals_07b0838c.HasConfiguredValue ? __Equals_07b0838c.ConfiguredValue - : default; + // 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 (__Equals_07b0838c_lock) + { + __Equals_07b0838c_calls.Add((a, b, c)); + for (var __i = __Equals_07b0838c_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Equals_07b0838c_entries[__i]; + if ((__entry.Matcher_a is not { } __m_a || __m_a.Matches(a)) && (__entry.Matcher_b is not { } __m_b || __m_b.Matches(b)) && (__entry.Matcher_c is not { } __m_c || __m_c.Matches(c))) + { + // 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; + } + } + } + return default; } } @@ -30,8 +69,31 @@ internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, params int[] values) => new global::Compono.ReturnConfigBuilder(ref __self.__Equals_4731af18); - public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, long a, long b, long c) => - new global::Compono.ReturnConfigBuilder(ref __self.__Equals_07b0838c); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, long a, long b, long c) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Equals_07b0838c_Entry(); + lock (__self.__Equals_07b0838c_lock) { __self.__Equals_07b0838c_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder EqualsMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match a, global::Compono.Match b, global::Compono.Match c) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Equals_07b0838c_Entry(); + __entry.Matcher_a = a; + __entry.Matcher_b = b; + __entry.Matcher_c = c; + lock (__self.__Equals_07b0838c_lock) { __self.__Equals_07b0838c_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -58,8 +120,33 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, params int[] values) => new(__self.Instance.__Equals_4731af18.ConfiguredCallCount, "global::TestNamespace.IRepository.Equals"); - public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, long a, long b, long c) => - new(__self.Instance.__Equals_07b0838c.ConfiguredCallCount, "global::TestNamespace.IRepository.Equals"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, long a, long b, long c) + { + int __count; + lock (__self.Instance.__Equals_07b0838c_lock) { __count = __self.Instance.__Equals_07b0838c_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Equals"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier EqualsMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match a, global::Compono.Match b, global::Compono.Match c) + { + int __count; + lock (__self.Instance.__Equals_07b0838c_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Equals_07b0838c_calls) + { + if (a.Matches(call.Item1) && b.Matches(call.Item2) && c.Matches(call.Item3)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Equals"); + } } 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 3589544c..67d21eb3 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 @@ -6,22 +6,60 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { internal global::Compono.ReturnConfig __Equals_37c3f22f; - internal global::Compono.ReturnConfig __Equals_693d6b44; + // ADR-0050: multi-entry response configuration - replaces the single + // __Equals_693d6b44/__Equals_693d6b44_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Equals_693d6b44_Entry + { + internal global::Compono.Match? Matcher_a; + internal global::Compono.Match? Matcher_b; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Equals_693d6b44_Entry> __Equals_693d6b44_entries = []; + internal readonly global::System.Collections.Generic.List<(int, int)> __Equals_693d6b44_calls = []; + internal readonly object __Equals_693d6b44_lock = new(); bool global::TestNamespace.IRepository.Equals(global::System.Span value) { __Equals_37c3f22f.RecordCall(); - return __Equals_37c3f22f.HasConfiguredException ? throw __Equals_37c3f22f.ConfiguredException + return __Equals_37c3f22f.HasConfiguredSequence ? __Equals_37c3f22f.NextSequenceOutcome() + : __Equals_37c3f22f.HasConfiguredException ? throw __Equals_37c3f22f.ConfiguredException : __Equals_37c3f22f.HasConfiguredValue ? __Equals_37c3f22f.ConfiguredValue : default; } bool global::TestNamespace.IRepository.Equals(int a, int b) { - __Equals_693d6b44.RecordCall(); - return __Equals_693d6b44.HasConfiguredException ? throw __Equals_693d6b44.ConfiguredException - : __Equals_693d6b44.HasConfiguredValue ? __Equals_693d6b44.ConfiguredValue - : default; + // 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 (__Equals_693d6b44_lock) + { + __Equals_693d6b44_calls.Add((a, b)); + for (var __i = __Equals_693d6b44_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Equals_693d6b44_entries[__i]; + if ((__entry.Matcher_a is not { } __m_a || __m_a.Matches(a)) && (__entry.Matcher_b is not { } __m_b || __m_b.Matches(b))) + { + // 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; + } + } + } + return default; } } @@ -30,8 +68,30 @@ internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, global::System.Span value) => new global::Compono.ReturnConfigBuilder(ref __self.__Equals_37c3f22f); - public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, int a, int b) => - new global::Compono.ReturnConfigBuilder(ref __self.__Equals_693d6b44); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Equals(this global::TestNamespace_IRepository_e3198068_Double __self, int a, int b) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Equals_693d6b44_Entry(); + lock (__self.__Equals_693d6b44_lock) { __self.__Equals_693d6b44_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder EqualsMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match a, global::Compono.Match b) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Equals_693d6b44_Entry(); + __entry.Matcher_a = a; + __entry.Matcher_b = b; + lock (__self.__Equals_693d6b44_lock) { __self.__Equals_693d6b44_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -58,8 +118,33 @@ internal static class TestNamespace_IRepository_e3198068_DoubleVerification public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::System.Span value) => new(__self.Instance.__Equals_37c3f22f.ConfiguredCallCount, "global::TestNamespace.IRepository.Equals"); - public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int a, int b) => - new(__self.Instance.__Equals_693d6b44.ConfiguredCallCount, "global::TestNamespace.IRepository.Equals"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Equals(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int a, int b) + { + int __count; + lock (__self.Instance.__Equals_693d6b44_lock) { __count = __self.Instance.__Equals_693d6b44_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Equals"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier EqualsMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match a, global::Compono.Match b) + { + int __count; + lock (__self.Instance.__Equals_693d6b44_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Equals_693d6b44_calls) + { + if (a.Matches(call.Item1) && b.Matches(call.Item2)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Equals"); + } } 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 bb4a1110..6ad3f098 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 @@ -11,14 +11,18 @@ internal sealed class TestNamespace_IWidget_34aa79b8_Double : global::TestNamesp void global::TestNamespace.IWidget.Process(T value) { __Process_9f03e88f.RecordCall(); - if (__Process_9f03e88f.HasConfiguredException) + if (__Process_9f03e88f.HasConfiguredSequence) + __Process_9f03e88f.NextSequenceOutcome(); + else if (__Process_9f03e88f.HasConfiguredException) throw __Process_9f03e88f.ConfiguredException; } void global::TestNamespace.IWidget.Process(string label, T value) { __Process_5792c437.RecordCall(); - if (__Process_5792c437.HasConfiguredException) + if (__Process_5792c437.HasConfiguredSequence) + __Process_5792c437.NextSequenceOutcome(); + else if (__Process_5792c437.HasConfiguredException) throw __Process_5792c437.ConfiguredException; } } 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 3dafd429..696180c3 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 @@ -11,14 +11,18 @@ internal sealed class TestNamespace_IWidget_34aa79b8_Double : global::TestNamesp void global::TestNamespace.IWidget.Process<__self>(__self value) { __Process_9f03e88f.RecordCall(); - if (__Process_9f03e88f.HasConfiguredException) + if (__Process_9f03e88f.HasConfiguredSequence) + __Process_9f03e88f.NextSequenceOutcome(); + else if (__Process_9f03e88f.HasConfiguredException) throw __Process_9f03e88f.ConfiguredException; } void global::TestNamespace.IWidget.Process<__self>(global::System.Collections.Generic.IEnumerable<__self> values) { __Process_22a72316.RecordCall(); - if (__Process_22a72316.HasConfiguredException) + if (__Process_22a72316.HasConfiguredSequence) + __Process_22a72316.NextSequenceOutcome(); + else if (__Process_22a72316.HasConfiguredException) throw __Process_22a72316.ConfiguredException; } } 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 37d715ea..13de2e3b 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 @@ -11,14 +11,18 @@ internal sealed class TestNamespace_IWidget_34aa79b8_Double : global::TestNamesp void global::TestNamespace.IWidget.Process(T value) { __Process_9f03e88f.RecordCall(); - if (__Process_9f03e88f.HasConfiguredException) + if (__Process_9f03e88f.HasConfiguredSequence) + __Process_9f03e88f.NextSequenceOutcome(); + else if (__Process_9f03e88f.HasConfiguredException) throw __Process_9f03e88f.ConfiguredException; } void global::TestNamespace.IWidget.Process(global::System.Collections.Generic.IEnumerable values) { __Process_22a72316.RecordCall(); - if (__Process_22a72316.HasConfiguredException) + if (__Process_22a72316.HasConfiguredSequence) + __Process_22a72316.NextSequenceOutcome(); + else if (__Process_22a72316.HasConfiguredException) throw __Process_22a72316.ConfiguredException; } } 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 6674379c..4abccee8 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 @@ -11,14 +11,18 @@ internal sealed class TestNamespace_IThing_7b7b47c0_Double : global::TestNamespa void global::TestNamespace.IThing.M() { __M_467634cd.RecordCall(); - if (__M_467634cd.HasConfiguredException) + if (__M_467634cd.HasConfiguredSequence) + __M_467634cd.NextSequenceOutcome(); + else if (__M_467634cd.HasConfiguredException) throw __M_467634cd.ConfiguredException; } void global::TestNamespace.IThing.M() { __M_4478703e.RecordCall(); - if (__M_4478703e.HasConfiguredException) + if (__M_4478703e.HasConfiguredSequence) + __M_4478703e.NextSequenceOutcome(); + else if (__M_4478703e.HasConfiguredException) throw __M_4478703e.ConfiguredException; } } 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 60c1bf93..1c083032 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 @@ -11,7 +11,8 @@ internal sealed class TestNamespace_IThing_7b7b47c0_Double : global::TestNamespa string? global::TestNamespace.IThing.ToString() { __ToString_467634cd.RecordCall(); - return __ToString_467634cd.HasConfiguredException ? throw __ToString_467634cd.ConfiguredException + return __ToString_467634cd.HasConfiguredSequence ? __ToString_467634cd.NextSequenceOutcome() + : __ToString_467634cd.HasConfiguredException ? throw __ToString_467634cd.ConfiguredException : __ToString_467634cd.HasConfiguredValue ? __ToString_467634cd.ConfiguredValue : default; } @@ -19,7 +20,8 @@ internal sealed class TestNamespace_IThing_7b7b47c0_Double : global::TestNamespa string? global::TestNamespace.IThing.ToString(T value) { __ToString_9f03e88f.RecordCall(); - return __ToString_9f03e88f.HasConfiguredException ? throw __ToString_9f03e88f.ConfiguredException + return __ToString_9f03e88f.HasConfiguredSequence ? __ToString_9f03e88f.NextSequenceOutcome() + : __ToString_9f03e88f.HasConfiguredException ? throw __ToString_9f03e88f.ConfiguredException : __ToString_9f03e88f.HasConfiguredValue ? __ToString_9f03e88f.ConfiguredValue : default; } 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 8978d50c..06b9e1e8 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __M_b9dfaa09; - internal global::Compono.ReturnConfig __M_1a56931a; + // ADR-0050: multi-entry response configuration - replaces the single + // __M_b9dfaa09/__M_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_b9dfaa09_Entry> __M_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __M_b9dfaa09_calls = []; + internal readonly object __M_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __M_1a56931a/__M_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_1a56931a_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_1a56931a_Entry> __M_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __M_1a56931a_calls = []; + internal readonly object __M_1a56931a_lock = new(); void global::TestNamespace.IRepository.M(int value) { - __M_b9dfaa09.RecordCall(); - if (__M_b9dfaa09.HasConfiguredException) - throw __M_b9dfaa09.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_b9dfaa09_lock) + { + __M_b9dfaa09_calls.Add(value); + for (var __i = __M_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_b9dfaa09_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.M(string value) { - __M_1a56931a.RecordCall(); - if (__M_1a56931a.HasConfiguredException) - throw __M_1a56931a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_1a56931a_lock) + { + __M_1a56931a_calls.Add(value); + for (var __i = __M_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_1a56931a_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, int value = default) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_b9dfaa09); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, int value = default) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_b9dfaa09_Entry(); + lock (__self.__M_b9dfaa09_lock) { __self.__M_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_b9dfaa09_Entry(); + __entry.Matcher_value = value; + lock (__self.__M_b9dfaa09_lock) { __self.__M_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_1a56931a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + __entry.Matcher_value = value; + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value = default) => - new(__self.Instance.__M_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value = default) + { + int __count; + lock (__self.Instance.__M_b9dfaa09_lock) { __count = __self.Instance.__M_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) => - new(__self.Instance.__M_1a56931a.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__M_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_b9dfaa09_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) { __count = __self.Instance.__M_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_1a56931a_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } } 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 8bf83007..5bbb947c 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __M_f8c02d84; - internal global::Compono.ReturnConfig __M_1a56931a; + // ADR-0050: multi-entry response configuration - replaces the single + // __M_f8c02d84/__M_f8c02d84_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_f8c02d84_Entry + { + internal global::Compono.Match? Matcher_mode; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_f8c02d84_Entry> __M_f8c02d84_entries = []; + internal readonly global::System.Collections.Generic.List __M_f8c02d84_calls = []; + internal readonly object __M_f8c02d84_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __M_1a56931a/__M_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_1a56931a_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_1a56931a_Entry> __M_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __M_1a56931a_calls = []; + internal readonly object __M_1a56931a_lock = new(); void global::TestNamespace.IRepository.M(global::TestNamespace.Mode mode) { - __M_f8c02d84.RecordCall(); - if (__M_f8c02d84.HasConfiguredException) - throw __M_f8c02d84.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_f8c02d84_lock) + { + __M_f8c02d84_calls.Add(mode); + for (var __i = __M_f8c02d84_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_f8c02d84_entries[__i]; + if ((__entry.Matcher_mode is not { } __m_mode || __m_mode.Matches(mode))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.M(string value) { - __M_1a56931a.RecordCall(); - if (__M_1a56931a.HasConfiguredException) - throw __M_1a56931a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_1a56931a_lock) + { + __M_1a56931a_calls.Add(value); + for (var __i = __M_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_1a56931a_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Mode mode = (global::TestNamespace.Mode)1) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_f8c02d84); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Mode mode = (global::TestNamespace.Mode)1) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_f8c02d84_Entry(); + lock (__self.__M_f8c02d84_lock) { __self.__M_f8c02d84_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match mode) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_f8c02d84_Entry(); + __entry.Matcher_mode = mode; + lock (__self.__M_f8c02d84_lock) { __self.__M_f8c02d84_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_1a56931a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + __entry.Matcher_value = value; + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Mode mode = (global::TestNamespace.Mode)1) => - new(__self.Instance.__M_f8c02d84.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Mode mode = (global::TestNamespace.Mode)1) + { + int __count; + lock (__self.Instance.__M_f8c02d84_lock) { __count = __self.Instance.__M_f8c02d84_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) => - new(__self.Instance.__M_1a56931a.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match mode) + { + int __count; + lock (__self.Instance.__M_f8c02d84_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_f8c02d84_calls) + { + if (mode.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) { __count = __self.Instance.__M_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_1a56931a_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } } 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 e1472cbc..19a2d4e1 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __M_0e236ea3; - internal global::Compono.ReturnConfig __M_1a56931a; + // ADR-0050: multi-entry response configuration - replaces the single + // __M_0e236ea3/__M_0e236ea3_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_0e236ea3_Entry + { + internal global::Compono.Match? Matcher_mode; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_0e236ea3_Entry> __M_0e236ea3_entries = []; + internal readonly global::System.Collections.Generic.List __M_0e236ea3_calls = []; + internal readonly object __M_0e236ea3_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __M_1a56931a/__M_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_1a56931a_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_1a56931a_Entry> __M_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __M_1a56931a_calls = []; + internal readonly object __M_1a56931a_lock = new(); void global::TestNamespace.IRepository.M(global::TestNamespace.Mode? mode) { - __M_0e236ea3.RecordCall(); - if (__M_0e236ea3.HasConfiguredException) - throw __M_0e236ea3.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_0e236ea3_lock) + { + __M_0e236ea3_calls.Add(mode); + for (var __i = __M_0e236ea3_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_0e236ea3_entries[__i]; + if ((__entry.Matcher_mode is not { } __m_mode || __m_mode.Matches(mode))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.M(string value) { - __M_1a56931a.RecordCall(); - if (__M_1a56931a.HasConfiguredException) - throw __M_1a56931a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_1a56931a_lock) + { + __M_1a56931a_calls.Add(value); + for (var __i = __M_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_1a56931a_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Mode? mode = (global::TestNamespace.Mode)1) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_0e236ea3); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Mode? mode = (global::TestNamespace.Mode)1) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_0e236ea3_Entry(); + lock (__self.__M_0e236ea3_lock) { __self.__M_0e236ea3_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match mode) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_0e236ea3_Entry(); + __entry.Matcher_mode = mode; + lock (__self.__M_0e236ea3_lock) { __self.__M_0e236ea3_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_1a56931a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + __entry.Matcher_value = value; + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Mode? mode = (global::TestNamespace.Mode)1) => - new(__self.Instance.__M_0e236ea3.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Mode? mode = (global::TestNamespace.Mode)1) + { + int __count; + lock (__self.Instance.__M_0e236ea3_lock) { __count = __self.Instance.__M_0e236ea3_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) => - new(__self.Instance.__M_1a56931a.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match mode) + { + int __count; + lock (__self.Instance.__M_0e236ea3_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_0e236ea3_calls) + { + if (mode.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) { __count = __self.Instance.__M_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_1a56931a_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } } 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 84e380fc..27985a5a 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __M_b9dfaa09; - internal global::Compono.ReturnConfig __M_1a56931a; + // ADR-0050: multi-entry response configuration - replaces the single + // __M_b9dfaa09/__M_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_b9dfaa09_Entry> __M_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __M_b9dfaa09_calls = []; + internal readonly object __M_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __M_1a56931a/__M_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __M_1a56931a_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__M_1a56931a_Entry> __M_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __M_1a56931a_calls = []; + internal readonly object __M_1a56931a_lock = new(); void global::TestNamespace.IRepository.M(int value) { - __M_b9dfaa09.RecordCall(); - if (__M_b9dfaa09.HasConfiguredException) - throw __M_b9dfaa09.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_b9dfaa09_lock) + { + __M_b9dfaa09_calls.Add(value); + for (var __i = __M_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_b9dfaa09_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.M(string value) { - __M_1a56931a.RecordCall(); - if (__M_1a56931a.HasConfiguredException) - throw __M_1a56931a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__M_1a56931a_lock) + { + __M_1a56931a_calls.Add(value); + for (var __i = __M_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __M_1a56931a_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, int value = 0) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_b9dfaa09); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, int value = 0) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_b9dfaa09_Entry(); + lock (__self.__M_b9dfaa09_lock) { __self.__M_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_b9dfaa09_Entry(); + __entry.Matcher_value = value; + lock (__self.__M_b9dfaa09_lock) { __self.__M_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder M(this global::TestNamespace_IRepository_e3198068_Double __self, string value) => - new global::Compono.ReturnConfigBuilder(ref __self.__M_1a56931a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder MMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__M_1a56931a_Entry(); + __entry.Matcher_value = value; + lock (__self.__M_1a56931a_lock) { __self.__M_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value = 0) => - new(__self.Instance.__M_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value = 0) + { + int __count; + lock (__self.Instance.__M_b9dfaa09_lock) { __count = __self.Instance.__M_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } - public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) => - new(__self.Instance.__M_1a56931a.ConfiguredCallCount, "global::TestNamespace.IRepository.M"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__M_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_b9dfaa09_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier M(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) { __count = __self.Instance.__M_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.M"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier MMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__M_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__M_1a56931a_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.M"); + } } 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 35290b60..2245bb60 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __Save_b9dfaa09; - internal global::Compono.ReturnConfig __Save_1a56931a; + // ADR-0050: multi-entry response configuration - replaces the single + // __Save_b9dfaa09/__Save_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Save_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher___self; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Save_b9dfaa09_Entry> __Save_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Save_b9dfaa09_calls = []; + internal readonly object __Save_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Save_1a56931a/__Save_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Save_1a56931a_Entry + { + internal global::Compono.Match? Matcher___self; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Save_1a56931a_Entry> __Save_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Save_1a56931a_calls = []; + internal readonly object __Save_1a56931a_lock = new(); void global::TestNamespace.IRepository.Save(int __self) { - __Save_b9dfaa09.RecordCall(); - if (__Save_b9dfaa09.HasConfiguredException) - throw __Save_b9dfaa09.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Save_b9dfaa09_lock) + { + __Save_b9dfaa09_calls.Add(__self); + for (var __i = __Save_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Save_b9dfaa09_entries[__i]; + if ((__entry.Matcher___self is not { } __m___self || __m___self.Matches(__self))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.Save(string __self) { - __Save_1a56931a.RecordCall(); - if (__Save_1a56931a.HasConfiguredException) - throw __Save_1a56931a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Save_1a56931a_lock) + { + __Save_1a56931a_calls.Add(__self); + for (var __i = __Save_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Save_1a56931a_entries[__i]; + if ((__entry.Matcher___self is not { } __m___self || __m___self.Matches(__self))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder Save(this global::TestNamespace_IRepository_e3198068_Double ___self, int __self) => - new global::Compono.ReturnConfigBuilder(ref ___self.__Save_b9dfaa09); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Save(this global::TestNamespace_IRepository_e3198068_Double ___self, int __self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Save_b9dfaa09_Entry(); + lock (___self.__Save_b9dfaa09_lock) { ___self.__Save_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder SaveMatching(this global::TestNamespace_IRepository_e3198068_Double ___self, global::Compono.Match __self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Save_b9dfaa09_Entry(); + __entry.Matcher___self = __self; + lock (___self.__Save_b9dfaa09_lock) { ___self.__Save_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Save(this global::TestNamespace_IRepository_e3198068_Double ___self, string __self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Save_1a56931a_Entry(); + lock (___self.__Save_1a56931a_lock) { ___self.__Save_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder Save(this global::TestNamespace_IRepository_e3198068_Double ___self, string __self) => - new global::Compono.ReturnConfigBuilder(ref ___self.__Save_1a56931a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder SaveMatching(this global::TestNamespace_IRepository_e3198068_Double ___self, global::Compono.Match __self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Save_1a56931a_Entry(); + __entry.Matcher___self = __self; + lock (___self.__Save_1a56931a_lock) { ___self.__Save_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier Save(this global::TestNamespace_IRepository_e3198068_DoubleVerifier ___self, int __self) => - new(___self.Instance.__Save_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.Save"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Save(this global::TestNamespace_IRepository_e3198068_DoubleVerifier ___self, int __self) + { + int __count; + lock (___self.Instance.__Save_b9dfaa09_lock) { __count = ___self.Instance.__Save_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Save"); + } - public static global::Compono.CallVerifier Save(this global::TestNamespace_IRepository_e3198068_DoubleVerifier ___self, string __self) => - new(___self.Instance.__Save_1a56931a.ConfiguredCallCount, "global::TestNamespace.IRepository.Save"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier SaveMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier ___self, global::Compono.Match __self) + { + int __count; + lock (___self.Instance.__Save_b9dfaa09_lock) + { + __count = 0; + foreach (var call in ___self.Instance.__Save_b9dfaa09_calls) + { + if (__self.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Save"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Save(this global::TestNamespace_IRepository_e3198068_DoubleVerifier ___self, string __self) + { + int __count; + lock (___self.Instance.__Save_1a56931a_lock) { __count = ___self.Instance.__Save_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Save"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier SaveMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier ___self, global::Compono.Match __self) + { + int __count; + lock (___self.Instance.__Save_1a56931a_lock) + { + __count = 0; + foreach (var call in ___self.Instance.__Save_1a56931a_calls) + { + if (__self.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Save"); + } } 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 1fa202e0..d46b718d 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __Save_b9dfaa09; - internal global::Compono.ReturnConfig __Save_1a56931a; + // ADR-0050: multi-entry response configuration - replaces the single + // __Save_b9dfaa09/__Save_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Save_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_self; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Save_b9dfaa09_Entry> __Save_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Save_b9dfaa09_calls = []; + internal readonly object __Save_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Save_1a56931a/__Save_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Save_1a56931a_Entry + { + internal global::Compono.Match? Matcher_self; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Save_1a56931a_Entry> __Save_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Save_1a56931a_calls = []; + internal readonly object __Save_1a56931a_lock = new(); void global::TestNamespace.IRepository.Save(int self) { - __Save_b9dfaa09.RecordCall(); - if (__Save_b9dfaa09.HasConfiguredException) - throw __Save_b9dfaa09.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Save_b9dfaa09_lock) + { + __Save_b9dfaa09_calls.Add(self); + for (var __i = __Save_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Save_b9dfaa09_entries[__i]; + if ((__entry.Matcher_self is not { } __m_self || __m_self.Matches(self))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.Save(string self) { - __Save_1a56931a.RecordCall(); - if (__Save_1a56931a.HasConfiguredException) - throw __Save_1a56931a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Save_1a56931a_lock) + { + __Save_1a56931a_calls.Add(self); + for (var __i = __Save_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Save_1a56931a_entries[__i]; + if ((__entry.Matcher_self is not { } __m_self || __m_self.Matches(self))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder Save(this global::TestNamespace_IRepository_e3198068_Double __self, int self) => - new global::Compono.ReturnConfigBuilder(ref __self.__Save_b9dfaa09); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Save(this global::TestNamespace_IRepository_e3198068_Double __self, int self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Save_b9dfaa09_Entry(); + lock (__self.__Save_b9dfaa09_lock) { __self.__Save_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder SaveMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Save_b9dfaa09_Entry(); + __entry.Matcher_self = self; + lock (__self.__Save_b9dfaa09_lock) { __self.__Save_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Save(this global::TestNamespace_IRepository_e3198068_Double __self, string self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Save_1a56931a_Entry(); + lock (__self.__Save_1a56931a_lock) { __self.__Save_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder Save(this global::TestNamespace_IRepository_e3198068_Double __self, string self) => - new global::Compono.ReturnConfigBuilder(ref __self.__Save_1a56931a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder SaveMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match self) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Save_1a56931a_Entry(); + __entry.Matcher_self = self; + lock (__self.__Save_1a56931a_lock) { __self.__Save_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier Save(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int self) => - new(__self.Instance.__Save_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.Save"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Save(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int self) + { + int __count; + lock (__self.Instance.__Save_b9dfaa09_lock) { __count = __self.Instance.__Save_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Save"); + } - public static global::Compono.CallVerifier Save(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string self) => - new(__self.Instance.__Save_1a56931a.ConfiguredCallCount, "global::TestNamespace.IRepository.Save"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier SaveMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match self) + { + int __count; + lock (__self.Instance.__Save_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Save_b9dfaa09_calls) + { + if (self.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Save"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Save(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string self) + { + int __count; + lock (__self.Instance.__Save_1a56931a_lock) { __count = __self.Instance.__Save_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Save"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier SaveMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match self) + { + int __count; + lock (__self.Instance.__Save_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Save_1a56931a_calls) + { + if (self.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Save"); + } } 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 new file mode 100644 index 00000000..8295a2e0 --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,247 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + // ADR-0050: multi-entry response configuration - replaces the single + // __Delete_b9dfaa09/__Delete_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Delete_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Delete_b9dfaa09_Entry> __Delete_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Delete_b9dfaa09_calls = []; + internal readonly object __Delete_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Delete_1a56931a/__Delete_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Delete_1a56931a_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Delete_1a56931a_Entry> __Delete_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Delete_1a56931a_calls = []; + internal readonly object __Delete_1a56931a_lock = new(); + + bool global::TestNamespace.IRepository.Delete(int id) + { + // 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 (__Delete_b9dfaa09_lock) + { + __Delete_b9dfaa09_calls.Add(id); + for (var __i = __Delete_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Delete_b9dfaa09_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.Delete(string id) + { + // 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 (__Delete_1a56931a_lock) + { + __Delete_1a56931a_calls.Add(id); + for (var __i = __Delete_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Delete_1a56931a_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // 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; + } + } + } + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Delete(this global::TestNamespace_IRepository_e3198068_Double __self, int id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Delete_b9dfaa09_Entry(); + lock (__self.__Delete_b9dfaa09_lock) { __self.__Delete_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder DeleteMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Delete_b9dfaa09_Entry(); + __entry.Matcher_id = id; + lock (__self.__Delete_b9dfaa09_lock) { __self.__Delete_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Delete(this global::TestNamespace_IRepository_e3198068_Double __self, string id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Delete_1a56931a_Entry(); + lock (__self.__Delete_1a56931a_lock) { __self.__Delete_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder DeleteMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Delete_1a56931a_Entry(); + __entry.Matcher_id = id; + lock (__self.__Delete_1a56931a_lock) { __self.__Delete_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Delete(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) + { + int __count; + lock (__self.Instance.__Delete_b9dfaa09_lock) { __count = __self.Instance.__Delete_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Delete"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier DeleteMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Delete_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Delete_b9dfaa09_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Delete"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Delete(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) + { + int __count; + lock (__self.Instance.__Delete_1a56931a_lock) { __count = __self.Instance.__Delete_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Delete"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier DeleteMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Delete_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Delete_1a56931a_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Delete"); + } + +} + +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.OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState#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.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 be481b88..3bb5fe81 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __Get_b9dfaa09; - internal global::Compono.ReturnConfig __Get_1a56931a; + // ADR-0050: multi-entry response configuration - replaces the single + // __Get_b9dfaa09/__Get_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Get_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Get_b9dfaa09_Entry> __Get_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Get_b9dfaa09_calls = []; + internal readonly object __Get_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Get_1a56931a/__Get_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Get_1a56931a_Entry + { + internal global::Compono.Match? Matcher_id; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Get_1a56931a_Entry> __Get_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Get_1a56931a_calls = []; + internal readonly object __Get_1a56931a_lock = new(); void global::TestNamespace.IRepository.Get(int id) { - __Get_b9dfaa09.RecordCall(); - if (__Get_b9dfaa09.HasConfiguredException) - throw __Get_b9dfaa09.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Get_b9dfaa09_lock) + { + __Get_b9dfaa09_calls.Add(id); + for (var __i = __Get_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Get_b9dfaa09_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.Get(string id) { - __Get_1a56931a.RecordCall(); - if (__Get_1a56931a.HasConfiguredException) - throw __Get_1a56931a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Get_1a56931a_lock) + { + __Get_1a56931a_calls.Add(id); + for (var __i = __Get_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Get_1a56931a_entries[__i]; + if ((__entry.Matcher_id is not { } __m_id || __m_id.Matches(id))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, int id) => - new global::Compono.ReturnConfigBuilder(ref __self.__Get_b9dfaa09); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, int id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_b9dfaa09_Entry(); + lock (__self.__Get_b9dfaa09_lock) { __self.__Get_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_b9dfaa09_Entry(); + __entry.Matcher_id = id; + lock (__self.__Get_b9dfaa09_lock) { __self.__Get_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, string id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_1a56931a_Entry(); + lock (__self.__Get_1a56931a_lock) { __self.__Get_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder Get(this global::TestNamespace_IRepository_e3198068_Double __self, string id) => - new global::Compono.ReturnConfigBuilder(ref __self.__Get_1a56931a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder GetMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match id) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Get_1a56931a_Entry(); + __entry.Matcher_id = id; + lock (__self.__Get_1a56931a_lock) { __self.__Get_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) => - new(__self.Instance.__Get_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.Get"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int id) + { + int __count; + lock (__self.Instance.__Get_b9dfaa09_lock) { __count = __self.Instance.__Get_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Get"); + } - public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) => - new(__self.Instance.__Get_1a56931a.ConfiguredCallCount, "global::TestNamespace.IRepository.Get"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Get_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Get_b9dfaa09_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Get(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string id) + { + int __count; + lock (__self.Instance.__Get_1a56931a_lock) { __count = __self.Instance.__Get_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Get"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier GetMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match id) + { + int __count; + lock (__self.Instance.__Get_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Get_1a56931a_calls) + { + if (id.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Get"); + } } 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 dee69228..2e4cad96 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 @@ -5,33 +5,147 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __ToString_b9dfaa09; - internal global::Compono.ReturnConfig __ToString_1a56931a; + // ADR-0050: multi-entry response configuration - replaces the single + // __ToString_b9dfaa09/__ToString_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __ToString_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_format; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__ToString_b9dfaa09_Entry> __ToString_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __ToString_b9dfaa09_calls = []; + internal readonly object __ToString_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __ToString_1a56931a/__ToString_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __ToString_1a56931a_Entry + { + internal global::Compono.Match? Matcher_format; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__ToString_1a56931a_Entry> __ToString_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __ToString_1a56931a_calls = []; + internal readonly object __ToString_1a56931a_lock = new(); string? global::TestNamespace.IRepository.ToString(int format) { - __ToString_b9dfaa09.RecordCall(); - return __ToString_b9dfaa09.HasConfiguredException ? throw __ToString_b9dfaa09.ConfiguredException - : __ToString_b9dfaa09.HasConfiguredValue ? __ToString_b9dfaa09.ConfiguredValue - : default; + // 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 (__ToString_b9dfaa09_lock) + { + __ToString_b9dfaa09_calls.Add(format); + for (var __i = __ToString_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __ToString_b9dfaa09_entries[__i]; + if ((__entry.Matcher_format is not { } __m_format || __m_format.Matches(format))) + { + // 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; + } + } + } + return default; } string? global::TestNamespace.IRepository.ToString(string format) { - __ToString_1a56931a.RecordCall(); - return __ToString_1a56931a.HasConfiguredException ? throw __ToString_1a56931a.ConfiguredException - : __ToString_1a56931a.HasConfiguredValue ? __ToString_1a56931a.ConfiguredValue - : default; + // 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 (__ToString_1a56931a_lock) + { + __ToString_1a56931a_calls.Add(format); + for (var __i = __ToString_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __ToString_1a56931a_entries[__i]; + if ((__entry.Matcher_format is not { } __m_format || __m_format.Matches(format))) + { + // 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; + } + } + } + return default; } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder ToString(this global::TestNamespace_IRepository_e3198068_Double __self, int format) => - new global::Compono.ReturnConfigBuilder(ref __self.__ToString_b9dfaa09); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder ToString(this global::TestNamespace_IRepository_e3198068_Double __self, int format) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__ToString_b9dfaa09_Entry(); + lock (__self.__ToString_b9dfaa09_lock) { __self.__ToString_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder ToStringMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match format) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__ToString_b9dfaa09_Entry(); + __entry.Matcher_format = format; + lock (__self.__ToString_b9dfaa09_lock) { __self.__ToString_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder ToString(this global::TestNamespace_IRepository_e3198068_Double __self, string format) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__ToString_1a56931a_Entry(); + lock (__self.__ToString_1a56931a_lock) { __self.__ToString_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder ToString(this global::TestNamespace_IRepository_e3198068_Double __self, string format) => - new global::Compono.ReturnConfigBuilder(ref __self.__ToString_1a56931a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder ToStringMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match format) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__ToString_1a56931a_Entry(); + __entry.Matcher_format = format; + lock (__self.__ToString_1a56931a_lock) { __self.__ToString_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -55,11 +169,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier ToString(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int format) => - new(__self.Instance.__ToString_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.ToString"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier ToString(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int format) + { + int __count; + lock (__self.Instance.__ToString_b9dfaa09_lock) { __count = __self.Instance.__ToString_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.ToString"); + } - public static global::Compono.CallVerifier ToString(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string format) => - new(__self.Instance.__ToString_1a56931a.ConfiguredCallCount, "global::TestNamespace.IRepository.ToString"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier ToStringMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match format) + { + int __count; + lock (__self.Instance.__ToString_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__ToString_b9dfaa09_calls) + { + if (format.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.ToString"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier ToString(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string format) + { + int __count; + lock (__self.Instance.__ToString_1a56931a_lock) { __count = __self.Instance.__ToString_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.ToString"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier ToStringMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match format) + { + int __count; + lock (__self.Instance.__ToString_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__ToString_1a56931a_calls) + { + if (format.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.ToString"); + } } 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 0556b197..3199eac6 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 @@ -5,33 +5,147 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __ToString_4a96ce8f; - internal global::Compono.ReturnConfig __ToString_b9dfaa09; + // ADR-0050: multi-entry response configuration - replaces the single + // __ToString_4a96ce8f/__ToString_4a96ce8f_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __ToString_4a96ce8f_Entry + { + internal global::Compono.Match? Matcher_values; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__ToString_4a96ce8f_Entry> __ToString_4a96ce8f_entries = []; + internal readonly global::System.Collections.Generic.List __ToString_4a96ce8f_calls = []; + internal readonly object __ToString_4a96ce8f_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __ToString_b9dfaa09/__ToString_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __ToString_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_format; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__ToString_b9dfaa09_Entry> __ToString_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __ToString_b9dfaa09_calls = []; + internal readonly object __ToString_b9dfaa09_lock = new(); string? global::TestNamespace.IRepository.ToString(object[] values) { - __ToString_4a96ce8f.RecordCall(); - return __ToString_4a96ce8f.HasConfiguredException ? throw __ToString_4a96ce8f.ConfiguredException - : __ToString_4a96ce8f.HasConfiguredValue ? __ToString_4a96ce8f.ConfiguredValue - : default; + // 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 (__ToString_4a96ce8f_lock) + { + __ToString_4a96ce8f_calls.Add(values); + for (var __i = __ToString_4a96ce8f_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __ToString_4a96ce8f_entries[__i]; + if ((__entry.Matcher_values is not { } __m_values || __m_values.Matches(values))) + { + // 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; + } + } + } + return default; } string? global::TestNamespace.IRepository.ToString(int format) { - __ToString_b9dfaa09.RecordCall(); - return __ToString_b9dfaa09.HasConfiguredException ? throw __ToString_b9dfaa09.ConfiguredException - : __ToString_b9dfaa09.HasConfiguredValue ? __ToString_b9dfaa09.ConfiguredValue - : default; + // 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 (__ToString_b9dfaa09_lock) + { + __ToString_b9dfaa09_calls.Add(format); + for (var __i = __ToString_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __ToString_b9dfaa09_entries[__i]; + if ((__entry.Matcher_format is not { } __m_format || __m_format.Matches(format))) + { + // 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; + } + } + } + return default; } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder ToString(this global::TestNamespace_IRepository_e3198068_Double __self, params object[] values) => - new global::Compono.ReturnConfigBuilder(ref __self.__ToString_4a96ce8f); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder ToString(this global::TestNamespace_IRepository_e3198068_Double __self, params object[] values) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__ToString_4a96ce8f_Entry(); + lock (__self.__ToString_4a96ce8f_lock) { __self.__ToString_4a96ce8f_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder ToStringMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match values) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__ToString_4a96ce8f_Entry(); + __entry.Matcher_values = values; + lock (__self.__ToString_4a96ce8f_lock) { __self.__ToString_4a96ce8f_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder ToString(this global::TestNamespace_IRepository_e3198068_Double __self, int format) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__ToString_b9dfaa09_Entry(); + lock (__self.__ToString_b9dfaa09_lock) { __self.__ToString_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder ToString(this global::TestNamespace_IRepository_e3198068_Double __self, int format) => - new global::Compono.ReturnConfigBuilder(ref __self.__ToString_b9dfaa09); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder ToStringMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match format) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__ToString_b9dfaa09_Entry(); + __entry.Matcher_format = format; + lock (__self.__ToString_b9dfaa09_lock) { __self.__ToString_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -55,11 +169,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier ToString(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, params object[] values) => - new(__self.Instance.__ToString_4a96ce8f.ConfiguredCallCount, "global::TestNamespace.IRepository.ToString"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier ToString(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, params object[] values) + { + int __count; + lock (__self.Instance.__ToString_4a96ce8f_lock) { __count = __self.Instance.__ToString_4a96ce8f_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.ToString"); + } - public static global::Compono.CallVerifier ToString(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int format) => - new(__self.Instance.__ToString_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IRepository.ToString"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier ToStringMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match values) + { + int __count; + lock (__self.Instance.__ToString_4a96ce8f_lock) + { + __count = 0; + foreach (var call in __self.Instance.__ToString_4a96ce8f_calls) + { + if (values.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.ToString"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier ToString(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int format) + { + int __count; + lock (__self.Instance.__ToString_b9dfaa09_lock) { __count = __self.Instance.__ToString_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.ToString"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier ToStringMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match format) + { + int __count; + lock (__self.Instance.__ToString_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__ToString_b9dfaa09_calls) + { + if (format.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.ToString"); + } } 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 22bd0a5a..32aa75fd 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __Handle_50c4849c; - internal global::Compono.ReturnConfig __Handle_3b0d1db3; + // ADR-0050: multi-entry response configuration - replaces the single + // __Handle_50c4849c/__Handle_50c4849c_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Handle_50c4849c_Entry + { + internal global::Compono.Match.Inner>? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Handle_50c4849c_Entry> __Handle_50c4849c_entries = []; + internal readonly global::System.Collections.Generic.List.Inner> __Handle_50c4849c_calls = []; + internal readonly object __Handle_50c4849c_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Handle_3b0d1db3/__Handle_3b0d1db3_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Handle_3b0d1db3_Entry + { + internal global::Compono.Match.Inner>? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Handle_3b0d1db3_Entry> __Handle_3b0d1db3_entries = []; + internal readonly global::System.Collections.Generic.List.Inner> __Handle_3b0d1db3_calls = []; + internal readonly object __Handle_3b0d1db3_lock = new(); void global::TestNamespace.IRepository.Handle(global::TestNamespace.Outer.Inner value) { - __Handle_50c4849c.RecordCall(); - if (__Handle_50c4849c.HasConfiguredException) - throw __Handle_50c4849c.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Handle_50c4849c_lock) + { + __Handle_50c4849c_calls.Add(value); + for (var __i = __Handle_50c4849c_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Handle_50c4849c_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IRepository.Handle(global::TestNamespace.Outer.Inner value) { - __Handle_3b0d1db3.RecordCall(); - if (__Handle_3b0d1db3.HasConfiguredException) - throw __Handle_3b0d1db3.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Handle_3b0d1db3_lock) + { + __Handle_3b0d1db3_calls.Add(value); + for (var __i = __Handle_3b0d1db3_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Handle_3b0d1db3_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder Handle(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Outer.Inner value) => - new global::Compono.ReturnConfigBuilder(ref __self.__Handle_50c4849c); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Handle(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Outer.Inner value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Handle_50c4849c_Entry(); + lock (__self.__Handle_50c4849c_lock) { __self.__Handle_50c4849c_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder HandleMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match.Inner> value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Handle_50c4849c_Entry(); + __entry.Matcher_value = value; + lock (__self.__Handle_50c4849c_lock) { __self.__Handle_50c4849c_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Handle(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Outer.Inner value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Handle_3b0d1db3_Entry(); + lock (__self.__Handle_3b0d1db3_lock) { __self.__Handle_3b0d1db3_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder Handle(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Outer.Inner value) => - new global::Compono.ReturnConfigBuilder(ref __self.__Handle_3b0d1db3); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder HandleMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match.Inner> value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Handle_3b0d1db3_Entry(); + __entry.Matcher_value = value; + lock (__self.__Handle_3b0d1db3_lock) { __self.__Handle_3b0d1db3_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier Handle(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Outer.Inner value) => - new(__self.Instance.__Handle_50c4849c.ConfiguredCallCount, "global::TestNamespace.IRepository.Handle"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Handle(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Outer.Inner value) + { + int __count; + lock (__self.Instance.__Handle_50c4849c_lock) { __count = __self.Instance.__Handle_50c4849c_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Handle"); + } - public static global::Compono.CallVerifier Handle(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Outer.Inner value) => - new(__self.Instance.__Handle_3b0d1db3.ConfiguredCallCount, "global::TestNamespace.IRepository.Handle"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier HandleMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match.Inner> value) + { + int __count; + lock (__self.Instance.__Handle_50c4849c_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Handle_50c4849c_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Handle"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Handle(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Outer.Inner value) + { + int __count; + lock (__self.Instance.__Handle_3b0d1db3_lock) { __count = __self.Instance.__Handle_3b0d1db3_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Handle"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier HandleMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match.Inner> value) + { + int __count; + lock (__self.Instance.__Handle_3b0d1db3_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Handle_3b0d1db3_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Handle"); + } } 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 85475657..fa52c41c 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __Handle_3b7828aa; - internal global::Compono.ReturnConfig __Handle_b5b0a4ae; + // ADR-0050: multi-entry response configuration - replaces the single + // __Handle_3b7828aa/__Handle_3b7828aa_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Handle_3b7828aa_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Handle_3b7828aa_Entry> __Handle_3b7828aa_entries = []; + internal readonly global::System.Collections.Generic.List __Handle_3b7828aa_calls = []; + internal readonly object __Handle_3b7828aa_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Handle_b5b0a4ae/__Handle_b5b0a4ae_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Handle_b5b0a4ae_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Handle_b5b0a4ae_Entry> __Handle_b5b0a4ae_entries = []; + internal readonly global::System.Collections.Generic.List __Handle_b5b0a4ae_calls = []; + internal readonly object __Handle_b5b0a4ae_lock = new(); void global::TestNamespace.IBaseA.Handle(global::TestNamespace.Collision.hgQWPcvxVjdw value) { - __Handle_3b7828aa.RecordCall(); - if (__Handle_3b7828aa.HasConfiguredException) - throw __Handle_3b7828aa.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Handle_3b7828aa_lock) + { + __Handle_3b7828aa_calls.Add(value); + for (var __i = __Handle_3b7828aa_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Handle_3b7828aa_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IBaseB.Handle(global::TestNamespace.Collision.cTtIHWbVrHlp value) { - __Handle_b5b0a4ae.RecordCall(); - if (__Handle_b5b0a4ae.HasConfiguredException) - throw __Handle_b5b0a4ae.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Handle_b5b0a4ae_lock) + { + __Handle_b5b0a4ae_calls.Add(value); + for (var __i = __Handle_b5b0a4ae_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Handle_b5b0a4ae_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder Handle(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Collision.hgQWPcvxVjdw value) => - new global::Compono.ReturnConfigBuilder(ref __self.__Handle_3b7828aa); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Handle(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Collision.hgQWPcvxVjdw value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Handle_3b7828aa_Entry(); + lock (__self.__Handle_3b7828aa_lock) { __self.__Handle_3b7828aa_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder HandleMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Handle_3b7828aa_Entry(); + __entry.Matcher_value = value; + lock (__self.__Handle_3b7828aa_lock) { __self.__Handle_3b7828aa_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Handle(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Collision.cTtIHWbVrHlp value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Handle_b5b0a4ae_Entry(); + lock (__self.__Handle_b5b0a4ae_lock) { __self.__Handle_b5b0a4ae_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder Handle(this global::TestNamespace_IRepository_e3198068_Double __self, global::TestNamespace.Collision.cTtIHWbVrHlp value) => - new global::Compono.ReturnConfigBuilder(ref __self.__Handle_b5b0a4ae); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder HandleMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Handle_b5b0a4ae_Entry(); + __entry.Matcher_value = value; + lock (__self.__Handle_b5b0a4ae_lock) { __self.__Handle_b5b0a4ae_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier Handle(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Collision.hgQWPcvxVjdw value) => - new(__self.Instance.__Handle_3b7828aa.ConfiguredCallCount, "global::TestNamespace.IBaseA.Handle"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Handle(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Collision.hgQWPcvxVjdw value) + { + int __count; + lock (__self.Instance.__Handle_3b7828aa_lock) { __count = __self.Instance.__Handle_3b7828aa_calls.Count; } + return new(__count, "global::TestNamespace.IBaseA.Handle"); + } - public static global::Compono.CallVerifier Handle(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Collision.cTtIHWbVrHlp value) => - new(__self.Instance.__Handle_b5b0a4ae.ConfiguredCallCount, "global::TestNamespace.IBaseB.Handle"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier HandleMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__Handle_3b7828aa_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Handle_3b7828aa_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IBaseA.Handle"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Handle(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::TestNamespace.Collision.cTtIHWbVrHlp value) + { + int __count; + lock (__self.Instance.__Handle_b5b0a4ae_lock) { __count = __self.Instance.__Handle_b5b0a4ae_calls.Count; } + return new(__count, "global::TestNamespace.IBaseB.Handle"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier HandleMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__Handle_b5b0a4ae_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Handle_b5b0a4ae_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IBaseB.Handle"); + } } 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 a4cbe745..fd7bd65f 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 @@ -5,31 +5,155 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IResponseBuilder_ed393682_Double : global::TestNamespace.IResponseBuilder { - internal global::Compono.ReturnConfig __Speak_1a56931a; - internal global::Compono.ReturnConfig __Speak_22f1fe0a; + // ADR-0050: multi-entry response configuration - replaces the single + // __Speak_1a56931a/__Speak_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Speak_1a56931a_Entry + { + internal global::Compono.Match? Matcher_text; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Speak_1a56931a_Entry> __Speak_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Speak_1a56931a_calls = []; + internal readonly object __Speak_1a56931a_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Speak_22f1fe0a/__Speak_22f1fe0a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Speak_22f1fe0a_Entry + { + internal global::Compono.Match? Matcher_parts; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Speak_22f1fe0a_Entry> __Speak_22f1fe0a_entries = []; + internal readonly global::System.Collections.Generic.List __Speak_22f1fe0a_calls = []; + internal readonly object __Speak_22f1fe0a_lock = new(); void global::TestNamespace.IResponseBuilder.Speak(string? text) { - __Speak_1a56931a.RecordCall(); - if (__Speak_1a56931a.HasConfiguredException) - throw __Speak_1a56931a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Speak_1a56931a_lock) + { + __Speak_1a56931a_calls.Add(text); + for (var __i = __Speak_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Speak_1a56931a_entries[__i]; + if ((__entry.Matcher_text is not { } __m_text || __m_text.Matches(text))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } void global::TestNamespace.IResponseBuilder.Speak(global::TestNamespace.ISsml[] parts) { - __Speak_22f1fe0a.RecordCall(); - if (__Speak_22f1fe0a.HasConfiguredException) - throw __Speak_22f1fe0a.ConfiguredException; + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. + lock (__Speak_22f1fe0a_lock) + { + __Speak_22f1fe0a_calls.Add(parts); + for (var __i = __Speak_22f1fe0a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Speak_22f1fe0a_entries[__i]; + if ((__entry.Matcher_parts is not { } __m_parts || __m_parts.Matches(parts))) + { + // ADR-0050: no `break` here (Codex review, PR #108 round 6) - see the + // value-returning branch below for the full reasoning; a matched entry with + // neither a configured exception nor a configured value must not shadow an + // older, configured matching entry. Void members still have a genuine + // "configured" state distinct from "incomplete" - `HasConfiguredValue` is set + // by `.Returns(default)` (a `global::Compono.Unit`) even though there's nothing + // to return - so it must + // stop the scan (`return;`) exactly like the value-returning branch below, not + // be treated as equivalent to an unconfigured/incomplete entry (Codex review, + // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } + if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; + if (__entry.Config.HasConfiguredValue) return; + } + } + } } } internal static class TestNamespace_IResponseBuilder_ed393682_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder Speak(this global::TestNamespace_IResponseBuilder_ed393682_Double __self, string? text) => - new global::Compono.ReturnConfigBuilder(ref __self.__Speak_1a56931a); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Speak(this global::TestNamespace_IResponseBuilder_ed393682_Double __self, string? text) + { + var __entry = new global::TestNamespace_IResponseBuilder_ed393682_Double.__Speak_1a56931a_Entry(); + lock (__self.__Speak_1a56931a_lock) { __self.__Speak_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder SpeakMatching(this global::TestNamespace_IResponseBuilder_ed393682_Double __self, global::Compono.Match text) + { + var __entry = new global::TestNamespace_IResponseBuilder_ed393682_Double.__Speak_1a56931a_Entry(); + __entry.Matcher_text = text; + lock (__self.__Speak_1a56931a_lock) { __self.__Speak_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Speak(this global::TestNamespace_IResponseBuilder_ed393682_Double __self, params global::TestNamespace.ISsml[] parts) + { + var __entry = new global::TestNamespace_IResponseBuilder_ed393682_Double.__Speak_22f1fe0a_Entry(); + lock (__self.__Speak_22f1fe0a_lock) { __self.__Speak_22f1fe0a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } - public static global::Compono.ReturnConfigBuilder Speak(this global::TestNamespace_IResponseBuilder_ed393682_Double __self, params global::TestNamespace.ISsml[] parts) => - new global::Compono.ReturnConfigBuilder(ref __self.__Speak_22f1fe0a); + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder SpeakMatching(this global::TestNamespace_IResponseBuilder_ed393682_Double __self, global::Compono.Match parts) + { + var __entry = new global::TestNamespace_IResponseBuilder_ed393682_Double.__Speak_22f1fe0a_Entry(); + __entry.Matcher_parts = parts; + lock (__self.__Speak_22f1fe0a_lock) { __self.__Speak_22f1fe0a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,11 +177,61 @@ internal static class TestNamespace_IResponseBuilder_ed393682_VerifyExtension internal static class TestNamespace_IResponseBuilder_ed393682_DoubleVerification { - public static global::Compono.CallVerifier Speak(this global::TestNamespace_IResponseBuilder_ed393682_DoubleVerifier __self, string? text) => - new(__self.Instance.__Speak_1a56931a.ConfiguredCallCount, "global::TestNamespace.IResponseBuilder.Speak"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Speak(this global::TestNamespace_IResponseBuilder_ed393682_DoubleVerifier __self, string? text) + { + int __count; + lock (__self.Instance.__Speak_1a56931a_lock) { __count = __self.Instance.__Speak_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IResponseBuilder.Speak"); + } - public static global::Compono.CallVerifier Speak(this global::TestNamespace_IResponseBuilder_ed393682_DoubleVerifier __self, params global::TestNamespace.ISsml[] parts) => - new(__self.Instance.__Speak_22f1fe0a.ConfiguredCallCount, "global::TestNamespace.IResponseBuilder.Speak"); + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier SpeakMatching(this global::TestNamespace_IResponseBuilder_ed393682_DoubleVerifier __self, global::Compono.Match text) + { + int __count; + lock (__self.Instance.__Speak_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Speak_1a56931a_calls) + { + if (text.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IResponseBuilder.Speak"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Speak(this global::TestNamespace_IResponseBuilder_ed393682_DoubleVerifier __self, params global::TestNamespace.ISsml[] parts) + { + int __count; + lock (__self.Instance.__Speak_22f1fe0a_lock) { __count = __self.Instance.__Speak_22f1fe0a_calls.Count; } + return new(__count, "global::TestNamespace.IResponseBuilder.Speak"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier SpeakMatching(this global::TestNamespace_IResponseBuilder_ed393682_DoubleVerifier __self, global::Compono.Match parts) + { + int __count; + lock (__self.Instance.__Speak_22f1fe0a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Speak_22f1fe0a_calls) + { + if (parts.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IResponseBuilder.Speak"); + } } 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 abb823ae..969f7d34 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa string? global::TestNamespace.IRepository.GetName() { __GetName.RecordCall(); - return __GetName.HasConfiguredException ? throw __GetName.ConfiguredException + return __GetName.HasConfiguredSequence ? __GetName.NextSequenceOutcome() + : __GetName.HasConfiguredException ? throw __GetName.ConfiguredException : __GetName.HasConfiguredValue ? __GetName.ConfiguredValue : default; } 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 143562a5..ba1ab9cf 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 @@ -10,7 +10,9 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa void global::TestNamespace.IRepository.Get() { __Get.RecordCall(); - if (__Get.HasConfiguredException) + if (__Get.HasConfiguredSequence) + __Get.NextSequenceOutcome(); + else if (__Get.HasConfiguredException) throw __Get.ConfiguredException; } } 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 21004ec1..0f0849a9 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 @@ -5,7 +5,18 @@ [global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository { - internal global::Compono.ReturnConfig __Value_b9dfaa09; + // ADR-0050: multi-entry response configuration - replaces the single + // __Value_b9dfaa09/__Value_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Value_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_offset; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Value_b9dfaa09_Entry> __Value_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Value_b9dfaa09_calls = []; + internal readonly object __Value_b9dfaa09_lock = new(); int global::TestNamespace.IBaseA.Value { @@ -19,17 +30,63 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa int global::TestNamespace.IBaseB.Value(int offset) { - __Value_b9dfaa09.RecordCall(); - return __Value_b9dfaa09.HasConfiguredException ? throw __Value_b9dfaa09.ConfiguredException - : __Value_b9dfaa09.HasConfiguredValue ? __Value_b9dfaa09.ConfiguredValue - : default; + // 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 (__Value_b9dfaa09_lock) + { + __Value_b9dfaa09_calls.Add(offset); + for (var __i = __Value_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Value_b9dfaa09_entries[__i]; + if ((__entry.Matcher_offset is not { } __m_offset || __m_offset.Matches(offset))) + { + // 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; + } + } + } + return default; } } internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration { - public static global::Compono.ReturnConfigBuilder Value(this global::TestNamespace_IRepository_e3198068_Double __self, int offset) => - new global::Compono.ReturnConfigBuilder(ref __self.__Value_b9dfaa09); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Value(this global::TestNamespace_IRepository_e3198068_Double __self, int offset) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Value_b9dfaa09_Entry(); + lock (__self.__Value_b9dfaa09_lock) { __self.__Value_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder ValueMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match offset) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Value_b9dfaa09_Entry(); + __entry.Matcher_offset = offset; + lock (__self.__Value_b9dfaa09_lock) { __self.__Value_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } } @@ -53,8 +110,33 @@ internal static class TestNamespace_IRepository_e3198068_VerifyExtension internal static class TestNamespace_IRepository_e3198068_DoubleVerification { - public static global::Compono.CallVerifier Value(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int offset) => - new(__self.Instance.__Value_b9dfaa09.ConfiguredCallCount, "global::TestNamespace.IBaseB.Value"); + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Value(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int offset) + { + int __count; + lock (__self.Instance.__Value_b9dfaa09_lock) { __count = __self.Instance.__Value_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IBaseB.Value"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier ValueMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match offset) + { + int __count; + lock (__self.Instance.__Value_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Value_b9dfaa09_calls) + { + if (offset.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IBaseB.Value"); + } } 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 c8ab0c9e..dd4e1d28 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 @@ -32,7 +32,8 @@ internal sealed class __Value_DimFallback : global::TestNamespace.IRepository get { __Value.RecordCall(); - return __Value.HasConfiguredException ? throw __Value.ConfiguredException + return __Value.HasConfiguredSequence ? __Value.NextSequenceOutcome() + : __Value.HasConfiguredException ? throw __Value.ConfiguredException : __Value.HasConfiguredValue ? __Value.ConfiguredValue : ((global::TestNamespace.IRepository)(this.__Value_dimHelper ??= new __Value_DimFallback(this))).Value; } 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 new file mode 100644 index 00000000..05bdb4ee --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected#TestNamespace.IRepository_e3198068.TestDouble.g.verified.cs @@ -0,0 +1,253 @@ +//HintName: TestNamespace.IRepository_e3198068.TestDouble.g.cs +// +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "REPLACED")] +internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNamespace.IRepository +{ + // ADR-0050: multi-entry response configuration - replaces the single + // __Seek_b9dfaa09/__Seek_b9dfaa09_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Seek_b9dfaa09_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Seek_b9dfaa09_Entry> __Seek_b9dfaa09_entries = []; + internal readonly global::System.Collections.Generic.List __Seek_b9dfaa09_calls = []; + internal readonly object __Seek_b9dfaa09_lock = new(); + // ADR-0050: multi-entry response configuration - replaces the single + // __Seek_1a56931a/__Seek_1a56931a_m_{param} shape with an ordered, append-only + // entry list. Configure() appends; dispatch scans in reverse (last-matching-registration-wins). + internal sealed class __Seek_1a56931a_Entry + { + internal global::Compono.Match? Matcher_value; + internal global::Compono.ReturnConfig Config; + } + + internal readonly global::System.Collections.Generic.List<__Seek_1a56931a_Entry> __Seek_1a56931a_entries = []; + internal readonly global::System.Collections.Generic.List __Seek_1a56931a_calls = []; + internal readonly object __Seek_1a56931a_lock = new(); + + bool global::TestNamespace.IRepository.Seek(out int value) + { + value = default; + return default; + } + + bool global::TestNamespace.IRepository.Seek(int value) + { + // 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 (__Seek_b9dfaa09_lock) + { + __Seek_b9dfaa09_calls.Add(value); + for (var __i = __Seek_b9dfaa09_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Seek_b9dfaa09_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // 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; + } + } + } + return default; + } + + bool global::TestNamespace.IRepository.Seek(string value) + { + // 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 (__Seek_1a56931a_lock) + { + __Seek_1a56931a_calls.Add(value); + for (var __i = __Seek_1a56931a_entries.Count - 1; __i >= 0; __i--) + { + var __entry = __Seek_1a56931a_entries[__i]; + if ((__entry.Matcher_value is not { } __m_value || __m_value.Matches(value))) + { + // 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; + } + } + } + return default; + } +} + +internal static class TestNamespace_IRepository_e3198068_DoubleConfiguration +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Seek(this global::TestNamespace_IRepository_e3198068_Double __self, int value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Seek_b9dfaa09_Entry(); + lock (__self.__Seek_b9dfaa09_lock) { __self.__Seek_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder SeekMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Seek_b9dfaa09_Entry(); + __entry.Matcher_value = value; + lock (__self.__Seek_b9dfaa09_lock) { __self.__Seek_b9dfaa09_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Configure() - real parameter + // types, unchanged signature/call sites - but now appends an always-matching entry to the + // shared per-overload entries list instead of returning a builder over a removed single field. + public static global::Compono.ReturnConfigBuilder Seek(this global::TestNamespace_IRepository_e3198068_Double __self, string value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Seek_1a56931a_Entry(); + lock (__self.__Seek_1a56931a_lock) { __self.__Seek_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - real Match parameters, + // appends a real-matcher entry to the SAME entries list the discriminator-only method above + // appends to, so a call the SUT actually makes through the real overload is visible to both + // surfaces consistently. Generic exactly when the discriminator-only method above is (Amendment + // 1's "extension becomes generic" rule, unaffected by matching-eligibility) - a same-parameter- + // types generic/non-generic overload pair would otherwise collide (CS0111) with a fixed arity. + public static global::Compono.ReturnConfigBuilder SeekMatching(this global::TestNamespace_IRepository_e3198068_Double __self, global::Compono.Match value) + { + var __entry = new global::TestNamespace_IRepository_e3198068_Double.__Seek_1a56931a_Entry(); + __entry.Matcher_value = value; + lock (__self.__Seek_1a56931a_lock) { __self.__Seek_1a56931a_entries.Add(__entry); } + return new global::Compono.ReturnConfigBuilder(ref __entry.Config); + } + +} + +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 +{ + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Seek(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, int value) + { + int __count; + lock (__self.Instance.__Seek_b9dfaa09_lock) { __count = __self.Instance.__Seek_b9dfaa09_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Seek"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier SeekMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__Seek_b9dfaa09_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Seek_b9dfaa09_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Seek"); + } + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: discriminator-only Verify() - real parameter + // types, unchanged signature - reads the shared per-overload call log's unfiltered Count + // instead of a removed field's ConfiguredCallCount. + public static global::Compono.CallVerifier Seek(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, string value) + { + int __count; + lock (__self.Instance.__Seek_1a56931a_lock) { __count = __self.Instance.__Seek_1a56931a_calls.Count; } + return new(__count, "global::TestNamespace.IRepository.Seek"); + } + + // New matching-specific member name (ADR-0044 Amendment 21) - reads the SAME call log, filtered + // by the supplied matchers, counting only real calls whose real arguments satisfy every one. + // Generic exactly when the discriminator-only method above is - same reasoning as Configure(). + public static global::Compono.CallVerifier SeekMatching(this global::TestNamespace_IRepository_e3198068_DoubleVerifier __self, global::Compono.Match value) + { + int __count; + lock (__self.Instance.__Seek_1a56931a_lock) + { + __count = 0; + foreach (var call in __self.Instance.__Seek_1a56931a_calls) + { + if (value.Matches(call)) + __count++; + } + } + return new(__count, "global::TestNamespace.IRepository.Seek"); + } + +} + +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.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected#TestNamespace.OrderService_7c62cf5a.CompositionPlan.g.verified.cs new file mode 100644 index 00000000..be9605ba --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected#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.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected.verified.txt b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected.verified.txt new file mode 100644 index 00000000..fffecf71 --- /dev/null +++ b/test/Compono.Generators.Tests/Snapshots/TestDoubleVerifyTests.RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected.verified.txt @@ -0,0 +1,17 @@ +{ + Diagnostics: [ + { + Message: 'TestNamespace.IRepository' declares member 'Seek' with parameter 'value' as a ref/out/in parameter. This overload has no Configure() surface, but it still dispatches via a deterministic default - its sibling overloads, and the rest of the interface, are unaffected., + Severity: Info, + WarningLevel: 1, + Descriptor: { + Id: CMP0030, + Title: Overload-scoped unsupported test-double parameter shape, + MessageFormat: '{0}' declares member '{1}' with parameter '{2}' as a ref/out/in parameter. This overload has no Configure() surface, but it still dispatches via a deterministic default - its sibling overloads, and the rest of the interface, are unaffected., + Category: Compono.TestDoubles, + DefaultSeverity: Info, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file 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 15e8ae0a..e8d3d419 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 @@ -24,11 +24,12 @@ internal sealed class __Seek_Entry void global::TestNamespace.IRepository.Seek(int offset) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock (__Seek_lock) { __Seek_calls.Add(offset); @@ -47,6 +48,10 @@ internal sealed class __Seek_Entry // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; if (__entry.Config.HasConfiguredValue) return; } 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 3708f43c..e546d795 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 @@ -10,7 +10,9 @@ internal sealed class TestNamespace_IThing_7b7b47c0_Double : global::TestNamespa void global::TestNamespace.IThing.Configure() { __Configure.RecordCall(); - if (__Configure.HasConfiguredException) + if (__Configure.HasConfiguredSequence) + __Configure.NextSequenceOutcome(); + else if (__Configure.HasConfiguredException) throw __Configure.ConfiguredException; } } 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 1d44852c..8285c865 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 @@ -10,7 +10,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa string global::TestNamespace.IRepository.Name() { __Name.RecordCall(); - return __Name.HasConfiguredException ? throw __Name.ConfiguredException + return __Name.HasConfiguredSequence ? __Name.NextSequenceOutcome() + : __Name.HasConfiguredException ? throw __Name.ConfiguredException : __Name.HasConfiguredValue ? __Name.ConfiguredValue : throw new global::Compono.TestDoubleNotConfiguredException( "'global::TestNamespace.IRepository.Name' was invoked without being configured - call Configure().Name().Returns(...) or .Throws(...) before invoking it."); 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 f8f2772b..a7917be3 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 @@ -12,7 +12,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa get { __Name.RecordCall(); - return __Name.HasConfiguredException ? throw __Name.ConfiguredException + return __Name.HasConfiguredSequence ? __Name.NextSequenceOutcome() + : __Name.HasConfiguredException ? throw __Name.ConfiguredException : __Name.HasConfiguredValue ? __Name.ConfiguredValue : default; } 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 f8f2772b..a7917be3 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 @@ -12,7 +12,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa get { __Name.RecordCall(); - return __Name.HasConfiguredException ? throw __Name.ConfiguredException + return __Name.HasConfiguredSequence ? __Name.NextSequenceOutcome() + : __Name.HasConfiguredException ? throw __Name.ConfiguredException : __Name.HasConfiguredValue ? __Name.ConfiguredValue : default; } 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 f8f2772b..a7917be3 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 @@ -12,7 +12,8 @@ internal sealed class TestNamespace_IRepository_e3198068_Double : global::TestNa get { __Name.RecordCall(); - return __Name.HasConfiguredException ? throw __Name.ConfiguredException + return __Name.HasConfiguredSequence ? __Name.NextSequenceOutcome() + : __Name.HasConfiguredException ? throw __Name.ConfiguredException : __Name.HasConfiguredValue ? __Name.ConfiguredValue : default; } 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 949d1dee..c85d4828 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 @@ -52,6 +52,10 @@ internal sealed class __Save_Entry // 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; } @@ -62,11 +66,12 @@ internal sealed class __Save_Entry void global::TestNamespace.IRepository.Save(string name) { - // 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. + // ADR-0050 (extended to overloaded members by ADR-0044 Amendment 21 / PLAN-0054 Phase 2): + // 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. lock (__Save_lock) { __Save_calls.Add(name); @@ -85,6 +90,10 @@ internal sealed class __Save_Entry // stop the scan (`return;`) exactly like the value-returning branch below, not // be treated as equivalent to an unconfigured/incomplete entry (Codex review, // PR #108 round 7). + // ADR-0054: a configured sequence still stops the scan - the sequence's own next + // outcome may itself be a configured exception (thrown by NextSequenceOutcome()), + // exactly mirroring the exception check immediately below. + if (__entry.Config.HasConfiguredSequence) { __entry.Config.NextSequenceOutcome(); return; } if (__entry.Config.HasConfiguredException) throw __entry.Config.ConfiguredException; if (__entry.Config.HasConfiguredValue) return; } @@ -97,7 +106,8 @@ internal sealed class __Save_Entry get { __Count.RecordCall(); - return __Count.HasConfiguredException ? throw __Count.ConfiguredException + return __Count.HasConfiguredSequence ? __Count.NextSequenceOutcome() + : __Count.HasConfiguredException ? throw __Count.ConfiguredException : __Count.HasConfiguredValue ? __Count.ConfiguredValue : default; } diff --git a/test/Compono.Generators.Tests/TestDoubleOverloadMatchingExecutionTests.cs b/test/Compono.Generators.Tests/TestDoubleOverloadMatchingExecutionTests.cs new file mode 100644 index 00000000..0afc6ebf --- /dev/null +++ b/test/Compono.Generators.Tests/TestDoubleOverloadMatchingExecutionTests.cs @@ -0,0 +1,285 @@ +namespace Compono.Generators.Tests; + +/// +/// Real end-to-end execution of ADR-0044 Amendment 21's overload-safe argument matching against a +/// real, generator-emitted double - not a hand-written unit test, and not just a compile-only +/// snapshot (TestDoubleVerifyTests.cs's own overload-matching fixtures). Proves the corrected +/// architecture: the matching-specific member name only configures/observes; the SUT-visible +/// dispatch always goes through the real overload, and both surfaces share the same +/// entries/call-log/lock state per overload (PLAN-0054 Phase 2's "Architecture (revised)"). +/// +/// Uses a stand-in shaped after the real dogfood-evidenced IAmazonDynamoDB.DeleteItemAsync +/// overload (a request object argument-matched by a member predicate, plus a +/// ) without taking a new external package +/// dependency, per PLAN-0054's acceptance criteria. A synchronous return +/// (rather than Task<DeleteItemResponse>) keeps every member's deterministic default +/// available, so no member here is ADR-0045 configuration-required (CMP0032) - that dimension is +/// already covered elsewhere (TestDoubleConfigurationRequiredExecutionTests.cs-style +/// coverage) and isn't what this file is proving. +/// +public sealed class TestDoubleOverloadMatchingExecutionTests +{ + private const string Source = """ + namespace TestNamespace; + + public sealed class DeleteItemRequest + { + public string TableName { get; init; } = ""; + } + + public interface IAmazonDynamoDB + { + bool DeleteItemAsync(DeleteItemRequest request, System.Threading.CancellationToken cancellationToken); + + bool DeleteItemAsync(string tableName, System.Threading.CancellationToken cancellationToken); + } + + public sealed class OrderService + { + public OrderService(IAmazonDynamoDB client) { } + } + + public static class EntryPoint + { + private static void Discover() => Compono.Composer.Create().Create(); + + public static object CreateDouble() + { + Compono.GeneratedTestDoubleRegistry.TryCreate(typeof(IAmazonDynamoDB), out var value); + return value!; + } + } + """; + + private static object? Run(string body, CancellationToken cancellationToken) => + GeneratorTestHelpers.CompileAndExecute( + new CodeGenerationOptions + { + SourceCode = Source.Replace("public static object CreateDouble()", body + "\n\n public static object CreateDouble()"), + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, + "TestNamespace.EntryPoint", + "Run", + cancellationToken); + + [Fact] + public void CoexistencePrecedence_MatchingEntryOverridesDiscriminatorFallback_ForMatchingCallsOnly() + { + // User-specified example: a broad discriminator-only Configure() registered first, then a + // narrower .Matching(...) override registered after it. A call matching the predicate gets + // the special response; a call that doesn't falls through to the discriminator's own + // always-matching entry. + var result = Run( + """ + public static object Run() + { + var client = (IAmazonDynamoDB)CreateDouble(); + + client.Configure().DeleteItemAsync(new DeleteItemRequest(), global::System.Threading.CancellationToken.None).Returns(false); + client.Configure().DeleteItemAsyncMatching( + global::Compono.Match.Is(x => x.TableName == "special"), + global::Compono.Match.Any()).Returns(true); + + var specialResult = client.DeleteItemAsync(new DeleteItemRequest { TableName = "special" }, default); + var fallbackResult = client.DeleteItemAsync(new DeleteItemRequest { TableName = "ordinary" }, default); + + return new[] { specialResult, fallbackResult }; + } + """, + TestContext.Current.CancellationToken); + + result.Should().BeEquivalentTo(new[] { true, false }, options => options.WithStrictOrdering()); + } + + [Fact] + public void SiblingOverloadIndependence_ConfiguringOneOverloadNeverAffectsTheOther() + { + var result = Run( + """ + public static object Run() + { + var client = (IAmazonDynamoDB)CreateDouble(); + + client.Configure().DeleteItemAsyncMatching( + global::Compono.Match.Any(), + global::Compono.Match.Any()).Returns(true); + client.Configure().DeleteItemAsync("orders", global::System.Threading.CancellationToken.None).Returns(false); + + var requestOverloadResult = client.DeleteItemAsync(new DeleteItemRequest(), default); + var nameOverloadResult = client.DeleteItemAsync("orders", default); + + client.Verify().DeleteItemAsyncMatching(global::Compono.Match.Any(), global::Compono.Match.Any()).Once(); + client.Verify().DeleteItemAsync("orders", global::System.Threading.CancellationToken.None).Once(); + + return new[] { requestOverloadResult, nameOverloadResult }; + } + """, + TestContext.Current.CancellationToken); + + result.Should().BeEquivalentTo(new[] { true, false }, options => options.WithStrictOrdering()); + } + + [Fact] + public void FilteredVerification_CountsOnlyRealCallsMatchingThePredicate() + { + var act = () => Run( + """ + public static object Run() + { + var client = (IAmazonDynamoDB)CreateDouble(); + client.Configure().DeleteItemAsync(new DeleteItemRequest(), global::System.Threading.CancellationToken.None).Returns(true); + + client.DeleteItemAsync(new DeleteItemRequest { TableName = "special" }, default); + client.DeleteItemAsync(new DeleteItemRequest { TableName = "ordinary" }, default); + client.DeleteItemAsync(new DeleteItemRequest { TableName = "special" }, default); + + client.Verify().DeleteItemAsyncMatching( + global::Compono.Match.Is(x => x.TableName == "special"), + global::Compono.Match.Any()).Exactly(2); + client.Verify().DeleteItemAsyncMatching( + global::Compono.Match.Is(x => x.TableName == "ordinary"), + global::Compono.Match.Any()).Once(); + + return client; + } + """, + TestContext.Current.CancellationToken); + + act.Should().NotThrow(); + } + + [Fact] + public void DiscriminatorVerification_StillReportsTotalRealCallCount_BackedByTheCallLogNow() + { + var act = () => Run( + """ + public static object Run() + { + var client = (IAmazonDynamoDB)CreateDouble(); + client.Configure().DeleteItemAsync(new DeleteItemRequest(), global::System.Threading.CancellationToken.None).Returns(true); + + client.DeleteItemAsync(new DeleteItemRequest(), default); + client.DeleteItemAsync(new DeleteItemRequest(), default); + client.DeleteItemAsync(new DeleteItemRequest(), default); + + client.Verify().DeleteItemAsync(new DeleteItemRequest(), global::System.Threading.CancellationToken.None).Exactly(3); + + return client; + } + """, + TestContext.Current.CancellationToken); + + act.Should().NotThrow(); + } + + [Fact] + public void SequencingOnAMatchingEligibleEntry_EveryRealCallStillRecordedInTheSharedCallLog() + { + var result = Run( + """ + public static object Run() + { + var client = (IAmazonDynamoDB)CreateDouble(); + + client.Configure().DeleteItemAsyncMatching( + global::Compono.Match.Is(x => x.TableName == "flaky"), + global::Compono.Match.Any()).ReturnsSequence( + global::Compono.SequenceOutcome.Throw(new global::System.InvalidOperationException("attempt 1 fails")), + true); + + var results = new object[2]; + try { client.DeleteItemAsync(new DeleteItemRequest { TableName = "flaky" }, default); } + catch (global::System.InvalidOperationException ex) { results[0] = ex.Message; } + results[1] = client.DeleteItemAsync(new DeleteItemRequest { TableName = "flaky" }, default); + + client.Verify().DeleteItemAsyncMatching( + global::Compono.Match.Is(x => x.TableName == "flaky"), + global::Compono.Match.Any()).Exactly(2); + client.Verify().DeleteItemAsync(new DeleteItemRequest(), global::System.Threading.CancellationToken.None).Exactly(2); + + return results; + } + """, + TestContext.Current.CancellationToken); + + result.Should().BeEquivalentTo(new object[] { "attempt 1 fails", true }, options => options.WithStrictOrdering()); + } + + [Fact] + public void LiteralShorthandOnTheMatchingNamedSurface_CompilesAndMatchesByEquality() + { + // Match's own implicit conversion from a literal T (Amendment 18) applies uniformly to + // any Match-typed parameter, including the new Matching-named surface's - there is no + // separate rule excluding it here. A literal argument becomes an equality matcher, exactly + // like it already does on the pre-existing non-overloaded matching-eligible surface. + var result = Run( + """ + public static object Run() + { + var client = (IAmazonDynamoDB)CreateDouble(); + var request = new DeleteItemRequest { TableName = "literal" }; + + client.Configure().DeleteItemAsyncMatching(request, global::System.Threading.CancellationToken.None).Returns(true); + + return new[] + { + client.DeleteItemAsync(request, default), + client.DeleteItemAsync(new DeleteItemRequest { TableName = "literal" }, default), + }; + } + """, + TestContext.Current.CancellationToken); + + // Reference-equality matching (DeleteItemRequest has no value equality) - the SAME instance + // matches, a distinct instance with equal property values does not. + result.Should().BeEquivalentTo(new[] { true, false }, options => options.WithStrictOrdering()); + } + + // PLAN-0054's own numeric-widening evidence for the matching-specific member name (Amendment 18's + // original CS0121 finding, re-verified on this surface): a bare literal is rejected only when TWO + // sibling overloads sharing the SAME "Matching" alias name have Match parameter types + // the literal implicitly converts to ambiguously (int widens to long) - a different, narrower + // scenario than the (non-ambiguous, unrelated-types) literal case proven above. + [Fact] + public void LiteralShorthandAmbiguousAcrossSiblingOverloads_FailsToCompile() + { + var act = () => GeneratorTestHelpers.CompileAndExecute( + new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + bool Get(int id); + + bool Get(long id); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + private static void Discover() => Compono.Composer.Create().Create(); + + public static object Run() + { + Compono.GeneratedTestDoubleRegistry.TryCreate(typeof(IRepository), out var value); + var repository = (IRepository)value!; + repository.Configure().GetMatching(5).Returns(true); + return repository; + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, + "TestNamespace.EntryPoint", + "Run", + TestContext.Current.CancellationToken); + + act.Should().Throw().WithMessage("*CS0121*"); + } +} diff --git a/test/Compono.Generators.Tests/TestDoubleSequentialResponseExecutionTests.cs b/test/Compono.Generators.Tests/TestDoubleSequentialResponseExecutionTests.cs new file mode 100644 index 00000000..2ad962f9 --- /dev/null +++ b/test/Compono.Generators.Tests/TestDoubleSequentialResponseExecutionTests.cs @@ -0,0 +1,201 @@ +using System.Reflection; + +namespace Compono.Generators.Tests; + +/// +/// Real end-to-end execution of ADR-0054's sequential/call-count-based responses against a real, +/// generator-emitted double - not a hand-written / +/// test (Compono.Tests.ReturnConfigSequenceTests, a +/// different assembly, not referenceable from a cref here). Proves the actual generated dispatch code reads +/// / +/// - the runtime type alone compiling and passing its own unit tests does not prove the generated +/// `Configure()`/dispatch bridge actually reaches it. Covers both dispatch shapes ADR-0054's +/// evidenced need touches: a zero-parameter member (the plain single-field dispatch path - +/// Count() below) and a real-parameter, matching-eligible member (the ADR-0050 entries-list +/// dispatch path - Save(string) below), since each has its own separate ternary/if-chain in +/// the generated code and either could have been missed independently. +/// +public sealed class TestDoubleSequentialResponseExecutionTests +{ + private const string Source = """ + namespace TestNamespace; + + public interface IRepository + { + int Count(); + bool Save(string name); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + private static void Discover() => Compono.Composer.Create().Create(); + + public static object CreateDouble() + { + Compono.GeneratedTestDoubleRegistry.TryCreate(typeof(IRepository), out var value); + return value!; + } + } + """; + + private static object? Run(string body, CancellationToken cancellationToken) => + GeneratorTestHelpers.CompileAndExecute( + new CodeGenerationOptions + { + SourceCode = Source.Replace("public static object CreateDouble()", body + "\n\n public static object CreateDouble()"), + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, + "TestNamespace.EntryPoint", + "Run", + cancellationToken); + + [Fact] + public void ZeroParameterMember_ReturnsSequence_ConsumedInOrder() + { + // IRepository.Count() has no parameters - not matching-eligible, so this exercises the + // plain single-ReturnConfig-field dispatch path. + var result = Run( + """ + public static object Run() + { + var repository = (IRepository)CreateDouble(); + repository.Configure().Count().ReturnsSequence(1, 2, 3); + + return new[] { repository.Count(), repository.Count(), repository.Count(), repository.Count() }; + } + """, + TestContext.Current.CancellationToken); + + result.Should().BeEquivalentTo(new[] { 1, 2, 3, 3 }, options => options.WithStrictOrdering()); + } + + [Fact] + public void ZeroParameterMember_ReturnsSequence_MixedExceptionAndValue_MatchesRealRetryShape() + { + var act = () => Run( + """ + public static object Run() + { + var repository = (IRepository)CreateDouble(); + repository.Configure().Count().ReturnsSequence( + global::Compono.SequenceOutcome.Throw(new global::System.InvalidOperationException("attempt 1")), + global::Compono.SequenceOutcome.Throw(new global::System.InvalidOperationException("attempt 2")), + 3); + + var results = new object[3]; + for (var i = 0; i < 3; i++) + { + try { results[i] = repository.Count(); } + catch (global::System.InvalidOperationException ex) { results[i] = ex.Message; } + } + return results; + } + """, + TestContext.Current.CancellationToken); + + act.Should().NotThrow() + .Which.Should().BeEquivalentTo( + new object[] { "attempt 1", "attempt 2", 3 }, + options => options.WithStrictOrdering()); + } + + [Fact] + public void ZeroParameterMember_ReturnsSequence_CallsStillCountTowardVerification() + { + var act = () => Run( + """ + public static object Run() + { + var repository = (IRepository)CreateDouble(); + repository.Configure().Count().ReturnsSequence(1, 2, 3); + + repository.Count(); + repository.Count(); + repository.Count(); + + repository.Verify().Count().Exactly(3); + return repository; + } + """, + TestContext.Current.CancellationToken); + + act.Should().NotThrow(); + } + + [Fact] + public void MatchingEligibleMember_ReturnsSequence_ConsumedInOrder() + { + // Save(string) has a real parameter and is matching-eligible - exercises the ADR-0050 + // entries-list dispatch path, using the zero-argument "compatibility" Configure() overload + // (always-matching entry) since this test doesn't need argument matching. + var result = Run( + """ + public static object Run() + { + var repository = (IRepository)CreateDouble(); + repository.Configure().Save().ReturnsSequence(false, false, true); + + return new[] { repository.Save("a"), repository.Save("b"), repository.Save("c"), repository.Save("d") }; + } + """, + TestContext.Current.CancellationToken); + + result.Should().BeEquivalentTo(new[] { false, false, true, true }, options => options.WithStrictOrdering()); + } + + [Fact] + public void MatchingEligibleMember_TwoIndependentEntries_MaintainIndependentSequenceOrdinals() + { + // ADR-0054: sequence state belongs to the matched entry, not the member - two + // argument-distinguished entries on the same member must not share one ordinal. + var result = Run( + """ + public static object Run() + { + var repository = (IRepository)CreateDouble(); + repository.Configure().Save(global::Compono.Match.Is(x => x == "alice")).ReturnsSequence(false, true); + repository.Configure().Save(global::Compono.Match.Is(x => x == "bob")).ReturnsSequence(true, false); + + return new[] + { + repository.Save("alice"), + repository.Save("bob"), + repository.Save("alice"), + repository.Save("bob"), + }; + } + """, + TestContext.Current.CancellationToken); + + result.Should().BeEquivalentTo(new[] { false, true, true, false }, options => options.WithStrictOrdering()); + } + + [Fact] + public void ReconfiguringTheSameEntry_ResetsTheOrdinal() + { + var result = Run( + """ + public static object Run() + { + var repository = (IRepository)CreateDouble(); + repository.Configure().Count().ReturnsSequence(1, 2, 3); + repository.Count(); + repository.Count(); + + // Reconfiguring the zero-argument entry replaces its sequence and resets the ordinal - + // the next call should get 100, not continue at the old sequence's third entry. + repository.Configure().Count().ReturnsSequence(100, 200); + + return new[] { repository.Count(), repository.Count(), repository.Count() }; + } + """, + TestContext.Current.CancellationToken); + + result.Should().BeEquivalentTo(new[] { 100, 200, 200 }, options => options.WithStrictOrdering()); + } +} diff --git a/test/Compono.Generators.Tests/TestDoubleVerifyTests.cs b/test/Compono.Generators.Tests/TestDoubleVerifyTests.cs index 6c212c58..8c53c11b 100644 --- a/test/Compono.Generators.Tests/TestDoubleVerifyTests.cs +++ b/test/Compono.Generators.Tests/TestDoubleVerifyTests.cs @@ -4144,4 +4144,453 @@ public static void Run(IWidget widget) """, MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, }, TestContext.Current.CancellationToken); + + // ADR-0044 Amendment 21 / PLAN-0054 Phase 2: an overloaded member with real parameters gets both + // its unchanged discriminator-only Configure()/Verify() surface AND a new matching-specific + // "Matching" member name taking real Match parameters directly, sharing the same + // entries/call-log/lock state per real overload. + [Fact] + public Task OverloadedMemberWithRealParameters_GeneratesMatchingSpecificMemberNameSharingSameOverloadState() => + GeneratorTestHelpers.Verify(new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + bool Delete(int id); + + bool Delete(string id); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Delete(1).Returns(true); + repository.Configure().DeleteMatching(Compono.Match.Is(id => id > 0)).Returns(true); + repository.Verify().DeleteMatching(Compono.Match.Any()).Never(); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, TestContext.Current.CancellationToken); + + // ADR-0044 Amendment 21 "Naming/collision policy": a real member literally named + // "Matching" whose own generated Configure() extension signature is genuinely + // identical to one of the alias's own generated overloads (a real CS0111 risk, confirmed by + // compiler spike) - the fallback hash-suffixed name is used instead, and both the real member's + // own surface and the overloaded member's matching surface remain independently reachable. + [Fact] + public Task OverloadMatchingAliasCollidesWithRealMemberOfThatName_FallsBackToHashSuffixedName() => + GeneratorTestHelpers.Verify(new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + bool Get(int id); + + bool Get(string id); + + // Non-overloaded, matching-eligible (ADR-0048) - its own generated Configure() + // extension is "GetMatching(Compono.Match value)", genuinely identical to + // the alias Get(string) would otherwise generate for itself. + bool GetMatching(string value); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Get(1).Returns(true); + repository.Configure().Get("x").Returns(true); + repository.Configure().GetMatching("value").Returns(true); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, TestContext.Current.CancellationToken); + + // Codex review, PR #115: the collision check must compare real C# signature identity, which + // never considers nullable-reference annotations - a real member's own generated + // "GetMatching(Compono.Match value)" genuinely collides (CS0111) with the alias's own + // "GetMatching(Compono.Match value)" at the CLR/overload-resolution level even though + // "string" and "string?" are different strings. The fallback hash-suffixed name must still fire. + [Fact] + public Task OverloadMatchingAliasCollidesDespiteNullableAnnotationMismatch_FallsBackToHashSuffixedName() => + GeneratorTestHelpers.Verify(new CodeGenerationOptions + { + SourceCode = """ + #nullable enable + namespace TestNamespace; + + public interface IRepository + { + bool Get(int id); + + bool Get(string id); + + // Non-overloaded, matching-eligible - its own generated Configure() extension is + // "GetMatching(Compono.Match value)" - nullable-annotated, but the SAME + // real signature as the alias Get(string) would otherwise generate for itself + // ("GetMatching(Compono.Match value)") once nullable annotations are erased, + // exactly like the real compiler treats them for CS0111 purposes. + bool GetMatching(string? value); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Get(1).Returns(true); + repository.Configure().Get("x").Returns(true); + repository.Configure().GetMatching((string?)"value").Returns(true); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, TestContext.Current.CancellationToken); + + // Codex review, PR #115: the collision check must consider EVERY real member sharing the + // alias's literal name, not just a non-overloaded matching-eligible one - a real, ordinary + // OVERLOADED "FooMatching" family (FooMatching(Match)/FooMatching(string)) still emits an + // ordinary per-overload discriminator extension using each overload's own real declared type + // unwrapped, and "FooMatching(Match value)"'s own declared type genuinely collides with the + // Match-wrapped alias Foo(int) would otherwise generate for itself. + [Fact] + public Task OverloadMatchingAliasCollidesWithOverloadedRealFamily_FallsBackToHashSuffixedName() => + GeneratorTestHelpers.Verify(new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + bool Foo(int id); + + bool Foo(string id); + + // A real, ordinary overloaded family sharing "FooMatching" - one overload's own + // declared parameter type literally IS Compono.Match, colliding with the + // Match-wrapped alias Foo(int) would otherwise generate for itself. + bool FooMatching(Compono.Match value); + + bool FooMatching(string value); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Foo(1).Returns(true); + repository.Configure().Foo("x").Returns(true); + repository.Configure().FooMatching(Compono.Match.Any()).Returns(true); + repository.Configure().FooMatching("value").Returns(true); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, TestContext.Current.CancellationToken); + + // Codex review, PR #115 (round 3): the collision check must also consider a real + // ADR-0049 closed-instantiation-eligible member, not just an ordinary real member - a + // non-overloaded closed-instantiation-eligible member with real matched parameters emits a + // Match-wrapped generic Configure() extension exactly like the alias does, so it can + // collide with an overloaded GENERIC member's own alias just as easily as any other real + // member can. Foo(int)/Foo(string) generates the alias FooMatching(Match); the real + // closed-instantiation-eligible FooMatching(int) generates the exact same signature. + [Fact] + public Task OverloadMatchingAliasCollidesWithClosedInstantiationEligibleRealMember_FallsBackToHashSuffixedName() => + GeneratorTestHelpers.Verify(new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + bool Foo(int id) where T : class; + + bool Foo(string id); + + // Non-overloaded, ADR-0049 closed-instantiation-eligible with a real matched + // parameter - its own generated Configure() extension is + // "FooMatching(Compono.Match value)", genuinely identical to the alias + // Foo(int) would otherwise generate for itself. + T? FooMatching(int value) where T : class; + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Foo(1).Returns(true); + repository.Configure().Foo("x").Returns(true); + repository.Configure().FooMatching(1).Returns("value"); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, TestContext.Current.CancellationToken); + + // Codex review, PR #115 (round 3): generic arity is part of real C# signature identity, not + // just the parameter types - a real non-generic FooMatching(int) (Match-wrapped signature, + // arity 0) must NOT be treated as colliding with the generic alias FooMatching(Match) + // (arity 1) that Foo(int) generates for itself, even though the parameter types alone match. + // Both aliases (the generic one and the non-generic Foo(string) sibling's) must keep their + // natural names - the fallback must not fire on an arity-only mismatch. + [Fact] + public Task OverloadMatchingAliasArityDiffersFromRealMemberOfSameName_KeepsNaturalName() => + GeneratorTestHelpers.Verify(new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + bool Foo(int id) where T : class; + + bool Foo(string id); + + // Non-overloaded, matching-eligible, NON-generic - its own generated Configure() + // extension is "FooMatching(Compono.Match value)" (arity 0), which shares + // parameter types with, but not the generic arity of, the alias + // "FooMatching(Compono.Match value)" (arity 1) Foo(int) generates for + // itself - a real, distinguishable overload pair, not a collision. + bool FooMatching(int value); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Foo(1).Returns(true); + repository.Configure().Foo("x").Returns(true); + repository.Configure().FooMatching(Compono.Match.Any()).Returns(true); + repository.Configure().FooMatching(Compono.Match.Any()).Returns(true); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, TestContext.Current.CancellationToken); + + // Codex review, PR #115 (round 4): a candidate's own TypeParameters.Length is not always its + // generated extension's actual arity - a SOLO (non-overloaded, non-closed-instantiation) + // generic real member's own extension is emitted non-generic (ADR-0044 Requirement 2's "one + // backing slot covers every closed instantiation" rule, mirrored onto a matching-eligible + // member's own extension too), regardless of the method's own real generic arity. Foo(int) + // (arity 0) plus Foo(string) (arity 1) generates the alias FooMatching (arity 0, + // Match) for Foo(int); the solo real generic FooMatching(int) - despite its own + // TypeParameters.Length being 1 - actually emits the SAME non-generic + // "FooMatching(Compono.Match value)" signature, a genuine collision the arity-naive + // comparison would miss. + [Fact] + public Task OverloadMatchingAliasCollidesWithSoloGenericRealMemberEmittingNonGenericExtension_FallsBackToHashSuffixedName() => + GeneratorTestHelpers.Verify(new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + bool Foo(int id); + + bool Foo(string id); + + // Solo (non-overloaded), generic, but T is unused - not closed-instantiation- + // eligible (the return type doesn't depend on T) - so ADR-0044 Requirement 2's + // rule applies: its own generated Configure()/Verify() extension is emitted + // NON-generic ("FooMatching(Compono.Match value)"), matching the alias + // Foo(int) generates for itself despite this method's own TypeParameters.Length + // being 1. + bool FooMatching(int value); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Foo(1).Returns(true); + repository.Configure().Foo("x").Returns(true); + repository.Configure().FooMatching(1).Returns(true); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, TestContext.Current.CancellationToken); + + // Sibling to the collision test above - a real member literally named "Matching" whose + // own signature does NOT collide with either of the alias's own generated overloads keeps the + // natural, non-hash-suffixed name (the fallback only fires on a genuine signature collision, not + // merely a name collision). + [Fact] + public Task OverloadMatchingAliasNameMatchesButSignatureDoesNotCollide_KeepsNaturalName() => + GeneratorTestHelpers.Verify(new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + bool Get(int id); + + bool Get(string id); + + // Non-overloaded, matching-eligible - its own generated Configure() extension is + // "GetMatching(Compono.Match flag)", which matches neither alias overload's + // own generated signature ("GetMatching(Compono.Match)"/"GetMatching(Compono.Match)"). + bool GetMatching(bool flag); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Get(1).Returns(true); + repository.Configure().Get("x").Returns(true); + repository.Configure().GetMatching(true).Returns(true); + repository.Configure().GetMatching(Compono.Match.Any()).Returns(true); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, TestContext.Current.CancellationToken); + + // PLAN-0054 Phase 2 throwaway spike (kept as real regression coverage, not deleted after + // answering the question): a member that is both generic AND overloaded, with real parameters + // that do NOT reference its own type parameter (so it's matching-eligible-shaped, unlike the + // Requirement-2/ILogger-shaped or the closed-instantiation-eligible cases) - Amendment 1's + // "extension becomes generic" rule (isOverloaded && isGenericMethod) applies to the matching- + // specific member name exactly like it already applies to the discriminator-only surface, or a + // same-parameter-types generic/non-generic overload pair collides (CS0111) with a fixed arity. + [Fact] + public Task GenericAndOverloadedMatchingEligibleMember_GeneratesGenericConfigureAndMatchingExtensions() => + GeneratorTestHelpers.Verify(new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + void Process(int index, string label); + + void Process(int index, string label); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Process(1, "a").Returns(default); + repository.Configure().Process(1, "a").Returns(default); + repository.Configure().ProcessMatching(Compono.Match.Any(), Compono.Match.Any()).Returns(default); + repository.Configure().ProcessMatching(Compono.Match.Any(), Compono.Match.Any()).Returns(default); + repository.Verify().ProcessMatching(Compono.Match.Any(), Compono.Match.Any()).Never(); + repository.Verify().ProcessMatching(Compono.Match.Any(), Compono.Match.Any()).Never(); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, TestContext.Current.CancellationToken); + + // PLAN-0054 Phase 2 throwaway spike: a ref/out-shaped overload never gets a configuration + // surface at all (ADR-0044 Amendment 5's existing overload-set-internal partial support), so it + // can never become matching-eligible either - no new rule was needed, since + // IsOverloadMatchingEligible already requires WouldGetConfigurationSurface, which already + // excludes it. Three-way overload set so "Seek" still has >= 2 surface-worthy siblings even with + // the ref/out one excluded (a two-way ref/out + one real sibling set wouldn't exercise + // IsOverloadMatchingEligible at all - it falls to the pre-existing solo-member ADR-0048 path + // instead, a different, already-covered case). The ref/out sibling keeps its existing + // fallback-only dispatch body unaffected; its two matching-eligible-shaped siblings each get the + // new Matching-named surface, independently. + [Fact] + public Task RefOutOverloadSibling_StaysFallbackOnly_MatchingEligibleSiblingsUnaffected() => + GeneratorTestHelpers.VerifyWithInfoDiagnostic(new CodeGenerationOptions + { + SourceCode = """ + namespace TestNamespace; + + public interface IRepository + { + bool Seek(out int value); + + bool Seek(int value); + + bool Seek(string value); + } + + public sealed class OrderService + { + public OrderService(IRepository repository) { } + } + + public static class EntryPoint + { + public static void Run(IRepository repository) + { + Compono.Composer.Create().Create(); + repository.Configure().Seek(1).Returns(true); + repository.Configure().Seek("k").Returns(true); + repository.Configure().SeekMatching(Compono.Match.Any()).Returns(true); + repository.Configure().SeekMatching(Compono.Match.Any()).Returns(true); + } + } + """, + MSBuildProperties = new Dictionary { ["ComponoGeneratedTestDoubles"] = "true" }, + }, "CMP0030", TestContext.Current.CancellationToken); } diff --git a/test/Compono.TestDoubles.AotSmokeTest/Program.cs b/test/Compono.TestDoubles.AotSmokeTest/Program.cs index 1b751947..1e158433 100644 --- a/test/Compono.TestDoubles.AotSmokeTest/Program.cs +++ b/test/Compono.TestDoubles.AotSmokeTest/Program.cs @@ -310,6 +310,97 @@ private static async Task Main() $"Expected configured IDefaultHandler.CanHandle(...) to return the configured " + $"value (false), got {configuredDimResult}."); + // ADR-0054: sequential/call-count-based responses under Native AOT - a mixed + // exception/value sequence on a Task-returning member (the real evidenced shape), + // exhaustion repeating the final outcome, call recording staying independent of + // response consumption (RecordCall() fires even on the two throwing calls), and two + // independently-configured ADR-0050 entries maintaining independent ordinals. + // + // A void member's sequence carries no value dimension (Compono.Unit is a marker, not a + // real payload), but exception-only sequencing still applies and is meaningful - "throw + // on the first call, succeed silently after" - proven here on the overloaded Send(string) + // discriminator. A fresh double, not the `gateway` instance above (which already recorded + // an unrelated Send("hello") call) - Send(string)'s Verify() is discriminator-only, an + // unfiltered per-overload count regardless of the argument's actual value, so reusing + // `gateway` here would inflate the count this test asserts on, unrelated to ADR-0054. + var sequencedGateway = composer.Create(); + sequencedGateway.Configure().Send("sequenced").ReturnsSequence(SequenceOutcome.Throw(new InvalidOperationException("first call fails")), default(Unit)); + + var firstSendThrew = false; + try { sequencedGateway.Send("sequenced"); } + catch (InvalidOperationException) { firstSendThrew = true; } + + sequencedGateway.Send("sequenced"); // second call: exhausted-but-one-element-left, succeeds silently + sequencedGateway.Send("sequenced"); // third call: exhaustion repeats the final (non-throwing) outcome + + if (!firstSendThrew) + throw new InvalidOperationException("Expected the first sequenced Send(\"sequenced\") call to throw."); + + sequencedGateway.Verify().Send("sequenced").Exactly(3); + + var retryGateway = composer.Create(); + var attempt1 = new InvalidOperationException("attempt 1 fails"); + var attempt2 = new InvalidOperationException("attempt 2 fails"); + retryGateway.Configure().CountAsync().ReturnsSequence(SequenceOutcome.Throw(attempt1), SequenceOutcome.Throw(attempt2), Task.FromResult(42)); + + var sequenceResults = new List(); + for (var i = 0; i < 4; i++) + { + try { sequenceResults.Add(await retryGateway.CountAsync()); } + catch (InvalidOperationException ex) { sequenceResults.Add(ex.Message); } + } + + var expectedSequence = new object[] { "attempt 1 fails", "attempt 2 fails", 42, 42 }; + if (!sequenceResults.SequenceEqual(expectedSequence)) + throw new InvalidOperationException( + $"Expected sequential CountAsync() results [{string.Join(", ", expectedSequence)}], " + + $"got [{string.Join(", ", sequenceResults)}]."); + + retryGateway.Verify().CountAsync().Exactly(4); + + // Independent ADR-0050 entries own independent sequence ordinals. + accountRepository.Configure().Withdraw("acct-seq-1", Match.Any(), Match.Any()).ReturnsSequence(false, true); + accountRepository.Configure().Withdraw("acct-seq-2", Match.Any(), Match.Any()).ReturnsSequence(true, false); + + var seq1First = accountRepository.Withdraw("acct-seq-1", 1m, true); + var seq2First = accountRepository.Withdraw("acct-seq-2", 1m, true); + var seq1Second = accountRepository.Withdraw("acct-seq-1", 1m, true); + var seq2Second = accountRepository.Withdraw("acct-seq-2", 1m, true); + + if (seq1First || !seq2First || !seq1Second || seq2Second) + throw new InvalidOperationException( + $"Expected independent per-entry sequence ordinals (false,true / true,false), got " + + $"({seq1First},{seq2First},{seq1Second},{seq2Second})."); + + // 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 + // through the real IGateway.Send(string) overload, and both surfaces observe the same + // calls) and sibling-overload independence (configuring Send(string)'s matching surface + // never affects Send(int, string)'s own, independent entries/call-log/Verify() count). + var matchingGateway = composer.Create(); + matchingGateway.Configure().Send("ignored").Throws(new InvalidOperationException("fallback")); + matchingGateway.Configure().SendMatching(Match.Is(m => m == "special")).Returns(default(Unit)); + + var specialThrew = false; + try { matchingGateway.Send("special"); } catch (InvalidOperationException) { specialThrew = true; } + + var otherThrew = false; + try { matchingGateway.Send("other"); } catch (InvalidOperationException) { otherThrew = true; } + + if (specialThrew) + throw new InvalidOperationException("Expected Send(\"special\") to match the narrower .Matching(...) entry and NOT throw."); + + if (!otherThrew) + throw new InvalidOperationException("Expected Send(\"other\") to fall through to the broad discriminator entry and throw."); + + matchingGateway.Configure().SendMatching(Match.Any(), Match.Any()).Returns(default(Unit)); + matchingGateway.Send(1, "retry"); + + matchingGateway.Verify().SendMatching(Match.Is(m => m == "special")).Once(); + matchingGateway.Verify().Send("ignored").Exactly(2); + matchingGateway.Verify().SendMatching(Match.Any(), Match.Any()).Once(); + Console.WriteLine( $"PASS: generated doubles (composer.Create() + UseGeneratedTestDoubles(), full " + $"base-interface closure, overloaded member, generic method, call verification, " + diff --git a/test/Compono.Tests/ReturnConfigSequenceTests.cs b/test/Compono.Tests/ReturnConfigSequenceTests.cs new file mode 100644 index 00000000..9798af51 --- /dev/null +++ b/test/Compono.Tests/ReturnConfigSequenceTests.cs @@ -0,0 +1,269 @@ +namespace Compono.Tests; + +/// +/// Exercises and +/// - ADR-0054's sequential/call-count-based +/// response capability. Covers the acceptance scenarios that ADR named explicitly: value chains, +/// mixed exception/value chains, exhaustion, reconfiguration, call recording staying independent of +/// response consumption, and concurrent consumption not corrupting ordinal state. +/// +public sealed class ReturnConfigSequenceTests +{ + [Fact] + public void ReturnsSequence_ValueThenValueThenValue_ConsumedInOrder() + { + var slot = new ReturnConfig(); + new ReturnConfigBuilder(ref slot).ReturnsSequence(false, false, true); + + slot.HasConfiguredSequence.Should().BeTrue(); + slot.NextSequenceOutcome().Should().BeFalse(); + slot.NextSequenceOutcome().Should().BeFalse(); + slot.NextSequenceOutcome().Should().BeTrue(); + } + + [Fact] + public void ReturnsSequence_ExceptionThenValue_ThrowsThenReturns() + { + var slot = new ReturnConfig(); + var exception = new InvalidOperationException("first call fails"); + new ReturnConfigBuilder(ref slot).ReturnsSequence(SequenceOutcome.Throw(exception), "second call succeeds"); + + var act = () => slot.NextSequenceOutcome(); + act.Should().Throw().Which.Should().BeSameAs(exception); + + slot.NextSequenceOutcome().Should().Be("second call succeeds"); + } + + [Fact] + public void ReturnsSequence_ExceptionThenExceptionThenValue_MatchesRealRetryShape() + { + var slot = new ReturnConfig(); + var first = new InvalidOperationException("attempt 1"); + var second = new InvalidOperationException("attempt 2"); + new ReturnConfigBuilder(ref slot).ReturnsSequence(SequenceOutcome.Throw(first), SequenceOutcome.Throw(second), "attempt 3 succeeds"); + + Invoking(() => slot.NextSequenceOutcome()).Should().Throw().Which.Should().BeSameAs(first); + Invoking(() => slot.NextSequenceOutcome()).Should().Throw().Which.Should().BeSameAs(second); + slot.NextSequenceOutcome().Should().Be("attempt 3 succeeds"); + + static Action Invoking(Action action) => action; + } + + [Fact] + public void ReturnsSequence_AfterExhaustion_RepeatsFinalOutcome() + { + var slot = new ReturnConfig(); + new ReturnConfigBuilder(ref slot).ReturnsSequence(1, 2, 3); + + slot.NextSequenceOutcome().Should().Be(1); + slot.NextSequenceOutcome().Should().Be(2); + slot.NextSequenceOutcome().Should().Be(3); + + // Exhausted - every further call repeats the final configured outcome (ADR-0054). + slot.NextSequenceOutcome().Should().Be(3); + slot.NextSequenceOutcome().Should().Be(3); + } + + [Fact] + public void ReturnsSequence_CalledAgain_ReplacesSequenceAndResetsOrdinal() + { + var slot = new ReturnConfig(); + var builder = new ReturnConfigBuilder(ref slot); + builder.ReturnsSequence(1, 2, 3); + slot.NextSequenceOutcome().Should().Be(1); + slot.NextSequenceOutcome().Should().Be(2); + + builder.ReturnsSequence(100, 200); + + // Ordinal reset to 0 against the NEW sequence, not continued against the old one. + slot.NextSequenceOutcome().Should().Be(100); + slot.NextSequenceOutcome().Should().Be(200); + slot.NextSequenceOutcome().Should().Be(200); + } + + [Fact] + public void Returns_AfterReturnsSequence_ClearsSequenceState() + { + var slot = new ReturnConfig(); + var builder = new ReturnConfigBuilder(ref slot); + builder.ReturnsSequence(1, 2, 3); + + builder.Returns(42); + + slot.HasConfiguredSequence.Should().BeFalse(); + slot.HasConfiguredValue.Should().BeTrue(); + slot.ConfiguredValue.Should().Be(42); + } + + [Fact] + public void Throws_AfterReturnsSequence_ClearsSequenceState() + { + var slot = new ReturnConfig(); + var builder = new ReturnConfigBuilder(ref slot); + builder.ReturnsSequence(1, 2, 3); + + builder.Throws(new InvalidOperationException("boom")); + + slot.HasConfiguredSequence.Should().BeFalse(); + slot.HasConfiguredException.Should().BeTrue(); + } + + [Fact] + public void ReturnsSequence_AfterReturns_ClearsPriorSingleValue() + { + var slot = new ReturnConfig(); + var builder = new ReturnConfigBuilder(ref slot); + builder.Returns(42); + + builder.ReturnsSequence(1, 2, 3); + + slot.HasConfiguredValue.Should().BeFalse(); + slot.HasConfiguredSequence.Should().BeTrue(); + slot.NextSequenceOutcome().Should().Be(1); + } + + [Fact] + public void ReturnsSequence_EmptyArray_ThrowsArgumentException() + { + var slot = new ReturnConfig(); + var builder = new ReturnConfigBuilder(ref slot); + ArgumentException? thrown = null; + + try { builder.ReturnsSequence(); } + catch (ArgumentException ex) { thrown = ex; } + + thrown.Should().NotBeNull(); + } + + [Fact] + public void RecordCall_IsIndependentOfSequenceConsumption_EvenWhenSequenceThrows() + { + var slot = new ReturnConfig(); + new ReturnConfigBuilder(ref slot).ReturnsSequence( + SequenceOutcome.Throw(new InvalidOperationException("fails")), + SequenceOutcome.Throw(new InvalidOperationException("fails")), + 42); + + for (var i = 0; i < 3; i++) + { + slot.RecordCall(); + try { slot.NextSequenceOutcome(); } + catch (InvalidOperationException) { /* expected for the first two calls */ } + } + + slot.ConfiguredCallCount.Should().Be(3); + } + + [Fact] + public void NextSequenceOutcome_ConcurrentConsumption_EveryOrdinalClaimedExactlyOnce() + { + const int sequenceLength = 500; + var outcomes = Enumerable.Range(0, sequenceLength) + .Select(i => (SequenceOutcome)i) + .ToArray(); + var slot = new ReturnConfig(); + new ReturnConfigBuilder(ref slot).ReturnsSequence(outcomes); + + var results = new int[sequenceLength]; + Parallel.For(0, sequenceLength, _ => + { + var value = slot.NextSequenceOutcome(); + // Each concurrently-consumed outcome is recorded at its OWN value's index - a corrupted + // ordinal (two threads claiming the same index, or an index skipped/repeated + // unexpectedly) would show up as a lost or duplicated write here. + Interlocked.Increment(ref results[value]); + }); + + results.Should().OnlyContain(count => count == 1); + } + + [Fact] + public void ReturnsSequence_TExactlyException_ValueConversionAndThrowBothResolveUnambiguously() + { + var slot = new ReturnConfig(); + var valueOutcome = new ArgumentNullException("configured as a VALUE, not thrown"); + var thrownException = new InvalidOperationException("configured via SequenceOutcome.Throw"); + new ReturnConfigBuilder(ref slot).ReturnsSequence(valueOutcome, SequenceOutcome.Throw(thrownException)); + + // T-conversion: returned as an ordinary value, never thrown. + slot.NextSequenceOutcome().Should().BeSameAs(valueOutcome); + + // SequenceOutcome.Throw: thrown, unambiguously distinct from the value case above. + var act = () => slot.NextSequenceOutcome(); + act.Should().Throw().Which.Should().BeSameAs(thrownException); + } + + [Fact] + public void ReturnsSequence_TIsInvalidOperationException_ValueConversionResolvesAsValueNotThrow() + { + var slot = new ReturnConfig(); + var valueOutcome = new InvalidOperationException("configured as a value"); + new ReturnConfigBuilder(ref slot).ReturnsSequence(valueOutcome); + + slot.NextSequenceOutcome().Should().BeSameAs(valueOutcome); + } + + [Fact] + public void ReturnsSequence_TIsObject_ThrowStillResolvesAsThrowAndValueConversionStillWorks() + { + var slot = new ReturnConfig(); + var thrown = new InvalidOperationException("thrown for T=object"); + new ReturnConfigBuilder(ref slot).ReturnsSequence(SequenceOutcome.Throw(thrown), "a plain object value"); + + var act = () => slot.NextSequenceOutcome(); + act.Should().Throw().Which.Should().BeSameAs(thrown); + + slot.NextSequenceOutcome().Should().Be("a plain object value"); + } + + [Fact] + public void ReturnsSequence_TIsNullableException_NullValueViaTConversionResolvesAsNullNotThrow() + { + var slot = new ReturnConfig(); + new ReturnConfigBuilder(ref slot).ReturnsSequence((Exception?)null); + + slot.NextSequenceOutcome().Should().BeNull(); + } + + [Fact] + public void ReturnsSequence_ReferenceTypeNullViaTConversion_ResolvesAsNull() + { + var slot = new ReturnConfig(); + new ReturnConfigBuilder(ref slot).ReturnsSequence((string?)null, "second"); + + slot.NextSequenceOutcome().Should().BeNull(); + slot.NextSequenceOutcome().Should().Be("second"); + } + + [Fact] + public void ThrownOutcome_DefaultValue_ConversionToSequenceOutcomeThrowsArgumentException() + { + var defaultThrown = default(SequenceOutcome.ThrownOutcome); + + var act = () => + { + SequenceOutcome outcome = defaultThrown; + return outcome; + }; + + act.Should().Throw(); + } + + // Codex review, PR #115: ReturnsSequence(params SequenceOutcome[]) must snapshot the array + // rather than store the caller's own reference - a caller can pass an existing named array + // through the params parameter (not just an inline literal), and mutating an element afterward + // must not change a response that was already configured. + [Fact] + public void ReturnsSequence_CallerMutatesArrayAfterConfiguring_ConfiguredSequenceUnaffected() + { + var slot = new ReturnConfig(); + var outcomes = new SequenceOutcome[] { 1, 2, 3 }; + new ReturnConfigBuilder(ref slot).ReturnsSequence(outcomes); + + outcomes[0] = 999; + + slot.NextSequenceOutcome().Should().Be(1); + slot.NextSequenceOutcome().Should().Be(2); + slot.NextSequenceOutcome().Should().Be(3); + } +}