diff --git a/TUnit.Core/GenericTestMetadata.cs b/TUnit.Core/GenericTestMetadata.cs index 8bb798ef7c..19f1950f1a 100644 --- a/TUnit.Core/GenericTestMetadata.cs +++ b/TUnit.Core/GenericTestMetadata.cs @@ -66,7 +66,7 @@ public override Func> createInstance = async (testContext) => { // Try to create instance with ClassConstructor attribute - var attributes = metadata.AttributeFactory(); + var attributes = metadata.GetOrCreateAttributes(); var classInstance = await ClassConstructorHelper.TryCreateInstanceWithClassConstructor( attributes, TestClassType, diff --git a/TUnit.Core/TestMetadata.cs b/TUnit.Core/TestMetadata.cs index 49cb6279ed..1ef7e6c30e 100644 --- a/TUnit.Core/TestMetadata.cs +++ b/TUnit.Core/TestMetadata.cs @@ -49,6 +49,17 @@ public abstract class TestMetadata public required Func AttributeFactory { get; init; } + private Attribute[]? _cachedAttributes; + + /// + /// Returns the cached attributes array, creating it from on first call. + /// Subsequent calls return the same array without re-invoking the factory. + /// + internal Attribute[] GetOrCreateAttributes() + { + return _cachedAttributes ??= AttributeFactory(); + } + /// /// Pre-extracted repeat count from RepeatAttribute. /// Null if no repeat attribute is present (defaults to 0 at usage site). diff --git a/TUnit.Core/TestMetadata`1.cs b/TUnit.Core/TestMetadata`1.cs index b759c25bf8..7fa09fe4b1 100644 --- a/TUnit.Core/TestMetadata`1.cs +++ b/TUnit.Core/TestMetadata`1.cs @@ -86,7 +86,7 @@ public override Func CreateInstance(TestMetadata metadata, Type[] resolved // First try to create instance with ClassConstructor attribute // Use attributes from context if available - var attributes = builderContext.InitializedAttributes ?? metadata.AttributeFactory(); + var attributes = builderContext.InitializedAttributes ?? metadata.GetOrCreateAttributes(); var instance = await ClassConstructorHelper.TryCreateInstanceWithClassConstructor( attributes, @@ -154,7 +154,7 @@ public async Task> BuildTestsFromMetadataAsy var repeatCount = metadata.RepeatCount ?? 0; // Create and initialize attributes ONCE - var attributes = await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()); + var attributes = await InitializeAttributesAsync(metadata.GetOrCreateAttributes()); if (metadata.ClassDataSources.Any(ds => ds is IAccessesInstanceData)) { @@ -1017,7 +1017,7 @@ private static void CollectAllDependencies(AbstractExecutableTest test, HashSet< /// private static string? GetBasicSkipReason(TestMetadata metadata, Attribute[]? cachedAttributes = null) { - var attributes = cachedAttributes ?? metadata.AttributeFactory(); + var attributes = cachedAttributes ?? metadata.GetOrCreateAttributes(); SkipAttribute? firstSkipAttribute = null; @@ -1047,7 +1047,7 @@ private static void CollectAllDependencies(AbstractExecutableTest test, HashSet< private async ValueTask CreateTestContextAsync(string testId, TestMetadata metadata, TestData testData, TestBuilderContext testBuilderContext) { // Use attributes from context if available, or create new ones - var attributes = testBuilderContext.InitializedAttributes ?? await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()); + var attributes = testBuilderContext.InitializedAttributes ?? await InitializeAttributesAsync(metadata.GetOrCreateAttributes()); if (testBuilderContext.DataSourceAttribute != null && testBuilderContext.DataSourceAttribute is not NoDataSource) { @@ -1138,7 +1138,7 @@ private async Task CreateFailedTestForDataGenerationErro private async Task CreateFailedTestDetails(TestMetadata metadata, string testId) { - var attributes = (await InitializeAttributesAsync(metadata.AttributeFactory.Invoke())); + var attributes = (await InitializeAttributesAsync(metadata.GetOrCreateAttributes())); return new TestDetails(attributes) { TestId = testId, @@ -1524,7 +1524,7 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( var repeatCount = metadata.RepeatCount ?? 0; // Initialize attributes - var attributes = await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()); + var attributes = await InitializeAttributesAsync(metadata.GetOrCreateAttributes()); // Create base context with ClassConstructor if present // StateBag and Events are lazy-initialized for performance diff --git a/TUnit.Engine/Building/TestBuilderPipeline.cs b/TUnit.Engine/Building/TestBuilderPipeline.cs index b87c3e01bb..5a012458bd 100644 --- a/TUnit.Engine/Building/TestBuilderPipeline.cs +++ b/TUnit.Engine/Building/TestBuilderPipeline.cs @@ -54,7 +54,7 @@ private TestBuilderContext CreateTestBuilderContext(TestMetadata metadata) }; // Check for ClassConstructor attribute and set it early if present - var attributes = metadata.AttributeFactory(); + var attributes = metadata.GetOrCreateAttributes(); // Look for any attribute that inherits from ClassConstructorAttribute // This handles both ClassConstructorAttribute and ClassConstructorAttribute @@ -246,7 +246,7 @@ private async Task GenerateDynamicTests(TestMetadata m : baseDisplayName; // Get attributes first - var attributes = metadata.AttributeFactory(); + var attributes = metadata.GetOrCreateAttributes(); // Create TestDetails for dynamic tests var testDetails = new TestDetails(attributes) @@ -345,7 +345,7 @@ private async IAsyncEnumerable BuildTestsFromSingleMetad var repeatCount = resolvedMetadata.RepeatCount ?? 0; // Get attributes for test details - var attributes = resolvedMetadata.AttributeFactory?.Invoke() ?? []; + var attributes = resolvedMetadata.GetOrCreateAttributes(); // Dynamic tests need to honor attributes like RepeatCount, RetryCount, etc. // We'll create multiple test instances based on RepeatCount