diff --git a/TUnit.Engine/Extensions/TestContextExtensions.cs b/TUnit.Engine/Extensions/TestContextExtensions.cs index e9637f4259..3b64e65d7c 100644 --- a/TUnit.Engine/Extensions/TestContextExtensions.cs +++ b/TUnit.Engine/Extensions/TestContextExtensions.cs @@ -4,44 +4,119 @@ namespace TUnit.Engine.Extensions; internal static class TestContextExtensions { - private static object?[] GetInternal(TestContext testContext) + public static IEnumerable GetEligibleEventObjects(this TestContext testContext) { - var testClassArgs = testContext.Metadata.TestDetails.TestClassArguments; - var attributes = testContext.Metadata.TestDetails.GetAllAttributes(); - var testMethodArgs = testContext.Metadata.TestDetails.TestMethodArguments; - var injectedProps = testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments; - - // Pre-calculate capacity to avoid reallocations - var capacity = 3 + testClassArgs.Length + attributes.Count + testMethodArgs.Length + injectedProps.Count; - var result = new List(capacity); - - result.Add(testContext.ClassConstructor); - result.Add(testContext.Events); - result.AddRange(testClassArgs); - result.Add(testContext.Metadata.TestDetails.ClassInstance); - result.AddRange(attributes); - result.AddRange(testMethodArgs); - - // Manual loop instead of .Select() to avoid LINQ allocation - foreach (var prop in injectedProps) + // Return cached result if available + if (testContext.CachedEligibleEventObjects != null) { - result.Add(prop.Value); + return testContext.CachedEligibleEventObjects; } - return result.ToArray(); + // Build result directly with single allocation + var result = BuildEligibleEventObjects(testContext); + testContext.CachedEligibleEventObjects = result; + return result; } - public static IEnumerable GetEligibleEventObjects(this TestContext testContext) + private static object[] BuildEligibleEventObjects(TestContext testContext) { - // Return cached result if available - if (testContext.CachedEligibleEventObjects != null) + 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 testContext.CachedEligibleEventObjects; + 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; + } } - // Materialize and cache the result - var result = GetInternal(testContext).OfType().ToArray(); - 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; + } }