// 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 } }