Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ internal partial class TestMethodInfo

internal object?[] ResolveArguments(object?[] arguments)
{
ParameterInfo[] parametersInfo = MethodInfo.GetParameters();
// Use the cached ParameterTypes property rather than calling MethodInfo.GetParameters()
// (which allocates a fresh ParameterInfo[] on every call). ResolveArguments only reads the
// array, so sharing the cached instance is safe.
ParameterInfo[] parametersInfo = ParameterTypes;
int requiredParameterCount = 0;
bool hasParamsValue = false;
object? paramsValues = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ internal sealed class TestMethodRunner
/// </summary>
private readonly TestMethodInfo _testMethodInfo;

/// <summary>
/// Lazily-created <see cref="ReflectionTestMethodInfo"/> wrapper reused across all data rows of a
/// data-driven test. Both <see cref="TestMethodInfo.MethodInfo"/> and <see cref="TestMethod.DisplayName"/>
/// are immutable for the lifetime of a <see cref="TestMethodRunner"/>, so a single wrapper can be shared
/// instead of allocating one per row.
/// </summary>
private ReflectionTestMethodInfo? _cachedReflectionMethodInfo;

/// <summary>
/// Initializes a new instance of the <see cref="TestMethodRunner"/> class.
/// </summary>
Expand Down Expand Up @@ -423,9 +431,9 @@ private async Task<TestResult[]> ExecuteTestWithDataSourceAsync(ITestContext exe
// and both GetDisplayName and ComputeDefaultDisplayName need to be consulted.
if (displayNameFromTestDataRow is null && testDataSource is not null)
{
var reflectionMethodInfo = new ReflectionTestMethodInfo(_testMethodInfo.MethodInfo, _test.DisplayName);
displayName = testDataSource.GetDisplayName(reflectionMethodInfo, data)
?? TestDataSourceUtilities.ComputeDefaultDisplayName(reflectionMethodInfo, data)
_cachedReflectionMethodInfo ??= new ReflectionTestMethodInfo(_testMethodInfo.MethodInfo, _test.DisplayName);
displayName = testDataSource.GetDisplayName(_cachedReflectionMethodInfo, data)
?? TestDataSourceUtilities.ComputeDefaultDisplayName(_cachedReflectionMethodInfo, data)
?? displayName;
}
else
Expand Down Expand Up @@ -488,11 +496,26 @@ private async Task<TestResult[]> ExecuteTestAsync(ITestContext executionContext,
{
try
{
ExecutionContext? capturedContext = testMethodInfo.Parent.ExecutionContext ?? testMethodInfo.Parent.Parent.ExecutionContext;

// Fast path: when no ExecutionContext was captured by [AssemblyInitialize] / [ClassInitialize]
// (the common case), RunOnContext with a null context simply invokes the action directly on the
// current thread. Skip the TaskCompletionSource + closure + delegate allocations and just await
// the executor directly.
if (capturedContext is null)
{
using (TestContextImplementation.SetCurrentTestContext(executionContext as TestContext))
{
testMethodInfo.TestContext = executionContext;
return await _testMethodInfo.Executor.ExecuteAsync(testMethodInfo).ConfigureAwait(false);
}
}

var tcs = new TaskCompletionSource<TestResult[]>();

#pragma warning disable VSTHRD101 // Avoid unsupported async delegates
ExecutionContextHelpers.RunOnContext(
testMethodInfo.Parent.ExecutionContext ?? testMethodInfo.Parent.Parent.ExecutionContext,
capturedContext,
async () =>
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,16 +524,14 @@ private SynchronizedStringBuilder GetTestContextMessagesStringBuilder()
/// <returns>A fresh context suitable for one folded data-driven iteration.</returns>
internal TestContextImplementation CloneForDataDrivenIteration()
{
// Take a shallow snapshot of the current property bag so that the clone starts with
// the same properties (including TestNameLabel / FullyQualifiedTestClassNameLabel and
// anything merged from AssemblyInitialize / ClassInitialize) but is otherwise isolated.
// Per-iteration mutations to the clone's property bag won't leak back to this instance
// Pass testMethod: null and testClassFullName: null because the relevant labels
// (including TestNameLabel / FullyQualifiedTestClassNameLabel and anything merged from
// AssemblyInitialize / ClassInitialize) are already in _properties. In that branch the
// constructor takes a shallow copy of the passed dictionary, so passing _properties
// directly gives the clone an isolated property bag without an extra intermediate copy:
// per-iteration mutations to the clone's property bag won't leak back to this instance
// nor to subsequent iterations.
var snapshot = new Dictionary<string, object?>(_properties);

// Pass testMethod: null and testClassFullName: null because the relevant labels are
// already in the snapshot. The constructor will copy the snapshot as-is.
var clone = new TestContextImplementation(testMethod: null, testClassFullName: null, snapshot, _messageLogger, _testRunCancellationToken);
var clone = new TestContextImplementation(testMethod: null, testClassFullName: null, _properties, _messageLogger, _testRunCancellationToken);

// Preserve TestRunCount so user code that observes it (e.g. retry-aware tests) sees
// the same value it would see in the unfolded path. TestRunCount represents the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ internal sealed class ReflectionTestMethodInfo : MethodInfo
{
private readonly MethodInfo _methodInfo;

private ParameterInfo[]? _parameters;

public ReflectionTestMethodInfo(MethodInfo methodInfo, string? displayName)
{
_methodInfo = methodInfo;
Expand Down Expand Up @@ -41,7 +43,10 @@ public ReflectionTestMethodInfo(MethodInfo methodInfo, string? displayName)

public override MethodImplAttributes GetMethodImplementationFlags() => _methodInfo.GetMethodImplementationFlags();

public override ParameterInfo[] GetParameters() => _methodInfo.GetParameters();
// MethodInfo.GetParameters() allocates a fresh ParameterInfo[] on every call (CLR safety
// guarantee). The wrapped MethodInfo is immutable, so cache the array and share it across
// callers of this wrapper (e.g. display-name computation for every data-driven row).
public override ParameterInfo[] GetParameters() => _parameters ??= _methodInfo.GetParameters();

public override object? Invoke(object? obj, BindingFlags invokeAttr, Binder? binder, object?[]? parameters, CultureInfo? culture) => _methodInfo.Invoke(obj, invokeAttr, binder, parameters, culture);

Expand Down