diff --git a/Directory.Packages.props b/Directory.Packages.props index 067fb557aa..6cf5f65432 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,7 @@ true + 8.0.0 8.2.2 17.11.4 3.11.0-beta1.24508.2 @@ -31,6 +32,7 @@ + diff --git a/TestFx.sln b/TestFx.sln index 5817cde0bc..e1460ab160 100644 --- a/TestFx.sln +++ b/TestFx.sln @@ -209,6 +209,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Testing.Platform. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Testing.Extensions.MSBuild", "src\Platform\Microsoft.Testing.Extensions.MSBuild\Microsoft.Testing.Extensions.MSBuild.csproj", "{8CE782A2-7374-4916-9C69-1F87E51A64A9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSTest.DataSource.Csv", "src\TestFramework\TestFramework.Extensions.Csv\MSTest.DataSource.Csv.csproj", "{536A4485-F77F-4617-8449-2DCB387514AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSTest.DataSource.Xml", "src\TestFramework\TestFramework.Extensions.Xml\MSTest.DataSource.Xml.csproj", "{7BF7BE9E-8D46-47ED-B45D-121C2252DF16}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -483,6 +487,14 @@ Global {8CE782A2-7374-4916-9C69-1F87E51A64A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {8CE782A2-7374-4916-9C69-1F87E51A64A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CE782A2-7374-4916-9C69-1F87E51A64A9}.Release|Any CPU.Build.0 = Release|Any CPU + {536A4485-F77F-4617-8449-2DCB387514AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {536A4485-F77F-4617-8449-2DCB387514AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {536A4485-F77F-4617-8449-2DCB387514AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {536A4485-F77F-4617-8449-2DCB387514AB}.Release|Any CPU.Build.0 = Release|Any CPU + {7BF7BE9E-8D46-47ED-B45D-121C2252DF16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BF7BE9E-8D46-47ED-B45D-121C2252DF16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BF7BE9E-8D46-47ED-B45D-121C2252DF16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BF7BE9E-8D46-47ED-B45D-121C2252DF16}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -567,6 +579,8 @@ Global {573C617F-6BB2-403A-AD87-E00A7FD537F0} = {BB874DF1-44FE-415A-B634-A6B829107890} {F422398C-72CD-43EA-AC8E-E0DBD08E5563} = {BB874DF1-44FE-415A-B634-A6B829107890} {8CE782A2-7374-4916-9C69-1F87E51A64A9} = {6AEE1440-FDF0-4729-8196-B24D0E333550} + {536A4485-F77F-4617-8449-2DCB387514AB} = {E48AC786-E150-4F41-9A16-32F02E4493D8} + {7BF7BE9E-8D46-47ED-B45D-121C2252DF16} = {E48AC786-E150-4F41-9A16-32F02E4493D8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {31E0F4D5-975A-41CC-933E-545B2201FAF9} diff --git a/eng/verify-nupkgs.ps1 b/eng/verify-nupkgs.ps1 index d119680355..a025aa042c 100644 --- a/eng/verify-nupkgs.ps1 +++ b/eng/verify-nupkgs.ps1 @@ -22,6 +22,8 @@ function Confirm-NugetPackages { "MSTest.Sdk" = 15; "MSTest.Internal.TestFx.Documentation" = 10; "MSTest.TestFramework" = 130; + "MSTest.DataSource.Csv" = 7; + "MSTest.DataSource.Xml" = 7; "MSTest.TestAdapter" = 76; "MSTest" = 6; "MSTest.Analyzers" = 10; diff --git a/src/TestFramework/TestFramework.Extensions.Csv/CsvDataSourceAttribute.cs b/src/TestFramework/TestFramework.Extensions.Csv/CsvDataSourceAttribute.cs new file mode 100644 index 0000000000..69ef54d2d7 --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Csv/CsvDataSourceAttribute.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Data; +using System.Data.OleDb; +using System.Globalization; +using System.Reflection; + +using Microsoft.VisualStudio.TestTools.UnitTesting.Internal; + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Attribute to define dynamic data from a CSV file for a test method. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public sealed class CsvDataSourceAttribute : Attribute, ITestDataSource +{ + // Template used to map from a filename to a DB connection string + private const string CsvConnectionTemplate = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Persist Security Info=False;Extended Properties=\"text;HDR=YES;FMT=Delimited\""; + private const string CsvConnectionTemplate64 = "Provider=Microsoft.Ace.OLEDB.12.0;Data Source={0};Persist Security Info=False;Extended Properties=\"text;HDR=YES;FMT=Delimited\""; + + public CsvDataSourceAttribute(string fileName) + => FileName = fileName; + + internal string FileName { get; } + + IEnumerable ITestDataSource.GetData(MethodInfo methodInfo) + { + // TODO: Avoid using OleDb and instead parse Csv directly. Maybe use https://www.nuget.org/packages/CsvHelper? Write our own? + // When that happens, the OleDb dependency in the csproj should be + // removed, and relevant tests for CsvDataSource should run on Linux. + + // We better work with a full path, if nothing else, errors become easier to report + string fullPath = Path.GetFullPath(FileName); + if (!File.Exists(fullPath)) + { + // TODO: Localize. + throw new FileNotFoundException($"Csv file '{fullPath}' cannot be found.", fullPath); + } + + using OleDbConnection connection = new(); + + // We have to use the name of the folder which contains the CSV file in the connection string + // If target platform is x64, then use CsvConnectionTemplate64 connection string. + connection.ConnectionString = IntPtr.Size == 8 + ? string.Format(CultureInfo.InvariantCulture, CsvConnectionTemplate64, Path.GetDirectoryName(fullPath)) + : string.Format(CultureInfo.InvariantCulture, CsvConnectionTemplate, Path.GetDirectoryName(fullPath)); + + // We have to open the connection now, before we try to quote + // the table name, otherwise QuoteIdentifier fails (for OleDb, go figure!) + // The connection will get closed when we dispose of it + connection.Open(); + + using OleDbCommandBuilder commandBuilder = new(); + string tableName = Path.GetFileName(fullPath).Replace('.', '#'); + string quotedTableName = commandBuilder.QuoteIdentifier(tableName, connection); + + using OleDbCommand command = new() + { + Connection = connection, + CommandText = $"SELECT * FROM {quotedTableName}", + }; + + using OleDbDataAdapter dataAdapter = new() + { + SelectCommand = command, + }; + + DataTable table = new() + { + Locale = CultureInfo.InvariantCulture, + }; + + dataAdapter.Fill(table); + + object?[][] dataRows = new object?[table.Rows.Count][]; + for (int i = 0; i < dataRows.Length; i++) + { + dataRows[i] = [table.Rows[i]]; + } + + return dataRows; + } + + string? ITestDataSource.GetDisplayName(MethodInfo methodInfo, object?[]? data) + => TestDataSourceUtilities.ComputeDefaultDisplayName(methodInfo, data, DynamicDataAttribute.TestIdGenerationStrategy); +} diff --git a/src/TestFramework/TestFramework.Extensions.Csv/MSTest.DataSource.Csv.csproj b/src/TestFramework/TestFramework.Extensions.Csv/MSTest.DataSource.Csv.csproj new file mode 100644 index 0000000000..32fe076e70 --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Csv/MSTest.DataSource.Csv.csproj @@ -0,0 +1,39 @@ + + + + netstandard2.0 + + true + + + + true + MSTest.DataSource.Csv + MSTest TestFramework Unittest MSTestV2 Microsoft Test Testing TDD Framework + + MSTest is Microsoft supported Test Framework. + + This package includes the functionality needed for writing Csv-based data source unit tests. + + Supported platforms: + - .NET Standard 2.0 + + + + + Microsoft.VisualStudio.TestTools.UnitTesting + + + + + + + + + + + + + + + diff --git a/src/TestFramework/TestFramework.Extensions.Csv/PACKAGE.md b/src/TestFramework/TestFramework.Extensions.Csv/PACKAGE.md new file mode 100644 index 0000000000..f4776dfbe5 --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Csv/PACKAGE.md @@ -0,0 +1,9 @@ +# MSTest.TestFramework + +MSTest is Microsoft supported Test Framework. + +This package includes the functionality needed for writing Csv-based data source unit tests. + +Supported platforms: + +- .NET Standard 2.0 diff --git a/src/TestFramework/TestFramework.Extensions.Csv/PublicAPI/PublicAPI.Shipped.txt b/src/TestFramework/TestFramework.Extensions.Csv/PublicAPI/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..ab058de62d --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Csv/PublicAPI/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/TestFramework/TestFramework.Extensions.Csv/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework.Extensions.Csv/PublicAPI/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..f90f6a5cfa --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Csv/PublicAPI/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +#nullable enable +Microsoft.VisualStudio.TestTools.UnitTesting.CsvDataSourceAttribute +Microsoft.VisualStudio.TestTools.UnitTesting.CsvDataSourceAttribute.CsvDataSourceAttribute(string! fileName) -> void diff --git a/src/TestFramework/TestFramework.Extensions.Xml/MSTest.DataSource.Xml.csproj b/src/TestFramework/TestFramework.Extensions.Xml/MSTest.DataSource.Xml.csproj new file mode 100644 index 0000000000..0e69ddcb1a --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Xml/MSTest.DataSource.Xml.csproj @@ -0,0 +1,37 @@ + + + + netstandard2.0 + + true + + + + true + MSTest.DataSource.Xml + MSTest TestFramework Unittest MSTestV2 Microsoft Test Testing TDD Framework + + MSTest is Microsoft supported Test Framework. + + This package includes the functionality needed for writing Xml-based data source unit tests. + + Supported platforms: + - .NET Standard 2.0 + + + + + Microsoft.VisualStudio.TestTools.UnitTesting + + + + + + + + + + + + + diff --git a/src/TestFramework/TestFramework.Extensions.Xml/PACKAGE.md b/src/TestFramework/TestFramework.Extensions.Xml/PACKAGE.md new file mode 100644 index 0000000000..f4776dfbe5 --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Xml/PACKAGE.md @@ -0,0 +1,9 @@ +# MSTest.TestFramework + +MSTest is Microsoft supported Test Framework. + +This package includes the functionality needed for writing Csv-based data source unit tests. + +Supported platforms: + +- .NET Standard 2.0 diff --git a/src/TestFramework/TestFramework.Extensions.Xml/PublicAPI/PublicAPI.Shipped.txt b/src/TestFramework/TestFramework.Extensions.Xml/PublicAPI/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..ab058de62d --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Xml/PublicAPI/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/TestFramework/TestFramework.Extensions.Xml/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework.Extensions.Xml/PublicAPI/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..b3e13a0ed0 --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Xml/PublicAPI/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +#nullable enable +Microsoft.VisualStudio.TestTools.UnitTesting.XmlDataSourceAttribute +Microsoft.VisualStudio.TestTools.UnitTesting.XmlDataSourceAttribute.XmlDataSourceAttribute(string! fileName, string! tableName) -> void diff --git a/src/TestFramework/TestFramework.Extensions.Xml/XmlDataSourceAttribute.cs b/src/TestFramework/TestFramework.Extensions.Xml/XmlDataSourceAttribute.cs new file mode 100644 index 0000000000..7783afb167 --- /dev/null +++ b/src/TestFramework/TestFramework.Extensions.Xml/XmlDataSourceAttribute.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Data; +using System.Globalization; +using System.Reflection; +using System.Xml; + +using Microsoft.VisualStudio.TestTools.UnitTesting.Internal; + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Attribute to define dynamic data from an XML file for a test method. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public sealed class XmlDataSourceAttribute : Attribute, ITestDataSource +{ + public XmlDataSourceAttribute(string fileName, string tableName) + { + FileName = fileName; + TableName = tableName; + } + + internal string FileName { get; } + + internal string TableName { get; } + + IEnumerable ITestDataSource.GetData(MethodInfo methodInfo) + { + string fullPath = Path.GetFullPath(FileName); + if (!File.Exists(fullPath)) + { + // TODO: Localize. + throw new FileNotFoundException($"Xml file '{fullPath}' cannot be found.", fullPath); + } + + DataSet dataSet = new() + { + Locale = CultureInfo.CurrentCulture, + }; + + // ReadXml should use the overload with XmlReader to avoid DTD processing + dataSet.ReadXml(new XmlTextReader(fullPath)); + + DataTable table = dataSet.Tables[TableName]; + + object?[][] dataRows = new object?[table.Rows.Count][]; + for (int i = 0; i < dataRows.Length; i++) + { + dataRows[i] = [table.Rows[i]]; + } + + return dataRows; + } + + string? ITestDataSource.GetDisplayName(MethodInfo methodInfo, object?[]? data) + => TestDataSourceUtilities.ComputeDefaultDisplayName(methodInfo, data, DynamicDataAttribute.TestIdGenerationStrategy); +} diff --git a/src/TestFramework/TestFramework/TestFramework.csproj b/src/TestFramework/TestFramework/TestFramework.csproj index 8f67b0069d..76d4f1d20c 100644 --- a/src/TestFramework/TestFramework/TestFramework.csproj +++ b/src/TestFramework/TestFramework/TestFramework.csproj @@ -31,6 +31,8 @@ + + diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DataSourceTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DataSourceTests.cs index 375dedad5f..8efe66ddb9 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DataSourceTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DataSourceTests.cs @@ -24,10 +24,15 @@ public class DataSourceTests : AcceptanceTestBase + + PreserveNewest + + PreserveNewest + @@ -52,6 +57,7 @@ public class DataSourceTests : AcceptanceTestBase #file MyTestClass.cs +using System.Data; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] @@ -61,7 +67,7 @@ public class MyTestClass [DataTestMethod] [DataSource("TestData")] - public void TestSum() + public void TestSumDataSource() { int expected = (int)TestContext.DataRow["expectedSum"]; int num1 = (int)TestContext.DataRow["num1"]; @@ -69,6 +75,26 @@ public void TestSum() Assert.AreEqual(expected, num1 + num2); } + [TestMethod] + [CsvDataSource("TestData.csv")] + public void TestSumCsv(DataRow dataRow) + { + int expected = (int)dataRow["expectedSum"]; + int num1 = (int)dataRow["num1"]; + int num2 = (int)dataRow["num2"]; + Assert.AreEqual(expected, num1 + num2); + } + + [TestMethod] + [XmlDataSource("TestData.xml", "MyTable")] + public void TestSumXml(DataRow dataRow) + { + int expected = (int)dataRow["ExpectedSum"]; + int num1 = (int)dataRow["Num1"]; + int num2 = (int)dataRow["Num2"]; + Assert.AreEqual(expected, num1 + num2); + } + [TestMethod] public void MyTest() { @@ -81,6 +107,31 @@ public void MyTest() 5,6,11 10,30,40 1,1,1 + +#file TestData.xml + + + + 1 + 1 + 2 + + + 5 + 6 + 11 + + + 10 + 30 + 40 + + + 1 + 1 + 1 + + """; private readonly AcceptanceFixture _acceptanceFixture; @@ -112,6 +163,6 @@ await DotnetCli.RunAsync( TestHostResult result = await testHost.ExecuteAsync(); result.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); - result.AssertOutputContainsSummary(failed: 1, passed: 4, skipped: 0); + result.AssertOutputContainsSummary(failed: 3, passed: 10, skipped: 0); } }