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
2 changes: 2 additions & 0 deletions src/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,8 @@ private static MockMemberModel CreateIndexerModel(IPropertySymbol indexer, ref i
DeclaringInterfaceName = declaringInterfaceName,
NullableAnnotation = indexer.Type.NullableAnnotation.ToString(),
SmartDefault = indexer.Type.GetSmartDefault(indexer.Type.IsNullableAnnotated()),
IsAbstractMember = indexer.IsAbstract,
IsVirtualMember = indexer.IsVirtual || indexer.IsOverride,
OverrideAccessModifier = overrideAccessModifier,
GetterAccessModifier = GetAccessorAccessModifier(indexer.GetMethod, overrideAccessModifier, compilationAssembly),
SetterAccessModifier = GetAccessorAccessModifier(indexer.SetMethod, overrideAccessModifier, compilationAssembly),
Expand Down
39 changes: 39 additions & 0 deletions tests/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2382,6 +2382,45 @@ public class TestUsage
AssertNoGeneratedError(source, "CS0535");
}

[Test]
public Task Abstract_Class_With_Abstract_Indexer()
{
// Regression #6516: an abstract indexer took the virtual-member path and emitted a
// `base[...]` fallback — CS0205, there is no base implementation. Abstract indexers must
// dispatch through the engine only, like abstract methods/properties (DbDataReader shape).
var source = """
using TUnit.Mocks;

public abstract class Repository
{
public abstract object this[int index] { get; }
public abstract object this[string key] { get; }
public abstract string this[int a, int b] { get; set; }
public virtual string this[bool flag] { get => "base"; set { } }
}

public class TestUsage
{
void M()
{
var mock = Repository.Mock();
}
}
""";

var output = GetGeneratedOutput(source);

// Virtual indexer keeps its base fallback; abstract ones must not reference base at all.
AssertContains(output, "return base[flag];");
AssertDoesNotContain(output, "return base[index];");
AssertDoesNotContain(output, "return base[key];");
AssertDoesNotContain(output, "base[a, b]");

AssertNoGeneratedError(source, "CS0205");

return VerifyGeneratorOutput(source);
}

private static void AssertNoGeneratedError(string source, string errorId)
{
foreach (var diagnostic in GetGeneratedCompilationErrors(source))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// <auto-generated/>
#pragma warning disable
#nullable enable

file sealed class RepositoryMockImpl : global::Repository, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
{
private readonly global::TUnit.Mocks.MockEngine<global::Repository> _engine;

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; }

[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
internal RepositoryMockImpl(global::TUnit.Mocks.MockEngine<global::Repository> engine) : base()
{
_engine = engine;
}

public override object this[int index]
{
get => _engine.HandleCallWithReturn<object>(0, "get_Item", new object?[] { index }, default!);
}

public override object this[string key]
{
get => _engine.HandleCallWithReturn<object>(1, "get_Item", new object?[] { key }, default!);
}

public override string this[int a, int b]
{
get => _engine.HandleCallWithReturn<string>(2, "get_Item", new object?[] { a, b }, "");
set => _engine.HandleCall(3, "set_Item", new object?[] { a, b, value });
}

public override string this[bool flag]
{
get
{
if (_engine.TryHandleCallWithReturn<string>(4, "get_Item", new object?[] { flag }, "", out var __result))
{
return __result;
}
return base[flag];
}
set
{
if (!_engine.TryHandleCall(5, "set_Item", new object?[] { flag, value }))
{
base[flag] = value;
}
}
}

[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 static class RepositoryPartialMockFactory
{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Register()
{
global::TUnit.Mocks.MockRegistry.RegisterFactory<global::Repository>(Create);
}

private static global::TUnit.Mocks.Mock<global::Repository> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::Repository>(behavior);
var impl = new RepositoryMockImpl(engine);
engine.Raisable = impl;
var mock = new global::TUnit.Mocks.Mock<global::Repository>(impl, engine);
return mock;
}
}


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

// <auto-generated/>
#pragma warning disable
#nullable enable

namespace TUnit.Mocks.Generated
{
public static class Repository_MockMemberExtensions
{
public static global::TUnit.Mocks.MockMethodCall<object> Item(this global::TUnit.Mocks.Mock<global::Repository> mock, global::TUnit.Mocks.Arguments.Arg<int> index)
{
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { index.Matcher };
return new global::TUnit.Mocks.MockMethodCall<object>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "get_Item", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<object> Item(this global::TUnit.Mocks.Mock<global::Repository> mock, global::TUnit.Mocks.Arguments.Arg<string> key)
{
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { key.Matcher };
return new global::TUnit.Mocks.MockMethodCall<object>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "get_Item", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<string> Item(this global::TUnit.Mocks.Mock<global::Repository> mock, global::TUnit.Mocks.Arguments.Arg<int> a, global::TUnit.Mocks.Arguments.Arg<int> b)
{
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { a.Matcher, b.Matcher };
return new global::TUnit.Mocks.MockMethodCall<string>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "get_Item", matchers);
}

public static global::TUnit.Mocks.VoidMockMethodCall SetItem(this global::TUnit.Mocks.Mock<global::Repository> mock, global::TUnit.Mocks.Arguments.Arg<int> a, global::TUnit.Mocks.Arguments.Arg<int> b, global::TUnit.Mocks.Arguments.Arg<string> value)
{
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { a.Matcher, b.Matcher, value.Matcher };
return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "set_Item", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<string> Item(this global::TUnit.Mocks.Mock<global::Repository> mock, global::TUnit.Mocks.Arguments.Arg<bool> flag)
{
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { flag.Matcher };
return new global::TUnit.Mocks.MockMethodCall<string>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 4, "get_Item", matchers);
}

public static global::TUnit.Mocks.VoidMockMethodCall SetItem(this global::TUnit.Mocks.Mock<global::Repository> mock, global::TUnit.Mocks.Arguments.Arg<bool> flag, global::TUnit.Mocks.Arguments.Arg<string> value)
{
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { flag.Matcher, value.Matcher };
return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 5, "set_Item", matchers);
}

#if NET9_0_OR_GREATER
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
public static void Reset(this global::TUnit.Mocks.Mock<global::Repository> mock)
=> global::TUnit.Mocks.Mock.Reset(mock);

[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
public static void VerifyAll(this global::TUnit.Mocks.Mock<global::Repository> mock)
=> global::TUnit.Mocks.Mock.VerifyAll(mock);

[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
public static void VerifyNoOtherCalls(this global::TUnit.Mocks.Mock<global::Repository> mock)
=> global::TUnit.Mocks.Mock.VerifyNoOtherCalls(mock);

[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
public static void SetupAllProperties(this global::TUnit.Mocks.Mock<global::Repository> mock)
=> global::TUnit.Mocks.Mock.SetupAllProperties(mock);

[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
public static global::TUnit.Mocks.Diagnostics.MockDiagnostics GetDiagnostics(this global::TUnit.Mocks.Mock<global::Repository> mock)
=> global::TUnit.Mocks.Mock.GetDiagnostics(mock);

[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
public static void SetState(this global::TUnit.Mocks.Mock<global::Repository> mock, string? stateName)
=> global::TUnit.Mocks.Mock.SetState(mock, stateName);

[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
public static void InState(this global::TUnit.Mocks.Mock<global::Repository> mock, string stateName, global::System.Action<global::TUnit.Mocks.Mock<global::Repository>> configure)
=> global::TUnit.Mocks.Mock.InState(mock, stateName, configure);

extension(global::TUnit.Mocks.Mock<global::Repository> mock)
{
public global::System.Collections.Generic.IReadOnlyList<global::TUnit.Mocks.Verification.CallRecord> Invocations => global::TUnit.Mocks.Mock.Invocations(mock);

public global::TUnit.Mocks.MockBehavior Behavior => global::TUnit.Mocks.Mock.Behavior(mock);

public global::TUnit.Mocks.IDefaultValueProvider? DefaultValueProvider
{
get => global::TUnit.Mocks.Mock.GetDefaultValueProvider(mock);
set => global::TUnit.Mocks.Mock.SetDefaultValueProvider(mock, value);
}
}
#endif
}
}


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

// <auto-generated/>
#pragma warning disable
#nullable enable

namespace TUnit.Mocks
{
public static class Repository_MockStaticExtension
{
extension(global::Repository _)
{
public static global::TUnit.Mocks.Mock<global::Repository> Mock()
{
return global::TUnit.Mocks.Mock.Of<global::Repository>();
}

public static global::TUnit.Mocks.Mock<global::Repository> Mock(global::TUnit.Mocks.MockBehavior behavior)
{
return global::TUnit.Mocks.Mock.Of<global::Repository>(behavior);
}
}
}
}


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

// <auto-generated/>
#pragma warning disable
#nullable enable

namespace TUnit.Mocks.Generated;
89 changes: 89 additions & 0 deletions tests/TUnit.Mocks.Tests/Issue6516Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using TUnit.Mocks;
using TUnit.Mocks.Arguments;
using TUnit.Mocks.Verification;

namespace TUnit.Mocks.Tests;

// Regression: https://github.com/thomhurst/TUnit/issues/6516
// The indexer model never carried IsAbstractMember, so an abstract indexer on a mocked
// abstract class took the virtual-member path and emitted a `return base[...];` /
// `base[...] = value;` fallback — CS0205, there is no base implementation to call.
// Abstract indexers must dispatch through the engine only, exactly like abstract
// methods and abstract non-indexer properties already do.

#region Test types

// The issue's repro shape (mirrors DbDataReader's `this[int]` / `this[string]`).
public abstract class AbstractIndexerRepository
{
public abstract object this[int index] { get; }
public abstract object this[string key] { get; }
}

// Exercises the abstract setter branch too.
public abstract class AbstractReadWriteIndexer
{
public abstract string this[int index] { get; set; }
}

// Virtual indexer must keep its base fallback.
public class VirtualIndexerStore
{
public virtual string this[int index]
{
get => $"base-{index}";
set { }
}
}

#endregion

public class Issue6516Tests
{
[Test]
public async Task Abstract_GetOnly_Indexers_Compile_And_Return_Default_When_Unconfigured()
{
var mock = AbstractIndexerRepository.Mock();

await Assert.That(mock.Object[0]).IsNull();
await Assert.That(mock.Object["missing"]).IsNull();
}

[Test]
public async Task Abstract_Indexer_Configured_Via_Setup_Returns_Value()
{
var mock = AbstractIndexerRepository.Mock();
mock.Item(3).Returns("three");
mock.Item("name").Returns("by-key");

await Assert.That(mock.Object[3]).IsEqualTo("three");
await Assert.That(mock.Object["name"]).IsEqualTo("by-key");

mock.Item(3).WasCalled(Times.Once);
mock.Item("name").WasCalled(Times.Once);
}

[Test]
public async Task Abstract_ReadWrite_Indexer_Setter_Dispatches_Through_Engine()
{
var mock = AbstractReadWriteIndexer.Mock();
mock.Item(1).Returns("one");

await Assert.That(mock.Object[1]).IsEqualTo("one");

mock.Object[7] = "seven";

mock.SetItem(7, "seven").WasCalled(Times.Once);
mock.SetItem(Any<int>(), Any<string>()).WasCalled(Times.Once);
}

[Test]
public async Task Virtual_Indexer_Still_Falls_Back_To_Base_When_Unconfigured()
{
var mock = VirtualIndexerStore.Mock();
mock.Item(1).Returns("configured");

await Assert.That(mock.Object[1]).IsEqualTo("configured");
await Assert.That(mock.Object[2]).IsEqualTo("base-2");
}
}
Loading