From cca4b1e14ffc8e4f7afe0a5a8de8ba40373af1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Thu, 30 Apr 2026 15:27:01 +0200 Subject: [PATCH 1/2] Cleanup Sources.MockClass.cs --- .../Sources/Sources.MockClass.cs | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index 0b7f6585..df1c5cbb 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -224,7 +224,7 @@ public static string MockClass(string name, Class @class, bool hasOverloadResolu sb.Append("\t\t\t=> CreateMock(null, setup, constructorParameters);").AppendLine(); sb.AppendLine(); - AppendTypedCreateMockOverloads(sb, @class, constructors, setupType, escapedClassName, createMockReturns); + AppendTypedCreateMockOverloads(sb, @class, constructors!.Value, setupType, escapedClassName, createMockReturns); } sb.AppendXmlSummary( @@ -739,16 +739,6 @@ static bool TryCastWithDefaultValue(object?[] values, int index, TValue @class.HasRequiredMembers); } } - else - { - sb.Append("\t\t/// ").AppendLine(); - sb.Append("\t\tpublic ").Append(name).Append("(global::Mockolate.MockRegistry mockRegistry)").AppendLine(); - sb.Append("\t\t{").AppendLine(); - sb.Append("\t\t\tthis.").Append(mockRegistryName).Append(" = mockRegistry;").AppendLine(); - sb.Append("\t\t}").AppendLine(); - sb.AppendLine(); - AppendMockSubject_BehaviorConstructor(sb, name); - } AppendMockSubject_ImplementClass(sb, @class, mockRegistryName, null, memberIds, memberIdPrefix); sb.AppendLine(); @@ -1773,13 +1763,8 @@ private static void ImplementMockForInterface(StringBuilder sb, string mockRegis #pragma warning restore S107 // Methods should not have too many parameters private static void AppendTypedCreateMockOverloads(StringBuilder sb, Class @class, - EquatableArray? constructors, string setupType, string escapedClassName, string createMockReturns) + EquatableArray constructors, string setupType, string escapedClassName, string createMockReturns) { - if (constructors is null) - { - return; - } - // Seeded signatures track the hand-written CreateMock overloads so typed overloads that // would collide with them are skipped. The key order mirrors the emitted C# signature: // "mockBehavior? | setup? | ctor-param-types...". @@ -1795,7 +1780,7 @@ private static void AppendTypedCreateMockOverloads(StringBuilder sb, Class @clas $"global::Mockolate.MockBehavior|global::System.Action<{setupType}>|object?[]", }; - foreach (Method constructor in constructors.Value) + foreach (Method constructor in constructors) { if (constructor.Parameters.Count == 0) { From 49f55e0dd9b5f5101fd40b96944b2e84522bc004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Thu, 30 Apr 2026 17:00:09 +0200 Subject: [PATCH 2/2] Add missing unit tests --- .../MockTests.ClassTests.MethodTests.cs | 86 +++++++++++++++++-- .../MockTests.ClassTests.PropertiesTests.cs | 35 ++++++++ .../MockTests.CombinationTests.cs | 62 +++++++++++-- .../MockTests.RefStructTests.cs | 72 ++++++++++++++++ 4 files changed, 242 insertions(+), 13 deletions(-) diff --git a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs index 9d89b0e2..aa563af2 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs @@ -33,7 +33,9 @@ public class MyService await That(result.Sources["Mock.MyService.g.cs"]) .DoesNotContain("global::MyCode.MyService wraps)") .IgnoringNewlineStyle() - .Because("the pattern-match cast must not bind a local named `wraps` while the user's parameter already occupies that name").And + .Because( + "the pattern-match cast must not bind a local named `wraps` while the user's parameter already occupies that name") + .And .Contains("global::MyCode.MyService wraps1)") .IgnoringNewlineStyle().And .Contains("wraps1.Run(wraps);") @@ -205,7 +207,8 @@ public interface IMyService await That(result.Diagnostics).IsEmpty(); await That(result.Sources).ContainsKey("Mock.IMyService.g.cs"); await That(result.Sources["Mock.IMyService.g.cs"]) - .Contains("throw new global::System.NotSupportedException(\"Mockolate: methods with a generic type parameter declaring 'allows ref struct' are not supported. Method 'global::MyCode.IMyService.G8'.\");"); + .Contains( + "throw new global::System.NotSupportedException(\"Mockolate: methods with a generic type parameter declaring 'allows ref struct' are not supported. Method 'global::MyCode.IMyService.G8'.\");"); } [Fact] @@ -271,7 +274,9 @@ public interface IMyService await That(result.Sources["Mock.IMyService.g.cs"]) .Contains("IOutParameter outParam_11") .IgnoringNewlineStyle() - .Because("the numbered cast variable must not reuse the parameter name (the base is renamed so `outParam1`/`outParam2` parameters and `outParam_1` cast don't collide)").And + .Because( + "the numbered cast variable must not reuse the parameter name (the base is renamed so `outParam1`/`outParam2` parameters and `outParam_1` cast don't collide)") + .And .Contains("IOutParameter outParam_12") .IgnoringNewlineStyle(); } @@ -306,7 +311,8 @@ await That(result.Sources["Mock.IMyService.g.cs"]) .IgnoringNewlineStyle().And .Contains("methodSetup?.TriggerCallbacks(result);") .IgnoringNewlineStyle().And - .Contains("return methodSetup?.TryGetReturnValue(result, out var returnValue) == true ? returnValue : this.MockRegistry.Behavior.DefaultValue.Generate(default(int)!, result);") + .Contains( + "return methodSetup?.TryGetReturnValue(result, out var returnValue) == true ? returnValue : this.MockRegistry.Behavior.DefaultValue.Generate(default(int)!, result);") .IgnoringNewlineStyle(); } @@ -446,6 +452,41 @@ await That(result.Sources["Mock.IMyService__IMyServiceBase1__IMyServiceBase2.g.c .Contains("long global::MyCode.IMyServiceBase2.Value()").Once(); } + [Fact] + public async Task + MultipleStaticAbstractMethods_WithCollidingSignature_ShouldEmitStaticKeywordOnExplicitImplementation() + { + GeneratorResult result = Generator + .Run(""" + using Mockolate; + + namespace MyCode; + public class Program + { + public static void Main(string[] args) + { + _ = IDerived.CreateMock(); + } + } + + public interface IBase + { + static abstract int Compute(); + } + + public interface IDerived : IBase + { + new static abstract int Compute(); + } + """); + + await That(result.Sources).ContainsKey("Mock.IDerived.g.cs"); + await That(result.Sources["Mock.IDerived.g.cs"]) + .Contains("static int global::MyCode.IBase.Compute(") + .Because( + "a colliding static abstract method on a base interface must be emitted as an explicit static implementation, with the static keyword preserved"); + } + [Fact] public async Task ParameterNamedI_ShouldNotCollideWithVerifyLambdaVariable() { @@ -1357,6 +1398,40 @@ public interface IMyService await That(result.Diagnostics).IsEmpty(); } + [Fact] + public async Task StaticAbstractMethodWithParameter_ShouldAppendArgumentToFastBuffer() + { + GeneratorResult result = Generator + .Run(""" + using Mockolate; + + namespace MyCode; + + public class Program + { + public static void Main(string[] args) + { + _ = IStaticOps.CreateMock(); + } + } + + public interface IStaticOps + { + static abstract int DoIt(int value); + } + """); + + await That(result.Sources).ContainsKey("Mock.IStaticOps.g.cs"); + await That(result.Sources["Mock.IStaticOps.g.cs"]) + .Contains("global::Mockolate.Interactions.FastMethod1Buffer").And + .Contains("MockRegistryProvider.Value.Interactions).Buffers[") + .Because( + "static methods record interactions via the typed FastMethodBuffer accessed through the AsyncLocal-backed registry") + .And + .Contains("Append(\"global::MyCode.IStaticOps.DoIt\", value)") + .Because("the static-method fast-buffer branch must include the call argument when arity > 0"); + } + [Fact] public async Task VirtualMethodOverride_WithConstrainedGeneric_ShouldNotRepeatConstraints() { @@ -1395,7 +1470,8 @@ await That(result.Sources["Mock.MyService.g.cs"]) public override bool MyMethod(T entity) { """).IgnoringNewlineStyle().And - .DoesNotContain("public override bool MyMethod(T entity)\n\t\t\twhere T :").IgnoringNewlineStyle() + .DoesNotContain("public override bool MyMethod(T entity)\n\t\t\twhere T :") + .IgnoringNewlineStyle() .Because("CS0460: constraints on override methods are inherited from the base method"); } } diff --git a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.PropertiesTests.cs b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.PropertiesTests.cs index a6cbcb79..c51f297a 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.PropertiesTests.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.PropertiesTests.cs @@ -85,6 +85,41 @@ await That(result.Sources["Mock.IMyService__IMyServiceBase1__IMyServiceBase2.g.c .Contains("long global::MyCode.IMyServiceBase2.Value").Once(); } + [Fact] + public async Task + MultipleStaticAbstractProperties_WithCollidingName_ShouldEmitStaticKeywordOnExplicitImplementation() + { + GeneratorResult result = Generator + .Run(""" + using Mockolate; + + namespace MyCode; + public class Program + { + public static void Main(string[] args) + { + _ = IDerived.CreateMock(); + } + } + + public interface IBase + { + static abstract int Counter { get; } + } + + public interface IDerived : IBase + { + new static abstract int Counter { get; } + } + """); + + await That(result.Sources).ContainsKey("Mock.IDerived.g.cs"); + await That(result.Sources["Mock.IDerived.g.cs"]) + .Contains("static int global::MyCode.IBase.Counter") + .Because( + "a colliding static abstract property on a base interface must be emitted as an explicit static implementation, with the static keyword preserved"); + } + [Fact] public async Task RefReturn_ShouldCompile() { diff --git a/Tests/Mockolate.SourceGenerators.Tests/MockTests.CombinationTests.cs b/Tests/Mockolate.SourceGenerators.Tests/MockTests.CombinationTests.cs index dfc10d7f..c6931b33 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/MockTests.CombinationTests.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/MockTests.CombinationTests.cs @@ -40,12 +40,19 @@ public interface IMyInterface await That(result.Sources).ContainsKey("Mock.MyService__IMyInterface.g.cs"); await That(result.Sources["Mock.MyService__IMyInterface.g.cs"]) - .Contains("static bool TryCast(object?[] values, int index, global::Mockolate.MockBehavior behavior, out TValue result)").And - .Contains("static bool TryCastWithDefaultValue(object?[] values, int index, TValue defaultValue, global::Mockolate.MockBehavior behavior, out TValue result)").And - .Contains("if (mock.MockRegistry.ConstructorParameters is null || mock.MockRegistry.ConstructorParameters.Length == 0)") + .Contains( + "static bool TryCast(object?[] values, int index, global::Mockolate.MockBehavior behavior, out TValue result)") + .And + .Contains( + "static bool TryCastWithDefaultValue(object?[] values, int index, TValue defaultValue, global::Mockolate.MockBehavior behavior, out TValue result)") + .And + .Contains( + "if (mock.MockRegistry.ConstructorParameters is null || mock.MockRegistry.ConstructorParameters.Length == 0)") .Because("the parameterless dispatch branch must be emitted").And - .Contains("else if (mock.MockRegistry.ConstructorParameters.Length >= 1 && mock.MockRegistry.ConstructorParameters.Length <= 2") - .Because("the 'else if' arity dispatch chain must include the optional-range branch for a mixed-required + defaulted ctor"); + .Contains( + "else if (mock.MockRegistry.ConstructorParameters.Length >= 1 && mock.MockRegistry.ConstructorParameters.Length <= 2") + .Because( + "the 'else if' arity dispatch chain must include the optional-range branch for a mixed-required + defaulted ctor"); } [Fact] @@ -85,6 +92,42 @@ await That(result.Sources["Mock.MyService__IMyInterface.g.cs"]) .DoesNotContain("static bool TryCastWithDefaultValue"); } + [Fact] + public async Task + CombinationOnConcreteClassWithStaticAbstractInterface_ShouldPrimeMockRegistryProviderInBaseClassConstructor() + { + GeneratorResult result = Generator + .Run(""" + using Mockolate; + + namespace MyCode; + + public class Program + { + public static void Main(string[] args) + { + _ = MyService.CreateMock().Implementing(); + } + } + + public class MyService + { + public MyService() { } + } + + public interface IStaticAware + { + static abstract int Counter { get; } + } + """); + + await That(result.Sources).ContainsKey("Mock.MyService__IStaticAware.g.cs"); + await That(result.Sources["Mock.MyService__IStaticAware.g.cs"]) + .Contains("MockRegistryProvider.Value = mockRegistry;") + .Because( + "when a concrete base class is combined with an interface declaring static abstract members, the (MockRegistry) constructor must prime the AsyncLocal so virtual calls during base-class construction can resolve the registry"); + } + [Fact] public async Task CombinationWithProtectedEventOnBaseClass_ShouldEmitProtectedRaiseRegion() { @@ -152,7 +195,8 @@ public interface IExtra await That(result.Sources).ContainsKey("Mock.MyService__IExtra.g.cs"); await That(result.Sources["Mock.MyService__IExtra.g.cs"]) .Contains("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]") - .Because("AppendMockSubject_BaseClassConstructor must stamp SetsRequiredMembers on the generated constructor when the base class declares any `required` member"); + .Because( + "AppendMockSubject_BaseClassConstructor must stamp SetsRequiredMembers on the generated constructor when the base class declares any `required` member"); } [Fact] @@ -188,7 +232,8 @@ public interface IStaticEvents await That(result.Sources["Mock.IBase__IStaticEvents.g.cs"]) .Contains("IMockStaticRaiseOnIStaticEvents").And .Contains("#region IMockStaticRaiseOnIStaticEvents").And - .Contains("internal static readonly global::System.Threading.AsyncLocal MockRegistryProvider"); + .Contains( + "internal static readonly global::System.Threading.AsyncLocal MockRegistryProvider"); } [Fact] @@ -226,7 +271,8 @@ await That(result.Sources["Mock.IBase__IStaticAware.g.cs"]) .Contains("IMockStaticVerifyForIStaticAware").And .Contains("#region IMockStaticSetupForIStaticAware").And .Contains("#region IMockStaticVerifyForIStaticAware").And - .Contains("internal static readonly global::System.Threading.AsyncLocal MockRegistryProvider") + .Contains( + "internal static readonly global::System.Threading.AsyncLocal MockRegistryProvider") .Because("the AsyncLocal field is required so static accessors can find the registry"); } } diff --git a/Tests/Mockolate.SourceGenerators.Tests/MockTests.RefStructTests.cs b/Tests/Mockolate.SourceGenerators.Tests/MockTests.RefStructTests.cs index fa498b8f..9804b78d 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/MockTests.RefStructTests.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/MockTests.RefStructTests.cs @@ -478,6 +478,78 @@ await That(result.Sources["Mock.IRefStructWriter.g.cs"]) .Contains("RefStructMethodInvocation(\"global::MyCode.IRefStructWriter.set_Item\", \"key\", \"value\")"); } + [Fact] + public async Task MethodReturningNonSpanRefStruct_WithRefStructParameter_ShouldEmitNotSupportedExceptionAndSkipSetupSurface() + { + GeneratorResult result = Generator + .Run(""" + using System; + using Mockolate; + + namespace MyCode; + + public readonly ref struct Packet(int id) { public int Id { get; } = id; } + + public interface IPacketTransformer + { + Packet Wrap(Packet input); + } + + public class Program + { + public static void Main(string[] args) + { + _ = IPacketTransformer.CreateMock(); + } + } + """); + + await That(result.Sources).ContainsKey("Mock.IPacketTransformer.g.cs"); + await That(result.Sources["Mock.IPacketTransformer.g.cs"]) + .Contains("Mockolate: methods returning a non-span ref struct are not supported. Method 'global::MyCode.IPacketTransformer.Wrap'.") + .Because("the method body must throw NotSupportedException because non-Span ref-struct return types cannot flow through the setup pipeline").And + .DoesNotContain("IRefStructReturnMethodSetup") + .Because("the setup-interface declaration must be skipped for unsupported ref-struct return types").And + .DoesNotContain("new global::Mockolate.Setup.RefStructReturnMethodSetup") + .Because("the setup-interface implementation must be skipped for unsupported ref-struct return types"); + } + + [Fact] + public async Task MethodWithOutRefStructParameter_ShouldEmitNotSupportedExceptionAndSkipSetupSurface() + { + GeneratorResult result = Generator + .Run(""" + using System; + using Mockolate; + + namespace MyCode; + + public readonly ref struct Packet(int id) { public int Id { get; } = id; } + + public interface IPacketBag + { + void Take(out Packet packet); + } + + public class Program + { + public static void Main(string[] args) + { + _ = IPacketBag.CreateMock(); + } + } + """); + + await That(result.Sources).ContainsKey("Mock.IPacketBag.g.cs"); + await That(result.Sources["Mock.IPacketBag.g.cs"]) + .Contains("Mockolate: out/ref ref-struct parameters are not supported. Method 'global::MyCode.IPacketBag.Take'.") + .Because("the method body must throw NotSupportedException because the ref-struct out parameter cannot flow through the setup pipeline").And + .DoesNotContain("IRefStructVoidMethodSetup") + .Because("the setup-interface declaration must be skipped for unsupported ref-struct signatures").And + .DoesNotContain("new global::Mockolate.Setup.RefStructVoidMethodSetup") + .Because("the setup-interface implementation must be skipped for unsupported ref-struct signatures"); + } + [Fact] public async Task MixedParameters_RefStructPlusValueType_ShouldRouteThroughRefStructPipeline() {