Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 3 additions & 18 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -739,16 +739,6 @@ static bool TryCastWithDefaultValue<TValue>(object?[] values, int index, TValue
@class.HasRequiredMembers);
}
}
else
{
sb.Append("\t\t/// <inheritdoc cref=\"").Append(name).Append("\" />").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();
Expand Down Expand Up @@ -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<Method>? constructors, string setupType, string escapedClassName, string createMockReturns)
EquatableArray<Method> 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...".
Expand All @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);")
Expand Down Expand Up @@ -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<T>'.\");");
.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<T>'.\");");
}

[Fact]
Expand Down Expand Up @@ -271,7 +274,9 @@ public interface IMyService
await That(result.Sources["Mock.IMyService.g.cs"])
.Contains("IOutParameter<int> 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<int> outParam_12")
.IgnoringNewlineStyle();
}
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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<int>").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()
{
Expand Down Expand Up @@ -1395,7 +1470,8 @@ await That(result.Sources["Mock.MyService.g.cs"])
public override bool MyMethod<T>(T entity)
{
""").IgnoringNewlineStyle().And
.DoesNotContain("public override bool MyMethod<T>(T entity)\n\t\t\twhere T :").IgnoringNewlineStyle()
.DoesNotContain("public override bool MyMethod<T>(T entity)\n\t\t\twhere T :")
.IgnoringNewlineStyle()
.Because("CS0460: constraints on override methods are inherited from the base method");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TValue>(object?[] values, int index, global::Mockolate.MockBehavior behavior, out TValue result)").And
.Contains("static bool TryCastWithDefaultValue<TValue>(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<TValue>(object?[] values, int index, global::Mockolate.MockBehavior behavior, out TValue result)")
.And
.Contains(
"static bool TryCastWithDefaultValue<TValue>(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]
Expand Down Expand Up @@ -85,6 +92,42 @@ await That(result.Sources["Mock.MyService__IMyInterface.g.cs"])
.DoesNotContain("static bool TryCastWithDefaultValue<TValue>");
}

[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<IStaticAware>();
}
}

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()
{
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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<global::Mockolate.MockRegistry> MockRegistryProvider");
.Contains(
"internal static readonly global::System.Threading.AsyncLocal<global::Mockolate.MockRegistry> MockRegistryProvider");
}

[Fact]
Expand Down Expand Up @@ -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<global::Mockolate.MockRegistry> MockRegistryProvider")
.Contains(
"internal static readonly global::System.Threading.AsyncLocal<global::Mockolate.MockRegistry> MockRegistryProvider")
.Because("the AsyncLocal field is required so static accessors can find the registry");
}
}
Expand Down
Loading
Loading