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

return VerifyGeneratorOutput(source);
}

[Test]
public void Global_Namespace_Mock_Does_Not_Emit_Empty_Namespace_Prefix()
{
// Regression: when an interface lives in the global namespace, the
// builders previously emitted `global::.IGreeterMock` because they
// unconditionally interpolated `$"global::{mockNamespace}.{TypeName}"`
// and `mockNamespace` is "" for global-namespace types in
// non-fallback mode. That syntax fails to compile (CS1001).
//
// We assert the textual form rather than calling AssertGeneratedCodeCompiles
// because the generated `extension(...)` blocks are not parseable by the
// test-pinned Roslyn (see SnapshotTestBase.AssertGeneratedCodeHasNoNullableWarnings
// remarks). Searching for the broken `global::.` substring directly catches
// the regression no matter where it surfaces (MockStaticExtensionBuilder,
// MockMembersBuilder, GetMockableTypeName, etc.).
var source = """
using TUnit.Mocks;

public interface IGreeter
{
string Greet(string name);
}

public class TestUsage
{
void M()
{
var mock = Mock.Of<IGreeter>();
_ = mock.Greet("hi");
}
}
""";

var generated = RunGenerator(source);
var combined = string.Join("\n", generated);

if (combined.Contains("global::."))
{
var lines = combined.Split('\n')
.Select((line, idx) => (line, idx))
.Where(t => t.line.Contains("global::."))
.Select(t => $" line {t.idx + 1}: {t.line.Trim()}")
.ToList();

throw new InvalidOperationException(
"Generated code contains invalid 'global::.' (empty-namespace) prefix. "
+ "A builder is concatenating `$\"global::{ns}.{Name}\"` without guarding "
+ "for empty `ns`. Use `MockImplBuilder.GetGlobalMockNamespacePrefix(model)` "
+ "instead.\nOffending lines:\n"
+ string.Join("\n", lines));
}
}

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

namespace MyApp;

public interface IGreeter { string Greet(string name); }

// User already has a class named IGreeterMock — generator must fall back.
public class IGreeterMock { }

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

return VerifyGeneratorOutput(source);
}
}
162 changes: 162 additions & 0 deletions TUnit.Mocks.SourceGenerator.Tests/MockNamespaceConflictTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Mocks.SourceGenerator.Discovery;

namespace TUnit.Mocks.SourceGenerator.Tests;

/// <summary>
/// Tests for <see cref="MockNamespaceConflictDetector"/> — the helper that decides
/// whether placing a generated mock alongside its target type would collide with an
/// existing user-declared type in the same namespace.
/// </summary>
public class MockNamespaceConflictTests : SnapshotTestBase
{
[Test]
public async Task NoConflict_ReturnsFalse()
{
var source = """
namespace MyApp
{
public interface IGreeter
{
string Greet(string name);
}
}
""";

var (compilation, target) = CompileAndGetType(source, "MyApp.IGreeter");

var result = MockNamespaceConflictDetector.HasConflict(compilation, target);

await Assert.That(result).IsFalse();
}

[Test]
public async Task TypeNamedFooMockExists_ReturnsTrue()
{
var source = """
namespace MyApp
{
public interface IGreeter
{
string Greet(string name);
}

public class IGreeterMock
{
}
}
""";

var (compilation, target) = CompileAndGetType(source, "MyApp.IGreeter");

var result = MockNamespaceConflictDetector.HasConflict(compilation, target);

await Assert.That(result).IsTrue();
}

[Test]
public async Task EventsExtensionsCollision_AlwaysFlagged()
{
// The detector treats every emitted-suffix collision as a conflict, regardless
// of whether the target type actually has events. Keeping the rule symmetric
// ensures both call sites — model construction and auto-mock factory references
// — reach the same fallback decision without re-deriving event lists.
var source = """
namespace MyApp
{
public interface IGreeter
{
string Greet(string name);
}

public static class IGreeter_MockEventsExtensions
{
}
}
""";

var (compilation, target) = CompileAndGetType(source, "MyApp.IGreeter");

var result = MockNamespaceConflictDetector.HasConflict(compilation, target);

await Assert.That(result).IsTrue();
}

[Test]
public async Task GlobalNamespace_ChecksGlobalTypes()
{
var source = """
public interface IGreeter
{
string Greet(string name);
}

public class IGreeterMock
{
}
""";

var (compilation, target) = CompileAndGetType(source, "IGreeter");

var result = MockNamespaceConflictDetector.HasConflict(compilation, target);

await Assert.That(result).IsTrue();
}

[Test]
public async Task NestedNamespace_ChecksDeepPath()
{
var (compilation, type) = CompileAndGetType("""
namespace A.B.C;
public interface IGreeter { string Greet(string name); }
public class IGreeterMock { }
""", "A.B.C.IGreeter");

await Assert.That(MockNamespaceConflictDetector.HasConflict(compilation, type)).IsTrue();
}

[Test]
public async Task GenericType_DetectsCollisionByBareName()
{
var (compilation, type) = CompileAndGetType("""
namespace MyApp;
public interface IGreeter<T> { T Greet(string name); }
public class IGreeterMock { }
""", "MyApp.IGreeter`1");

await Assert.That(MockNamespaceConflictDetector.HasConflict(compilation, type)).IsTrue();
}

[Test]
public async Task ExternalAssemblyType_WithoutCollisionInConsumer_ReturnsFalse()
{
// Documents the cross-assembly contract: types referenced from another assembly
// are checked against the consumer's compilation. Absence of a colliding type in
// the consumer's view of that namespace returns false (no conflict).
var externalRef = CreateExternalAssemblyReference("""
namespace ExternalLib;
public interface IGreeter { string Greet(string name); }
""", assemblyName: "ExtLib");

// Consumer source declares NO ExternalLib namespace at all.
var tree = CSharpSyntaxTree.ParseText("class C {}",
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview));
var compilation = CSharpCompilation.Create(
"Consumer", [tree], GetCachedReferences().Append(externalRef));

var type = compilation.GetTypeByMetadataName("ExternalLib.IGreeter")!;

await Assert.That(MockNamespaceConflictDetector.HasConflict(compilation, type)).IsFalse();
}

private static (Compilation, INamedTypeSymbol) CompileAndGetType(string source, string fullyQualifiedName)
{
var tree = CSharpSyntaxTree.ParseText(source,
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview));
var compilation = CSharpCompilation.Create("Test", [tree], GetCachedReferences());
var type = compilation.GetTypeByMetadataName(fullyQualifiedName)
?? throw new InvalidOperationException($"Type not found: {fullyQualifiedName}");
return (compilation, type);
}
}
7 changes: 7 additions & 0 deletions TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ public abstract class SnapshotTestBase
private static readonly Lazy<List<PortableExecutableReference>> _references = new(LoadReferences);
private static readonly UTF8Encoding SnapshotEncoding = new(encoderShouldEmitUTF8Identifier: false);

/// <summary>
/// Returns the shared, lazily-loaded set of metadata references used by the test compilations
/// (current AppDomain assemblies plus the <c>ref/TUnit.Mocks.dll</c> if present). Derived
/// test classes should reuse this instead of re-discovering references per test invocation.
/// </summary>
protected static IEnumerable<MetadataReference> GetCachedReferences() => _references.Value;

private static List<PortableExecutableReference> LoadReferences()
{
var refs = AppDomain.CurrentDomain.GetAssemblies()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,53 @@
#pragma warning disable
#nullable enable

namespace TUnit.Mocks.Generated
file sealed class StaticAbstractImplMockImpl : global::StaticAbstractImpl, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
{
file sealed class StaticAbstractImplMockImpl : global::StaticAbstractImpl, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
{
private readonly global::TUnit.Mocks.MockEngine<global::StaticAbstractImpl> _engine;
private readonly global::TUnit.Mocks.MockEngine<global::StaticAbstractImpl> _engine;

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; }
[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 StaticAbstractImplMockImpl(global::TUnit.Mocks.MockEngine<global::StaticAbstractImpl> engine) : base()
{
_engine = engine;
}
[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
internal StaticAbstractImplMockImpl(global::TUnit.Mocks.MockEngine<global::StaticAbstractImpl> engine) : base()
{
_engine = engine;
}

public override int InstanceValue
public override int InstanceValue
{
get
{
get
if (_engine.TryHandleCallWithReturn<int>(0, "get_InstanceValue", global::System.Array.Empty<object?>(), default, out var __result))
{
if (_engine.TryHandleCallWithReturn<int>(0, "get_InstanceValue", global::System.Array.Empty<object?>(), default, out var __result))
{
return __result;
}
return base.InstanceValue;
return __result;
}
return base.InstanceValue;
}
}

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

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

Expand All @@ -62,15 +59,12 @@ namespace TUnit.Mocks.Generated
#pragma warning disable
#nullable enable

namespace TUnit.Mocks.Generated
public static class StaticAbstractImpl_MockMemberExtensions
{
public static class StaticAbstractImpl_MockMemberExtensions
extension(global::TUnit.Mocks.Mock<global::StaticAbstractImpl> mock)
{
extension(global::TUnit.Mocks.Mock<global::StaticAbstractImpl> mock)
{
public global::TUnit.Mocks.PropertyMockCall<int> InstanceValue
=> new(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, 0, "InstanceValue", true, false);
}
public global::TUnit.Mocks.PropertyMockCall<int> InstanceValue
=> new(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, 0, "InstanceValue", true, false);
}
}

Expand Down
Loading
Loading