diff --git a/TUnit.Core.SourceGenerator.Tests/Bugs/6365/Tests6365.cs b/TUnit.Core.SourceGenerator.Tests/Bugs/6365/Tests6365.cs new file mode 100644 index 00000000000..8173b91f457 --- /dev/null +++ b/TUnit.Core.SourceGenerator.Tests/Bugs/6365/Tests6365.cs @@ -0,0 +1,15 @@ + +namespace TUnit.Core.SourceGenerator.Tests.Bugs._6365; + +internal class Tests6365 : TestsBase +{ + [Test] + public Task Test() => RunTest(Path.Combine(Git.RootDirectory.FullName, + "TUnit.TestProject", + "Bugs", + "6365", + "Tests.cs"), + async generatedFiles => + { + }); +} diff --git a/TUnit.Core.SourceGenerator.Tests/Tests6365.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests6365.Test.verified.txt new file mode 100644 index 00000000000..d154b08049e --- /dev/null +++ b/TUnit.Core.SourceGenerator.Tests/Tests6365.Test.verified.txt @@ -0,0 +1,99 @@ +// +#pragma warning disable + +#nullable enable +namespace TUnit.Generated; +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute] +[global::System.CodeDom.Compiler.GeneratedCode("TUnit", "VERSION_SCRUBBED")] +internal static class TUnit_TestProject_Bugs__6365_Tests__TestSource +{ + private static readonly global::TUnit.Core.ClassMetadata __classMetadata = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.Bugs._6365.Tests", new global::TUnit.Core.ClassMetadata + { + Type = typeof(global::TUnit.TestProject.Bugs._6365.Tests), + TypeInfo = new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.Bugs._6365.Tests)), + Name = "Tests", + Namespace = "TUnit.TestProject.Bugs._6365", + Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", "TestsBase`1"), + Parameters = global::System.Array.Empty(), + Properties = global::System.Array.Empty(), + Parent = null + }); + private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.Bugs._6365.Tests); + private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("Typed_MethodDataSource_Uses_Provider_Instance", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] +{ +global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProject.Bugs._6365.Tests.TestCase), "testCase", new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.Bugs._6365.Tests.TestCase)), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.Bugs._6365.Tests).GetMethod("Typed_MethodDataSource_Uses_Provider_Instance", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(global::TUnit.TestProject.Bugs._6365.Tests.TestCase) }, null)!.GetParameters()[0]) +}); + private static global::TUnit.TestProject.Bugs._6365.Tests __CreateInstance(global::System.Type[] typeArgs, object?[] args) + { + return new global::TUnit.TestProject.Bugs._6365.Tests(); + } + private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.Bugs._6365.Tests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) + { + switch (methodIndex) + { + case 0: + { + try + { + switch (args.Length) + { + case 1: + { + return new global::System.Threading.Tasks.ValueTask(instance.Typed_MethodDataSource_Uses_Provider_Instance(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } + default: + throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); + } + } + catch (global::System.Exception ex) + { + return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex)); + } + } + default: + throw new global::System.ArgumentOutOfRangeException(nameof(methodIndex)); + } + } + private static global::System.Attribute[] __Attributes(int groupIndex) + { + switch (groupIndex) + { + case 0: + { + return + [ + new global::TUnit.Core.TestAttribute(), + new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass) + ]; + } + default: + throw new global::System.ArgumentOutOfRangeException(nameof(groupIndex)); + } + } + public static readonly global::TUnit.Core.TestEntry[] Entries = new global::TUnit.Core.TestEntry[] + { + global::TUnit.Core.TestEntryFactory.Create( + methodName: "Typed_MethodDataSource_Uses_Provider_Instance", + fullyQualifiedName: "TUnit.TestProject.Bugs._6365.Tests.Typed_MethodDataSource_Uses_Provider_Instance", + filePath: "", + lineNumber: 8, + hasDataSource: true, + testDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.MethodDataSourceAttribute(typeof(global::TUnit.TestProject.Bugs._6365.Tests.Provider), "GetTestCases") + { + Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromEnumerable(static () => new global::TUnit.TestProject.Bugs._6365.Tests.Provider().GetTestCases()), + }, +}, + methodMetadata: __mm_0, + createInstance: __CreateInstance, + invokeBody: __Invoke, + methodIndex: 0, + createAttributes: __Attributes, + attributeGroupIndex: 0), + }; +} +internal static partial class TUnit_TestRegistration +{ + static readonly int _r_TUnit_TestProject_Bugs__6365_Tests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_Bugs__6365_Tests__TestSource.Entries); +} diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index d4ede2cc0ca..8c8bcc32893 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -1636,11 +1636,22 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu // Determine if the data source is static or instance-based var isStatic = dataSourceMethod?.IsStatic ?? dataSourceProperty?.GetMethod?.IsStatic ?? true; - // Use InstanceMethodDataSourceAttribute for instance-based data sources - // This implements IAccessesInstanceData which tells the engine to create an instance early - var attrTypeName = isStatic - ? "global::TUnit.Core.MethodDataSourceAttribute" - : "global::TUnit.Core.InstanceMethodDataSourceAttribute"; + // InstanceMethodDataSourceAttribute means "use the test class instance". If the + // attribute explicitly targets a separate provider type, create that provider in + // the generated factory instead of asking the engine for the test instance. + var usesTestClassInstance = !isStatic && CanUseTestClassInstance(typeSymbol, targetType); + var canGenerateFactory = isStatic || usesTestClassInstance || CanCreateProviderInstance(targetType); + + if (!canGenerateFactory) + { + var generatedCode = CodeGenerationHelpers.GenerateAttributeInstantiation(attr); + writer.AppendLine($"{generatedCode},"); + return; + } + + var attrTypeName = usesTestClassInstance + ? "global::TUnit.Core.InstanceMethodDataSourceAttribute" + : "global::TUnit.Core.MethodDataSourceAttribute"; if (attr.ConstructorArguments is [ @@ -1651,6 +1662,10 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu // For instance data sources, we still use the same constructor signature writer.AppendLine($"new {attrTypeName}(typeof({typeArg.GloballyQualified()}), \"{methodName}\")"); } + else if (attr.AttributeClass is { IsGenericType: true } && !isStatic && !usesTestClassInstance) + { + writer.AppendLine($"new {attrTypeName}(typeof({targetType.GloballyQualified()}), \"{methodName}\")"); + } else { // MethodDataSource(string) constructor — ClassProvidingDataSource is intentionally null here @@ -1687,7 +1702,7 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu // a per-data-source compiler-generated state machine class (#6227). Only the per-test-unique // invocation is emitted, as a static lambda. writer.Append("Factory = static dataGeneratorMetadata => "); - EmitDataSourceFactoryInvocation(writer, dataSourceMember, targetType, hasArguments ? argumentsProperty.Value : (TypedConstant?)null); + EmitDataSourceFactoryInvocation(writer, dataSourceMember, targetType, usesTestClassInstance, hasArguments ? argumentsProperty.Value : (TypedConstant?)null); writer.AppendLine(","); writer.Unindent(); @@ -1700,7 +1715,7 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu /// member's declared return type (mirroring the previous inline-iterator branch order) /// and whether the member is static or instance-based. /// - private static void EmitDataSourceFactoryInvocation(CodeWriter writer, ISymbol dataSourceMember, ITypeSymbol targetType, TypedConstant? arguments) + private static void EmitDataSourceFactoryInvocation(CodeWriter writer, ISymbol dataSourceMember, ITypeSymbol targetType, bool usesTestClassInstance, TypedConstant? arguments) { var dataSourceMethod = dataSourceMember as IMethodSymbol; var dataSourceProperty = dataSourceMember as IPropertySymbol; @@ -1731,7 +1746,7 @@ private static void EmitDataSourceFactoryInvocation(CodeWriter writer, ISymbol d kind = "Value"; } - var helper = isStatic + var helper = isStatic || !usesTestClassInstance ? $"global::TUnit.Core.Helpers.DataSourceFactories.From{kind}" : $"global::TUnit.Core.Helpers.DataSourceFactories.FromInstance{kind}"; @@ -1749,9 +1764,13 @@ private static void EmitDataSourceFactoryInvocation(CodeWriter writer, ISymbol d memberAccess = dataSourceProperty!.Name; } - var receiver = isStatic ? fullyQualifiedType : $"(({fullyQualifiedType})instance)"; - var lambdaHeader = isStatic ? "static () =>" : "static instance =>"; - var metadataArgument = isStatic ? "" : "dataGeneratorMetadata, "; + var receiver = isStatic + ? fullyQualifiedType + : usesTestClassInstance + ? $"(({fullyQualifiedType})instance)" + : $"new {fullyQualifiedType}()"; + var lambdaHeader = usesTestClassInstance ? "static instance =>" : "static () =>"; + var metadataArgument = usesTestClassInstance ? "dataGeneratorMetadata, " : ""; if (hasArguments) { @@ -1776,6 +1795,42 @@ private static bool IsAsyncEnumerable(ITypeSymbol type) return InterfaceHelper.IsAsyncEnumerable(type); } + private static bool CanUseTestClassInstance(ITypeSymbol testClassType, ITypeSymbol targetType) + { + if (SymbolEqualityComparer.Default.Equals(testClassType, targetType)) + { + return true; + } + + for (var baseType = testClassType.BaseType; baseType is not null; baseType = baseType.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(baseType, targetType)) + { + return true; + } + } + + return targetType.TypeKind == TypeKind.Interface + && testClassType.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, targetType)); + } + + private static bool CanCreateProviderInstance(ITypeSymbol targetType) + { + if (targetType.IsValueType) + { + return true; + } + + if (targetType is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsAbstract: false } namedType) + { + return false; + } + + return namedType.InstanceConstructors.Any(ctor => + ctor.Parameters.Length == 0 + && ctor.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal or Accessibility.ProtectedOrInternal); + } + private static bool IsValueTask(ITypeSymbol type) { return type.ToDisplayString().StartsWith("System.Threading.Tasks.ValueTask"); diff --git a/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs b/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs index 6d5aebb08d0..ef6b341a9aa 100644 --- a/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs @@ -580,7 +580,7 @@ private static bool IsAsyncEnumerable([DynamicallyAccessedMembers(DynamicallyAcc testClassInstance = null; } - if (testClassInstance != null) + if (testClassInstance != null && targetType.IsInstanceOfType(testClassInstance)) { return testClassInstance; } diff --git a/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs b/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs index c47792c537f..ab2bbff871b 100644 --- a/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs +++ b/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs @@ -218,6 +218,11 @@ private static bool TargetsInstanceMember(MethodDataSourceAttribute methodDataSo var targetType = methodDataSource.ClassProvidingDataSource ?? testClass; var memberName = methodDataSource.MethodNameProvidingDataSource; + if (!targetType.IsAssignableFrom(testClass)) + { + return false; + } + // GetMember returns all matching members (it never throws AmbiguousMatchException for // overloads, unlike GetMethod). Conservatively treat the data source as instance-targeting // if ANY matching member is an instance member, so the engine pre-creates a properly diff --git a/TUnit.TestProject/Bugs/6365/Tests.cs b/TUnit.TestProject/Bugs/6365/Tests.cs new file mode 100644 index 00000000000..916781f7d0f --- /dev/null +++ b/TUnit.TestProject/Bugs/6365/Tests.cs @@ -0,0 +1,37 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._6365; + +[EngineTest(ExpectedResult.Pass)] +public class Tests +{ + [Test] + [MethodDataSource(nameof(Provider.GetTestCases))] + public async Task Typed_MethodDataSource_Uses_Provider_Instance(TestCase testCase) + { + await Assert.That(testCase).IsEqualTo(new TestCase(1, 2)); + } + + public sealed record TestCase(int A, double B); + + public sealed class Provider : ProviderBase + { + protected override IEnumerable GenerateTestCases() + { + yield return new TestCase(1, 2); + } + } + + public abstract class ProviderBase + { + public IEnumerable GetTestCases() + { + foreach (var testCase in GenerateTestCases()) + { + yield return testCase; + } + } + + protected abstract IEnumerable GenerateTestCases(); + } +}