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

[Test]
public void Generic_Interface_Inheriting_IEnumerable_In_Transitive_AutoMock_Generates_Open_Generic_Mock()
{
// Regression for #5567: transitive auto-mock generation for a generic
// interface should emit a reusable open-generic mock shape, not duplicate
// closed/open artifacts that break the user's build.
var source = """
#nullable enable
using System.Collections.Generic;
using TUnit.Mocks;

public interface ITest
{
ITestEnum<string> TestEnum { get; }
ITestEnum<T> Create<T>();
}

public interface ITestEnum<T> : IEnumerable<T>
{
T? GetTest();
}

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

var generated = RunGenerator(source);
var combined = string.Join(Environment.NewLine, generated);

if (!combined.Contains("class ITestEnum_T_MockImpl<T>", StringComparison.Ordinal)
|| !combined.Contains("class ITestEnum_T_Mock<T>", StringComparison.Ordinal)
|| !combined.Contains("RegisterOpenGenericFactory(", StringComparison.Ordinal)
|| !combined.Contains("typeof(global::ITestEnum<>)", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Expected open-generic transitive mock generation artifacts were not produced.");
}
}

[Test]
public Task Interface_Inheriting_Multiple_Interfaces()
{
Expand Down
57 changes: 53 additions & 4 deletions TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace TUnit.Mocks.SourceGenerator.Tests;
public abstract class SnapshotTestBase
{
private static readonly Lazy<List<PortableExecutableReference>> _references = new(LoadReferences);
private static readonly UTF8Encoding SnapshotEncoding = new(encoderShouldEmitUTF8Identifier: false);

private static List<PortableExecutableReference> LoadReferences()
{
Expand Down Expand Up @@ -125,12 +126,60 @@ protected static Task VerifyGeneratorOutput(
return VerifySnapshot(RunGeneratorAndFormat(source), testName, filePath);
}

protected static void AssertGeneratedCodeCompiles(
string source,
IEnumerable<MetadataReference>? additionalReferences = null,
CSharpParseOptions? parseOptions = null)
{
var compileErrors = GetGeneratedCompilationErrors(source, additionalReferences, parseOptions);

if (compileErrors.Count > 0)
{
var errorMessages = string.Join(Environment.NewLine, compileErrors.Select(e => e.ToString()));
throw new InvalidOperationException($"Generated compilation failed:{Environment.NewLine}{errorMessages}");
}
}

protected static IReadOnlyList<Diagnostic> GetGeneratedCompilationErrors(
string source,
IEnumerable<MetadataReference>? additionalReferences = null,
CSharpParseOptions? parseOptions = null)
{
parseOptions ??= CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview);
var syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions);

IEnumerable<MetadataReference> refs = additionalReferences is null
? _references.Value
: _references.Value.Concat(additionalReferences);

var inputCompilation = CSharpCompilation.Create(
"TestAssembly",
[syntaxTree],
refs,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

var generator = new MockGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], parseOptions: parseOptions);
driver = driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var outputCompilation, out var generatorDiagnostics);

var generatorErrors = generatorDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
if (generatorErrors.Count > 0)
{
var errorMessages = string.Join(Environment.NewLine, generatorErrors.Select(e => e.ToString()));
throw new InvalidOperationException($"Generator produced errors:{Environment.NewLine}{errorMessages}");
}

return outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.ToList();
}

private static async Task VerifySnapshot(
string generatedOutput,
string testName,
string filePath)
{
generatedOutput = NormalizeNewlines(generatedOutput);
generatedOutput = NormalizeNewlines(generatedOutput).TrimStart('\uFEFF');

var testDir = Path.GetDirectoryName(filePath)!;
var receivedPath = Path.Combine(testDir, "Snapshots", $"{testName}.received.txt");
Expand All @@ -142,18 +191,18 @@ private static async Task VerifySnapshot(
if (!File.Exists(verifiedPath))
{
// Write .received.txt for review and fail — never auto-accept
await File.WriteAllTextAsync(receivedPath, generatedOutput);
await File.WriteAllTextAsync(receivedPath, generatedOutput, SnapshotEncoding);
throw new InvalidOperationException(
$"No verified snapshot found for '{testName}'.\n" +
$"Review: {receivedPath}\n" +
$"Accept by renaming to '.verified.txt'.");
}

var verified = NormalizeNewlines(await File.ReadAllTextAsync(verifiedPath));
var verified = NormalizeNewlines(await File.ReadAllTextAsync(verifiedPath)).TrimStart('\uFEFF');

if (!string.Equals(generatedOutput, verified, StringComparison.Ordinal))
{
await File.WriteAllTextAsync(receivedPath, generatedOutput);
await File.WriteAllTextAsync(receivedPath, generatedOutput, SnapshotEncoding);
throw new InvalidOperationException(
$"Snapshot mismatch for '{testName}'.\n" +
$"Received: {receivedPath}\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ namespace TUnit.Mocks
{
public static class MyService_MockStaticExtension
{
extension(global::MyService)
extension(global::MyService _)
{
public static global::TUnit.Mocks.Mock<global::MyService> Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ namespace TUnit.Mocks
{
public static class MyService_MockStaticExtension
{
extension(global::MyService)
extension(global::MyService _)
{
public static global::TUnit.Mocks.Mock<global::MyService> Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ namespace TUnit.Mocks
{
public static class MyService_MockStaticExtension
{
extension(global::MyService)
extension(global::MyService _)
{
public static global::TUnit.Mocks.Mock<global::MyService> Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose)
{
Expand Down
Loading
Loading