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
26 changes: 26 additions & 0 deletions TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,32 @@ void M()
return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_With_RefStruct_Parameters()
{
var source = """
using System;
using TUnit.Mocks;

public interface IBufferProcessor
{
void Process(ReadOnlySpan<byte> data);
int Parse(ReadOnlySpan<char> text);
string GetName();
}

public class TestUsage
{
void M()
{
var mock = Mock.Of<IBufferProcessor>();
}
}
""";

return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_With_Mixed_Members()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
internal static class IBufferProcessor_MockFactory
{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Register()
{
global::TUnit.Mocks.Mock.RegisterFactory<global::IBufferProcessor>(Create);
}

private static global::TUnit.Mocks.Mock<global::IBufferProcessor> Create(global::TUnit.Mocks.MockBehavior behavior)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IBufferProcessor>(behavior);
var impl = new IBufferProcessor_MockImpl(engine);
engine.Raisable = impl;
var mock = new global::TUnit.Mocks.Mock<global::IBufferProcessor>(impl, engine);
return mock;
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
internal sealed class IBufferProcessor_MockImpl : global::IBufferProcessor, global::TUnit.Mocks.IRaisable
{
private readonly global::TUnit.Mocks.MockEngine<global::IBufferProcessor> _engine;

internal IBufferProcessor_MockImpl(global::TUnit.Mocks.MockEngine<global::IBufferProcessor> engine)
{
_engine = engine;
}

public void Process(global::System.ReadOnlySpan<byte> data)
{
_engine.HandleCall(0, "Process", global::System.Array.Empty<object?>());
}

public int Parse(global::System.ReadOnlySpan<char> text)
{
return _engine.HandleCallWithReturn<int>(1, "Parse", global::System.Array.Empty<object?>(), default);
}

public string GetName()
{
return _engine.HandleCallWithReturn<string>(2, "GetName", global::System.Array.Empty<object?>(), "");
}

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public void RaiseEvent(string eventName, object? args)
{
throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock.");
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
public static class IBufferProcessor_MockMemberExtensions
{
public static global::TUnit.Mocks.VoidMockMethodCall Process(this global::TUnit.Mocks.Mock<global::IBufferProcessor> mock)
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.VoidMockMethodCall(mock.Engine, 0, "Process", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<int> Parse(this global::TUnit.Mocks.Mock<global::IBufferProcessor> mock)
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<int>(mock.Engine, 1, "Parse", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<string> GetName(this global::TUnit.Mocks.Mock<global::IBufferProcessor> mock)
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<string>(mock.Engine, 2, "GetName", matchers);
}
}
}
130 changes: 120 additions & 10 deletions TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,17 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});");
}
else if (method.IsRefStructReturn)
{
writer.AppendLine($"if (_engine.TryHandleCall({method.MemberId}, \"{method.Name}\", {argsArray}))");
writer.AppendLine("{");
writer.IncreaseIndent();
EmitOutRefReadback(writer, method);
writer.AppendLine("return default;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});");
}
else
{
writer.AppendLine($"if (_engine.TryHandleCallWithReturn<{method.ReturnType}>({method.MemberId}, \"{method.Name}\", {argsArray}, {method.SmartDefault}, out var __result))");
Expand All @@ -259,7 +270,31 @@ private static void GenerateWrapProperty(CodeWriter writer, MockMemberModel prop

if (prop.HasGetter)
{
if (prop.IsAbstractMember)
if (prop.IsRefStructReturn)
{
if (prop.IsAbstractMember)
{
writer.AppendLine("get");
writer.OpenBrace();
writer.AppendLine($"_engine.HandleCall({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty<object?>());");
writer.AppendLine("return default;");
writer.CloseBrace();
}
else
{
writer.AppendLine("get");
writer.OpenBrace();
writer.AppendLine($"if (_engine.TryHandleCall({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty<object?>()))");
writer.AppendLine("{");
writer.IncreaseIndent();
writer.AppendLine("return default;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{prop.Name};");
writer.CloseBrace();
}
}
else if (prop.IsAbstractMember)
{
writer.AppendLine($"get => _engine.HandleCallWithReturn<{prop.ReturnType}>({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty<object?>(), {prop.SmartDefault});");
}
Expand All @@ -280,15 +315,19 @@ private static void GenerateWrapProperty(CodeWriter writer, MockMemberModel prop

if (prop.HasSetter)
{
var setterArgs = prop.IsRefStructReturn
? "global::System.Array.Empty<object?>()"
: "new object?[] { value }";

if (prop.IsAbstractMember)
{
writer.AppendLine($"set => _engine.HandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", new object?[] {{ value }});");
writer.AppendLine($"set => _engine.HandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", {setterArgs});");
}
else
{
writer.AppendLine("set");
writer.OpenBrace();
writer.AppendLine($"if (!_engine.TryHandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", new object?[] {{ value }}))");
writer.AppendLine($"if (!_engine.TryHandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", {setterArgs}))");
writer.AppendLine("{");
writer.IncreaseIndent();
writer.AppendLine($"_wrappedInstance.{prop.Name} = value;");
Expand Down Expand Up @@ -475,6 +514,18 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel
writer.AppendLine("}");
writer.AppendLine($"return base.{method.Name}({argPassList});");
}
else if (method.IsRefStructReturn)
{
// synchronous method returning ref struct — use void dispatch, fall back to base
writer.AppendLine($"if (_engine.TryHandleCall({method.MemberId}, \"{method.Name}\", {argsArray}))");
writer.AppendLine("{");
writer.IncreaseIndent();
EmitOutRefReadback(writer, method);
writer.AppendLine("return default;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{method.Name}({argPassList});");
}
else
{
// synchronous method with return value
Expand Down Expand Up @@ -566,6 +617,15 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode
}
}
}
else if (method.IsRefStructReturn)
{
// Synchronous method returning a ref struct — can't use HandleCallWithReturn<T> because
// ref structs can't be generic type arguments. Use void dispatch for call tracking,
// callbacks, and throws. Return default (e.g. ReadOnlySpan<byte>.Empty).
writer.AppendLine($"_engine.HandleCall({method.MemberId}, \"{method.Name}\", {argsArray});");
EmitOutRefReadback(writer, method);
writer.AppendLine("return default;");
}
else
{
// Synchronous method with return value — need to read back out/ref before returning
Expand All @@ -589,12 +649,32 @@ private static void GenerateInterfaceProperty(CodeWriter writer, MockMemberModel

if (prop.HasGetter)
{
writer.AppendLine($"get => _engine.HandleCallWithReturn<{prop.ReturnType}>({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty<object?>(), {prop.SmartDefault});");
if (prop.IsRefStructReturn)
{
// ref struct property — can't use HandleCallWithReturn<T>, use void dispatch + return default
writer.AppendLine("get");
writer.OpenBrace();
writer.AppendLine($"_engine.HandleCall({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty<object?>());");
writer.AppendLine("return default;");
writer.CloseBrace();
}
else
{
writer.AppendLine($"get => _engine.HandleCallWithReturn<{prop.ReturnType}>({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty<object?>(), {prop.SmartDefault});");
}
}

if (prop.HasSetter)
{
writer.AppendLine($"set => _engine.HandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", new object?[] {{ value }});");
if (prop.IsRefStructReturn)
{
// ref struct property — can't box value, use empty args
writer.AppendLine($"set => _engine.HandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", global::System.Array.Empty<object?>());");
}
else
{
writer.AppendLine($"set => _engine.HandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", new object?[] {{ value }});");
}
}

writer.CloseBrace();
Expand All @@ -608,7 +688,31 @@ private static void GeneratePartialProperty(CodeWriter writer, MockMemberModel p

if (prop.HasGetter)
{
if (prop.IsAbstractMember)
if (prop.IsRefStructReturn)
{
if (prop.IsAbstractMember)
{
writer.AppendLine("get");
writer.OpenBrace();
writer.AppendLine($"_engine.HandleCall({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty<object?>());");
writer.AppendLine("return default;");
writer.CloseBrace();
}
else
{
writer.AppendLine("get");
writer.OpenBrace();
writer.AppendLine($"if (_engine.TryHandleCall({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty<object?>()))");
writer.AppendLine("{");
writer.IncreaseIndent();
writer.AppendLine("return default;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{prop.Name};");
writer.CloseBrace();
}
}
else if (prop.IsAbstractMember)
{
writer.AppendLine($"get => _engine.HandleCallWithReturn<{prop.ReturnType}>({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty<object?>(), {prop.SmartDefault});");
}
Expand All @@ -630,16 +734,20 @@ private static void GeneratePartialProperty(CodeWriter writer, MockMemberModel p

if (prop.HasSetter)
{
var setterArgs = prop.IsRefStructReturn
? "global::System.Array.Empty<object?>()"
: "new object?[] { value }";

if (prop.IsAbstractMember)
{
writer.AppendLine($"set => _engine.HandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", new object?[] {{ value }});");
writer.AppendLine($"set => _engine.HandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", {setterArgs});");
}
else
{
// Virtual property setter: try engine, fall back to base
writer.AppendLine("set");
writer.OpenBrace();
writer.AppendLine($"if (!_engine.TryHandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", new object?[] {{ value }}))");
writer.AppendLine($"if (!_engine.TryHandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", {setterArgs}))");
writer.AppendLine("{");
writer.IncreaseIndent();
writer.AppendLine($"base.{prop.Name} = value;");
Expand Down Expand Up @@ -838,6 +946,7 @@ private static void EmitOutRefReadback(CodeWriter writer, MockMemberModel method
for (int i = 0; i < method.Parameters.Length; i++)
{
var p = method.Parameters[i];
if (p.IsRefStruct) continue; // ref structs can't be cast from object
if (p.Direction == ParameterDirection.Out || p.Direction == ParameterDirection.Ref)
{
writer.AppendLine($"if (__outRef.TryGetValue({i}, out var __v{i})) {p.Name} = ({p.FullyQualifiedType})__v{i}!;");
Expand All @@ -848,8 +957,9 @@ private static void EmitOutRefReadback(CodeWriter writer, MockMemberModel method

private static string GetArgsArrayExpression(MockMemberModel method)
{
// Only include non-out parameters in args array
var matchableParams = method.Parameters.Where(p => p.Direction != ParameterDirection.Out).ToList();
// Only include non-out, non-ref-struct parameters in args array
// (ref structs cannot be boxed into object?[])
var matchableParams = method.Parameters.Where(p => p.Direction != ParameterDirection.Out && !p.IsRefStruct).ToList();
if (matchableParams.Count == 0) return "global::System.Array.Empty<object?>()";
var args = string.Join(", ", matchableParams.Select(p => p.Name));
return $"new object?[] {{ {args} }}";
Expand Down
Loading
Loading