// 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;
///
/// Handles folded / DynamicData-style () data-driven tests.
///
internal sealed partial class TestMethodRunner
{
private async Task TryExecuteFoldedDataDrivenTestsAsync(List 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);
FinalizeIterationAssertionFailureDiagnostics(iterationContext, outerContext, testResults);
// Sync the iteration context's outcome with the final (post-cleanup) result
// before it is disposed. RunTestMethod sets the context outcome *before* running
// [TestCleanup]/Dispose, so a row whose body passes but whose cleanup fails would
// otherwise dispose the clone while it still reports "Passed" — causing per-test
// resources keyed off the outcome (e.g. TestTempDirectory retention) to treat a
// failed row as passing. The fast path already does this via SetOutcome after
// aggregation; the folded path must do the same for its per-row clone.
iterationContext.SetOutcome(GetAggregateOutcome(testResults));
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 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);
executionContext.SetDisplayName(displayName);
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;
}
private static void FinalizeIterationAssertionFailureDiagnostics(
TestContextImplementation iterationContext,
TestContextImplementation outerContext,
TestResult[] results)
{
#if !WINDOWS_UWP && !WIN_UI
if (results.Length == 0)
{
iterationContext.TransferAssertionFailureDiagnosticsTo(outerContext);
}
else
{
iterationContext.FinalizeAssertionFailureDiagnosticsExecution(results, resetCaptureBudget: false);
}
#endif
}
}