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
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using UTF = Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;

/// <summary>
/// Handles folded / <c>DynamicData</c>-style (<see cref="UTF.ITestDataSource"/>) data-driven tests.
/// </summary>
internal sealed partial class TestMethodRunner
{
private async Task<bool> TryExecuteFoldedDataDrivenTestsAsync(List<TestResult> results)
{
bool hasTestDataSource = false;
var outerContext = (TestContextImplementation)_testContext.Context;

foreach (Attribute attribute in PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached(_testMethodInfo.MethodInfo))
{
if (attribute is not UTF.ITestDataSource testDataSource)
{
continue;
}

hasTestDataSource = true;
if (testDataSource is ITestDataSourceIgnoreCapability { IgnoreMessage: { } ignoreMessage })
{
results.Add(TestResult.CreateIgnoredResult(ignoreMessage));
continue;
}

// This code is to execute tests. To discover the tests code is in AssemblyEnumerator.TryUnfoldITestDataSource.
// Any change made here should be reflected in AssemblyEnumerator.TryUnfoldITestDataSource as well.
bool dataSourceHasData = false;
foreach (object?[] data in testDataSource.GetData(_testMethodInfo.MethodInfo))
{
dataSourceHasData = true;

// Create a fresh TestContextImplementation per iteration so the folded path is
// structurally equivalent to the unfolded path (where each row gets its own
// context). This isolates per-row state (captured output, diagnostic messages,
// result files, outcome, exception, property bag mutations, ...) so a leak in
// any current or future TestContextImplementation field cannot accumulate across
// rows. See https://github.com/microsoft/testfx/issues/7933.
TestContextImplementation iterationContext = outerContext.CloneForDataDrivenIteration();
try
{
TestResult[] testResults = await ExecuteTestWithDataSourceAsync(iterationContext, testDataSource, data, actualDataAlreadyHandledDuringDiscovery: false).ConfigureAwait(false);

results.AddRange(testResults);
}
finally
{
_testMethodInfo.SetArguments(null);
iterationContext.Dispose();
}
}

if (!dataSourceHasData)
{
if (!MSTestSettings.CurrentSettings.ConsiderEmptyDataSourceAsInconclusive)
{
throw testDataSource.GetExceptionForEmptyDataSource(_testMethodInfo.MethodInfo);
}

var inconclusiveResult = new TestResult
{
Outcome = UnitTestOutcome.Inconclusive,
};
results.Add(inconclusiveResult);
}
}

return hasTestDataSource;
}

private async Task<TestResult[]> ExecuteTestWithDataRowAsync(ITestContext executionContext, object dataRow, int rowIndex)
{
string displayName = string.Format(CultureInfo.CurrentCulture, Resource.DataDrivenResultDisplayName, _test.DisplayName, rowIndex);
Stopwatch? stopwatch = null;

TestResult[]? testResults;
try
{
stopwatch = Stopwatch.StartNew();
executionContext.SetDataRow(dataRow);
testResults = await ExecuteTestAsync(executionContext, _testMethodInfo).ConfigureAwait(false);
}
finally
{
stopwatch?.Stop();
executionContext.SetDataRow(null);
}

foreach (TestResult testResult in testResults)
{
testResult.DisplayName = displayName;
testResult.Duration = stopwatch.Elapsed;
}

return testResults;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting.Internal;

using UTF = Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;

/// <summary>
/// Handles <see cref="DataSourceAttribute"/>-attributed data-driven tests.
/// </summary>
internal sealed partial class TestMethodRunner
{
private async Task<bool> TryExecuteDataSourceBasedTestsAsync(List<TestResult> results)
{
// Iterate cached attributes directly to preserve the previous semantics:
// execute only when there is exactly one DataSourceAttribute.
bool hasSingleDataSource = false;
foreach (Attribute attribute in PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached(_testMethodInfo.MethodInfo))
{
if (attribute is not DataSourceAttribute)
{
continue;
}

if (hasSingleDataSource)
{
return false;
}

hasSingleDataSource = true;
}

if (!hasSingleDataSource)
{
return false;
}

await ExecuteTestFromDataSourceAttributeAsync(results).ConfigureAwait(false);
return true;
}

private async Task ExecuteTestFromDataSourceAttributeAsync(List<TestResult> results)
{
var watch = Stopwatch.StartNew();

try
{
IEnumerable<object>? dataRows = PlatformServiceProvider.Instance.TestDataSource.GetData(_testMethodInfo, _testContext);
if (dataRows == null)
{
var inconclusiveResult = new TestResult
{
Outcome = UnitTestOutcome.Inconclusive,
Duration = watch.Elapsed,
};
results.Add(inconclusiveResult);
return;
}

try
{
int rowIndex = 0;
var outerContext = (TestContextImplementation)_testContext.Context;

foreach (object dataRow in dataRows)
{
// Create a fresh TestContextImplementation per row for the same structural
// reason as in TryExecuteFoldedDataDrivenTestsAsync — each row should
// start with no accumulated per-test state.
TestContextImplementation iterationContext = outerContext.CloneForDataDrivenIteration();
try
{
TestResult[] testResults = await ExecuteTestWithDataRowAsync(iterationContext, dataRow, rowIndex++).ConfigureAwait(false);
results.AddRange(testResults);
}
finally
{
iterationContext.Dispose();
}
}
}
finally
{
_testContext.SetDataConnection(null);
_testContext.SetDataRow(null);
}
}
catch (Exception ex)
{
var failedResult = new TestResult
{
Outcome = UnitTestOutcome.Error,
TestFailureException = ex,
Duration = watch.Elapsed,
};
results.Add(failedResult);
}
}

private async Task<TestResult[]> ExecuteTestWithDataSourceAsync(ITestContext executionContext, UTF.ITestDataSource? testDataSource, object?[]? data, bool actualDataAlreadyHandledDuringDiscovery)
{
string? displayName = StringEx.IsNullOrWhiteSpace(_test.DisplayName)
? _test.Name
: _test.DisplayName;

string? displayNameFromTestDataRow = null;
string? ignoreFromTestDataRow = null;
if (!actualDataAlreadyHandledDuringDiscovery && data is not null &&
TestDataSourceHelpers.TryHandleITestDataRow(data, _testMethodInfo.ParameterTypes, out data, out ignoreFromTestDataRow, out displayNameFromTestDataRow))
{
// Handled already.
}
else if (!actualDataAlreadyHandledDuringDiscovery && TestDataSourceHelpers.IsDataConsideredSingleArgumentValue(data, _testMethodInfo.ParameterTypes))
{
// SPECIAL CASE:
// This condition is a duplicate of the condition in GetInvokeResultAsync.
//
// The known scenario we know of that shows importance of that check is if we have DynamicData using this member
//
// public static IEnumerable<object[]> GetData()
// {
// yield return new object[] { ("Hello", "World") };
// }
//
// If the test method has a single parameter which is 'object[]', then we should pass the tuple array as is.
// Note that normally, the array in this code path represents the arguments of the test method.
// However, GetInvokeResultAsync uses the above check to mean "the whole array is the single argument to the test method"
}
else if (!actualDataAlreadyHandledDuringDiscovery && data?.Length == 1 && TestDataSourceHelpers.TryHandleTupleDataSource(data[0], _testMethodInfo.ParameterTypes, out object?[] tupleExpandedToArray))
{
data = tupleExpandedToArray;
}

// PERF: Extract ReflectionTestMethodInfo to avoid allocating it twice when testDataSource is not null
// and both GetDisplayName and ComputeDefaultDisplayName need to be consulted.
if (displayNameFromTestDataRow is null && testDataSource is not null)
{
ReflectionTestMethodInfo reflectionMethodInfo = _cachedReflectionMethodInfo ??= new ReflectionTestMethodInfo(_testMethodInfo.MethodInfo, _test.DisplayName);
displayName = testDataSource.GetDisplayName(reflectionMethodInfo, data)
?? TestDataSourceUtilities.ComputeDefaultDisplayName(reflectionMethodInfo, data)
?? displayName;
}
else
{
displayName = displayNameFromTestDataRow ?? displayName;
}

var stopwatch = Stopwatch.StartNew();
_testMethodInfo.SetArguments(data);
executionContext.SetTestData(data);
executionContext.SetDisplayName(displayName);

TestResult[] testResults = ignoreFromTestDataRow is not null
? [TestResult.CreateIgnoredResult(ignoreFromTestDataRow)]
: await ExecuteTestAsync(executionContext, _testMethodInfo).ConfigureAwait(false);

stopwatch.Stop();

foreach (TestResult testResult in testResults)
{
if (testResult.Duration == TimeSpan.Zero)
{
testResult.Duration = stopwatch.Elapsed;
}

testResult.DisplayName = displayName;
}

return testResults;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;

/// <summary>
/// Core single-test execution logic.
/// </summary>
internal sealed partial class TestMethodRunner
{
private async Task<TestResult[]> ExecuteTestAsync(ITestContext executionContext, TestMethodInfo testMethodInfo)
{
try
{
ExecutionContext? capturedContext = testMethodInfo.Parent.ExecutionContext
?? testMethodInfo.Parent.Parent.ExecutionContext;

// Fast path: when no ExecutionContext was captured (the common case),
// ExecutionContextHelpers.RunOnContext would simply call the action inline.
// Skip the TaskCompletionSource bridge, async-lambda closure, and Action
// delegate allocations entirely.
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(
capturedContext,
async () =>
{
try
{
using (TestContextImplementation.SetCurrentTestContext(executionContext as TestContext))
{
testMethodInfo.TestContext = executionContext;
tcs.SetResult(await _testMethodInfo.Executor.ExecuteAsync(testMethodInfo).ConfigureAwait(false));
}
}
catch (Exception e)
{
tcs.SetException(e);
}
});
#pragma warning restore VSTHRD101 // Avoid unsupported async delegates
return await tcs.Task.ConfigureAwait(false);
}
catch (Exception ex)
{
return
[
new TestResult
{
TestFailureException = new InvalidOperationException(
string.Format(
CultureInfo.CurrentCulture,
Resource.UTA_ExecuteThrewException,
_testMethodInfo.Executor.GetType().FullName,
ex.ToString()),
ex),
},
];
}
}

/// <summary>
/// Updates each given result with new execution and parent execution identifiers.
/// </summary>
/// <param name="results">Results.</param>
private static void UpdateResultsWithParentInfo(List<TestResult> results)
{
foreach (TestResult result in results)
{
result.ExecutionId = Guid.NewGuid();
result.ParentExecId = Guid.NewGuid();
}
}
}
Loading