Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions TUnit.Core/ExecutableTest`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ public override async Task<object> CreateInstanceAsync()

public override async Task InvokeTestAsync(object instance, CancellationToken cancellationToken)
{
var indexed = _metadata.IndexedInvokeBody;
if (indexed is not null)
{
await indexed((T)instance, _metadata.MethodIndex, Arguments, cancellationToken).ConfigureAwait(false);
return;
}

await _metadata.InvokeTypedTest!((T)instance, Arguments, cancellationToken).ConfigureAwait(false);
}
}
17 changes: 7 additions & 10 deletions TUnit.Core/TestEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,14 @@ public sealed class TestEntry<
/// <summary>
/// Constructs a TestMetadata&lt;T&gt; from this entry's data and delegates.
/// </summary>
// Cached delegates and arrays — built once from immutable fields
private Func<T, object?[], CancellationToken, ValueTask>? _cachedInvokeTypedTest;
private Func<Attribute[]>? _cachedAttributeFactory;
// Cached arrays — built once from immutable fields. The class-shared InvokeBody/CreateAttributes
// delegates and their indexes are forwarded directly onto the resulting TestMetadata so there is
// no per-test closure allocation on the hot path.
private PropertyDataSource[]? _cachedPropertyDataSources;
private PropertyInjectionData[]? _cachedPropertyInjections;

internal TestMetadata<T> ToTestMetadata(string testSessionId)
{
if (_cachedInvokeTypedTest is null)
Interlocked.CompareExchange(ref _cachedInvokeTypedTest, (instance, args, ct) => InvokeBody(instance, MethodIndex, args, ct), null);
if (_cachedAttributeFactory is null)
Interlocked.CompareExchange(ref _cachedAttributeFactory, () => CreateAttributes(AttributeGroupIndex), null);

return new TestMetadata<T>
{
TestName = MethodName,
Expand All @@ -125,8 +120,10 @@ internal TestMetadata<T> ToTestMetadata(string testSessionId)
PropertyDataSources = _cachedPropertyDataSources ??= BuildPropertyDataSources(),
PropertyInjections = _cachedPropertyInjections ??= BuildPropertyInjections(),
InstanceFactory = CreateInstance,
InvokeTypedTest = _cachedInvokeTypedTest,
AttributeFactory = _cachedAttributeFactory,
IndexedInvokeBody = InvokeBody,
MethodIndex = MethodIndex,
IndexedAttributeFactory = CreateAttributes,
AttributeGroupIndex = AttributeGroupIndex,
FilePath = FilePath,
LineNumber = LineNumber,
MethodMetadata = MethodMetadata,
Expand Down
30 changes: 26 additions & 4 deletions TUnit.Core/TestMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,39 @@ public abstract class TestMetadata

public Type[]? GenericMethodTypeArguments { get; init; }

public required Func<Attribute[]> AttributeFactory { get; init; }
public Func<Attribute[]>? AttributeFactory { get; init; }

/// <summary>
/// Class-shared indexed attribute factory. When set, used in preference to <see cref="AttributeFactory"/>
/// so TestEntry-emitted metadata avoids allocating a per-test closure over <see cref="AttributeGroupIndex"/>.
/// </summary>
public Func<int, Attribute[]>? IndexedAttributeFactory { get; init; }

/// <summary>Index passed to <see cref="IndexedAttributeFactory"/> when dispatching.</summary>
public int AttributeGroupIndex { get; init; }

private Attribute[]? _cachedAttributes;

/// <summary>
/// Returns the cached attributes array, creating it from <see cref="AttributeFactory"/> on first call.
/// Subsequent calls return the same array without re-invoking the factory.
/// Returns the cached attributes array, creating it from <see cref="IndexedAttributeFactory"/>
/// (preferred) or <see cref="AttributeFactory"/> on first call. Subsequent calls return the same array.
/// </summary>
internal Attribute[] GetOrCreateAttributes()
{
return _cachedAttributes ??= AttributeFactory();
if (_cachedAttributes is not null)
{
return _cachedAttributes;
}

var indexed = IndexedAttributeFactory;
if (indexed is not null)
{
return _cachedAttributes = indexed(AttributeGroupIndex);
}

var factory = AttributeFactory
?? throw new InvalidOperationException($"No attribute factory configured for test '{TestName}'.");
return _cachedAttributes = factory();
}

/// <summary>
Expand Down
14 changes: 12 additions & 2 deletions TUnit.Core/TestMetadata`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ public class TestMetadata<
/// </summary>
public Func<T, object?[], CancellationToken, ValueTask>? InvokeTypedTest { get; init; }

/// <summary>
/// Class-shared indexed invoker emitted by the source generator. All tests in a class share this
/// delegate and dispatch via <see cref="MethodIndex"/>, so the TestEntry → TestMetadata bridge can
/// forward the static delegate without allocating a per-test closure capturing <c>this</c>.
/// </summary>
public Func<T, int, object?[], CancellationToken, ValueTask>? IndexedInvokeBody { get; init; }

/// <summary>Index passed to <see cref="IndexedInvokeBody"/> when dispatching.</summary>
public int MethodIndex { get; init; }




Expand All @@ -77,14 +87,14 @@ public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecut
return cached;
}

if (InstanceFactory != null && InvokeTypedTest != null)
if (InstanceFactory != null && (InvokeTypedTest != null || IndexedInvokeBody != null))
{
Interlocked.CompareExchange<Func<ExecutableTestCreationContext, TestMetadata, AbstractExecutableTest>?>(
ref _cachedExecutableTestFactory, CreateTypedExecutableTest, null);
return _cachedExecutableTestFactory!;
}

throw new InvalidOperationException($"InstanceFactory and InvokeTypedTest must be set for {typeof(T).Name}");
throw new InvalidOperationException($"InstanceFactory and an invoker (InvokeTypedTest or IndexedInvokeBody) must be set for {typeof(T).Name}");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1587,7 +1587,8 @@ namespace
public abstract class TestMetadata
{
protected TestMetadata() { }
public required <[]> AttributeFactory { get; init; }
public <[]>? AttributeFactory { get; init; }
public int AttributeGroupIndex { get; init; }
public required .IDataSourceAttribute[] ClassDataSources { get; init; }
public abstract <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; }
public required .IDataSourceAttribute[] DataSources { get; init; }
Expand All @@ -1596,6 +1597,7 @@ namespace
public .GenericMethodInfo? GenericMethodInfo { get; init; }
public []? GenericMethodTypeArguments { get; init; }
public .GenericTypeInfo? GenericTypeInfo { get; init; }
public <int, []>? IndexedAttributeFactory { get; init; }
public int InheritanceDepth { get; set; }
public <[], object?[], object> InstanceFactory { get; init; }
public required int LineNumber { get; init; }
Expand Down Expand Up @@ -1638,8 +1640,10 @@ namespace
{
public TestMetadata() { }
public override <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; }
public <T, int, object?[], .CancellationToken, .>? IndexedInvokeBody { get; init; }
public new <[], object?[], T>? InstanceFactory { get; init; }
public <T, object?[], .CancellationToken, .>? InvokeTypedTest { get; init; }
public int MethodIndex { get; init; }
public new <T, object?[], .>? TestInvoker { get; init; }
public void UseRuntimeDataGeneration(string testSessionId) { }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1587,7 +1587,8 @@ namespace
public abstract class TestMetadata
{
protected TestMetadata() { }
public required <[]> AttributeFactory { get; init; }
public <[]>? AttributeFactory { get; init; }
public int AttributeGroupIndex { get; init; }
public required .IDataSourceAttribute[] ClassDataSources { get; init; }
public abstract <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; }
public required .IDataSourceAttribute[] DataSources { get; init; }
Expand All @@ -1596,6 +1597,7 @@ namespace
public .GenericMethodInfo? GenericMethodInfo { get; init; }
public []? GenericMethodTypeArguments { get; init; }
public .GenericTypeInfo? GenericTypeInfo { get; init; }
public <int, []>? IndexedAttributeFactory { get; init; }
public int InheritanceDepth { get; set; }
public <[], object?[], object> InstanceFactory { get; init; }
public required int LineNumber { get; init; }
Expand Down Expand Up @@ -1638,8 +1640,10 @@ namespace
{
public TestMetadata() { }
public override <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; }
public <T, int, object?[], .CancellationToken, .>? IndexedInvokeBody { get; init; }
public new <[], object?[], T>? InstanceFactory { get; init; }
public <T, object?[], .CancellationToken, .>? InvokeTypedTest { get; init; }
public int MethodIndex { get; init; }
public new <T, object?[], .>? TestInvoker { get; init; }
public void UseRuntimeDataGeneration(string testSessionId) { }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1587,7 +1587,8 @@ namespace
public abstract class TestMetadata
{
protected TestMetadata() { }
public required <[]> AttributeFactory { get; init; }
public <[]>? AttributeFactory { get; init; }
public int AttributeGroupIndex { get; init; }
public required .IDataSourceAttribute[] ClassDataSources { get; init; }
public abstract <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; }
public required .IDataSourceAttribute[] DataSources { get; init; }
Expand All @@ -1596,6 +1597,7 @@ namespace
public .GenericMethodInfo? GenericMethodInfo { get; init; }
public []? GenericMethodTypeArguments { get; init; }
public .GenericTypeInfo? GenericTypeInfo { get; init; }
public <int, []>? IndexedAttributeFactory { get; init; }
public int InheritanceDepth { get; set; }
public <[], object?[], object> InstanceFactory { get; init; }
public required int LineNumber { get; init; }
Expand Down Expand Up @@ -1638,8 +1640,10 @@ namespace
{
public TestMetadata() { }
public override <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; }
public <T, int, object?[], .CancellationToken, .>? IndexedInvokeBody { get; init; }
public new <[], object?[], T>? InstanceFactory { get; init; }
public <T, object?[], .CancellationToken, .>? InvokeTypedTest { get; init; }
public int MethodIndex { get; init; }
public new <T, object?[], .>? TestInvoker { get; init; }
public void UseRuntimeDataGeneration(string testSessionId) { }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1528,7 +1528,8 @@ namespace
public abstract class TestMetadata
{
protected TestMetadata() { }
public required <[]> AttributeFactory { get; init; }
public <[]>? AttributeFactory { get; init; }
public int AttributeGroupIndex { get; init; }
public required .IDataSourceAttribute[] ClassDataSources { get; init; }
public abstract <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; }
public required .IDataSourceAttribute[] DataSources { get; init; }
Expand All @@ -1537,6 +1538,7 @@ namespace
public .GenericMethodInfo? GenericMethodInfo { get; init; }
public []? GenericMethodTypeArguments { get; init; }
public .GenericTypeInfo? GenericTypeInfo { get; init; }
public <int, []>? IndexedAttributeFactory { get; init; }
public int InheritanceDepth { get; set; }
public <[], object?[], object> InstanceFactory { get; init; }
public required int LineNumber { get; init; }
Expand Down Expand Up @@ -1576,8 +1578,10 @@ namespace
{
public TestMetadata() { }
public override <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; }
public <T, int, object?[], .CancellationToken, .>? IndexedInvokeBody { get; init; }
public new <[], object?[], T>? InstanceFactory { get; init; }
public <T, object?[], .CancellationToken, .>? InvokeTypedTest { get; init; }
public int MethodIndex { get; init; }
public new <T, object?[], .>? TestInvoker { get; init; }
public void UseRuntimeDataGeneration(string testSessionId) { }
}
Expand Down
Loading