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
15 changes: 15 additions & 0 deletions TUnit.Core.SourceGenerator.Tests/Bugs/6365/Tests6365.cs
Original file line number Diff line number Diff line change
@@ -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 =>
{
});
}
99 changes: 99 additions & 0 deletions TUnit.Core.SourceGenerator.Tests/Tests6365.Test.verified.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// <auto-generated/>
#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<global::TUnit.Core.ParameterMetadata>(),
Properties = global::System.Array.Empty<global::TUnit.Core.PropertyMetadata>(),
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<global::TUnit.TestProject.Bugs._6365.Tests.TestCase>(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<global::TUnit.TestProject.Bugs._6365.Tests>[] Entries = new global::TUnit.Core.TestEntry<global::TUnit.TestProject.Bugs._6365.Tests>[]
{
global::TUnit.Core.TestEntryFactory.Create<global::TUnit.TestProject.Bugs._6365.Tests>(
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<global::TUnit.TestProject.Bugs._6365.Tests>(static () => TUnit_TestProject_Bugs__6365_Tests__TestSource.Entries);
}
77 changes: 66 additions & 11 deletions TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
[
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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.
/// </summary>
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;
Expand Down Expand Up @@ -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}";

Expand All @@ -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)
{
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ private static bool IsAsyncEnumerable([DynamicallyAccessedMembers(DynamicallyAcc
testClassInstance = null;
}

if (testClassInstance != null)
if (testClassInstance != null && targetType.IsInstanceOfType(testClassInstance))
{
return testClassInstance;
}
Expand Down
5 changes: 5 additions & 0 deletions TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions TUnit.TestProject/Bugs/6365/Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs._6365;

[EngineTest(ExpectedResult.Pass)]
public class Tests
{
[Test]
[MethodDataSource<Provider>(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<TestCase> GenerateTestCases()
{
yield return new TestCase(1, 2);
}
}

public abstract class ProviderBase
{
public IEnumerable<TestCase> GetTestCases()
{
foreach (var testCase in GenerateTestCases())
{
yield return testCase;
}
}

protected abstract IEnumerable<TestCase> GenerateTestCases();
}
}
Loading