// 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.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; internal sealed class TestMethodFilter { /// /// Supported properties for filtering. /// private readonly Dictionary _supportedProperties; internal TestMethodFilter() => _supportedProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) { [AdapterTestProperties.TestCategoryProperty.Label] = AdapterTestProperties.TestCategoryProperty, [AdapterTestProperties.PriorityProperty.Label] = AdapterTestProperties.PriorityProperty, [TestCaseProperties.FullyQualifiedName.Label] = TestCaseProperties.FullyQualifiedName, [TestCaseProperties.DisplayName.Label] = TestCaseProperties.DisplayName, [TestCaseProperties.Id.Label] = TestCaseProperties.Id, [AdapterTestProperties.TestClassNameProperty.Label] = AdapterTestProperties.TestClassNameProperty, }; /// /// Returns ITestCaseFilterExpression for TestProperties supported by adapter. /// /// The current context of the run. /// Logger used to report diagnostic messages (for example, filter parse errors). /// Indicates that the filter is unsupported/has an error. /// A filter expression. internal ITestCaseFilterExpression? GetFilterExpression(IDiscoveryContext? context, IAdapterMessageLogger logger, out bool filterHasError) { filterHasError = false; if (context is null) { return null; } ITestCaseFilterExpression? filter = null; try { filter = context is IRunContext runContext ? GetTestCaseFilterFromRunContext(runContext) : GetTestCaseFilterFromDiscoveryContext(context, logger); } catch (TestPlatformFormatException ex) { filterHasError = true; logger.SendMessage(MessageLevel.Error, ex.Message); } return filter; } /// /// Builds a platform-agnostic for the properties supported by the adapter. /// /// The current context of the run. /// Logger used to report diagnostic messages (for example, filter parse errors). /// Indicates that the filter is unsupported/has an error. /// /// A filter that evaluates instances, or when no /// filter applies (in which case every element is included). /// internal ITestElementFilter? GetTestElementFilter(IDiscoveryContext? context, IAdapterMessageLogger logger, out bool filterHasError) { ITestCaseFilterExpression? filterExpression = GetFilterExpression(context, logger, out filterHasError); return filterExpression is null ? null : new TestElementFilter(this, filterExpression); } /// /// Provides TestProperty for property name 'propertyName' as used in filter. /// /// The property name. /// a TestProperty instance. internal TestProperty PropertyProvider(string propertyName) { _supportedProperties.TryGetValue(propertyName, out TestProperty? testProperty); DebugEx.Assert(testProperty != null, "Invalid property queried"); return testProperty; } /// /// Provides value of TestProperty corresponding to property name 'propertyName' as used in filter. /// /// The current test case. /// Property name. /// The property value. internal object? PropertyValueProvider(TestCase? currentTest, string? propertyName) { if (currentTest is not null && propertyName is not null) { if (_supportedProperties.TryGetValue(propertyName, out TestProperty? testProperty)) { // Test case might not have defined this property. In that case GetPropertyValue() // would return default value. For filtering, if property is not defined return null. if (currentTest.Properties.Contains(testProperty)) { return currentTest.GetPropertyValue(testProperty); } } else { // Everything that it's not a supported property we use traits foreach (Trait trait in currentTest.Traits) { if (trait.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) { return trait.Value; } } } } return null; } /// /// Gets filter expression from run context. /// /// Run context. /// Filter expression. private ITestCaseFilterExpression? GetTestCaseFilterFromRunContext(IRunContext context) => context.GetTestCaseFilter(_supportedProperties.Keys, PropertyProvider); /// /// Gets filter expression from discovery context. /// /// Discovery context. /// The logger to log exception messages too. /// Filter expression. [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:'target parameter' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "GetTestCaseFilter is part of the VSTest discovery contract on the concrete DiscoveryContext type; the runtime guarantees the method exists on supported hosts.")] private ITestCaseFilterExpression? GetTestCaseFilterFromDiscoveryContext(IDiscoveryContext context, IAdapterMessageLogger logger) { try { // GetTestCaseFilter is present in DiscoveryContext but not in IDiscoveryContext interface. MethodInfo? methodGetTestCaseFilter = context.GetType().GetRuntimeMethod("GetTestCaseFilter", [typeof(IEnumerable), typeof(Func)]); return (ITestCaseFilterExpression?)methodGetTestCaseFilter?.Invoke(context, [_supportedProperties.Keys, (Func)PropertyProvider]); } catch (TargetInvocationException ex) { // In case of UWP .Net Native Tool Chain compilation. Invoking methods via Reflection doesn't work, hence discovery always fails. // Hence throwing exception only if it is of type TargetInvocationException(i.e. Method got invoked but something went wrong in GetTestCaseFilter Method) throw ex.InnerException!; } catch (Exception ex) { logger.SendMessage(MessageLevel.Warning, ex.Message); } return null; } /// /// Platform-agnostic filter that evaluates a against a VSTest /// . This is the single point where the neutral element model is /// translated to the VSTest test case in order to reuse the host's filter matching. /// private sealed class TestElementFilter : ITestElementFilter { private readonly TestMethodFilter _testMethodFilter; private readonly ITestCaseFilterExpression _filterExpression; public TestElementFilter(TestMethodFilter testMethodFilter, ITestCaseFilterExpression filterExpression) { _testMethodFilter = testMethodFilter; _filterExpression = filterExpression; } public bool Matches(UnitTestElement testElement) { // Reuse the element's cached host TestCase so that a matched test is not converted a second time // when it is subsequently reported to the discovery sink. TestCase testCase = testElement.GetOrCreateHostTestCase(); return _filterExpression.MatchTestCase(testCase, propertyName => _testMethodFilter.PropertyValueProvider(testCase, propertyName)); } } } /// /// Adapter-boundary that closes over the VSTest discovery/run context /// and produces the neutral via . Created at the /// boundary and passed into the platform services engine/discoverer so the filter (and any parse-error report) /// is built at the exact point the engine needs it, preserving the original per-source timing. /// internal sealed class TestElementFilterProvider : ITestElementFilterProvider { private readonly TestMethodFilter _testMethodFilter = new(); private readonly IDiscoveryContext? _context; public TestElementFilterProvider(IDiscoveryContext? context) => _context = context; public ITestElementFilter? GetTestElementFilter(IAdapterMessageLogger logger, out bool filterHasError) => _testMethodFilter.GetTestElementFilter(_context, logger, out filterHasError); }