diff --git a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 1c5e905790..af0d054e02 100644 --- a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -1725,40 +1725,7 @@ private static string StripGlobalPrefix(string name) => name.StartsWith("global::") ? name.Substring("global::".Length) : name; private static string SanitizeIdentifier(string name) - { - name = name.Replace("global::", ""); - - var sb = new System.Text.StringBuilder(name.Length); - var lastWasUnderscore = false; - - foreach (var c in name) - { - if (c == ' ') - continue; - - if (char.IsLetterOrDigit(c) || c == '_') - { - if (c == '_') - { - if (lastWasUnderscore) - continue; - lastWasUnderscore = true; - } - else - { - lastWasUnderscore = false; - } - sb.Append(c); - } - else if (!lastWasUnderscore) - { - sb.Append('_'); - lastWasUnderscore = true; - } - } - - return sb.ToString(); - } + => IdentifierEscaping.SanitizeIdentifier(name); /// /// Root namespace for fallback-mode mock emission, used when the original namespace diff --git a/src/TUnit.Mocks.SourceGenerator/Diagnostics.cs b/src/TUnit.Mocks.SourceGenerator/Diagnostics.cs new file mode 100644 index 0000000000..c6bf7dcff5 --- /dev/null +++ b/src/TUnit.Mocks.SourceGenerator/Diagnostics.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis; + +namespace TUnit.Mocks.SourceGenerator; + +/// +/// Diagnostics reported by the generator itself. Everything the analyzer can see at a call site +/// belongs in TUnit.Mocks.Analyzers (TM001-TM007); this file is for failures only the generator +/// can observe, which is currently just whole-compilation name collisions. +/// +internal static class Diagnostics +{ + public static readonly DiagnosticDescriptor TM008_GeneratedNameCollision = new( + id: "TM008", + title: "Mocked types produce the same generated name", + messageFormat: "Cannot mock '{0}' because it produces the same generated name '{1}' as '{2}'. Rename one of the types or namespaces.", + category: "TUnit.Mocks", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Generated type and file names are derived from the mocked type's fully qualified name with separators replaced by underscores. Two types can still map to the same name when their namespaces differ only in how underscores and dots are arranged (e.g. 'A_.B.IFoo' and 'A._B.IFoo'). Emitting both would give Roslyn duplicate hint names, which discards every mock in the compilation without saying why, so generation is skipped for the colliding types and reported here instead." + ); +} diff --git a/src/TUnit.Mocks.SourceGenerator/Discovery/GeneratedNameCollisionDetector.cs b/src/TUnit.Mocks.SourceGenerator/Discovery/GeneratedNameCollisionDetector.cs new file mode 100644 index 0000000000..945cd4b790 --- /dev/null +++ b/src/TUnit.Mocks.SourceGenerator/Discovery/GeneratedNameCollisionDetector.cs @@ -0,0 +1,82 @@ +using TUnit.Mocks.SourceGenerator.Builders; +using TUnit.Mocks.SourceGenerator.Models; + +namespace TUnit.Mocks.SourceGenerator.Discovery; + +/// +/// Flags mocked types whose generated names collide. +/// +/// Generated type and AddSource hint names come from the mocked type's fully qualified name +/// with separators mapped to _. doubles +/// literal underscores so the realistic cases stay distinct, but no mapping onto +/// [A-Za-z0-9_] can be injective while a separator and an underscore both render as runs of +/// _: a run of three cannot say whether it was underscore-then-separator or the reverse, so +/// A_.B.IFoo and A._B.IFoo still meet at A___B_IFoo. +/// +/// +/// A duplicate hint name aborts the whole generator, which costs the user every mock in the +/// compilation and reports only a CS8785 warning pointing at the generator rather than at either +/// type. Skipping the colliding types and reporting TM008 keeps the rest of the compilation's +/// mocks and names both culprits. See issue #6505. +/// +/// +internal static class GeneratedNameCollisionDetector +{ + /// + /// Returns in input order, with + /// set on every model that shares its generated name with another. + /// + internal static List Annotate(IEnumerable models) + { + var ordered = models.ToList(); + + // The name alone is not the key: a multi-interface combo and the secondary setup surface + // for the same (primary, interface) pair intentionally share a composite name and are told + // apart by the hint-name suffix, so they must not be flagged. + var groups = new Dictionary<(bool IsSecondaryMemberSurface, string Name), List>(); + + foreach (var model in ordered) + { + var key = (model.IsSecondaryMemberSurface, MockImplBuilder.GetCompositeSafeName(model)); + + if (!groups.TryGetValue(key, out var group)) + { + groups[key] = group = new List(); + } + + group.Add(model); + } + + if (groups.Count == ordered.Count) + { + return ordered; + } + + var annotated = new List(ordered.Count); + + foreach (var model in ordered) + { + var group = groups[(model.IsSecondaryMemberSurface, MockImplBuilder.GetCompositeSafeName(model))]; + + // Same target mocked in more than one mode (Mock.Of and Mock.Wrap of one type, say) + // reaches this point as separate models sharing an identity. Only distinct targets + // meeting at one name are a #6505 collision. + var others = group + .Where(other => Identity(other) != Identity(model)) + .Select(other => other.FullyQualifiedName) + .Distinct() + .ToList(); + + annotated.Add(others.Count == 0 + ? model + : model with { CollidesWith = string.Join(", ", others) }); + } + + return annotated; + } + + private static string Identity(MockTypeModel model) + => model.AdditionalInterfaceNames.Length == 0 + ? model.FullyQualifiedName + : model.FullyQualifiedName + "|" + string.Join("|", model.AdditionalInterfaceNames); +} diff --git a/src/TUnit.Mocks.SourceGenerator/Extensions/TypeSymbolExtensions.cs b/src/TUnit.Mocks.SourceGenerator/Extensions/TypeSymbolExtensions.cs index e641c05946..619729e418 100644 --- a/src/TUnit.Mocks.SourceGenerator/Extensions/TypeSymbolExtensions.cs +++ b/src/TUnit.Mocks.SourceGenerator/Extensions/TypeSymbolExtensions.cs @@ -304,40 +304,5 @@ private static string StripGlobalPrefix(string name) => name.StartsWith("global::") ? name.Substring("global::".Length) : name; private static string SanitizeIdentifier(string name) - { - name = name.Replace("global::", ""); - - var sb = new StringBuilder(name.Length); - var lastWasUnderscore = false; - - foreach (var c in name) - { - if (c == ' ') - continue; - - if (char.IsLetterOrDigit(c) || c == '_') - { - if (c == '_') - { - if (lastWasUnderscore) - continue; - - lastWasUnderscore = true; - } - else - { - lastWasUnderscore = false; - } - - sb.Append(c); - } - else if (!lastWasUnderscore) - { - sb.Append('_'); - lastWasUnderscore = true; - } - } - - return sb.ToString(); - } + => IdentifierEscaping.SanitizeIdentifier(name); } diff --git a/src/TUnit.Mocks.SourceGenerator/IdentifierEscaping.cs b/src/TUnit.Mocks.SourceGenerator/IdentifierEscaping.cs index 651679acaf..fd9449eb99 100644 --- a/src/TUnit.Mocks.SourceGenerator/IdentifierEscaping.cs +++ b/src/TUnit.Mocks.SourceGenerator/IdentifierEscaping.cs @@ -1,3 +1,4 @@ +using System.Text; using Microsoft.CodeAnalysis.CSharp; namespace TUnit.Mocks.SourceGenerator; @@ -26,4 +27,52 @@ internal static class IdentifierEscaping // interface implementation matching against the source-declared member name. internal static string EscapeIdentifier(string name) => SyntaxFacts.GetKeywordKind(name) != SyntaxKind.None ? "@" + name : name; + + /// + /// Turns a type name (fully qualified, or a bare name with generic arguments) into a single + /// C# identifier, used for generated type names and AddSource hint names. + /// + /// The mapping must be injective: a hint-name clash makes Roslyn drop the generated sources + /// for both types with no diagnostic, so the user only ever sees the downstream + /// CS1061/CS0117 from the missing surface. Every literal _ is therefore doubled before + /// separators become _, so a single underscore in the result always came from a + /// separator. Without that, A_B.IFoo and A.B.IFoo both produced + /// A_B_IFoo — see issue #6505. + /// + /// + internal static string SanitizeIdentifier(string name) + { + name = name.Replace("global::", ""); + + var sb = new StringBuilder(name.Length); + var lastWasSeparator = false; + + foreach (var c in name) + { + if (c == ' ') + { + continue; + } + + if (c == '_') + { + sb.Append("__"); + lastWasSeparator = false; + } + else if (char.IsLetterOrDigit(c)) + { + sb.Append(c); + lastWasSeparator = false; + } + else if (!lastWasSeparator) + { + // Runs of separators still collapse to one '_' ("IFoo" -> "IFoo_T_"); only the + // separator/underscore distinction has to survive. + sb.Append('_'); + lastWasSeparator = true; + } + } + + return sb.ToString(); + } } diff --git a/src/TUnit.Mocks.SourceGenerator/MockGenerator.cs b/src/TUnit.Mocks.SourceGenerator/MockGenerator.cs index 9be21f689e..7055d807dc 100644 --- a/src/TUnit.Mocks.SourceGenerator/MockGenerator.cs +++ b/src/TUnit.Mocks.SourceGenerator/MockGenerator.cs @@ -57,12 +57,27 @@ namespace TUnit.Mocks.Generated; foreach (var m in mockOfTypes) set.Add(m); foreach (var m in attributeTypes) set.Add(m); foreach (var m in extensionInvocations) set.Add(m); - return set; + + // Flag types that would emit the same generated names before anything is written: + // duplicate hint names abort the generator and take every mock in the compilation + // with them. See issue #6505. + return GeneratedNameCollisionDetector.Annotate(set); }); // Step 3: Generate source for each unique type context.RegisterSourceOutput(distinctTypes, (spc, model) => { + if (model.CollidesWith is not null) + { + spc.ReportDiagnostic(Diagnostic.Create( + Diagnostics.TM008_GeneratedNameCollision, + Location.None, + model.FullyQualifiedName, + MockImplBuilder.GetCompositeSafeName(model), + model.CollidesWith)); + return; + } + if (model.IsSecondaryMemberSurface) { // Pair model: the shared setup/verify surface for one additional interface of a diff --git a/src/TUnit.Mocks.SourceGenerator/Models/MockTypeModel.cs b/src/TUnit.Mocks.SourceGenerator/Models/MockTypeModel.cs index d15bf18020..dd3d4e9c57 100644 --- a/src/TUnit.Mocks.SourceGenerator/Models/MockTypeModel.cs +++ b/src/TUnit.Mocks.SourceGenerator/Models/MockTypeModel.cs @@ -63,6 +63,15 @@ internal sealed record MockTypeModel : IEquatable /// The C# visibility keyword to emit on generated wrapper/extension types. public string Visibility => IsPublic ? "public" : "internal"; + /// + /// Set by when another mocked type in + /// the same compilation sanitizes to the same generated name (see issue #6505). Generation is + /// skipped and TM008 is reported, because emitting both would give Roslyn duplicate hint names + /// — which silently discards every mock in the compilation. Holds the other type's fully + /// qualified name (comma separated when more than one collides). + /// + public string? CollidesWith { get; init; } + /// /// True for a class target that exposes no constructor the generated impl could chain to. /// The impl derives from the target, so with every constructor private (or cross-assembly @@ -98,6 +107,7 @@ public bool Equals(MockTypeModel? other) && Constructors.Equals(other.Constructors) && HasStaticAbstractMembers == other.HasStaticAbstractMembers && IsSecondaryMemberSurface == other.IsSecondaryMemberSurface + && CollidesWith == other.CollidesWith && SecondaryMemberIdMaps.Equals(other.SecondaryMemberIdMaps); } @@ -120,6 +130,7 @@ public override int GetHashCode() hash = hash * 31 + AdditionalInterfaceNames.GetHashCode(); hash = hash * 31 + HasStaticAbstractMembers.GetHashCode(); hash = hash * 31 + IsSecondaryMemberSurface.GetHashCode(); + hash = hash * 31 + (CollidesWith?.GetHashCode() ?? 0); hash = hash * 31 + SecondaryMemberIdMaps.GetHashCode(); return hash; } diff --git a/src/TUnit.Mocks.SourceGenerator/TUnit.Mocks.SourceGenerator.csproj b/src/TUnit.Mocks.SourceGenerator/TUnit.Mocks.SourceGenerator.csproj index 6133d99340..3b73f95f29 100644 --- a/src/TUnit.Mocks.SourceGenerator/TUnit.Mocks.SourceGenerator.csproj +++ b/src/TUnit.Mocks.SourceGenerator/TUnit.Mocks.SourceGenerator.csproj @@ -10,6 +10,8 @@ TUnit.Mocks.SourceGenerator false false + + RS2008 diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6505Tests.cs b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6505Tests.cs new file mode 100644 index 0000000000..5960eb31a1 --- /dev/null +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6505Tests.cs @@ -0,0 +1,166 @@ +namespace TUnit.Mocks.SourceGenerator.Tests; + +/// +/// Regression: https://github.com/thomhurst/TUnit/issues/6505 +/// Generated type names and AddSource hint names come from a sanitized form of the mocked +/// type's fully qualified name. That sanitization used to map every separator to _ and +/// collapse runs, so A_B.IFoo and A.B.IFoo both produced A_B_IFoo. Roslyn +/// drops both sources on a duplicate hint name without reporting anything, so mocking both types +/// in one compilation silently generated nothing for either. +/// +public class Issue6505Tests : SnapshotTestBase +{ + private const string UnderscoreNamespaceInterface = """ + namespace A_B { public interface IFoo { void Go(); } } + """; + + private const string DottedNamespaceInterface = """ + namespace A.B { public interface IFoo { void Stop(); } } + """; + + [Test] + public async Task Types_With_Colliding_Sanitized_Names_Both_Generate() + { + var source = $$""" + using TUnit.Mocks; + + {{UnderscoreNamespaceInterface}} + {{DottedNamespaceInterface}} + + public class Test + { + public void Run() + { + var one = Mock.Of(); + var two = Mock.Of(); + } + } + """; + + var generated = RunGenerator(source); + + // Each interface declares a distinct member, so both surfaces are visible in the output. + await Assert.That(generated.Any(g => g.Contains("void Go()"))).IsTrue(); + await Assert.That(generated.Any(g => g.Contains("void Stop()"))).IsTrue(); + } + + [Test] + public async Task Mocking_Both_Colliding_Types_Drops_No_Generated_File() + { + var underscoreOnly = RunGenerator(MockOf("A_B.IFoo", UnderscoreNamespaceInterface)); + var dottedOnly = RunGenerator(MockOf("A.B.IFoo", DottedNamespaceInterface)); + var both = RunGenerator(MockOf( + "A_B.IFoo", UnderscoreNamespaceInterface, + "A.B.IFoo", DottedNamespaceInterface)); + + // The post-initialization TUnit.Mocks.Generated namespace stub is the one file all three + // runs share; everything else must survive being generated alongside the other type. + await Assert.That(both.Length).IsEqualTo(underscoreOnly.Length + dottedOnly.Length - 1); + } + + [Test] + public async Task Sanitized_Names_Keep_Separators_And_Underscores_Apart() + { + // The realistic shapes must all stay distinct; a run of underscores in the output is only + // ever a literal underscore when it is doubled. + await Assert.That(IdentifierEscaping.SanitizeIdentifier("global::A.B.IFoo")).IsEqualTo("A_B_IFoo"); + await Assert.That(IdentifierEscaping.SanitizeIdentifier("global::A_B.IFoo")).IsEqualTo("A__B_IFoo"); + await Assert.That(IdentifierEscaping.SanitizeIdentifier("global::A.B_IFoo")).IsEqualTo("A_B__IFoo"); + await Assert.That(IdentifierEscaping.SanitizeIdentifier("global::A.IFoo")).IsEqualTo("A_IFoo_A_B_"); + } + + [Test] + public async Task Names_That_Still_Collide_Are_Reported_Instead_Of_Silently_Dropped() + { + // No mapping onto [A-Za-z0-9_] can be injective while both a separator and an underscore + // render as runs of '_': three underscores cannot say which order they came in, so + // 'A_.B.IFoo' and 'A._B.IFoo' both sanitize to 'A___B_IFoo'. That must fail loudly. + var source = """ + using TUnit.Mocks; + + namespace A_.B { public interface IFoo { void Go(); } } + namespace A._B { public interface IFoo { void Stop(); } } + + public class Test + { + public void Run() + { + var one = Mock.Of(); + var two = Mock.Of(); + } + } + """; + + var (sources, diagnostics) = RunGeneratorForDiagnostics(source); + + var collisions = diagnostics.Where(d => d.Id == "TM008").ToList(); + await Assert.That(collisions).HasCount(2); + await Assert.That(collisions[0].GetMessage()).Contains("A___B_IFoo"); + // Both culprits are named, so the message says what to rename. + await Assert.That(collisions.Select(d => d.GetMessage()).Any(m => m.Contains("A_.B.IFoo"))).IsTrue(); + await Assert.That(collisions.Select(d => d.GetMessage()).Any(m => m.Contains("A._B.IFoo"))).IsTrue(); + + // Nothing was emitted for either type, but the generator did not abort. + await Assert.That(sources.Any(s => s.Contains("void Go()"))).IsFalse(); + await Assert.That(sources.Any(s => s.Contains("void Stop()"))).IsFalse(); + } + + [Test] + public async Task A_Collision_Does_Not_Stop_Other_Mocks_In_The_Compilation() + { + // The pre-#6505 behaviour was a duplicate hint name aborting the generator, which took + // every unrelated mock in the compilation with it. + var source = """ + using TUnit.Mocks; + + namespace A_.B { public interface IFoo { void Go(); } } + namespace A._B { public interface IFoo { void Stop(); } } + + public interface IUnrelated { void Untouched(); } + + public class Test + { + public void Run() + { + var one = Mock.Of(); + var two = Mock.Of(); + var three = Mock.Of(); + } + } + """; + + var (sources, _) = RunGeneratorForDiagnostics(source); + + await Assert.That(sources.Any(s => s.Contains("void Untouched()"))).IsTrue(); + } + + private static string MockOf(string typeName, string declaration) => $$""" + using TUnit.Mocks; + + {{declaration}} + + public class Test + { + public void Run() + { + var one = Mock.Of<{{typeName}}>(); + } + } + """; + + private static string MockOf(string firstType, string firstDeclaration, string secondType, string secondDeclaration) => $$""" + using TUnit.Mocks; + + {{firstDeclaration}} + {{secondDeclaration}} + + public class Test + { + public void Run() + { + var one = Mock.Of<{{firstType}}>(); + var two = Mock.Of<{{secondType}}>(); + } + } + """; +} diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs b/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs index b1a8948709..b3ff9ec78e 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs @@ -90,6 +90,41 @@ protected static string[] RunGenerator( .ToArray(); } + /// + /// Runs the MockGenerator and returns its diagnostics alongside the generated files. Unlike + /// this does not throw on generator errors — use it when the + /// diagnostic is what the test is about. + /// + protected static (string[] Sources, IReadOnlyList Diagnostics) RunGeneratorForDiagnostics( + string source, + IEnumerable? additionalReferences = null) + { + var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); + var syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions); + + IEnumerable refs = additionalReferences is null + ? _references.Value + : _references.Value.Concat(additionalReferences); + + var compilation = CSharpCompilation.Create( + "TestAssembly", + [syntaxTree], + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + ).WithReferences(refs); + + var generator = new MockGenerator(); + GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], parseOptions: parseOptions); + + var runResult = driver.RunGenerators(compilation).GetRunResult(); + + var sources = runResult.GeneratedTrees + .OrderBy(t => GetGeneratedTreeSortKey(t.FilePath), StringComparer.Ordinal) + .Select(t => t.GetText().ToString()) + .ToArray(); + + return (sources, runResult.Diagnostics); + } + private static string GetGeneratedTreeSortKey(string filePath) { var normalizedPath = filePath.Replace('\\', '/'); diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Class_Primary_And_Explicit_Impl.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Class_Primary_And_Explicit_Impl.verified.txt index c11e60fb91..01067a5d7f 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Class_Primary_And_Explicit_Impl.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Class_Primary_And_Explicit_Impl.verified.txt @@ -2,105 +2,6 @@ #pragma warning disable #nullable enable -file sealed class DataContext_IInfraMockImpl : global::DataContext, global::IInfra, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject -{ - private readonly global::TUnit.Mocks.MockEngine _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 DataContext_IInfraMockImpl(global::TUnit.Mocks.MockEngine engine) : base() - { - _engine = engine; - } - - public override string GetName() - { - if (_engine.TryHandleCallWithReturn(0, "GetName", global::System.Array.Empty(), "", out var __result)) - { - return __result; - } - return base.GetName(); - } - - string global::IInfra.Instance - { - get => _engine.HandleCallWithReturn(1, "get_Instance", global::System.Array.Empty(), ""); - } - - [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 DataContext_IInfraPartialMockFactory -{ - [global::System.Runtime.CompilerServices.ModuleInitializer] - internal static void Register() - { - global::TUnit.Mocks.MockRegistry.RegisterMultiFactory(string.Join("|", new[] { typeof(global::DataContext).FullName, typeof(global::IInfra).FullName }), Create); - } - - private static readonly int[] _secondaryMap0 = new int[] { 1 }; - - private static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) - { - var engine = new global::TUnit.Mocks.MockEngine(behavior); - engine.RegisterSecondaryInterface(typeof(global::IInfra), _secondaryMap0); - var impl = new DataContext_IInfraMockImpl(engine); - engine.Raisable = impl; - var mock = new global::TUnit.Mocks.Mock(impl, engine); - return mock; - } -} - - -// ===== FILE SEPARATOR ===== - -// -#pragma warning disable -#nullable enable - -namespace TUnit.Mocks.Generated -{ - public static class DataContext_IInfra_MockMemberExtensions - { - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - internal static int __Id(global::TUnit.Mocks.MockEngine engine, int localId) - { - if (!engine.TryGetSecondaryMemberId(typeof(global::IInfra), localId, out var memberId)) - { - throw new global::System.InvalidOperationException(engine.HasSecondaryInterface(typeof(global::IInfra)) - ? "Member #" + localId + " of 'IInfra' has no setup mapping on this mock instance — it is not part of this combo's configurable surface." - : "This mock was not created with 'IInfra' as a secondary interface. Create it with Mock.Of() to configure its members."); - } - return memberId; - } - - extension(global::TUnit.Mocks.Mock mock) - { - public global::TUnit.Mocks.PropertyMockCall Instance - { - get - { - var __engine = global::TUnit.Mocks.MockRegistry.GetEngine(mock); - return new(__engine, __Id(__engine, 0), 0, "Instance", true, false); - } - } - } - } -} - - -// ===== FILE SEPARATOR ===== - -// -#pragma warning disable -#nullable enable - file sealed class DataContextMockImpl : global::DataContext, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; @@ -237,6 +138,105 @@ namespace TUnit.Mocks } +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +file sealed class DataContext_IInfraMockImpl : global::DataContext, global::IInfra, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject +{ + private readonly global::TUnit.Mocks.MockEngine _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 DataContext_IInfraMockImpl(global::TUnit.Mocks.MockEngine engine) : base() + { + _engine = engine; + } + + public override string GetName() + { + if (_engine.TryHandleCallWithReturn(0, "GetName", global::System.Array.Empty(), "", out var __result)) + { + return __result; + } + return base.GetName(); + } + + string global::IInfra.Instance + { + get => _engine.HandleCallWithReturn(1, "get_Instance", global::System.Array.Empty(), ""); + } + + [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 DataContext_IInfraPartialMockFactory +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterMultiFactory(string.Join("|", new[] { typeof(global::DataContext).FullName, typeof(global::IInfra).FullName }), Create); + } + + private static readonly int[] _secondaryMap0 = new int[] { 1 }; + + private static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + var engine = new global::TUnit.Mocks.MockEngine(behavior); + engine.RegisterSecondaryInterface(typeof(global::IInfra), _secondaryMap0); + var impl = new DataContext_IInfraMockImpl(engine); + engine.Raisable = impl; + var mock = new global::TUnit.Mocks.Mock(impl, engine); + return mock; + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class DataContext__IInfra_MockMemberExtensions + { + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal static int __Id(global::TUnit.Mocks.MockEngine engine, int localId) + { + if (!engine.TryGetSecondaryMemberId(typeof(global::IInfra), localId, out var memberId)) + { + throw new global::System.InvalidOperationException(engine.HasSecondaryInterface(typeof(global::IInfra)) + ? "Member #" + localId + " of 'IInfra' has no setup mapping on this mock instance — it is not part of this combo's configurable surface." + : "This mock was not created with 'IInfra' as a secondary interface. Create it with Mock.Of() to configure its members."); + } + return memberId; + } + + extension(global::TUnit.Mocks.Mock mock) + { + public global::TUnit.Mocks.PropertyMockCall Instance + { + get + { + var __engine = global::TUnit.Mocks.MockRegistry.GetEngine(mock); + return new(__engine, __Id(__engine, 0), 0, "Instance", true, false); + } + } + } + } +} + + // ===== FILE SEPARATOR ===== // diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Conflicting_Member_Names.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Conflicting_Member_Names.verified.txt index f3d273fb91..28ea6935f7 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Conflicting_Member_Names.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Conflicting_Member_Names.verified.txt @@ -2,111 +2,6 @@ #pragma warning disable #nullable enable -file sealed class IConflictA_IConflictBMockImpl : global::IConflictA, global::IConflictB, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject -{ - private readonly global::TUnit.Mocks.MockEngine _engine; - - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } - - internal IConflictA_IConflictBMockImpl(global::TUnit.Mocks.MockEngine engine) - { - _engine = engine; - } - - public string Tag - { - get => _engine.HandleCallWithReturn(0, "get_Tag", global::System.Array.Empty(), ""); - } - - int global::IConflictB.Tag - { - get => _engine.HandleCallWithReturn(1, "get_Tag", global::System.Array.Empty(), default); - } - - [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."); - } -} - -internal static class IConflictA_IConflictBMockFactory -{ - [global::System.Runtime.CompilerServices.ModuleInitializer] - internal static void Register() - { - global::TUnit.Mocks.MockRegistry.RegisterMultiFactory(string.Join("|", new[] { typeof(global::IConflictA).FullName, typeof(global::IConflictB).FullName }), Create); - } - - private static readonly int[] _secondaryMap0 = new int[] { 1 }; - - internal static global::TUnit.Mocks.Mock CreateAutoMock(global::TUnit.Mocks.MockBehavior behavior) - { - var engine = new global::TUnit.Mocks.MockEngine(behavior); - engine.RegisterSecondaryInterface(typeof(global::IConflictB), _secondaryMap0); - var impl = new IConflictA_IConflictBMockImpl(engine); - engine.Raisable = impl; - var mock = new global::TUnit.Mocks.Mock(impl, engine); - return mock; - } - - internal static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) - { - if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::IConflictA' does not support constructor arguments, but {constructorArgs.Length} were provided."); - var engine = new global::TUnit.Mocks.MockEngine(behavior); - engine.RegisterSecondaryInterface(typeof(global::IConflictB), _secondaryMap0); - var impl = new IConflictA_IConflictBMockImpl(engine); - engine.Raisable = impl; - var mock = new global::TUnit.Mocks.Mock(impl, engine); - return mock; - } -} - - -// ===== FILE SEPARATOR ===== - -// -#pragma warning disable -#nullable enable - -namespace TUnit.Mocks.Generated -{ - public static class IConflictA_IConflictB_MockMemberExtensions - { - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - internal static int __Id(global::TUnit.Mocks.MockEngine engine, int localId) - { - if (!engine.TryGetSecondaryMemberId(typeof(global::IConflictB), localId, out var memberId)) - { - throw new global::System.InvalidOperationException(engine.HasSecondaryInterface(typeof(global::IConflictB)) - ? "Member #" + localId + " of 'IConflictB' has no setup mapping on this mock instance — it is not part of this combo's configurable surface." - : "This mock was not created with 'IConflictB' as a secondary interface. Create it with Mock.Of() to configure its members."); - } - return memberId; - } - - extension(global::TUnit.Mocks.Mock mock) - { - public global::TUnit.Mocks.PropertyMockCall IConflictB_Tag - { - get - { - var __engine = global::TUnit.Mocks.MockRegistry.GetEngine(mock); - return new(__engine, __Id(__engine, 0), 0, "IConflictB_Tag", true, false); - } - } - } - } -} - - -// ===== FILE SEPARATOR ===== - -// -#pragma warning disable -#nullable enable - public sealed class IConflictAMock : global::TUnit.Mocks.Mock, global::IConflictA { [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -264,6 +159,111 @@ namespace TUnit.Mocks } +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +file sealed class IConflictA_IConflictBMockImpl : global::IConflictA, global::IConflictB, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject +{ + private readonly global::TUnit.Mocks.MockEngine _engine; + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + + internal IConflictA_IConflictBMockImpl(global::TUnit.Mocks.MockEngine engine) + { + _engine = engine; + } + + public string Tag + { + get => _engine.HandleCallWithReturn(0, "get_Tag", global::System.Array.Empty(), ""); + } + + int global::IConflictB.Tag + { + get => _engine.HandleCallWithReturn(1, "get_Tag", global::System.Array.Empty(), default); + } + + [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."); + } +} + +internal static class IConflictA_IConflictBMockFactory +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterMultiFactory(string.Join("|", new[] { typeof(global::IConflictA).FullName, typeof(global::IConflictB).FullName }), Create); + } + + private static readonly int[] _secondaryMap0 = new int[] { 1 }; + + internal static global::TUnit.Mocks.Mock CreateAutoMock(global::TUnit.Mocks.MockBehavior behavior) + { + var engine = new global::TUnit.Mocks.MockEngine(behavior); + engine.RegisterSecondaryInterface(typeof(global::IConflictB), _secondaryMap0); + var impl = new IConflictA_IConflictBMockImpl(engine); + engine.Raisable = impl; + var mock = new global::TUnit.Mocks.Mock(impl, engine); + return mock; + } + + internal static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::IConflictA' does not support constructor arguments, but {constructorArgs.Length} were provided."); + var engine = new global::TUnit.Mocks.MockEngine(behavior); + engine.RegisterSecondaryInterface(typeof(global::IConflictB), _secondaryMap0); + var impl = new IConflictA_IConflictBMockImpl(engine); + engine.Raisable = impl; + var mock = new global::TUnit.Mocks.Mock(impl, engine); + return mock; + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class IConflictA__IConflictB_MockMemberExtensions + { + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal static int __Id(global::TUnit.Mocks.MockEngine engine, int localId) + { + if (!engine.TryGetSecondaryMemberId(typeof(global::IConflictB), localId, out var memberId)) + { + throw new global::System.InvalidOperationException(engine.HasSecondaryInterface(typeof(global::IConflictB)) + ? "Member #" + localId + " of 'IConflictB' has no setup mapping on this mock instance — it is not part of this combo's configurable surface." + : "This mock was not created with 'IConflictB' as a secondary interface. Create it with Mock.Of() to configure its members."); + } + return memberId; + } + + extension(global::TUnit.Mocks.Mock mock) + { + public global::TUnit.Mocks.PropertyMockCall IConflictB_Tag + { + get + { + var __engine = global::TUnit.Mocks.MockRegistry.GetEngine(mock); + return new(__engine, __Id(__engine, 0), 0, "IConflictB_Tag", true, false); + } + } + } + } +} + + // ===== FILE SEPARATOR ===== // diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Secondary_Setup_Surface.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Secondary_Setup_Surface.verified.txt index e28c75f99a..f692751ae4 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Secondary_Setup_Surface.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Interface_Mock_With_Secondary_Setup_Surface.verified.txt @@ -2,128 +2,6 @@ #pragma warning disable #nullable enable -file sealed class IMultiLogger_IMultiDisposableMockImpl : global::IMultiLogger, global::IMultiDisposable, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject -{ - private readonly global::TUnit.Mocks.MockEngine _engine; - - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } - - internal IMultiLogger_IMultiDisposableMockImpl(global::TUnit.Mocks.MockEngine engine) - { - _engine = engine; - } - - public void Log(string message) - { - _engine.HandleCall(0, "Log", message); - } - - public void Dispose() - { - _engine.HandleCall(2, "Dispose", global::System.Array.Empty()); - } - - public string LastMessage - { - get => _engine.HandleCallWithReturn(1, "get_LastMessage", global::System.Array.Empty(), ""); - } - - public bool IsDisposed - { - get => _engine.HandleCallWithReturn(3, "get_IsDisposed", global::System.Array.Empty(), default); - } - - [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."); - } -} - -internal static class IMultiLogger_IMultiDisposableMockFactory -{ - [global::System.Runtime.CompilerServices.ModuleInitializer] - internal static void Register() - { - global::TUnit.Mocks.MockRegistry.RegisterMultiFactory(string.Join("|", new[] { typeof(global::IMultiLogger).FullName, typeof(global::IMultiDisposable).FullName }), Create); - } - - private static readonly int[] _secondaryMap0 = new int[] { 2, 3 }; - - internal static global::TUnit.Mocks.Mock CreateAutoMock(global::TUnit.Mocks.MockBehavior behavior) - { - var engine = new global::TUnit.Mocks.MockEngine(behavior); - engine.RegisterSecondaryInterface(typeof(global::IMultiDisposable), _secondaryMap0); - var impl = new IMultiLogger_IMultiDisposableMockImpl(engine); - engine.Raisable = impl; - var mock = new global::TUnit.Mocks.Mock(impl, engine); - return mock; - } - - internal static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) - { - if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::IMultiLogger' does not support constructor arguments, but {constructorArgs.Length} were provided."); - var engine = new global::TUnit.Mocks.MockEngine(behavior); - engine.RegisterSecondaryInterface(typeof(global::IMultiDisposable), _secondaryMap0); - var impl = new IMultiLogger_IMultiDisposableMockImpl(engine); - engine.Raisable = impl; - var mock = new global::TUnit.Mocks.Mock(impl, engine); - return mock; - } -} - - -// ===== FILE SEPARATOR ===== - -// -#pragma warning disable -#nullable enable - -namespace TUnit.Mocks.Generated -{ - public static class IMultiLogger_IMultiDisposable_MockMemberExtensions - { - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - internal static int __Id(global::TUnit.Mocks.MockEngine engine, int localId) - { - if (!engine.TryGetSecondaryMemberId(typeof(global::IMultiDisposable), localId, out var memberId)) - { - throw new global::System.InvalidOperationException(engine.HasSecondaryInterface(typeof(global::IMultiDisposable)) - ? "Member #" + localId + " of 'IMultiDisposable' has no setup mapping on this mock instance — it is not part of this combo's configurable surface." - : "This mock was not created with 'IMultiDisposable' as a secondary interface. Create it with Mock.Of() to configure its members."); - } - return memberId; - } - - public static global::TUnit.Mocks.VoidMockMethodCall Dispose(this global::TUnit.Mocks.Mock mock) - { - var matchers = global::System.Array.Empty(); - var __engine = global::TUnit.Mocks.MockRegistry.GetEngine(mock); - return new global::TUnit.Mocks.VoidMockMethodCall(__engine, __Id(__engine, 0), "Dispose", matchers); - } - - extension(global::TUnit.Mocks.Mock mock) - { - public global::TUnit.Mocks.PropertyMockCall IsDisposed - { - get - { - var __engine = global::TUnit.Mocks.MockRegistry.GetEngine(mock); - return new(__engine, __Id(__engine, 1), 0, "IsDisposed", true, false); - } - } - } - } -} - - -// ===== FILE SEPARATOR ===== - -// -#pragma warning disable -#nullable enable - public sealed class IMultiLoggerMock : global::TUnit.Mocks.Mock, global::IMultiLogger { [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -384,6 +262,128 @@ namespace TUnit.Mocks } +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +file sealed class IMultiLogger_IMultiDisposableMockImpl : global::IMultiLogger, global::IMultiDisposable, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject +{ + private readonly global::TUnit.Mocks.MockEngine _engine; + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + + internal IMultiLogger_IMultiDisposableMockImpl(global::TUnit.Mocks.MockEngine engine) + { + _engine = engine; + } + + public void Log(string message) + { + _engine.HandleCall(0, "Log", message); + } + + public void Dispose() + { + _engine.HandleCall(2, "Dispose", global::System.Array.Empty()); + } + + public string LastMessage + { + get => _engine.HandleCallWithReturn(1, "get_LastMessage", global::System.Array.Empty(), ""); + } + + public bool IsDisposed + { + get => _engine.HandleCallWithReturn(3, "get_IsDisposed", global::System.Array.Empty(), default); + } + + [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."); + } +} + +internal static class IMultiLogger_IMultiDisposableMockFactory +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterMultiFactory(string.Join("|", new[] { typeof(global::IMultiLogger).FullName, typeof(global::IMultiDisposable).FullName }), Create); + } + + private static readonly int[] _secondaryMap0 = new int[] { 2, 3 }; + + internal static global::TUnit.Mocks.Mock CreateAutoMock(global::TUnit.Mocks.MockBehavior behavior) + { + var engine = new global::TUnit.Mocks.MockEngine(behavior); + engine.RegisterSecondaryInterface(typeof(global::IMultiDisposable), _secondaryMap0); + var impl = new IMultiLogger_IMultiDisposableMockImpl(engine); + engine.Raisable = impl; + var mock = new global::TUnit.Mocks.Mock(impl, engine); + return mock; + } + + internal static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::IMultiLogger' does not support constructor arguments, but {constructorArgs.Length} were provided."); + var engine = new global::TUnit.Mocks.MockEngine(behavior); + engine.RegisterSecondaryInterface(typeof(global::IMultiDisposable), _secondaryMap0); + var impl = new IMultiLogger_IMultiDisposableMockImpl(engine); + engine.Raisable = impl; + var mock = new global::TUnit.Mocks.Mock(impl, engine); + return mock; + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class IMultiLogger__IMultiDisposable_MockMemberExtensions + { + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal static int __Id(global::TUnit.Mocks.MockEngine engine, int localId) + { + if (!engine.TryGetSecondaryMemberId(typeof(global::IMultiDisposable), localId, out var memberId)) + { + throw new global::System.InvalidOperationException(engine.HasSecondaryInterface(typeof(global::IMultiDisposable)) + ? "Member #" + localId + " of 'IMultiDisposable' has no setup mapping on this mock instance — it is not part of this combo's configurable surface." + : "This mock was not created with 'IMultiDisposable' as a secondary interface. Create it with Mock.Of() to configure its members."); + } + return memberId; + } + + public static global::TUnit.Mocks.VoidMockMethodCall Dispose(this global::TUnit.Mocks.Mock mock) + { + var matchers = global::System.Array.Empty(); + var __engine = global::TUnit.Mocks.MockRegistry.GetEngine(mock); + return new global::TUnit.Mocks.VoidMockMethodCall(__engine, __Id(__engine, 0), "Dispose", matchers); + } + + extension(global::TUnit.Mocks.Mock mock) + { + public global::TUnit.Mocks.PropertyMockCall IsDisposed + { + get + { + var __engine = global::TUnit.Mocks.MockRegistry.GetEngine(mock); + return new(__engine, __Id(__engine, 1), 0, "IsDisposed", true, false); + } + } + } + } +} + + // ===== FILE SEPARATOR ===== //