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();
+ }
+}