diff --git a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/TestApplicationBuilderExtensions.cs b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/TestApplicationBuilderExtensions.cs index 436ce80215..d42bf925d7 100644 --- a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/TestApplicationBuilderExtensions.cs +++ b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/TestApplicationBuilderExtensions.cs @@ -28,6 +28,7 @@ public static void AddMSTest(this ITestApplicationBuilder testApplicationBuilder testApplicationBuilder.AddRunSettingsService(extension); testApplicationBuilder.AddTestCaseFilterService(extension); testApplicationBuilder.AddTestRunParametersService(extension); + testApplicationBuilder.AddTreeNodeFilterService(extension); #pragma warning disable TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. testApplicationBuilder.AddMaximumFailedTestsService(extension); #pragma warning restore TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ContextAdapterBase.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ContextAdapterBase.cs index 26605913c9..3d60ed7e26 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ContextAdapterBase.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ContextAdapterBase.cs @@ -4,6 +4,8 @@ using Microsoft.Testing.Extensions.VSTestBridge.CommandLine; using Microsoft.Testing.Platform; using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Requests; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -11,7 +13,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel; internal abstract class ContextAdapterBase { - protected ContextAdapterBase(ICommandLineOptions commandLineOptions) + protected ContextAdapterBase(ICommandLineOptions commandLineOptions, ITestExecutionFilter filter) { if (commandLineOptions.TryGetOptionArgumentList( TestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName, @@ -21,16 +23,34 @@ protected ContextAdapterBase(ICommandLineOptions commandLineOptions) { FilterExpressionWrapper = new(filterExpressions[0]); } + + switch (filter) + { + case TestNodeUidListFilter uidListFilter: + FilterExpressionWrapper = new(CreateFilter(uidListFilter.TestNodeUids)); + break; + + case TreeNodeFilter treeNodeFilter: + TreeNodeFilter = treeNodeFilter; + break; + } } protected FilterExpressionWrapper? FilterExpressionWrapper { get; set; } + protected TreeNodeFilter? TreeNodeFilter { get; set; } + // NOTE: Implementation is borrowed from VSTest // MSTest relies on this method existing and access it through reflection: https://github.com/microsoft/testfx/blob/main/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs#L115 public ITestCaseFilterExpression? GetTestCaseFilter( IEnumerable? supportedProperties, Func propertyProvider) { + if (TreeNodeFilter is not null) + { + return new TreeNodeFilterExpression(TreeNodeFilter, supportedProperties, propertyProvider); + } + if (FilterExpressionWrapper is null) { return null; @@ -61,4 +81,59 @@ protected ContextAdapterBase(ICommandLineOptions commandLineOptions) return adapterSpecificTestCaseFilter; } + + // We use heuristic to understand if the filter should be a TestCaseId or FullyQualifiedName. + // We know that in VSTest TestCaseId is a GUID and FullyQualifiedName is a string. + private static string CreateFilter(TestNodeUid[] testNodesUid) + { + StringBuilder filter = new(); + + for (int i = 0; i < testNodesUid.Length; i++) + { + if (Guid.TryParse(testNodesUid[i].Value, out Guid guid)) + { + filter.Append("Id="); + filter.Append(guid.ToString()); + } + else + { + TestNodeUid currentTestNodeUid = testNodesUid[i]; + filter.Append("FullyQualifiedName="); + for (int k = 0; k < currentTestNodeUid.Value.Length; k++) + { + char currentChar = currentTestNodeUid.Value[k]; + switch (currentChar) + { + case '\\': + case '(': + case ')': + case '&': + case '|': + case '=': + case '!': + case '~': + // If the symbol is not escaped, add an escape character. + if (i - 1 < 0 || currentTestNodeUid.Value[k - 1] != '\\') + { + filter.Append('\\'); + } + + filter.Append(currentChar); + break; + + default: + filter.Append(currentChar); + break; + } + } + } + + if (i != testNodesUid.Length - 1) + { + filter.Append('|'); + } + } + + return filter.ToString(); + } } diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/DiscoveryContextAdapter.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/DiscoveryContextAdapter.cs index 6fe6796ddb..59efdff4c6 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/DiscoveryContextAdapter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/DiscoveryContextAdapter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Requests; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel; @@ -11,8 +12,8 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel; /// internal sealed class DiscoveryContextAdapter : ContextAdapterBase, IDiscoveryContext { - public DiscoveryContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings? runSettings) - : base(commandLineOptions) => RunSettings = runSettings; + public DiscoveryContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings? runSettings, ITestExecutionFilter filter) + : base(commandLineOptions, filter) => RunSettings = runSettings; public IRunSettings? RunSettings { get; } } diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/RunContextAdapter.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/RunContextAdapter.cs index 94c675b3d6..4302a98127 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/RunContextAdapter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/RunContextAdapter.cs @@ -3,7 +3,6 @@ using Microsoft.Testing.Platform; using Microsoft.Testing.Platform.CommandLine; -using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Requests; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -14,8 +13,8 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel; /// internal sealed class RunContextAdapter : ContextAdapterBase, IRunContext { - public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter? filter) - : base(commandLineOptions) + public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter) + : base(commandLineOptions, filter) { RoslynDebug.Assert(runSettings.SettingsXml is not null); @@ -23,8 +22,6 @@ public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings ru // Parse and take the results directory from the runsettings. TestRunDirectory = XElement.Parse(runSettings.SettingsXml).Descendants("ResultsDirectory").SingleOrDefault()?.Value; - - HandleFilter(filter); } // NOTE: Always false as it's TPv2 oriented and so not applicable to TA. @@ -53,73 +50,4 @@ public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings ru /// public IRunSettings? RunSettings { get; } - - private void HandleFilter(ITestExecutionFilter? filter) - { - // TODO: Handle TreeNodeFilter - switch (filter) - { - case TestNodeUidListFilter testNodeUidListFilter: - FilterExpressionWrapper = new(CreateFilter(testNodeUidListFilter.TestNodeUids)); - break; - - default: - break; - } - } - - // We use heuristic to understand if the filter should be a TestCaseId or FullyQualifiedName. - // We know that in VSTest TestCaseId is a GUID and FullyQualifiedName is a string. - private static string CreateFilter(TestNodeUid[] testNodesUid) - { - StringBuilder filter = new(); - - for (int i = 0; i < testNodesUid.Length; i++) - { - if (Guid.TryParse(testNodesUid[i].Value, out Guid guid)) - { - filter.Append("Id="); - filter.Append(guid.ToString()); - } - else - { - TestNodeUid currentTestNodeUid = testNodesUid[i]; - filter.Append("FullyQualifiedName="); - for (int k = 0; k < currentTestNodeUid.Value.Length; k++) - { - char currentChar = currentTestNodeUid.Value[k]; - switch (currentChar) - { - case '\\': - case '(': - case ')': - case '&': - case '|': - case '=': - case '!': - case '~': - // If the symbol is not escaped, add an escape character. - if (i - 1 < 0 || currentTestNodeUid.Value[k - 1] != '\\') - { - filter.Append('\\'); - } - - filter.Append(currentChar); - break; - - default: - filter.Append(currentChar); - break; - } - } - } - - if (i != testNodesUid.Length - 1) - { - filter.Append('|'); - } - } - - return filter.ToString(); - } } diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/TreeNodeFilterExpression.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/TreeNodeFilterExpression.cs new file mode 100644 index 0000000000..b4b95a5f01 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/TreeNodeFilterExpression.cs @@ -0,0 +1,44 @@ +// 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.Testing.Platform.Requests; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel; + +internal sealed class TreeNodeFilterExpression : ITestCaseFilterExpression +{ + private readonly TreeNodeFilter _treeNodeFilter; + private readonly IEnumerable? _supportedProperties; + private readonly Func _propertyProvider; + + public TreeNodeFilterExpression(TreeNodeFilter treeNodeFilter, IEnumerable? supportedProperties, Func propertyProvider) + { + _treeNodeFilter = treeNodeFilter; + _supportedProperties = supportedProperties; + _propertyProvider = propertyProvider; + } + + public string TestCaseFilterValue => _treeNodeFilter.Filter; + + public bool MatchTestCase(TestCase testCase, Func propertyValueProvider) + { + // TODO + string assemblyName = Path.GetFileNameWithoutExtension(testCase.Source); + ReadOnlySpan fullyQualifiedName = testCase.FullyQualifiedName.AsSpan(); + + int lastDot = fullyQualifiedName.LastIndexOf('.'); + ReadOnlySpan methodName = fullyQualifiedName.Slice(lastDot + 1); + fullyQualifiedName = fullyQualifiedName.Slice(0, lastDot); + + lastDot = fullyQualifiedName.LastIndexOf('.'); + ReadOnlySpan className = fullyQualifiedName.Slice(lastDot + 1); + fullyQualifiedName = fullyQualifiedName.Slice(0, lastDot); + + ReadOnlySpan @namespace = fullyQualifiedName; + + // TODO: PropertyBag argument + return _treeNodeFilter.MatchesFilter($"/{assemblyName}/{@namespace.ToString()}/{className.ToString()}/{methodName.ToString()}", new()); + } +} diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Requests/VSTestDiscoverTestExecutionRequestFactory.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Requests/VSTestDiscoverTestExecutionRequestFactory.cs index c2bc000b26..be34343cc5 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Requests/VSTestDiscoverTestExecutionRequestFactory.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Requests/VSTestDiscoverTestExecutionRequestFactory.cs @@ -45,7 +45,7 @@ public static VSTestDiscoverTestExecutionRequest CreateRequest( ICommandLineOptions commandLineOptions = serviceProvider.GetRequiredService(); RunSettingsAdapter runSettings = new(commandLineOptions, fileSystem, configuration, clientInfo, loggerFactory, messageLogger); - DiscoveryContextAdapter discoveryContext = new(commandLineOptions, runSettings); + DiscoveryContextAdapter discoveryContext = new(commandLineOptions, runSettings, discoverTestExecutionRequest.Filter); ITestApplicationModuleInfo testApplicationModuleInfo = serviceProvider.GetTestApplicationModuleInfo(); IMessageBus messageBus = serviceProvider.GetRequiredService(); diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs index f587f02457..8334c458a4 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs @@ -220,6 +220,20 @@ public void MatchAllFilterSubpathWithPropertyExpression() Assert.IsFalse(filter.MatchesFilter("/B/A/C/D", new PropertyBag(new KeyValuePairStringProperty("A", "B")))); } + [TestMethod] + public void MatchEmptyNamespaceWithAsterisk() + { + TreeNodeFilter filter = new("/AssemblyName/*/ClassName/MethodName"); + Assert.IsTrue(filter.MatchesFilter("/AssemblyName//ClassName/MethodName", new PropertyBag())); + } + + [TestMethod] + public void MatchEmptyNamespaceWithEmpty() + { + TreeNodeFilter filter = new("/AssemblyName//ClassName/MethodName"); + Assert.IsFalse(filter.MatchesFilter("/AssemblyName//ClassName/MethodName", new PropertyBag())); + } + [TestMethod] public void MatchAllFilterWithPropertyExpression_DoNotAllowInMiddleOfFilter() => Assert.ThrowsException(() => _ = new TreeNodeFilter("/**/Path[A=B]")); }