// 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;
///
/// Handles -attributed data-driven tests.
///
internal sealed partial class TestMethodRunner
{
private async Task TryExecuteDataSourceBasedTestsAsync(List 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 results)
{
var watch = Stopwatch.StartNew();
try
{
IEnumerable? dataRows = PlatformServiceProvider.Instance.TestDataSource.GetData(_testMethodInfo, _testContext);
if (dataRows is 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);
FinalizeIterationAssertionFailureDiagnostics(iterationContext, outerContext, testResults);
// Sync the clone's outcome with the final (post-cleanup) result before it is
// disposed, so a row that passes its body but fails [TestCleanup]/Dispose is
// not treated as passing by per-test resources keyed off the outcome (e.g.
// TestTempDirectory retention). See TryExecuteFoldedDataDrivenTestsAsync.
iterationContext.SetOutcome(GetAggregateOutcome(testResults));
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 ExecuteTestWithDataSourceAsync(ITestContext executionContext, UTF.ITestDataSource? testDataSource, object?[]? data, bool actualDataAlreadyHandledDuringDiscovery)
{
string? displayName = StringEx.IsNullOrWhiteSpace(_test.DisplayName)
? _test.Name
: _test.DisplayName;
string? displayNameFromTestDataRow = null;
string? argumentsDisplayNameFromTestDataRow = null;
string? ignoreFromTestDataRow = null;
if (!actualDataAlreadyHandledDuringDiscovery && data is not null &&
TestDataSourceHelpers.TryHandleITestDataRow(data, _testMethodInfo.ParameterTypes, out data, out ignoreFromTestDataRow, out displayNameFromTestDataRow, out _, out argumentsDisplayNameFromTestDataRow))
{
// 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 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;
}
if (displayNameFromTestDataRow is not null)
{
displayName = displayNameFromTestDataRow;
}
else if (!StringEx.IsNullOrWhiteSpace(argumentsDisplayNameFromTestDataRow))
{
ReflectionTestMethodInfo reflectionMethodInfo = _cachedReflectionMethodInfo ??= new ReflectionTestMethodInfo(_testMethodInfo.MethodInfo, _test.DisplayName);
displayName = TestDataSourceUtilities.ComputeDisplayName(reflectionMethodInfo, argumentsDisplayNameFromTestDataRow);
}
else if (testDataSource is not null)
{
ReflectionTestMethodInfo reflectionMethodInfo = _cachedReflectionMethodInfo ??= new ReflectionTestMethodInfo(_testMethodInfo.MethodInfo, _test.DisplayName);
displayName = testDataSource.GetDisplayName(reflectionMethodInfo, data)
?? TestDataSourceUtilities.ComputeDefaultDisplayName(reflectionMethodInfo, data)
?? 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;
}
}