Skip to content
Merged
Changes from all commits
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
131 changes: 103 additions & 28 deletions TUnit.Engine/Extensions/TestContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,119 @@ namespace TUnit.Engine.Extensions;

internal static class TestContextExtensions
{
private static object?[] GetInternal(TestContext testContext)
public static IEnumerable<object> 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<object?>(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<object> 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<object>().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<string, object?> props)
{
var count = 0;
foreach (var prop in props)
{
if (prop.Value != null)
{
count++;
}
}
return count;
}
}
Loading