diff --git a/TUnit.Core/Helpers/ValueListBuilder.cs b/TUnit.Core/Helpers/ValueListBuilder.cs index ad714b060c..174ce51fde 100644 --- a/TUnit.Core/Helpers/ValueListBuilder.cs +++ b/TUnit.Core/Helpers/ValueListBuilder.cs @@ -59,6 +59,14 @@ public void Append(T item) } } + public void AppendIfNotNull(T? item) + { + if (item != null) + { + Append(item); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(scoped ReadOnlySpan source) { diff --git a/TUnit.Engine/Extensions/TestContextExtensions.cs b/TUnit.Engine/Extensions/TestContextExtensions.cs index 27c00913fa..ab5192d62e 100644 --- a/TUnit.Engine/Extensions/TestContextExtensions.cs +++ b/TUnit.Engine/Extensions/TestContextExtensions.cs @@ -1,331 +1,61 @@ using TUnit.Core; -using TUnit.Core.Enums; -using TUnit.Core.Interfaces; -using TUnit.Engine.Utilities; +using TUnit.Core.Helpers; namespace TUnit.Engine.Extensions; internal static class TestContextExtensions { - /// - /// Ensures all event receiver caches are populated. Iterates through eligible objects once - /// and categorizes them by type in a single pass. - /// - /// - /// Class instances change in these scenarios: - /// - Test retries: A new instance is created for each retry attempt - /// - Keyed test instances: Different data combinations may use different instances - /// When this happens, eligible event objects may include the new instance (if it implements - /// event receiver interfaces), so all caches must be invalidated and rebuilt. - /// - private static void EnsureEventReceiversCached(TestContext testContext) + private static object[] GetInternal(TestContext testContext) { - var currentClassInstance = testContext.Metadata.TestDetails.ClassInstance; + var testClassArgs = testContext.Metadata.TestDetails.TestClassArguments; + var attributes = (List)testContext.Metadata.TestDetails.GetAllAttributes(); + var testMethodArgs = testContext.Metadata.TestDetails.TestMethodArguments; + var injectedProps = testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments; - // Check if caches are valid (populated and class instance hasn't changed) -#if NET - if (testContext.CachedTestStartReceiversEarly != null && - ReferenceEquals(testContext.CachedClassInstance, currentClassInstance)) + // Pre-calculate capacity to avoid reallocations + var capacity = 3 + testClassArgs.Length + attributes.Count + testMethodArgs.Length + injectedProps.Count; + var result = new ValueListBuilder(capacity); + + result.AppendIfNotNull(testContext.ClassConstructor); + result.AppendIfNotNull(testContext.Events); + foreach (var value in testClassArgs) { - return; + result.AppendIfNotNull(value); } -#else - if (testContext.CachedTestStartReceivers != null && - ReferenceEquals(testContext.CachedClassInstance, currentClassInstance)) + result.AppendIfNotNull(testContext.Metadata.TestDetails.ClassInstance); + foreach (var value in attributes) { - return; + result.AppendIfNotNull(value); } -#endif - - // Invalidate stale caches if class instance changed - if (testContext.CachedClassInstance != null && - !ReferenceEquals(testContext.CachedClassInstance, currentClassInstance)) + foreach (var value in testMethodArgs) { - testContext.InvalidateEventReceiverCaches(); + result.AppendIfNotNull(value); } - // Build caches - get eligible objects first - var eligibleObjects = BuildEligibleEventObjects(testContext); - testContext.CachedEligibleEventObjects = eligibleObjects; - - // Single pass: categorize each object by interface type -#if NET - List? startReceiversEarly = null; - List? startReceiversLate = null; - List? endReceiversEarly = null; - List? endReceiversLate = null; -#else - List? startReceivers = null; - List? endReceivers = null; -#endif - List? skippedReceivers = null; - List? discoveryReceivers = null; - List? registeredReceivers = null; - - foreach (var obj in eligibleObjects) + if (injectedProps.Count > 0) { - // Check each interface - an object can implement multiple - if (obj is ITestStartEventReceiver startReceiver) - { -#if NET - if (startReceiver.Stage == EventReceiverStage.Early) - { - startReceiversEarly ??= []; - startReceiversEarly.Add(startReceiver); - } - else - { - startReceiversLate ??= []; - startReceiversLate.Add(startReceiver); - } -#else - startReceivers ??= []; - startReceivers.Add(startReceiver); -#endif - } - - if (obj is ITestEndEventReceiver endReceiver) - { -#if NET - if (endReceiver.Stage == EventReceiverStage.Early) - { - endReceiversEarly ??= []; - endReceiversEarly.Add(endReceiver); - } - else - { - endReceiversLate ??= []; - endReceiversLate.Add(endReceiver); - } -#else - endReceivers ??= []; - endReceivers.Add(endReceiver); -#endif - } - - if (obj is ITestSkippedEventReceiver skippedReceiver) + foreach (var prop in injectedProps) { - skippedReceivers ??= []; - skippedReceivers.Add(skippedReceiver); + result.AppendIfNotNull(prop.Value); } - - if (obj is ITestDiscoveryEventReceiver discoveryReceiver) - { - discoveryReceivers ??= []; - discoveryReceivers.Add(discoveryReceiver); - } - - if (obj is ITestRegisteredEventReceiver registeredReceiver) - { - registeredReceivers ??= []; - registeredReceivers.Add(registeredReceiver); - } - } - - // Sort and apply scoped filtering, then cache -#if NET - testContext.CachedTestStartReceiversEarly = SortAndFilter(startReceiversEarly); - testContext.CachedTestStartReceiversLate = SortAndFilter(startReceiversLate); - testContext.CachedTestEndReceiversEarly = SortAndFilter(endReceiversEarly); - testContext.CachedTestEndReceiversLate = SortAndFilter(endReceiversLate); -#else - testContext.CachedTestStartReceivers = SortAndFilter(startReceivers); - testContext.CachedTestEndReceivers = SortAndFilter(endReceivers); -#endif - testContext.CachedTestSkippedReceivers = SortAndFilter(skippedReceivers); - testContext.CachedTestDiscoveryReceivers = SortAndFilter(discoveryReceivers); - testContext.CachedTestRegisteredReceivers = SortAndFilter(registeredReceivers); - - // Update cached class instance last - testContext.CachedClassInstance = currentClassInstance; - } - - private static T[] SortAndFilter(List? receivers) where T : class, IEventReceiver - { - if (receivers == null || receivers.Count == 0) - { - return []; } - // Sort by Order - receivers.Sort((a, b) => a.Order.CompareTo(b.Order)); - - // Apply scoped attribute filtering and return as array - var filtered = ScopedAttributeFilter.FilterScopedAttributes(receivers); - return filtered.ToArray(); + var arr = result.AsSpan().ToArray(); + result.Dispose(); + return arr; } public static IEnumerable GetEligibleEventObjects(this TestContext testContext) { - // Use EnsureEventReceiversCached which builds eligible objects as part of cache initialization - EnsureEventReceiversCached(testContext); - return testContext.CachedEligibleEventObjects!; - } - - private static object[] BuildEligibleEventObjects(TestContext testContext) - { - var details = testContext.Metadata.TestDetails; - var testClassArgs = details.TestClassArguments; - var attributes = details.GetAllAttributes(); - var testMethodArgs = details.TestMethodArguments; - var injectedProps = details.TestClassInjectedPropertyArguments; - - // Count non-null items first to allocate exact size - var count = CountNonNull(testContext.ClassConstructor) - + CountNonNull(testContext.Events) - + CountNonNullInArray(testClassArgs) - + CountNonNull(details.ClassInstance) - + attributes.Count // Attributes are never null - + CountNonNullInArray(testMethodArgs) - + CountNonNullValues(injectedProps); - - if (count == 0) + // Return cached result if available + if (testContext.CachedEligibleEventObjects != null) { - return []; - } - - // Single allocation with exact size - var result = new object[count]; - var index = 0; - - // Add items, skipping nulls - if (testContext.ClassConstructor is { } constructor) - { - result[index++] = constructor; - } - - if (testContext.Events is { } events) - { - result[index++] = events; - } - - foreach (var arg in testClassArgs) - { - if (arg is { } nonNullArg) - { - result[index++] = nonNullArg; - } - } - - if (details.ClassInstance is { } classInstance) - { - result[index++] = classInstance; - } - - foreach (var attr in attributes) - { - result[index++] = attr; - } - - foreach (var arg in testMethodArgs) - { - if (arg is { } nonNullArg) - { - result[index++] = nonNullArg; - } - } - - foreach (var prop in injectedProps) - { - if (prop.Value is { } value) - { - result[index++] = value; - } + return testContext.CachedEligibleEventObjects; } + // Materialize and cache the result + var result = GetInternal(testContext); + testContext.CachedEligibleEventObjects = result; return result; } - - private static int CountNonNull(object? obj) => obj != null ? 1 : 0; - - private static int CountNonNullInArray(object?[] array) - { - var count = 0; - foreach (var item in array) - { - if (item != null) - { - count++; - } - } - return count; - } - - private static int CountNonNullValues(IDictionary props) - { - var count = 0; - foreach (var prop in props) - { - if (prop.Value != null) - { - count++; - } - } - return count; - } - - /// - /// Gets pre-computed test start receivers (filtered, sorted, scoped-attribute filtered). - /// -#if NET - public static ITestStartEventReceiver[] GetTestStartReceivers(this TestContext testContext, EventReceiverStage stage) - { - EnsureEventReceiversCached(testContext); - return stage == EventReceiverStage.Early - ? testContext.CachedTestStartReceiversEarly! - : testContext.CachedTestStartReceiversLate!; - } -#else - public static ITestStartEventReceiver[] GetTestStartReceivers(this TestContext testContext) - { - EnsureEventReceiversCached(testContext); - return testContext.CachedTestStartReceivers!; - } -#endif - - /// - /// Gets pre-computed test end receivers (filtered, sorted, scoped-attribute filtered). - /// -#if NET - public static ITestEndEventReceiver[] GetTestEndReceivers(this TestContext testContext, EventReceiverStage stage) - { - EnsureEventReceiversCached(testContext); - return stage == EventReceiverStage.Early - ? testContext.CachedTestEndReceiversEarly! - : testContext.CachedTestEndReceiversLate!; - } -#else - public static ITestEndEventReceiver[] GetTestEndReceivers(this TestContext testContext) - { - EnsureEventReceiversCached(testContext); - return testContext.CachedTestEndReceivers!; - } -#endif - - /// - /// Gets pre-computed test skipped receivers (filtered, sorted, scoped-attribute filtered). - /// - public static ITestSkippedEventReceiver[] GetTestSkippedReceivers(this TestContext testContext) - { - EnsureEventReceiversCached(testContext); - return testContext.CachedTestSkippedReceivers!; - } - - /// - /// Gets pre-computed test discovery receivers (filtered, sorted, scoped-attribute filtered). - /// - public static ITestDiscoveryEventReceiver[] GetTestDiscoveryReceivers(this TestContext testContext) - { - EnsureEventReceiversCached(testContext); - return testContext.CachedTestDiscoveryReceivers!; - } - - /// - /// Gets pre-computed test registered receivers (filtered, sorted, scoped-attribute filtered). - /// - public static ITestRegisteredEventReceiver[] GetTestRegisteredReceivers(this TestContext testContext) - { - EnsureEventReceiversCached(testContext); - return testContext.CachedTestRegisteredReceivers!; - } }