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
95 changes: 95 additions & 0 deletions TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,99 @@ void M()

return VerifyGeneratorOutput(source);
}

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

public class ClientConfig { }

public interface IServiceFactory
{
string GetName();
static abstract ClientConfig CreateDefaultConfig();
static abstract string ServiceId { get; set; }
}

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

return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_With_Inherited_Static_Abstract_Members()
{
// Tests the case where an interface inherits static abstract members from a base interface.
// The generator resolves the call via CandidateSymbols (CS8920 workaround)
// and generates engine-dispatching implementations in the mock impl class.
var source = """
using TUnit.Mocks;

public class ClientConfig { }

public interface IServiceBase
{
static abstract ClientConfig CreateDefaultConfig();
}

public interface IMyService : IServiceBase
{
string GetName();
}

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

return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_With_Static_Abstract_Transitive_Return_Type()
{
// Simulates the AWS SDK scenario: a main interface returns a base interface
// that has static abstract members. The source generator should NOT generate a
// transitive mock for the base interface (which would trigger CS8920).
var source = """
using TUnit.Mocks;

public class ClientConfig { }

public interface IConfigProvider
{
ClientConfig Config { get; }
static abstract ClientConfig CreateDefault();
}

public interface IMyService
{
string GetValue(string key);
IConfigProvider GetConfigProvider();
}

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

return VerifyGeneratorOutput(source);
}
}
5 changes: 3 additions & 2 deletions TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ private static List<PortableExecutableReference> LoadReferences()
/// Runs the MockGenerator against the given source and returns the generated files
/// as an array of strings, ordered by hint name for stable snapshot comparison.
/// </summary>
protected static string[] RunGenerator(string source)
protected static string[] RunGenerator(string source, CSharpParseOptions? parseOptions = null)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
parseOptions ??= CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview);
var syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions);

var compilation = CSharpCompilation.Create(
"TestAssembly",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public interface IMyService_Mockable : global::IMyService
{
static global::ClientConfig global::IServiceBase.CreateDefaultConfig()
{
var __engine = IMyService_StaticEngine.Engine;
if (__engine is null) return default!;
var __result = __engine.HandleCallWithReturn<global::ClientConfig>(1, "CreateDefaultConfig", global::System.Array.Empty<object?>(), default!);
return __result;
}
}

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static class IMyService_StaticEngine
{
private static readonly global::System.Threading.AsyncLocal<global::TUnit.Mocks.IMockEngineAccess?> _engine = new();

internal static global::TUnit.Mocks.IMockEngineAccess? Engine
{
get => _engine.Value;
set => _engine.Value = value;
}
}
}


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

// <auto-generated/>
#nullable enable

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

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


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

// <auto-generated/>
#nullable enable

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

internal IMyService_MockImpl(global::TUnit.Mocks.MockEngine<global::TUnit.Mocks.Generated.IMyService_Mockable> engine)
{
_engine = engine;
if (IMyService_StaticEngine.Engine is not null)
{
throw new global::System.InvalidOperationException(
"Multiple mocks of an interface with static abstract members cannot be created in the same test context. " +
"Static member calls are routed via a shared AsyncLocal engine, so only one mock instance per type is supported per test.");
}
IMyService_StaticEngine.Engine = engine;
}

public string GetName()
{
return _engine.HandleCallWithReturn<string>(0, "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 IMyService_MockMemberExtensions
{
public static global::TUnit.Mocks.MockMethodCall<string> GetName(this global::TUnit.Mocks.Mock<global::TUnit.Mocks.Generated.IMyService_Mockable> mock)
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<string>(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "GetName", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<global::ClientConfig> CreateDefaultConfig(this global::TUnit.Mocks.Mock<global::TUnit.Mocks.Generated.IMyService_Mockable> mock)
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<global::ClientConfig>(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "CreateDefaultConfig", matchers);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public interface IServiceFactory_Mockable : global::IServiceFactory
{
static global::ClientConfig global::IServiceFactory.CreateDefaultConfig()
{
var __engine = IServiceFactory_StaticEngine.Engine;
if (__engine is null) return default!;
var __result = __engine.HandleCallWithReturn<global::ClientConfig>(1, "CreateDefaultConfig", global::System.Array.Empty<object?>(), default!);
return __result;
}

static string global::IServiceFactory.ServiceId
{
get
{
var __engine = IServiceFactory_StaticEngine.Engine;
if (__engine is null) return default!;
return __engine.HandleCallWithReturn<string>(2, "get_ServiceId", global::System.Array.Empty<object?>(), "");
}
set
{
var __engine = IServiceFactory_StaticEngine.Engine;
if (__engine is null) return;
__engine.HandleCall(3, "set_ServiceId", new object?[] { value });
}
}
}

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static class IServiceFactory_StaticEngine
{
private static readonly global::System.Threading.AsyncLocal<global::TUnit.Mocks.IMockEngineAccess?> _engine = new();

internal static global::TUnit.Mocks.IMockEngineAccess? Engine
{
get => _engine.Value;
set => _engine.Value = value;
}
}
}


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

// <auto-generated/>
#nullable enable

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

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


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

// <auto-generated/>
#nullable enable

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

internal IServiceFactory_MockImpl(global::TUnit.Mocks.MockEngine<global::TUnit.Mocks.Generated.IServiceFactory_Mockable> engine)
{
_engine = engine;
if (IServiceFactory_StaticEngine.Engine is not null)
{
throw new global::System.InvalidOperationException(
"Multiple mocks of an interface with static abstract members cannot be created in the same test context. " +
"Static member calls are routed via a shared AsyncLocal engine, so only one mock instance per type is supported per test.");
}
IServiceFactory_StaticEngine.Engine = engine;
}

public string GetName()
{
return _engine.HandleCallWithReturn<string>(0, "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 IServiceFactory_MockMemberExtensions
{
public static global::TUnit.Mocks.MockMethodCall<string> GetName(this global::TUnit.Mocks.Mock<global::TUnit.Mocks.Generated.IServiceFactory_Mockable> mock)
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<string>(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "GetName", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<global::ClientConfig> CreateDefaultConfig(this global::TUnit.Mocks.Mock<global::TUnit.Mocks.Generated.IServiceFactory_Mockable> mock)
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<global::ClientConfig>(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "CreateDefaultConfig", matchers);
}

extension(global::TUnit.Mocks.Mock<global::TUnit.Mocks.Generated.IServiceFactory_Mockable> mock)
{
public global::TUnit.Mocks.PropertyMockCall<string> ServiceId
=> new(global::TUnit.Mocks.Mock.GetEngine(mock), 2, 3, "ServiceId", true, true);
}
}
}
Loading
Loading