fix(mocks): emit the setup surface into the globally-imported namespace - #6504
Conversation
| // The setup/verify surface goes in the globally-imported namespace rather than beside the | ||
| // mocked type — an extension member is invisible unless its namespace is imported, which | ||
| // silently hid every setup for types from a namespace the test hadn't `using`'d (#6494). | ||
| using (writer.OptionalNamespaceBlock(MockImplBuilder.MemberSurfaceNamespace)) |
There was a problem hiding this comment.
Flat namespace exposes name collisions
When two mocked types have fully qualified names that sanitize identically, such as A_B.IFoo and A.B.IFoo, both surfaces are now emitted into TUnit.Mocks.Generated with the same generated class and wrapper names, causing duplicate-type compilation errors. The original namespaces previously kept these declarations separate, so the shared namespace needs a collision-resistant type identity.
Greptile SummaryThe PR makes generated mock setup, verification, and event extensions globally discoverable.
Confidence Score: 3/5The PR is not yet safe to merge because collision-equivalent fully qualified type names can still make generated mock surfaces fail compilation. The setup and event surfaces now share one namespace, while their declaration names still use a lossy sanitizer that maps distinct qualified names such as Files Needing Attention: src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs, src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs, src/TUnit.Mocks.SourceGenerator/Builders/MockEventsBuilder.cs
|
| Filename | Overview |
|---|---|
| src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs | Relocates setup and verification extensions and wrappers into a shared namespace, where the previously reported collision-prone generated names remain unchanged. |
| src/TUnit.Mocks.SourceGenerator/Builders/MockEventsBuilder.cs | Relocates event accessors into the shared generated namespace using the same non-injective sanitized type identity. |
| src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs | Introduces the common member-surface namespace but does not add collision-resistant generated identities. |
| tests/TUnit.Mocks.Tests/Issue6494Tests.cs | Covers globally discoverable setup, verification, and events for ordinary same-short-name interfaces, but not fully qualified names that sanitize identically. |
| docs/docs/writing-tests/mocking/index.md | Documents the generated namespace and the explicit using required when mock implicit usings are disabled. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Mocked type] --> B[Source generator]
B --> C[Implementation and factory beside mocked type]
B --> D[Setup and verification surface]
B --> E[Event surface]
D --> F[TUnit.Mocks.Generated]
E --> F
F --> G[Globally imported at call site]
Reviews (3): Last reviewed commit: "test(mocks): refresh snapshots after reb..." | Re-trigger Greptile
Code reviewReviewed the source generator changes ( Verified correctness of the core fix:
No high-signal bugs or CLAUDE.md violations found. The regression tests in Nit (non-blocking): the non-span ref-struct out/ref delegate emission path ( |
|
Addressed the snapshot-coverage nit in 1fbc04d. Since the delegate emission is the exact path this PR split out of the members loop, it was worth closing rather than deferring: |
Code review (follow-up)Reviewed the changes since my previous review — commit 1fbc04d, which addresses the snapshot-coverage nit I flagged. Verified:
No new issues found — this closes out the nit from my earlier review. Nice fix. |
|
On the flat-namespace collision finding (
Mocking either type alone yields 5 files. Same result on Filed as #6505 rather than folded in here: the fix is to make the identifier mapping injective (escape |
Setup, verify and event extensions were generated beside the mocked type. An extension member is only visible when its namespace is imported, so mocking a type whose namespace the test had not `using`'d compiled the Mock() call — that entry point already lives in TUnit.Mocks — but hid every member setup behind CS1061, which reads as "this member isn't supported". Reported as a name collision between two same-named ICluster interfaces; it reproduces with a single mock and no collision involved. Emit the extension classes and the call wrappers they return into TUnit.Mocks.Generated, which TUnit.Mocks.targets adds as a global using. Their names are derived from the fully qualified type name, so types sharing a short name across namespaces stay distinct. The impl, factory and wrapper types keep their current placement, as do the out/ref setter delegates the impl references by short name. Fixes #6494
Review nit: no snapshot covered the one construct that emits a namespace-scoped delegate, which is precisely the path this PR split out of the members loop. The new fixture pins both halves — the delegates stay in the mocked type's namespace (the impl references them by short name) while the setup surface moves to TUnit.Mocks.Generated.
…factories Rebase conflict resolution: #6503 added the Func<Task<T>> Returns overloads to the same generated call-wrapper types this branch moved into TUnit.Mocks.Generated. Regenerated rather than hand-merged.
1fbc04d to
426c1b1
Compare
|
Rebased onto main to clear the conflict. All 8 conflicts were generated-code snapshots: #6503 (async Regenerated rather than hand-merged them (426c1b1) — the diff against the pre-rebase snapshots is additions-only and entirely #6503's |
Code review (follow-up)Reviewed the changes since my last review — commit 426c1b1, rebasing onto main to pick up #6503's async Verified:
No new issues found. This PR is in good shape. |
Fixes #6494
Problem
The report attributed this to a name collision between
Cassandra.IClusterandCouchbase.ICluster. It isn't — it reproduces with a single mock and no collision:The
_MockMemberExtensions/_MockEventsExtensionsclasses were emitted beside the mocked type. Extension members are only found when their containing namespace is imported, and theMock()entry point is inTUnit.Mocks(a global using) so the call site compiles — leaving a mock whose entire setup/verify surface is invisible. The resulting error reads as "this member isn't supported", which is what cost the reporter debugging time.Fix
Emit the extension classes, the call wrappers they return, and the
Eventssurface intoTUnit.Mocks.Generated— added as a global using byTUnit.Mocks.targets. Those type names are already derived from the fully qualified type name, so two mocked types sharing a short name across namespaces remain distinct in the flat namespace.Unchanged: the impl, factory, bridge and
IFooMockwrapper types keep their current placement beside the mocked type, as do the out/ref setter delegates (named from the short name and referenced by the impl throughGetGlobalMockNamespacePrefix), which now emit in their own namespace block.Docs note added for projects that set
TUnitMockImplicitUsings=disable.Tests
tests/TUnit.Mocks.Tests/Issue6494Tests.cs— member setup, verification and event raising against interfaces in namespaces the test deliberately does not import, including the reported two-IClustershape.TUnit.Mocks.Tests1164 passed. Generator snapshots regenerated (59 files — namespace wrapper and re-indentation only) and passing on net8.0/net9.0/net10.0. Analyzers 30, Http 54, Logging 31.