Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offer alternative to serialization of data #4467

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
62 changes: 44 additions & 18 deletions src/Adapter/MSTest.TestAdapter/Discovery/AssemblyEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ internal ICollection<UnitTestElement> EnumerateAssembly(
DataRowAttribute.TestIdGenerationStrategy = testIdGenerationStrategy;
DynamicDataAttribute.TestIdGenerationStrategy = testIdGenerationStrategy;

TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy = ReflectHelper.GetTestDataSourceOptions(assembly)?.UnfoldingStrategy switch
TestDataSourceOptionsAttribute? testDataSourceOptionsAttribute = ReflectHelper.GetTestDataSourceOptions(assembly);
TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy = testDataSourceOptionsAttribute?.UnfoldingStrategy switch
{
// When strategy is auto we want to unfold
TestDataSourceUnfoldingStrategy.Auto => TestDataSourceUnfoldingStrategy.Unfold,
Expand All @@ -107,6 +108,14 @@ internal ICollection<UnitTestElement> EnumerateAssembly(
},
};

TestDataIdentifierStrategy dataSourcesIdentifierStrategy = testDataSourceOptionsAttribute?.IdentifierStrategy switch
{
// When strategy is null or auto we want to use the data contract serialization
null or TestDataIdentifierStrategy.Auto => TestDataIdentifierStrategy.DataContractSerialization,
// When strategy is set, let's use it
{ } value => value,
};

Dictionary<string, object>? testRunParametersFromRunSettings = RunSettingsUtilities.GetTestRunParameters(runSettingsXml);
foreach (Type type in types)
{
Expand All @@ -115,8 +124,7 @@ internal ICollection<UnitTestElement> EnumerateAssembly(
continue;
}

List<UnitTestElement> testsInType = DiscoverTestsInType(assemblyFileName, testRunParametersFromRunSettings, type, warnings, discoverInternals,
dataSourcesUnfoldingStrategy, testIdGenerationStrategy, fixturesTests);
List<UnitTestElement> testsInType = DiscoverTestsInType(assemblyFileName, testRunParametersFromRunSettings, type, warnings, discoverInternals, dataSourcesUnfoldingStrategy, dataSourcesIdentifierStrategy, testIdGenerationStrategy, fixturesTests);
tests.AddRange(testsInType);
}

Expand Down Expand Up @@ -216,6 +224,7 @@ private List<UnitTestElement> DiscoverTestsInType(
List<string> warningMessages,
bool discoverInternals,
TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy,
TestDataIdentifierStrategy dataSourcesIdentifierStrategy,
TestIdGenerationStrategy testIdGenerationStrategy,
HashSet<string> fixturesTests)
{
Expand Down Expand Up @@ -246,7 +255,7 @@ private List<UnitTestElement> DiscoverTestsInType(
AddFixtureTests(testMethodInfo, tests, fixturesTests);
}

if (TryUnfoldITestDataSources(test, testMethodInfo, dataSourcesUnfoldingStrategy, tests))
if (TryUnfoldITestDataSources(test, testMethodInfo, dataSourcesUnfoldingStrategy, dataSourcesIdentifierStrategy, tests))
{
continue;
}
Expand Down Expand Up @@ -348,7 +357,7 @@ static UnitTestElement GetFixtureTest(string classFullName, string assemblyLocat
}
}

private static bool TryUnfoldITestDataSources(UnitTestElement test, TestMethodInfo testMethodInfo, TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy, List<UnitTestElement> tests)
private static bool TryUnfoldITestDataSources(UnitTestElement test, TestMethodInfo testMethodInfo, TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy, TestDataIdentifierStrategy dataSourcesIdentifierStrategy, List<UnitTestElement> tests)
{
// It should always be `true`, but if any part of the chain is obsolete; it might not contain those.
// Since we depend on those properties, if they don't exist, we bail out early.
Expand All @@ -368,10 +377,12 @@ private static bool TryUnfoldITestDataSources(UnitTestElement test, TestMethodIn
try
{
bool isDataDriven = false;
int dataSourceIndex = -1;
foreach (ITestDataSource dataSource in testDataSources)
{
dataSourceIndex++;
isDataDriven = true;
if (!TryUnfoldITestDataSource(dataSource, dataSourcesUnfoldingStrategy, test, new(testMethodInfo.MethodInfo, test.DisplayName), tempListOfTests))
if (!TryUnfoldITestDataSource(dataSource, dataSourceIndex, dataSourcesUnfoldingStrategy, dataSourcesIdentifierStrategy, test, new(testMethodInfo.MethodInfo, test.DisplayName), tempListOfTests))
{
// TODO: Improve multi-source design!
// Ideally we would want to consider each data source separately but when one source cannot be expanded,
Expand Down Expand Up @@ -401,7 +412,7 @@ private static bool TryUnfoldITestDataSources(UnitTestElement test, TestMethodIn
}
}

private static bool TryUnfoldITestDataSource(ITestDataSource dataSource, TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy, UnitTestElement test, ReflectionTestMethodInfo methodInfo, List<UnitTestElement> tests)
private static bool TryUnfoldITestDataSource(ITestDataSource dataSource, int dataSourceIndex, TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy, TestDataIdentifierStrategy dataSourcesIdentifierStrategy, UnitTestElement test, ReflectionTestMethodInfo methodInfo, List<UnitTestElement> tests)
{
var unfoldingCapability = dataSource as ITestDataSourceUnfoldingCapability;

Expand Down Expand Up @@ -454,6 +465,7 @@ private static bool TryUnfoldITestDataSource(ITestDataSource dataSource, TestDat
// If strategy is DisplayName and we have a duplicate test name don't expand the test, bail out.
#pragma warning disable CS0618 // Type or member is obsolete
if (test.TestMethod.TestIdGenerationStrategy == TestIdGenerationStrategy.DisplayName
#pragma warning restore CS0618 // Type or member is obsolete
&& testDisplayNameFirstSeen.TryGetValue(discoveredTest.DisplayName!, out int firstIndexSeen))
{
string warning = string.Format(CultureInfo.CurrentCulture, Resource.CannotExpandIDataSourceAttribute_DuplicateDisplayName, firstIndexSeen, index, discoveredTest.DisplayName);
Expand All @@ -463,23 +475,37 @@ private static bool TryUnfoldITestDataSource(ITestDataSource dataSource, TestDat
// Duplicated display name so bail out. Caller will handle adding the original test.
return false;
}
#pragma warning restore CS0618 // Type or member is obsolete

try
TestDataIdentifierStrategy currentDataSourceIdentifierStrategy = (dataSource as ITestDataIdentifierStrategy)?.IdentifierStrategy ?? TestDataIdentifierStrategy.Auto;
if (currentDataSourceIdentifierStrategy == TestDataIdentifierStrategy.DataIndex
|| (currentDataSourceIdentifierStrategy == TestDataIdentifierStrategy.Auto && dataSourcesIdentifierStrategy == TestDataIdentifierStrategy.DataIndex))
{
discoveredTest.TestMethod.SerializedData = DataSerializationHelper.Serialize(d);
discoveredTest.TestMethod.SerializedData = new string[3]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am reusing the serialized data property as it's not used and allows me to avoid adding more properties to the TestCase object as it has some perf impact. This isn't perfect but as far as I can tell, there is no way for the serialized data to be serialized this way as we prefix with the data type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine for me to reuse. At least until we have a strong reason not to reuse.

{
TestDataIdentifierStrategy.DataIndex.ToString(),
dataSourceIndex.ToString(CultureInfo.InvariantCulture),
index.ToString(CultureInfo.InvariantCulture),
};
discoveredTest.TestMethod.DataType = DynamicDataType.ITestDataSource;
}
catch (SerializationException ex)
else
{
string warning = string.Format(CultureInfo.CurrentCulture, Resource.CannotExpandIDataSourceAttribute_CannotSerialize, index, discoveredTest.DisplayName);
warning += Environment.NewLine;
warning += ex.ToString();
warning = string.Format(CultureInfo.CurrentCulture, Resource.CannotExpandIDataSourceAttribute, test.TestMethod.ManagedTypeName, test.TestMethod.ManagedMethodName, warning);
PlatformServiceProvider.Instance.AdapterTraceLogger.LogWarning($"DynamicDataEnumerator: {warning}");
try
{
discoveredTest.TestMethod.SerializedData = DataSerializationHelper.Serialize(d);
discoveredTest.TestMethod.DataType = DynamicDataType.ITestDataSource;
}
catch (SerializationException ex)
{
string warning = string.Format(CultureInfo.CurrentCulture, Resource.CannotExpandIDataSourceAttribute_CannotSerialize, index, discoveredTest.DisplayName);
warning += Environment.NewLine;
warning += ex.ToString();
warning = string.Format(CultureInfo.CurrentCulture, Resource.CannotExpandIDataSourceAttribute, test.TestMethod.ManagedTypeName, test.TestMethod.ManagedMethodName, warning);
PlatformServiceProvider.Instance.AdapterTraceLogger.LogWarning($"DynamicDataEnumerator: {warning}");

// Serialization failed for the type, bail out. Caller will handle adding the original test.
return false;
// Serialization failed for the type, bail out. Caller will handle adding the original test.
return false;
}
}

discoveredTests.Add(discoveredTest);
Expand Down
53 changes: 44 additions & 9 deletions src/Adapter/MSTest.TestAdapter/Execution/TestMethodRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,17 +181,12 @@ internal UnitTestResult[] RunTestMethod()

bool isDataDriven = false;
var parentStopwatch = Stopwatch.StartNew();
if (_test.DataType == DynamicDataType.ITestDataSource)
if (TryExecuteITestDataSource(results))
{
object?[]? data = DataSerializationHelper.Deserialize(_test.SerializedData);
TestResult[] testResults = ExecuteTestWithDataSource(null, data);
results.AddRange(testResults);
}
else if (TryExecuteDataSourceBasedTests(results))
{
isDataDriven = true;
// Do nothing
}
else if (TryExecuteFoldedDataDrivenTests(results))
else if (TryExecuteDataSourceBasedTests(results)
|| TryExecuteFoldedDataDrivenTests(results))
{
isDataDriven = true;
}
Expand Down Expand Up @@ -254,6 +249,46 @@ internal UnitTestResult[] RunTestMethod()
return results.ToUnitTestResults();
}

private bool TryExecuteITestDataSource(List<TestResult> results)
{
if (_test.DataType != DynamicDataType.ITestDataSource)
{
return false;
}

UTF.ITestDataSource? dataSource;
object?[]? data;
if (_test.SerializedData?.Length == 3
&& Enum.TryParse(_test.SerializedData[0], out TestDataIdentifierStrategy _))
{
if (!int.TryParse(_test.SerializedData[1], out int dataSourceIndex)
|| !int.TryParse(_test.SerializedData[2], out int dataIndex))
{
// TODO
throw new InvalidOperationException();
Evangelink marked this conversation as resolved.
Show resolved Hide resolved
}

dataSource = _testMethodInfo.GetAttributes<Attribute>(false)?.OfType<UTF.ITestDataSource>().Skip(dataSourceIndex).FirstOrDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be a good idea to check with runtime team that it's safe to assume reflection will return attributes in a deterministic order. I think it should be a safe assumption, but just a sanity check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my tests it is deterministic but yes I'll double check with them.

if (dataSource is null)
{
// TODO
throw new InvalidOperationException();
Evangelink marked this conversation as resolved.
Show resolved Hide resolved
}

data = dataSource.GetData(_testMethodInfo.MethodInfo).Skip(dataIndex).FirstOrDefault();
}
else
{
dataSource = null;
data = DataSerializationHelper.Deserialize(_test.SerializedData);
}

TestResult[] testResults = ExecuteTestWithDataSource(dataSource, data);
results.AddRange(testResults);

return true;
}

private bool TryExecuteDataSourceBasedTests(List<TestResult> results)
{
DataSourceAttribute[] dataSourceAttribute = _testMethodInfo.GetAttributes<DataSourceAttribute>(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting;
/// Attribute to define in-line data for a test method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class DataRowAttribute : Attribute, ITestDataSource, ITestDataSourceUnfoldingCapability
public class DataRowAttribute : Attribute, ITestDataSource, ITestDataSourceUnfoldingCapability, ITestDataIdentifierStrategy
{
/// <summary>
/// Initializes a new instance of the <see cref="DataRowAttribute"/> class.
Expand Down Expand Up @@ -56,6 +56,9 @@ public DataRowAttribute(string?[]? stringArrayData)
/// <inheritdoc />
public TestDataSourceUnfoldingStrategy UnfoldingStrategy { get; set; } = TestDataSourceUnfoldingStrategy.Auto;

/// <inheritdoc />
public TestDataIdentifierStrategy IdentifierStrategy { get; set; } = TestDataIdentifierStrategy.Auto;

/// <inheritdoc />
public IEnumerable<object?[]> GetData(MethodInfo methodInfo) => [Data];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public enum DynamicDataSourceType
/// Attribute to define dynamic data for a test method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class DynamicDataAttribute : Attribute, ITestDataSource, ITestDataSourceEmptyDataSourceExceptionInfo, ITestDataSourceUnfoldingCapability
public sealed class DynamicDataAttribute : Attribute, ITestDataSource, ITestDataSourceEmptyDataSourceExceptionInfo, ITestDataSourceUnfoldingCapability, ITestDataIdentifierStrategy
{
private readonly string _dynamicDataSourceName;
private readonly DynamicDataSourceType _dynamicDataSourceType;
Expand Down Expand Up @@ -114,6 +114,9 @@ public DynamicDataAttribute(string dynamicDataSourceName, Type dynamicDataDeclar
/// <inheritdoc />
public TestDataSourceUnfoldingStrategy UnfoldingStrategy { get; set; } = TestDataSourceUnfoldingStrategy.Auto;

/// <inheritdoc />
public TestDataIdentifierStrategy IdentifierStrategy { get; set; } = TestDataIdentifierStrategy.Auto;

/// <inheritdoc />
public IEnumerable<object[]> GetData(MethodInfo methodInfo)
=> DynamicDataProvider.Instance.GetData(_dynamicDataDeclaringType, _dynamicDataSourceType, _dynamicDataSourceName, methodInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,43 @@ public sealed class TestDataSourceOptionsAttribute : Attribute
/// The <see cref="UnfoldingStrategy"/> to use when executing parameterized tests.
/// </param>
public TestDataSourceOptionsAttribute(TestDataSourceUnfoldingStrategy unfoldingStrategy)
=> UnfoldingStrategy = unfoldingStrategy;
: this(unfoldingStrategy, TestDataIdentifierStrategy.Auto)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TestDataSourceOptionsAttribute"/> class.
/// </summary>
/// <param name="identifierStrategy">
/// The <see cref="IdentifierStrategy"/> to use when executing parameterized tests.
/// </param>
public TestDataSourceOptionsAttribute(TestDataIdentifierStrategy identifierStrategy)
: this(TestDataSourceUnfoldingStrategy.Auto, identifierStrategy)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TestDataSourceOptionsAttribute"/> class.
/// </summary>
/// <param name="unfoldingStrategy">
/// The <see cref="UnfoldingStrategy"/> to use when executing parameterized tests.
/// </param>
/// <param name="identifierStrategy">
/// The <see cref="IdentifierStrategy"/> to use when executing parameterized tests.
/// </param>
public TestDataSourceOptionsAttribute(TestDataSourceUnfoldingStrategy unfoldingStrategy, TestDataIdentifierStrategy identifierStrategy)
{
UnfoldingStrategy = unfoldingStrategy;
IdentifierStrategy = identifierStrategy;
}

/// <summary>
/// Gets the test unfolding strategy.
/// </summary>
public TestDataSourceUnfoldingStrategy UnfoldingStrategy { get; }

/// <summary>
/// Gets the test data identifier strategy.
/// </summary>
public TestDataIdentifierStrategy IdentifierStrategy { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Defines the strategy for uniquely identifying test data.
/// This is only used when <see cref="ITestDataSourceUnfoldingCapability.UnfoldingStrategy" /> is set to <see cref="TestDataSourceUnfoldingStrategy.Unfold"/>.
/// </summary>
public interface ITestDataIdentifierStrategy
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this enum and interface only work when using the unfolding strategy, I was wondering if we should merge the 2 together so we have something like Auto, Fold, UnfoldUsingDataContractSerialization, UnfoldUsingDataIndex.

WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Evangelink You mean not needing the interface at all, and only relying on the existing enum? This is fine for me.

We will have to keep Unfold and make it the same as UnfoldUsingDataContractSerialization for backcompat I assume. But that's fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's what I meant. My last commit is doing the change, please review and let me know if you think that's better or worse.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks better to me

{
/// <summary>
/// Gets the strategy used to uniquely identify test data.
/// </summary>
TestDataIdentifierStrategy IdentifierStrategy { get; }
}

/// <summary>
/// Specifies the available strategies for identifying test data.
/// This is only relevant when <see cref="ITestDataSourceUnfoldingCapability.UnfoldingStrategy" /> is set to <see cref="TestDataSourceUnfoldingStrategy.Unfold"/>.
/// </summary>
public enum TestDataIdentifierStrategy : byte
{
/// <summary>
/// Automatically determines the identifier strategy based on the assembly-level attribute
/// <see cref="TestDataSourceOptionsAttribute" />. If no attribute is specified, the default is
/// <see cref="DataContractSerialization" />.
/// </summary>
Auto,

/// <summary>
/// Identifies test data using data contract serialization.
/// </summary>
DataContractSerialization,

/// <summary>
/// Identifies test data by its index in the data source.
/// </summary>
DataIndex,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the context of HotReload, if an attribute is removed, re-ordered, etc, is everything going to work fine?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will double check that scenario!

}
Loading