Skip to content

Commit

Permalink
Cleanup filter-related logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Youssef1313 committed Feb 3, 2025
1 parent 70e33dc commit f179116
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 148 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,7 @@ protected override async Task SynchronizedRunTestsAsync(VSTestRunTestExecutionRe
PlatformServiceProvider.Instance.AdapterTraceLogger = new BridgedTraceLogger(_loggerFactory.CreateLogger("mstest-trace"));
MSTestExecutor testExecutor = new(cancellationToken);

if (request.VSTestFilter.TestCases is { } testCases)
{
await testExecutor.RunTestsAsync(testCases, request.RunContext, request.FrameworkHandle, _configuration);
}
else
{
await testExecutor.RunTestsAsync(request.AssemblyPaths, request.RunContext, request.FrameworkHandle, _configuration);
}
await testExecutor.RunTestsAsync(request.AssemblyPaths, request.RunContext, request.FrameworkHandle, _configuration);
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
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;

namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;

internal abstract class ContextAdapterBase
{
public ContextAdapterBase(ICommandLineOptions commandLineOptions)
protected ContextAdapterBase(ICommandLineOptions commandLineOptions, ITestExecutionFilter filter)
{
if (commandLineOptions.TryGetOptionArgumentList(
TestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName,
Expand All @@ -21,16 +23,34 @@ public 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<string>? supportedProperties,
Func<string, TestProperty?> propertyProvider)
{
if (TreeNodeFilter is not null)
{
return new TreeNodeFilterExpression(TreeNodeFilter, supportedProperties, propertyProvider);
}

if (FilterExpressionWrapper is null)
{
return null;
Expand Down Expand Up @@ -61,4 +81,59 @@ public 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,8 +12,8 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;
/// </summary>
internal sealed class DiscoveryContextAdapter : ContextAdapterBase, IDiscoveryContext
{
public DiscoveryContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings? runSettings = null)
: base(commandLineOptions) => RunSettings = runSettings;
public DiscoveryContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings? runSettings, ITestExecutionFilter filter)
: base(commandLineOptions, filter) => RunSettings = runSettings;

public IRunSettings? RunSettings { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

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;

namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;
Expand All @@ -13,8 +13,8 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;
/// </summary>
internal sealed class RunContextAdapter : ContextAdapterBase, IRunContext
{
public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings runSettings)
: base(commandLineOptions)
public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter)
: base(commandLineOptions, filter)
{
RoslynDebug.Assert(runSettings.SettingsXml is not null);

Expand All @@ -24,9 +24,6 @@ public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings ru
TestRunDirectory = XElement.Parse(runSettings.SettingsXml).Descendants("ResultsDirectory").SingleOrDefault()?.Value;
}

public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings runSettings, TestNodeUid[] testNodeUids)
: this(commandLineOptions, runSettings) => FilterExpressionWrapper = new(CreateFilter(testNodeUids));

// NOTE: Always false as it's TPv2 oriented and so not applicable to TA.

/// <inheritdoc />
Expand All @@ -53,59 +50,4 @@ public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings ru

/// <inheritdoc />
public IRunSettings? RunSettings { get; }

// 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 TreeNodeFilter _treeNodeFilter;
private IEnumerable<string>? _supportedProperties;
private Func<string, TestProperty?> _propertyProvider;

public TreeNodeFilterExpression(TreeNodeFilter treeNodeFilter, IEnumerable<string>? supportedProperties, Func<string, TestProperty?> propertyProvider)
{
_treeNodeFilter = treeNodeFilter;
_supportedProperties = supportedProperties;
_propertyProvider = propertyProvider;
}

public string TestCaseFilterValue => _treeNodeFilter.Filter;

public bool MatchTestCase(TestCase testCase, Func<string, object?> propertyValueProvider)
=> true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.Requests;
/// </summary>
public sealed class VSTestDiscoverTestExecutionRequest : DiscoverTestExecutionRequest
{
internal VSTestDiscoverTestExecutionRequest(TestSessionContext session, VSTestTestExecutionFilter executionFilter, string[] assemblyPaths,
internal VSTestDiscoverTestExecutionRequest(TestSessionContext session, ITestExecutionFilter executionFilter, string[] assemblyPaths,
IDiscoveryContext discoveryContext, IMessageLogger messageLogger, ITestCaseDiscoverySink discoverySink)
: base(session, executionFilter)
{
Expand All @@ -23,8 +23,9 @@ internal VSTestDiscoverTestExecutionRequest(TestSessionContext session, VSTestTe
DiscoverySink = discoverySink;
}

[Obsolete("VSTestTestExecutionFilter always have null TestCases and should not be used.", error: true)]
public VSTestTestExecutionFilter VSTestFilter
=> (VSTestTestExecutionFilter)Filter;
=> VSTestTestExecutionFilter.Instance;

public string[] AssemblyPaths { get; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,15 @@

#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.

using Microsoft.Testing.Extensions.VSTestBridge.Capabilities;
using Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;
using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Configurations;
using Microsoft.Testing.Platform.Extensions.TestFramework;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Logging;
using Microsoft.Testing.Platform.Messages;
using Microsoft.Testing.Platform.OutputDevice;
using Microsoft.Testing.Platform.Requests;
using Microsoft.Testing.Platform.Services;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;

namespace Microsoft.Testing.Extensions.VSTestBridge.Requests;

Expand All @@ -25,37 +20,17 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.Requests;
/// </summary>
public sealed class VSTestDiscoverTestExecutionRequestFactory : ITestExecutionRequestFactory
{
private readonly ITestFrameworkCapabilities _testFrameworkCapabilities;
private readonly ITestFramework _testFrameworkAdapter;
private readonly ICommandLineOptions _commandLineService;
private readonly string[] _assemblyPaths;
private readonly VSTestTestExecutionFilter _testExecutionFilter;
private readonly IDiscoveryContext _discoveryContext;
private readonly IMessageLogger _messageLogger;
private readonly ITestCaseDiscoverySink _discoverySink;

internal VSTestDiscoverTestExecutionRequestFactory(ITestFrameworkCapabilities testFrameworkCapabilities, ITestFramework testFrameworkAdapter,
ICommandLineOptions commandLineService, string[] assemblyPaths, VSTestTestExecutionFilter testExecutionFilter,
IDiscoveryContext discoveryContext, IMessageLogger messageLogger, ITestCaseDiscoverySink discoverySink)
[Obsolete]
private VSTestDiscoverTestExecutionRequestFactory()
{
_testFrameworkCapabilities = testFrameworkCapabilities;
_testFrameworkAdapter = testFrameworkAdapter;
_commandLineService = commandLineService;
_assemblyPaths = assemblyPaths;
_discoveryContext = discoveryContext;
_messageLogger = messageLogger;
_discoverySink = discoverySink;
_testExecutionFilter = testExecutionFilter;
}

// This class is never instantiated.
// It's not possible to reach this method.
// The class should probably be static and not needing to implement the interface.
[Obsolete]
Task<TestExecutionRequest> ITestExecutionRequestFactory.CreateRequestAsync(Platform.TestHost.TestSessionContext session)
=> !_commandLineService.IsOptionSet(PlatformCommandLineProvider.VSTestAdapterModeOptionKey)
? throw new InvalidOperationException($"Command line argument {PlatformCommandLineProvider.VSTestAdapterModeOptionKey} is not set but we are in VSTest adapter mode. This is a bug in the adapter.")
: _testFrameworkCapabilities.GetCapability<IVSTestFlattenedTestNodesReportCapability>()?.IsSupported != true
? throw new InvalidOperationException($"Skipping test adapter {_testFrameworkAdapter.DisplayName} because it is not {nameof(IVSTestFlattenedTestNodesReportCapability)} capable.")
: !_commandLineService.IsOptionSet(PlatformCommandLineProvider.DiscoverTestsOptionKey)
? throw new NotSupportedException($"The {nameof(VSTestRunTestExecutionRequestFactory)} does not support creating a {nameof(DiscoverTestExecutionRequest)}.")
: Task.FromResult<TestExecutionRequest>(new VSTestDiscoverTestExecutionRequest(session, _testExecutionFilter, _assemblyPaths, _discoveryContext, _messageLogger, _discoverySink));
=> throw ApplicationStateGuard.Unreachable();

public static VSTestDiscoverTestExecutionRequest CreateRequest(
DiscoverTestExecutionRequest discoverTestExecutionRequest,
Expand All @@ -72,12 +47,12 @@ public static VSTestDiscoverTestExecutionRequest CreateRequest(

ICommandLineOptions commandLineOptions = serviceProvider.GetRequiredService<ICommandLineOptions>();
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<IMessageBus>();
TestCaseDiscoverySinkAdapter discoverySink = new(adapterExtension, discoverTestExecutionRequest.Session, testAssemblyPaths, testApplicationModuleInfo, loggerFactory, messageBus, adapterExtension.IsTrxEnabled, clientInfo, cancellationToken);

return new(discoverTestExecutionRequest.Session, new(), testAssemblyPaths, discoveryContext, messageLogger, discoverySink);
return new(discoverTestExecutionRequest.Session, discoverTestExecutionRequest.Filter, testAssemblyPaths, discoveryContext, messageLogger, discoverySink);
}
}
Loading

0 comments on commit f179116

Please sign in to comment.