From 88c694fb21ef0757a93a6444122173e4dcda3d9f Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 1 Nov 2026 18:12:13 +0000
Subject: [PATCH 01/10] fix(sourcegen): generate a data source factory for
[InstanceMethodDataSource] (#7225)
[InstanceMethodDataSource] went through the generic attribute writer and got no
Factory, so Native AOT fell back to a reflection member lookup that found no
trimmed field metadata. Route it through GenerateMethodDataSourceAttribute and
keep the declared attribute type so the engine still sees IAccessesInstanceData.
---
.../Generators/TestMetadataGenerator.cs | 10 +-
.../Bugs/7225/Tests7225.cs | 24 +++
.../Tests1821.Test.verified.txt | 5 +-
.../Tests7225.Test.verified.txt | 188 ++++++++++++++++++
.../InstanceMethodDataSourceFieldTests.cs | 38 ++++
5 files changed, 262 insertions(+), 3 deletions(-)
create mode 100644 tests/TUnit.Core.SourceGenerator.Tests/Bugs/7225/Tests7225.cs
create mode 100644 tests/TUnit.Core.SourceGenerator.Tests/Tests7225.Test.verified.txt
create mode 100644 tests/TUnit.TestProject/Bugs/_7225/InstanceMethodDataSourceFieldTests.cs
diff --git a/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
index d162a33ae39..d7565de5b44 100644
--- a/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
+++ b/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
@@ -2544,7 +2544,10 @@ private static void GenerateDataSourceAttribute(CodeWriter writer, CompilationCo
var attrName = attrClass.GloballyQualifiedNonGeneric();
- if (attrName == "global::TUnit.Core.MethodDataSourceAttribute")
+ // InstanceMethodDataSourceAttribute derives from MethodDataSourceAttribute and needs the
+ // same generated factory; without one, Native AOT falls back to a reflection lookup that
+ // finds no trimmed field or property metadata (#7225).
+ if (attrName is "global::TUnit.Core.MethodDataSourceAttribute" or "global::TUnit.Core.InstanceMethodDataSourceAttribute")
{
GenerateMethodDataSourceAttribute(writer, attr, typeSymbol, compilationContext.Compilation);
}
@@ -2916,7 +2919,10 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu
return;
}
- var attrTypeName = usesTestClassInstance
+ // Keep a declared [InstanceMethodDataSource] as one even when its member is static, so the
+ // engine still sees IAccessesInstanceData as it does in reflection mode.
+ var isDeclaredInstanceAttribute = attr.AttributeClass?.GloballyQualifiedNonGeneric() == "global::TUnit.Core.InstanceMethodDataSourceAttribute";
+ var attrTypeName = usesTestClassInstance || isDeclaredInstanceAttribute
? "global::TUnit.Core.InstanceMethodDataSourceAttribute"
: "global::TUnit.Core.MethodDataSourceAttribute";
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7225/Tests7225.cs b/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7225/Tests7225.cs
new file mode 100644
index 00000000000..659a7a30690
--- /dev/null
+++ b/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7225/Tests7225.cs
@@ -0,0 +1,24 @@
+namespace TUnit.Core.SourceGenerator.Tests.Bugs._7225;
+
+///
+/// [InstanceMethodDataSource] gets the same generated Factory as [MethodDataSource], so Native AOT
+/// does not fall back to a reflection member lookup (#7225).
+///
+internal class Tests7225 : TestsBase
+{
+ [Test]
+ public Task Test() => RunTest(Path.Combine(Git.TestsDirectory.FullName,
+ "TUnit.TestProject",
+ "Bugs",
+ "_7225",
+ "InstanceMethodDataSourceFieldTests.cs"),
+ async generatedFiles =>
+ {
+ var source = string.Join(Environment.NewLine, generatedFiles);
+ const string testClass = "global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests";
+ await Assert.That(source).Contains($"FromInstanceRuntimeValue(dataGeneratorMetadata, static instance => (({testClass})instance).Rows)");
+ await Assert.That(source).Contains($"FromInstanceEnumerable(dataGeneratorMetadata, static instance => (({testClass})instance).PropertyRows)");
+ await Assert.That(source).Contains($"FromEnumerable(static () => {testClass}.StaticRows())");
+ await Assert.That(source).DoesNotContain("new global::TUnit.Core.InstanceMethodDataSourceAttribute(\"Rows\"),");
+ });
+}
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt
index 3e2b7b1f61e..668fdbc8909 100644
--- a/tests/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt
+++ b/tests/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt
@@ -95,7 +95,10 @@ internal static class TUnit_TestProject_Bugs__1821_Tests__TestSource
hasDataSource: true,
testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
{
- new global::TUnit.Core.InstanceMethodDataSourceAttribute("DataSource"),
+ new global::TUnit.Core.InstanceMethodDataSourceAttribute("DataSource")
+ {
+ Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromInstanceEnumerable(dataGeneratorMetadata, static instance => ((global::TUnit.TestProject.Bugs._1821.Tests)instance).DataSource()),
+ },
},
classDataSources: new global::TUnit.Core.IDataSourceAttribute[]
{
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Tests7225.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/Tests7225.Test.verified.txt
new file mode 100644
index 00000000000..6cff3e87cbb
--- /dev/null
+++ b/tests/TUnit.Core.SourceGenerator.Tests/Tests7225.Test.verified.txt
@@ -0,0 +1,188 @@
+//
+#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__7225_InstanceMethodDataSourceFieldTests__TestSource
+{
+ private static readonly global::TUnit.Core.ClassMetadata __classMetadata = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests", new global::TUnit.Core.ClassMetadata
+ {
+ Type = typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests),
+ TypeInfo = new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests)),
+ Name = "InstanceMethodDataSourceFieldTests",
+ Namespace = "TUnit.TestProject.Bugs._7225",
+ Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", "TestsBase`1"),
+ Parameters = global::System.Array.Empty(),
+ Properties = new global::TUnit.Core.PropertyMetadata[]
+ {
+ new global::TUnit.Core.PropertyMetadata
+ {
+ ReflectionInfo = typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests).GetProperty("PropertyRows", typeof(global::System.Collections.Generic.IEnumerable)),
+ Type = typeof(global::System.Collections.Generic.IEnumerable),
+ Name = "PropertyRows",
+ IsStatic = false,
+ IsNullable = false,
+ Getter = o => ((global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests)o).PropertyRows,
+ ClassMetadata = null!,
+ ContainingTypeMetadata = null!
+ }
+ },
+ Parent = null
+ });
+ private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests);
+ private static global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests __CreateInstance(global::System.Type[] typeArgs, object?[] args)
+ {
+ return new global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests();
+ }
+ private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken)
+ {
+ try
+ {
+ switch (methodIndex)
+ {
+ case 0:
+ {
+ switch (args.Length)
+ {
+ case 1:
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Instance_Field(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])));
+ }
+ default:
+ throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
+ }
+ }
+ case 1:
+ {
+ switch (args.Length)
+ {
+ case 1:
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Instance_Property(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])));
+ }
+ default:
+ throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
+ }
+ }
+ case 2:
+ {
+ switch (args.Length)
+ {
+ case 1:
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Static_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])));
+ }
+ default:
+ throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
+ }
+ }
+ default:
+ throw new global::System.ArgumentOutOfRangeException(nameof(methodIndex));
+ }
+ }
+ catch (global::System.Exception ex)
+ {
+ return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex));
+ }
+ }
+ 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));
+ }
+ }
+ private static readonly global::System.Func __createInstance = __CreateInstance;
+ private static readonly global::System.Func __invoke = __Invoke;
+ private static readonly global::System.Func __attributes = __Attributes;
+ public static readonly global::TUnit.Core.TestEntry[] Entries = new global::TUnit.Core.TestEntry[]
+ {
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Instance_Field",
+ fullyQualifiedName: "TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests.Instance_Field",
+ filePath: "",
+ lineNumber: 18,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.InstanceMethodDataSourceAttribute("Rows")
+ {
+ Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromInstanceRuntimeValue(dataGeneratorMetadata, static instance => ((global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests)instance).Rows),
+ },
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests), "Instance_Field", false, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 0,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Instance_Property",
+ fullyQualifiedName: "TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests.Instance_Property",
+ filePath: "",
+ lineNumber: 25,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.InstanceMethodDataSourceAttribute("PropertyRows")
+ {
+ Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromInstanceEnumerable(dataGeneratorMetadata, static instance => ((global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests)instance).PropertyRows),
+ },
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests), "Instance_Property", false, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 1,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Static_Method",
+ fullyQualifiedName: "TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests.Static_Method",
+ filePath: "",
+ lineNumber: 32,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.InstanceMethodDataSourceAttribute("StaticRows")
+ {
+ Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromEnumerable(static () => global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests.StaticRows()),
+ },
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceFieldTests), "Static_Method", false, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 2,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
+ };
+}
+internal static partial class TUnit_TestRegistration
+{
+ static readonly int _r_TUnit_TestProject_Bugs__7225_InstanceMethodDataSourceFieldTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_Bugs__7225_InstanceMethodDataSourceFieldTests__TestSource.Entries);
+}
diff --git a/tests/TUnit.TestProject/Bugs/_7225/InstanceMethodDataSourceFieldTests.cs b/tests/TUnit.TestProject/Bugs/_7225/InstanceMethodDataSourceFieldTests.cs
new file mode 100644
index 00000000000..9b7eff56806
--- /dev/null
+++ b/tests/TUnit.TestProject/Bugs/_7225/InstanceMethodDataSourceFieldTests.cs
@@ -0,0 +1,38 @@
+using TUnit.TestProject.Attributes;
+
+namespace TUnit.TestProject.Bugs._7225;
+
+///
+/// [InstanceMethodDataSource] naming an instance field or property must get a generated factory, like
+/// [MethodDataSource] does; otherwise Native AOT falls back to a reflection lookup that finds no member.
+///
+[EngineTest(ExpectedResult.Pass)]
+public class InstanceMethodDataSourceFieldTests
+{
+ public readonly object Rows = new[] { 53, 54 };
+
+ public IEnumerable PropertyRows => [55, 56];
+
+ public static IEnumerable StaticRows() => [57, 58];
+
+ [Test]
+ [InstanceMethodDataSource(nameof(Rows))]
+ public async Task Instance_Field(int value)
+ {
+ await Assert.That(value).IsBetween(53, 54);
+ }
+
+ [Test]
+ [InstanceMethodDataSource(nameof(PropertyRows))]
+ public async Task Instance_Property(int value)
+ {
+ await Assert.That(value).IsBetween(55, 56);
+ }
+
+ [Test]
+ [InstanceMethodDataSource(nameof(StaticRows))]
+ public async Task Static_Method(int value)
+ {
+ await Assert.That(value).IsBetween(57, 58);
+ }
+}
From de2b2a1665b37d8822794850417e622f34f8cf0f Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Mon, 2 Nov 2026 06:50:05 +0000
Subject: [PATCH 02/10] fix(sourcegen): keep runtime row expansion and private
methods for [InstanceMethodDataSource] (#7225)
- An object-typed property or method named by [InstanceMethodDataSource] decides its rows from the
runtime value, as the attribute's reflection lookup did before it had a generated factory.
- A parameterless private or protected data source method is read through a constant-name
typeof(T).GetMethod lookup that Native AOT keeps, like private fields and properties, instead of a
direct call the generated class cannot compile.
- Share the InstanceMethodDataSourceAttribute name as a constant.
---
.../Generators/TestMetadataGenerator.cs | 59 +++--
.../Bugs/7225/Tests7225.cs | 22 ++
.../Tests7225.MemberShapes.verified.txt | 241 ++++++++++++++++++
...nstanceMethodDataSourceMemberShapeTests.cs | 48 ++++
4 files changed, 350 insertions(+), 20 deletions(-)
create mode 100644 tests/TUnit.Core.SourceGenerator.Tests/Tests7225.MemberShapes.verified.txt
create mode 100644 tests/TUnit.TestProject/Bugs/_7225/InstanceMethodDataSourceMemberShapeTests.cs
diff --git a/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
index d7565de5b44..f9cfe660702 100644
--- a/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
+++ b/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
@@ -22,6 +22,7 @@ namespace TUnit.Core.SourceGenerator.Generators;
public sealed class TestMetadataGenerator : IIncrementalGenerator
{
private const string GeneratedNamespace = "TUnit.Generated";
+ private const string InstanceMethodDataSourceAttributeName = "global::TUnit.Core.InstanceMethodDataSourceAttribute";
private static readonly DiagnosticDescriptor GenerationErrorDescriptor = new(
"TUNIT0999",
@@ -2547,7 +2548,7 @@ private static void GenerateDataSourceAttribute(CodeWriter writer, CompilationCo
// InstanceMethodDataSourceAttribute derives from MethodDataSourceAttribute and needs the
// same generated factory; without one, Native AOT falls back to a reflection lookup that
// finds no trimmed field or property metadata (#7225).
- if (attrName is "global::TUnit.Core.MethodDataSourceAttribute" or "global::TUnit.Core.InstanceMethodDataSourceAttribute")
+ if (attrName is "global::TUnit.Core.MethodDataSourceAttribute" or InstanceMethodDataSourceAttributeName)
{
GenerateMethodDataSourceAttribute(writer, attr, typeSymbol, compilationContext.Compilation);
}
@@ -2891,7 +2892,7 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu
if (dataSourceMember == null
|| (dataSourceMethod == null && dataSourceProperty == null && dataSourceField == null)
- || (dataSourceMethod == null && !CanReadDataSourceMember(dataSourceMember, compilation)))
+ || !CanReadDataSourceMember(dataSourceMember, compilation))
{
// Still generate the attribute even if method not found - it will fail at runtime with proper error
// Use CodeGenerationHelpers to properly handle any generics on the attribute
@@ -2921,9 +2922,9 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu
// Keep a declared [InstanceMethodDataSource] as one even when its member is static, so the
// engine still sees IAccessesInstanceData as it does in reflection mode.
- var isDeclaredInstanceAttribute = attr.AttributeClass?.GloballyQualifiedNonGeneric() == "global::TUnit.Core.InstanceMethodDataSourceAttribute";
+ var isDeclaredInstanceAttribute = attr.AttributeClass?.GloballyQualifiedNonGeneric() == InstanceMethodDataSourceAttributeName;
var attrTypeName = usesTestClassInstance || isDeclaredInstanceAttribute
- ? "global::TUnit.Core.InstanceMethodDataSourceAttribute"
+ ? InstanceMethodDataSourceAttributeName
: "global::TUnit.Core.MethodDataSourceAttribute";
if (attr.ConstructorArguments is
@@ -2983,7 +2984,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, usesTestClassInstance, hasArguments ? argumentsProperty.Value : (TypedConstant?)null, compilation);
+ EmitDataSourceFactoryInvocation(writer, dataSourceMember, targetType, usesTestClassInstance, hasArguments ? argumentsProperty.Value : (TypedConstant?)null, compilation, isDeclaredInstanceAttribute);
writer.AppendLine(",");
writer.Unindent();
@@ -2996,7 +2997,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, bool usesTestClassInstance, TypedConstant? arguments, Compilation compilation)
+ private static void EmitDataSourceFactoryInvocation(CodeWriter writer, ISymbol dataSourceMember, ITypeSymbol targetType, bool usesTestClassInstance, TypedConstant? arguments, Compilation compilation, bool decidesRowsFromRuntimeValue)
{
var dataSourceMethod = dataSourceMember as IMethodSymbol;
@@ -3075,10 +3076,11 @@ private static void EmitDataSourceFactoryInvocation(CodeWriter writer, ISymbol d
// A declared type such as object does not say whether the value is one row or many. Reflection mode, and the
// attribute's reflection fallback that read every field and every private or protected property before the
// generator could, decide from the runtime value, so these factories hand the value to that same logic
- // instead of FromValue. Accessible properties and methods keep FromValue, as their generated code always has
- // (https://github.com/thomhurst/TUnit/issues/7178).
- var isAccessibleMember = dataSourceMethod != null || IsAccessibleDataSourceMember(dataSourceMember, compilation);
- if (kind == "Value" && (dataSourceMember is IFieldSymbol || !isAccessibleMember))
+ // instead of FromValue. Accessible properties and methods of [MethodDataSource] keep FromValue, as their
+ // generated code always has (https://github.com/thomhurst/TUnit/issues/7178). [InstanceMethodDataSource] had no
+ // generated factory before #7225, so every member it names keeps the runtime-value behavior it always had.
+ var isAccessibleMember = IsAccessibleDataSourceMember(dataSourceMember, compilation);
+ if (kind == "Value" && (decidesRowsFromRuntimeValue || dataSourceMember is IFieldSymbol || !isAccessibleMember))
{
helper = isStatic || !usesTestClassInstance
? "global::TUnit.Core.Helpers.DataSourceFactories.FromRuntimeValue"
@@ -3088,17 +3090,24 @@ private static void EmitDataSourceFactoryInvocation(CodeWriter writer, ISymbol d
if (!isAccessibleMember)
{
- // The generated class cannot name a private or protected field or property, so the factory reads it by
- // reflection. GetField/GetProperty on a typeof() with a constant name and binding flags is understood
+ // The generated class cannot name a private or protected member, so the factory reads it by
+ // reflection. GetField/GetProperty/GetMethod on a typeof() with a constant name and binding flags is understood
// by the trimmer and the AOT compiler, which keep that member, so this works under native AOT
// without any trimming warning. The attribute's own reflection fallback finds the member from the
// test class type at run time, which neither of them can see.
- var lookup = dataSourceMember is IFieldSymbol ? "GetField" : "GetProperty";
var bindingFlags = (isStatic ? "global::System.Reflection.BindingFlags.Static" : "global::System.Reflection.BindingFlags.Instance")
+ " | global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.DeclaredOnly";
var target = isStatic ? "null" : usesTestClassInstance ? "instance" : $"new {fullyQualifiedType}()";
+ // CanReadDataSourceMember only lets a parameterless, non-generic method through, so it is looked up with no
+ // parameter types, which also picks it out from any overloads
+ var read = dataSourceMember switch
+ {
+ IFieldSymbol => $"GetField(\"{dataSourceMember.Name}\", {bindingFlags})!.GetValue({target})",
+ IPropertySymbol => $"GetProperty(\"{dataSourceMember.Name}\", {bindingFlags})!.GetValue({target})",
+ _ => $"GetMethod(\"{dataSourceMember.Name}\", {bindingFlags}, null, global::System.Type.EmptyTypes, null)!.Invoke({target}, null)"
+ };
var cast = kind is "Value" or "Enumerable" ? "" : $"({returnType.GloballyQualified()})";
- writer.Append($"{helper}({metadataArgument}{lambdaHeader} {cast}typeof({dataSourceMember.ContainingType.GloballyQualified()}).{lookup}(\"{dataSourceMember.Name}\", {bindingFlags})!.GetValue({target}))");
+ writer.Append($"{helper}({metadataArgument}{lambdaHeader} {cast}typeof({dataSourceMember.ContainingType.GloballyQualified()}).{read})");
return;
}
@@ -3178,7 +3187,7 @@ private static bool IsShadowedOnTargetType(IMethodSymbol declaringMethod, ITypeS
}
///
- /// Whether the generated class can read the data source field or property directly: the member and its getter
+ /// Whether the generated class can read the data source member directly: the member, and a property's getter,
/// are accessible from the test assembly, so not private or protected.
///
private static bool IsAccessibleDataSourceMember(ISymbol member, Compilation compilation)
@@ -3189,10 +3198,10 @@ private static bool IsAccessibleDataSourceMember(ISymbol member, Compilation com
}
///
- /// Whether a generated factory can read the data source field or property, directly or, for a private or
- /// protected member, by reflection on its declaring type. That needs a declaring type that the generated class
- /// can name without type parameters, a getter for a property, and a member type it can name when the factory
- /// casts the value. Otherwise the attribute keeps its reflection fallback.
+ /// Whether a generated factory can read the data source member, directly or, for a private or protected member,
+ /// by reflection on its declaring type. That needs a declaring type that the generated class can name without
+ /// type parameters, a getter for a property, a method without parameters or type parameters, and a member type it
+ /// can name when the factory casts the value. Otherwise the attribute keeps its reflection fallback.
///
private static bool CanReadDataSourceMember(ISymbol member, Compilation compilation)
{
@@ -3206,8 +3215,18 @@ private static bool CanReadDataSourceMember(ISymbol member, Compilation compilat
return true;
}
+ if (member is IMethodSymbol { Parameters.Length: > 0 } or IMethodSymbol { IsGenericMethod: true })
+ {
+ return false;
+ }
+
// EmitDataSourceFactoryInvocation casts the value for the async helpers, which take a typed delegate
- var memberType = member is IPropertySymbol property ? property.Type : ((IFieldSymbol)member).Type;
+ var memberType = member switch
+ {
+ IPropertySymbol property => property.Type,
+ IMethodSymbol method => method.ReturnType,
+ _ => ((IFieldSymbol)member).Type
+ };
var castsValue = IsAsyncEnumerable(memberType) || IsValueTask(memberType) || IsTask(memberType);
return compilation.IsSymbolAccessibleWithin(member.ContainingType, compilation.Assembly)
&& !CodeGenerationHelpers.ContainsTypeParameter(member.ContainingType)
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7225/Tests7225.cs b/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7225/Tests7225.cs
index 659a7a30690..4877851853e 100644
--- a/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7225/Tests7225.cs
+++ b/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7225/Tests7225.cs
@@ -21,4 +21,26 @@ public Task Test() => RunTest(Path.Combine(Git.TestsDirectory.FullName,
await Assert.That(source).Contains($"FromEnumerable(static () => {testClass}.StaticRows())");
await Assert.That(source).DoesNotContain("new global::TUnit.Core.InstanceMethodDataSourceAttribute(\"Rows\"),");
});
+
+ ///
+ /// The factory keeps the behavior [InstanceMethodDataSource] had when it read members by reflection: an object
+ /// value decides its rows at run time, and a private method is looked up
+ /// by a constant name that Native AOT keeps, as private fields and properties are.
+ ///
+ [Test]
+ public Task MemberShapes() => RunTest(Path.Combine(Git.TestsDirectory.FullName,
+ "TUnit.TestProject",
+ "Bugs",
+ "_7225",
+ "InstanceMethodDataSourceMemberShapeTests.cs"),
+ async generatedFiles =>
+ {
+ var source = string.Join(Environment.NewLine, generatedFiles);
+ const string testClass = "global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests";
+ await Assert.That(source).Contains($"FromInstanceRuntimeValue(dataGeneratorMetadata, static instance => (({testClass})instance).ObjectProperty)");
+ await Assert.That(source).Contains($"FromInstanceRuntimeValue(dataGeneratorMetadata, static instance => (({testClass})instance).ObjectMethod())");
+ await Assert.That(source).Contains($"FromInstanceEnumerable(dataGeneratorMetadata, static instance => (({testClass})instance).TupleRows)");
+ await Assert.That(source).Contains($"FromInstanceEnumerable(dataGeneratorMetadata, static instance => typeof({testClass}).GetMethod(\"PrivateRows\", ");
+ await Assert.That(source).DoesNotContain("new global::TUnit.Core.InstanceMethodDataSourceAttribute(\"PrivateRows\"),");
+ });
}
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Tests7225.MemberShapes.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/Tests7225.MemberShapes.verified.txt
new file mode 100644
index 00000000000..6d79800536e
--- /dev/null
+++ b/tests/TUnit.Core.SourceGenerator.Tests/Tests7225.MemberShapes.verified.txt
@@ -0,0 +1,241 @@
+//
+#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__7225_InstanceMethodDataSourceMemberShapeTests__TestSource
+{
+ private static readonly global::TUnit.Core.ClassMetadata __classMetadata = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests", new global::TUnit.Core.ClassMetadata
+ {
+ Type = typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests),
+ TypeInfo = new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests)),
+ Name = "InstanceMethodDataSourceMemberShapeTests",
+ Namespace = "TUnit.TestProject.Bugs._7225",
+ Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", "TestsBase`1"),
+ Parameters = global::System.Array.Empty(),
+ Properties = new global::TUnit.Core.PropertyMetadata[]
+ {
+ new global::TUnit.Core.PropertyMetadata
+ {
+ ReflectionInfo = typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests).GetProperty("ObjectProperty", typeof(object)),
+ Type = typeof(object),
+ Name = "ObjectProperty",
+ IsStatic = false,
+ IsNullable = false,
+ Getter = o => ((global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests)o).ObjectProperty,
+ ClassMetadata = null!,
+ ContainingTypeMetadata = null!
+ },
+ new global::TUnit.Core.PropertyMetadata
+ {
+ ReflectionInfo = typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests).GetProperty("TupleRows", typeof(global::System.Collections.Generic.IEnumerable>)),
+ Type = typeof(global::System.Collections.Generic.IEnumerable>),
+ Name = "TupleRows",
+ IsStatic = false,
+ IsNullable = false,
+ Getter = o => ((global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests)o).TupleRows,
+ ClassMetadata = null!,
+ ContainingTypeMetadata = null!
+ }
+ },
+ Parent = null
+ });
+ private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests);
+ private static global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests __CreateInstance(global::System.Type[] typeArgs, object?[] args)
+ {
+ return new global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests();
+ }
+ private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken)
+ {
+ try
+ {
+ switch (methodIndex)
+ {
+ case 0:
+ {
+ switch (args.Length)
+ {
+ case 1:
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Object_Property(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])));
+ }
+ default:
+ throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
+ }
+ }
+ case 1:
+ {
+ switch (args.Length)
+ {
+ case 1:
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Object_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])));
+ }
+ default:
+ throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
+ }
+ }
+ case 2:
+ {
+ // Special handling for single tuple parameter
+ if (args.Length == 2)
+ {
+ // Arguments are unwrapped tuple elements, reconstruct the tuple
+ return new global::System.Threading.Tasks.ValueTask(instance.Tuple_Single_Parameter((global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]))));
+ }
+ else if (args.Length == 1 && global::TUnit.Core.Helpers.DataSourceHelpers.IsTuple(args[0]))
+ {
+ // Rare case: tuple is wrapped as a single argument
+ return new global::System.Threading.Tasks.ValueTask(instance.Tuple_Single_Parameter(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0])));
+ }
+ else
+ {
+ throw new global::System.ArgumentException($"Expected 2 unwrapped elements or 1 wrapped tuple, but got {args.Length} arguments");
+ }
+ }
+ case 3:
+ {
+ switch (args.Length)
+ {
+ case 1:
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Private_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])));
+ }
+ default:
+ throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
+ }
+ }
+ default:
+ throw new global::System.ArgumentOutOfRangeException(nameof(methodIndex));
+ }
+ }
+ catch (global::System.Exception ex)
+ {
+ return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex));
+ }
+ }
+ 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));
+ }
+ }
+ private static readonly global::System.Func __createInstance = __CreateInstance;
+ private static readonly global::System.Func __invoke = __Invoke;
+ private static readonly global::System.Func __attributes = __Attributes;
+ public static readonly global::TUnit.Core.TestEntry[] Entries = new global::TUnit.Core.TestEntry[]
+ {
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Object_Property",
+ fullyQualifiedName: "TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests.Object_Property",
+ filePath: "",
+ lineNumber: 21,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.InstanceMethodDataSourceAttribute("ObjectProperty")
+ {
+ Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromInstanceRuntimeValue(dataGeneratorMetadata, static instance => ((global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests)instance).ObjectProperty),
+ },
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests), "Object_Property", false, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 0,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Object_Method",
+ fullyQualifiedName: "TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests.Object_Method",
+ filePath: "",
+ lineNumber: 28,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.InstanceMethodDataSourceAttribute("ObjectMethod")
+ {
+ Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromInstanceRuntimeValue(dataGeneratorMetadata, static instance => ((global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests)instance).ObjectMethod()),
+ },
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests), "Object_Method", false, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 1,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Tuple_Single_Parameter",
+ fullyQualifiedName: "TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests.Tuple_Single_Parameter",
+ filePath: "",
+ lineNumber: 35,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.InstanceMethodDataSourceAttribute("TupleRows")
+ {
+ Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromInstanceEnumerable(dataGeneratorMetadata, static instance => ((global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests)instance).TupleRows),
+ },
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests), "Tuple_Single_Parameter", false, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.ValueTuple), "value", new global::TUnit.Core.ConcreteType(typeof(global::System.ValueTuple)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 2,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Private_Method",
+ fullyQualifiedName: "TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests.Private_Method",
+ filePath: "",
+ lineNumber: 42,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.InstanceMethodDataSourceAttribute("PrivateRows")
+ {
+ Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromInstanceEnumerable(dataGeneratorMetadata, static instance => typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests).GetMethod("PrivateRows", global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.DeclaredOnly, null, global::System.Type.EmptyTypes, null)!.Invoke(instance, null)),
+ },
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7225.InstanceMethodDataSourceMemberShapeTests), "Private_Method", false, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 3,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
+ };
+}
+internal static partial class TUnit_TestRegistration
+{
+ static readonly int _r_TUnit_TestProject_Bugs__7225_InstanceMethodDataSourceMemberShapeTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_Bugs__7225_InstanceMethodDataSourceMemberShapeTests__TestSource.Entries);
+}
diff --git a/tests/TUnit.TestProject/Bugs/_7225/InstanceMethodDataSourceMemberShapeTests.cs b/tests/TUnit.TestProject/Bugs/_7225/InstanceMethodDataSourceMemberShapeTests.cs
new file mode 100644
index 00000000000..3528d5b4a41
--- /dev/null
+++ b/tests/TUnit.TestProject/Bugs/_7225/InstanceMethodDataSourceMemberShapeTests.cs
@@ -0,0 +1,48 @@
+using TUnit.TestProject.Attributes;
+
+namespace TUnit.TestProject.Bugs._7225;
+
+///
+/// [InstanceMethodDataSource] read every member by reflection in source-gen mode before #7225, so its generated
+/// factory must keep that behavior: an object value holding many rows supplies each row, a private method
+/// is still callable, and a single tuple parameter still receives the whole tuple.
+///
+[EngineTest(ExpectedResult.Pass)]
+public class InstanceMethodDataSourceMemberShapeTests
+{
+ public object ObjectProperty => new[] { 61, 62 };
+
+ public object ObjectMethod() => new[] { 63, 64 };
+
+ public IEnumerable<(int, int)> TupleRows => [(1, 2), (3, 4)];
+
+ private IEnumerable PrivateRows() => [65, 66];
+
+ [Test]
+ [InstanceMethodDataSource(nameof(ObjectProperty))]
+ public async Task Object_Property(int value)
+ {
+ await Assert.That(value).IsBetween(61, 62);
+ }
+
+ [Test]
+ [InstanceMethodDataSource(nameof(ObjectMethod))]
+ public async Task Object_Method(int value)
+ {
+ await Assert.That(value).IsBetween(63, 64);
+ }
+
+ [Test]
+ [InstanceMethodDataSource(nameof(TupleRows))]
+ public async Task Tuple_Single_Parameter((int, int) value)
+ {
+ await Assert.That(value.Item2 - value.Item1).IsEqualTo(1);
+ }
+
+ [Test]
+ [InstanceMethodDataSource(nameof(PrivateRows))]
+ public async Task Private_Method(int value)
+ {
+ await Assert.That(value).IsBetween(65, 66);
+ }
+}
From bedcaba9a397ad33e3b1d8a3d5fb5bdcf224ad0b Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Mon, 2 Nov 2026 07:14:13 +0000
Subject: [PATCH 03/10] fix(sourcegen): skip generic overloads and keep null as
an empty source for [InstanceMethodDataSource] (#7225)
A private parameterless data source method beside a parameterless generic
overload of the same name made the generated GetMethod lookup ambiguous; the
factory now filters out generic method definitions instead.
A null value from an [InstanceMethodDataSource] member became one null row,
so SkipIfEmpty no longer skipped the test as the reflection path does. The
generated factory now reads null as an empty source.
---
.../Generators/TestMetadataGenerator.cs | 24 ++++-
.../Bugs/7225/Tests7225.cs | 17 +--
.../Tests1821.Test.verified.txt | 2 +-
.../Tests7225.MemberShapes.verified.txt | 100 ++++++++++++++++--
.../Tests7225.Test.verified.txt | 6 +-
...nstanceMethodDataSourceMemberShapeTests.cs | 23 +++-
6 files changed, 148 insertions(+), 24 deletions(-)
diff --git a/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
index f9cfe660702..382202c6723 100644
--- a/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
+++ b/src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
@@ -3088,6 +3088,13 @@ private static void EmitDataSourceFactoryInvocation(CodeWriter writer, ISymbol d
metadataArgument = "dataGeneratorMetadata, ";
}
+ // [InstanceMethodDataSource] read its member by reflection before #7225, and that path, still used in
+ // reflection mode, treats a null value as a source with no rows, so SkipIfEmpty skips the test. The factory
+ // helpers would turn null into one row holding null, so a null value becomes an empty source here.
+ var nullAsEmpty = decidesRowsFromRuntimeValue && kind is "Value" or "Enumerable"
+ ? " ?? global::System.Array.Empty