Skip to content

Commit fa06a50

Browse files
thomhurstclaude
andcommitted
perf: cache attribute factory results on TestMetadata
Add GetOrCreateAttributes() that lazily caches the attribute array on first call. AttributeFactory was being invoked multiple times per test during building — now it runs once and subsequent calls return the cached array, avoiding redundant attribute instantiation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 67e51fa commit fa06a50

5 files changed

Lines changed: 22 additions & 11 deletions

File tree

TUnit.Core/GenericTestMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecut
6666
Func<TestContext, Task<object>> createInstance = async (testContext) =>
6767
{
6868
// Try to create instance with ClassConstructor attribute
69-
var attributes = metadata.AttributeFactory();
69+
var attributes = metadata.GetOrCreateAttributes();
7070
var classInstance = await ClassConstructorHelper.TryCreateInstanceWithClassConstructor(
7171
attributes,
7272
TestClassType,

TUnit.Core/TestMetadata.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ public abstract class TestMetadata
4949

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

52+
private Attribute[]? _cachedAttributes;
53+
54+
/// <summary>
55+
/// Returns the cached attributes array, creating it from <see cref="AttributeFactory"/> on first call.
56+
/// Subsequent calls return the same array without re-invoking the factory.
57+
/// </summary>
58+
internal Attribute[] GetOrCreateAttributes()
59+
{
60+
return _cachedAttributes ??= AttributeFactory();
61+
}
62+
5263
/// <summary>
5364
/// Pre-extracted repeat count from RepeatAttribute.
5465
/// Null if no repeat attribute is present (defaults to 0 at usage site).

TUnit.Core/TestMetadata`1.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecut
8686
return await context.TestClassInstanceFactory();
8787
}
8888

89-
var attributes = metadata.AttributeFactory();
89+
var attributes = metadata.GetOrCreateAttributes();
9090
var instance = await ClassConstructorHelper.TryCreateInstanceWithClassConstructor(
9191
attributes,
9292
TestClassType,

TUnit.Engine/Building/TestBuilder.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private async Task<object> CreateInstance(TestMetadata metadata, Type[] resolved
7373

7474
// First try to create instance with ClassConstructor attribute
7575
// Use attributes from context if available
76-
var attributes = builderContext.InitializedAttributes ?? metadata.AttributeFactory();
76+
var attributes = builderContext.InitializedAttributes ?? metadata.GetOrCreateAttributes();
7777

7878
var instance = await ClassConstructorHelper.TryCreateInstanceWithClassConstructor(
7979
attributes,
@@ -154,7 +154,7 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
154154
var repeatCount = metadata.RepeatCount ?? 0;
155155

156156
// Create and initialize attributes ONCE
157-
var attributes = await InitializeAttributesAsync(metadata.AttributeFactory.Invoke());
157+
var attributes = await InitializeAttributesAsync(metadata.GetOrCreateAttributes());
158158

159159
if (metadata.ClassDataSources.Any(ds => ds is IAccessesInstanceData))
160160
{
@@ -1017,7 +1017,7 @@ private static void CollectAllDependencies(AbstractExecutableTest test, HashSet<
10171017
/// </summary>
10181018
private static string? GetBasicSkipReason(TestMetadata metadata, Attribute[]? cachedAttributes = null)
10191019
{
1020-
var attributes = cachedAttributes ?? metadata.AttributeFactory();
1020+
var attributes = cachedAttributes ?? metadata.GetOrCreateAttributes();
10211021

10221022
SkipAttribute? firstSkipAttribute = null;
10231023

@@ -1047,7 +1047,7 @@ private static void CollectAllDependencies(AbstractExecutableTest test, HashSet<
10471047
private async ValueTask<TestContext> CreateTestContextAsync(string testId, TestMetadata metadata, TestData testData, TestBuilderContext testBuilderContext)
10481048
{
10491049
// Use attributes from context if available, or create new ones
1050-
var attributes = testBuilderContext.InitializedAttributes ?? await InitializeAttributesAsync(metadata.AttributeFactory.Invoke());
1050+
var attributes = testBuilderContext.InitializedAttributes ?? await InitializeAttributesAsync(metadata.GetOrCreateAttributes());
10511051

10521052
if (testBuilderContext.DataSourceAttribute != null && testBuilderContext.DataSourceAttribute is not NoDataSource)
10531053
{
@@ -1138,7 +1138,7 @@ private async Task<AbstractExecutableTest> CreateFailedTestForDataGenerationErro
11381138

11391139
private async Task<TestDetails> CreateFailedTestDetails(TestMetadata metadata, string testId)
11401140
{
1141-
var attributes = (await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()));
1141+
var attributes = (await InitializeAttributesAsync(metadata.GetOrCreateAttributes()));
11421142
return new TestDetails(attributes)
11431143
{
11441144
TestId = testId,
@@ -1524,7 +1524,7 @@ public async IAsyncEnumerable<AbstractExecutableTest> BuildTestsStreamingAsync(
15241524
var repeatCount = metadata.RepeatCount ?? 0;
15251525

15261526
// Initialize attributes
1527-
var attributes = await InitializeAttributesAsync(metadata.AttributeFactory.Invoke());
1527+
var attributes = await InitializeAttributesAsync(metadata.GetOrCreateAttributes());
15281528

15291529
// Create base context with ClassConstructor if present
15301530
// StateBag and Events are lazy-initialized for performance

TUnit.Engine/Building/TestBuilderPipeline.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ private TestBuilderContext CreateTestBuilderContext(TestMetadata metadata)
5454
};
5555

5656
// Check for ClassConstructor attribute and set it early if present
57-
var attributes = metadata.AttributeFactory();
57+
var attributes = metadata.GetOrCreateAttributes();
5858

5959
// Look for any attribute that inherits from ClassConstructorAttribute
6060
// This handles both ClassConstructorAttribute and ClassConstructorAttribute<T>
@@ -246,7 +246,7 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
246246
: baseDisplayName;
247247

248248
// Get attributes first
249-
var attributes = metadata.AttributeFactory();
249+
var attributes = metadata.GetOrCreateAttributes();
250250

251251
// Create TestDetails for dynamic tests
252252
var testDetails = new TestDetails(attributes)
@@ -345,7 +345,7 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
345345
var repeatCount = resolvedMetadata.RepeatCount ?? 0;
346346

347347
// Get attributes for test details
348-
var attributes = resolvedMetadata.AttributeFactory?.Invoke() ?? [];
348+
var attributes = resolvedMetadata.GetOrCreateAttributes();
349349

350350
// Dynamic tests need to honor attributes like RepeatCount, RetryCount, etc.
351351
// We'll create multiple test instances based on RepeatCount

0 commit comments

Comments
 (0)