// 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);
}