// 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.Extensions.VSTestBridge.CommandLine; using Microsoft.Testing.Extensions.VSTestBridge.Resources; using Microsoft.Testing.Platform; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Requests; using Microsoft.VisualStudio.TestPlatform.Common.Filtering; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel; internal abstract class ContextAdapterBase { private const string EmptyUidFilterExpression = "FullyQualifiedName=__MTP_EMPTY_UID_FILTER__&FullyQualifiedName!=__MTP_EMPTY_UID_FILTER__"; protected ContextAdapterBase(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter) : this(commandLineOptions, runSettings, filter, useFullyQualifiedNameAsUid: false) { } protected ContextAdapterBase(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter, bool useFullyQualifiedNameAsUid) { RunSettings = runSettings; // Stryker disable once Statement: This debug-only contract check has no release behavior. RoslynDebug.Assert(runSettings.SettingsXml is not null); string? filterFromRunsettings = XDocument.Parse(runSettings.SettingsXml).Element("RunSettings")?.Element("RunConfiguration")?.Element("TestCaseFilter")?.Value; string? filterFromCommandLineOption = null; if (commandLineOptions.TryGetOptionArgumentList( TestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName, out string[]? filterExpressions) && filterExpressions is not null && filterExpressions.Length == 1) { filterFromCommandLineOption = filterExpressions[0]; } HandleFilter(filter, filterFromRunsettings, filterFromCommandLineOption, useFullyQualifiedNameAsUid); } public IRunSettings? RunSettings { get; } private FilterExpressionWrapper? FilterExpressionWrapper { get; set; } private sealed class BridgeFilterExpression : ITestCaseFilterExpression { private readonly TestCaseFilterExpression _testCaseFilterExpression; public BridgeFilterExpression(TestCaseFilterExpression testCaseFilterExpression) => _testCaseFilterExpression = testCaseFilterExpression; public string TestCaseFilterValue => _testCaseFilterExpression.TestCaseFilterValue; public bool MatchTestCase(TestCase testCase, Func propertyValueProvider) => _testCaseFilterExpression.MatchTestCase(propertyValueProvider); } // 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 #pragma warning disable IDE0060 // Remove unused parameter - used via refl. public ITestCaseFilterExpression? GetTestCaseFilter( IEnumerable? supportedProperties, Func propertyProvider) #pragma warning restore IDE0060 // Remove unused parameter { if (FilterExpressionWrapper is null) { return null; } if (!RoslynString.IsNullOrEmpty(FilterExpressionWrapper.ParseError)) { throw new TestPlatformFormatException(FilterExpressionWrapper.ParseError, FilterExpressionWrapper.FilterString); } // MSTest relies on ITestCaseFilterExpression from VSTest object model. // The filter source package doesn't bring object model as very intended design. // We create our own BridgeFilterExpression here that just wraps the type from the source-only package return new BridgeFilterExpression(new TestCaseFilterExpression(FilterExpressionWrapper)); } private void HandleFilter(ITestExecutionFilter? filter, string? filterFromRunsettings, string? filterFromCommandLineOption, bool useFullyQualifiedNameAsUid) { // No filters at all, we can return immediately as there is nothing to do. // Stryker disable once Block: Continuing with an empty builder produces the same null filter. if (filter is null or NopFilter && filterFromRunsettings is null && filterFromCommandLineOption is null) { // Stryker disable once Statement: Continuing with an empty builder produces the same null filter. return; } var filterBuilder = new StringBuilder(); AppendFilter(filterFromRunsettings, filterBuilder); AppendFilter(filterFromCommandLineOption, filterBuilder); if (filter is not null) { foreach (ITestExecutionFilter leafFilter in TestExecutionFilterComposer.GetLeafFilters(filter)) { switch (leafFilter) { case NopFilter: break; case TestNodeUidListFilter testNodeUidListFilter: StartFilter(filterBuilder); if (testNodeUidListFilter.TestNodeUids.Length == 0) { filterBuilder.Append(EmptyUidFilterExpression); } else { BuildFilter(testNodeUidListFilter.TestNodeUids, filterBuilder, useFullyQualifiedNameAsUid); } EndFilter(filterBuilder); break; default: throw new NotSupportedException( string.Format( CultureInfo.InvariantCulture, ExtensionResources.UnsupportedTestExecutionFilter, leafFilter.GetType().FullName, "VSTestBridge")); } } } if (filterBuilder.Length > 0) { FilterExpressionWrapper = new FilterExpressionWrapper(filterBuilder.ToString()); } // Local functions static void AppendFilter(string? filter, StringBuilder builder) { if (filter is null) { return; } StartFilter(builder); builder.Append(filter); EndFilter(builder); } static void StartFilter(StringBuilder builder) { if (builder.Length > 0) { builder.Append(" & ("); } else { builder.Append('('); } } static void EndFilter(StringBuilder builder) => builder.Append(')'); } // The UID value is produced by ObjectModelConverters.ToTestNode, which sets it to either // TestCase.FullyQualifiedName or TestCase.Id depending on useFullyQualifiedNameAsUid. We use that // same discriminator here to decide the clause type, rather than guessing from the value with // Guid.TryParse. Guessing is wrong for a FullyQualifiedName that happens to be GUID-shaped (e.g. a // data-driven test whose display name is exactly a GUID string), which would then be emitted as an // Id= clause and select the wrong test (or no test). private static void BuildFilter(TestNodeUid[] testNodesUid, StringBuilder filter, bool useFullyQualifiedNameAsUid) { for (int i = 0; i < testNodesUid.Length; i++) { if (i > 0) { filter.Append('|'); } if (!useFullyQualifiedNameAsUid && Guid.TryParse(testNodesUid[i].Value, out Guid guid)) { filter.Append("Id="); filter.Append(guid.ToString()); continue; } TestNodeUid currentTestNodeUid = testNodesUid[i]; filter.Append("FullyQualifiedName="); // Use VSTest's canonical escaper rather than a hand-rolled loop. It escapes every filter // operator ('\', '(', ')', '&', '|', '=', '!', '~') unconditionally, which is exactly what // we need for a raw (un-escaped) test-node UID. filter.Append(FilterHelper.Escape(currentTestNodeUid.Value)); } } }