Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 16 additions & 40 deletions src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information.

using System.Web;

using Microsoft.Testing.Framework.Helpers;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Requests;

using PlatformTestNode = Microsoft.Testing.Platform.Extensions.Messages.TestNode;

namespace Microsoft.Testing.Framework;

internal sealed class BFSTestNodeVisitor
Expand All @@ -18,11 +18,6 @@ internal sealed class BFSTestNodeVisitor
public BFSTestNodeVisitor(IEnumerable<TestNode> rootTestNodes, ITestExecutionFilter testExecutionFilter,
TestArgumentsManager testArgumentsManager)
{
if (testExecutionFilter is not TreeNodeFilter and not TestNodeUidListFilter and not NopFilter)
{
throw new ArgumentOutOfRangeException(nameof(testExecutionFilter));
}

_rootTestNodes = rootTestNodes;
_testExecutionFilter = testExecutionFilter;
_testArgumentsManager = testArgumentsManager;
Expand All @@ -34,15 +29,15 @@ public async Task VisitAsync(Func<TestNode, TestNodeUid?, Task> onIncludedTestNo
{
// This is case sensitive, and culture insensitive, to keep UIDs unique, and comparable between different system.
Dictionary<TestNodeUid, List<TestNode>> testNodesByUid = [];
Queue<(TestNode CurrentNode, TestNodeUid? ParentNodeUid, StringBuilder NodeFullPath)> queue = new();
Queue<(TestNode CurrentNode, TestNodeUid? ParentNodeUid)> queue = new();
foreach (TestNode node in _rootTestNodes)
{
queue.Enqueue((node, null, new()));
queue.Enqueue((node, null));
}

while (queue.Count > 0)
{
(TestNode currentNode, TestNodeUid? parentNodeUid, StringBuilder nodeFullPath) = queue.Dequeue();
(TestNode currentNode, TestNodeUid? parentNodeUid) = queue.Dequeue();

if (!testNodesByUid.TryGetValue(currentNode.StableUid, out List<TestNode>? testNodes))
{
Expand All @@ -52,44 +47,28 @@ public async Task VisitAsync(Func<TestNode, TestNodeUid?, Task> onIncludedTestNo

testNodes.Add(currentNode);

StringBuilder nodeFullPathForChildren = new StringBuilder().Append(nodeFullPath);

if (nodeFullPathForChildren.Length == 0
|| nodeFullPathForChildren[^1] != TreeNodeFilter.PathSeparator)
if (TestArgumentsManager.IsExpandableTestNode(currentNode))
{
nodeFullPathForChildren.Append(TreeNodeFilter.PathSeparator);
currentNode = await _testArgumentsManager.ExpandTestNodeAsync(currentNode).ConfigureAwait(false);
}

// We want to encode the path fragment to avoid conflicts with the separator. We are using URL encoding because it is
// a well-known proven standard encoding that is reversible.
nodeFullPathForChildren.Append(EncodeString(currentNode.OverriddenEdgeName ?? currentNode.DisplayName));
string currentNodeFullPath = nodeFullPathForChildren.ToString();

// When we are filtering as tree filter and the current node does not match the filter, we skip the node and its children.
if (_testExecutionFilter is TreeNodeFilter treeNodeFilter)
PlatformTestNode platformTestNode = new()
{
if (!treeNodeFilter.MatchesFilter(currentNodeFullPath, CreatePropertyBagForFilter(currentNode.Properties)))
{
continue;
}
}
Uid = currentNode.StableUid.ToPlatformTestNodeUid(),
DisplayName = currentNode.DisplayName,
Properties = CreatePropertyBagForFilter(currentNode.Properties),
};

// If the node is expandable, we expand it (replacing the original node)
if (TestArgumentsManager.IsExpandableTestNode(currentNode))
if (!_testExecutionFilter.Matches(platformTestNode))
{
currentNode = await _testArgumentsManager.ExpandTestNodeAsync(currentNode).ConfigureAwait(false);
continue;
}

// If the node is not filtered out by the test execution filter, we call the callback with the node.
if (_testExecutionFilter is not TestNodeUidListFilter listFilter
|| listFilter.TestNodeUids.Any(uid => currentNode.StableUid.ToPlatformTestNodeUid() == uid))
{
await onIncludedTestNodeAsync(currentNode, parentNodeUid).ConfigureAwait(false);
}
await onIncludedTestNodeAsync(currentNode, parentNodeUid).ConfigureAwait(false);

foreach (TestNode childNode in currentNode.Tests)
{
queue.Enqueue((childNode, currentNode.StableUid, nodeFullPathForChildren));
queue.Enqueue((childNode, currentNode.StableUid));
}
}

Expand All @@ -106,7 +85,4 @@ private static PropertyBag CreatePropertyBagForFilter(IProperty[] properties)

return propertyBag;
}

private static string EncodeString(string value)
=> HttpUtility.UrlEncode(value);
}
21 changes: 6 additions & 15 deletions src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,26 +438,17 @@ private async Task<ResponseArgsBase> ExecuteRequestAsync(RequestArgsBase args, s
// catch and propagated as correct json rpc error
cancellationToken.ThrowIfCancellationRequested();

// Note: Currently the request generation and filtering isn't extensible
// in server mode, we create NoOp services, so that they're always available.
ITestExecutionFilter executionFilter = await _testSessionManager
.ResolveRequestFilterAsync(args, perRequestServiceProvider)
.ConfigureAwait(false);

ServerTestExecutionRequestFactory requestFactory = new(session =>
{
ICollection<TestNode>? testNodes = args.TestNodes;
string? filter = args.GraphFilter;
ITestExecutionFilter executionFilter = testNodes is not null
? new TestNodeUidListFilter(testNodes.Select(node => node.Uid).ToArray())
: filter is not null
? new TreeNodeFilter(filter)
: new NopFilter();

return method == JsonRpcMethods.TestingRunTests
method == JsonRpcMethods.TestingRunTests
? new RunTestExecutionRequest(session, executionFilter)
: method == JsonRpcMethods.TestingDiscoverTests
? new DiscoverTestExecutionRequest(session, executionFilter)
: throw new NotImplementedException($"Request not implemented '{method}'");
});
: throw new NotImplementedException($"Request not implemented '{method}'"));

// Build the per request objects
ServerTestExecutionFilterFactory filterFactory = new();
TestHostTestFrameworkInvoker invoker = new(perRequestServiceProvider);
PerRequestServerDataConsumer testNodeUpdateProcessor = new(perRequestServiceProvider, this, args.RunId, perRequestServiceProvider.GetTask());
Expand Down
20 changes: 17 additions & 3 deletions src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -457,14 +457,22 @@ await LogTestHostCreatedAsync(
// ServerMode and Console mode uses different host
if (hasServerFlag && isJsonRpcProtocol)
{
var testHostManager = (TestHostManager)TestHost;
if (!testHostManager.HasRequestFilterProviders())
{
testHostManager.AddRequestFilterProvider(sp => new TestNodeUidRequestFilterProvider());
testHostManager.AddRequestFilterProvider(sp => new TreeNodeRequestFilterProvider());
testHostManager.AddRequestFilterProvider(sp => new NopRequestFilterProvider());
}

// Build the server mode with the user preferences
IMessageHandlerFactory messageHandlerFactory = ServerModeManager.Build(serviceProvider);

// Build the test host
// note that we pass the BuildTestFrameworkAsync as callback because server mode will call it per-request
// this is not needed in console mode where we have only 1 request.
ServerTestHost serverTestHost =
new(serviceProvider, BuildTestFrameworkAsync, messageHandlerFactory, (TestFrameworkManager)TestFramework, (TestHostManager)TestHost);
new(serviceProvider, BuildTestFrameworkAsync, messageHandlerFactory, (TestFrameworkManager)TestFramework, testHostManager);

// If needed we wrap the host inside the TestHostControlledHost to automatically handle the shutdown of the connected pipe.
IHost actualTestHost = testControllerConnection is not null
Expand All @@ -483,8 +491,14 @@ await LogTestHostCreatedAsync(
}
else
{
// Add custom ITestExecutionFilterFactory to the service list if available
ActionResult<ITestExecutionFilterFactory> testExecutionFilterFactoryResult = await ((TestHostManager)TestHost).TryBuildTestExecutionFilterFactoryAsync(serviceProvider).ConfigureAwait(false);
var testHostManager = (TestHostManager)TestHost;
if (!testHostManager.HasFilterFactories())
{
testHostManager.AddTestExecutionFilterFactory(serviceProvider =>
new ConsoleTestExecutionFilterFactory(serviceProvider.GetCommandLineOptions()));
}

ActionResult<ITestExecutionFilterFactory> testExecutionFilterFactoryResult = await testHostManager.TryBuildTestExecutionFilterFactoryAsync(serviceProvider).ConfigureAwait(false);
if (testExecutionFilterFactoryResult.IsSuccess)
{
serviceProvider.TryAddService(testExecutionFilterFactoryResult.Result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,24 @@ Microsoft.Testing.Platform.Extensions.Messages.TestNodeStateProperty.TestNodeSta
*REMOVED*virtual Microsoft.Testing.Platform.Extensions.Messages.FileArtifactProperty.EqualityContract.get -> System.Type!
*REMOVED*virtual Microsoft.Testing.Platform.Extensions.Messages.FileArtifactProperty.Equals(Microsoft.Testing.Platform.Extensions.Messages.FileArtifactProperty? other) -> bool
*REMOVED*virtual Microsoft.Testing.Platform.Extensions.Messages.FileArtifactProperty.PrintMembers(System.Text.StringBuilder! builder) -> bool
Microsoft.Testing.Platform.Requests.ITestExecutionFilter.Matches(Microsoft.Testing.Platform.Extensions.Messages.TestNode! testNode) -> bool
Microsoft.Testing.Platform.Requests.TestNodeUidListFilter.Matches(Microsoft.Testing.Platform.Extensions.Messages.TestNode! testNode) -> bool
[TPEXP]Microsoft.Testing.Platform.Requests.AggregateTestExecutionFilter
[TPEXP]Microsoft.Testing.Platform.Requests.AggregateTestExecutionFilter.AggregateTestExecutionFilter(System.Collections.Generic.IReadOnlyCollection<Microsoft.Testing.Platform.Requests.ITestExecutionFilter!>! innerFilters) -> void
[TPEXP]Microsoft.Testing.Platform.Requests.AggregateTestExecutionFilter.InnerFilters.get -> System.Collections.Generic.IReadOnlyCollection<Microsoft.Testing.Platform.Requests.ITestExecutionFilter!>!
[TPEXP]Microsoft.Testing.Platform.Requests.AggregateTestExecutionFilter.Matches(Microsoft.Testing.Platform.Extensions.Messages.TestNode! testNode) -> bool
[TPEXP]Microsoft.Testing.Platform.Requests.IRequestFilterProvider
[TPEXP]Microsoft.Testing.Platform.Requests.IRequestFilterProvider.CanHandle(System.IServiceProvider! serviceProvider) -> bool
[TPEXP]Microsoft.Testing.Platform.Requests.IRequestFilterProvider.CreateFilterAsync(System.IServiceProvider! serviceProvider) -> System.Threading.Tasks.Task<Microsoft.Testing.Platform.Requests.ITestExecutionFilter!>!
[TPEXP]Microsoft.Testing.Platform.Requests.ITestExecutionFilterFactory
[TPEXP]Microsoft.Testing.Platform.Requests.ITestExecutionFilterFactory.TryCreateAsync() -> System.Threading.Tasks.Task<(bool Success, Microsoft.Testing.Platform.Requests.ITestExecutionFilter? TestExecutionFilter)>!
[TPEXP]Microsoft.Testing.Platform.Requests.ITestExecutionRequestContext
[TPEXP]Microsoft.Testing.Platform.Requests.ITestExecutionRequestContext.TestNodes.get -> System.Collections.Generic.ICollection<Microsoft.Testing.Platform.Extensions.Messages.TestNode!>?
[TPEXP]Microsoft.Testing.Platform.Requests.NopRequestFilterProvider
[TPEXP]Microsoft.Testing.Platform.Requests.NopRequestFilterProvider.NopRequestFilterProvider() -> void
[TPEXP]Microsoft.Testing.Platform.Requests.TestNodeUidRequestFilterProvider
[TPEXP]Microsoft.Testing.Platform.Requests.TestNodeUidRequestFilterProvider.TestNodeUidRequestFilterProvider() -> void
[TPEXP]Microsoft.Testing.Platform.Requests.TreeNodeRequestFilterProvider
[TPEXP]Microsoft.Testing.Platform.Requests.TreeNodeRequestFilterProvider.TreeNodeRequestFilterProvider() -> void
[TPEXP]Microsoft.Testing.Platform.TestHost.ITestHostManager.AddRequestFilterProvider(System.Func<System.IServiceProvider!, Microsoft.Testing.Platform.Requests.IRequestFilterProvider!>! requestFilterProvider) -> void
[TPEXP]Microsoft.Testing.Platform.TestHost.ITestHostManager.AddTestExecutionFilterFactory(System.Func<System.IServiceProvider!, Microsoft.Testing.Platform.Requests.ITestExecutionFilterFactory!>! testExecutionFilterFactory) -> void
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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.Extensions.Messages;

namespace Microsoft.Testing.Platform.Requests;

/// <summary>
/// Represents an aggregate filter that combines multiple test execution filters using AND logic.
/// A test node must match all inner filters to pass this aggregate filter.
/// </summary>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
[SuppressMessage("ApiDesign", "RS0016:Add public types and members to the declared API", Justification = "Experimental API")]
public sealed class AggregateTestExecutionFilter : ITestExecutionFilter
{
/// <summary>
/// Initializes a new instance of the <see cref="AggregateTestExecutionFilter"/> class.
/// </summary>
/// <param name="innerFilters">The collection of inner filters to aggregate.</param>
public AggregateTestExecutionFilter(IReadOnlyCollection<ITestExecutionFilter> innerFilters)
{
Guard.NotNull(innerFilters);
if (innerFilters.Count == 0)
{
throw new ArgumentException("At least one inner filter must be provided.", nameof(innerFilters));
}

InnerFilters = innerFilters;
}

/// <summary>
/// Gets the collection of inner filters.
/// </summary>
public IReadOnlyCollection<ITestExecutionFilter> InnerFilters { get; }

/// <inheritdoc />
public bool Matches(TestNode testNode)
{
// AND logic: all inner filters must match
foreach (ITestExecutionFilter filter in InnerFilters)
{
if (!filter.Matches(testNode))
{
return false;
}
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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.Extensions;

namespace Microsoft.Testing.Platform.Requests;

/// <summary>
/// Provides a filter for server-mode test execution requests.
/// Providers query request-specific information from the service provider.
/// </summary>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IRequestFilterProvider : IExtension
{
/// <summary>
/// Determines whether this provider can handle the current request context.
/// </summary>
/// <param name="serviceProvider">The service provider containing request-specific services.</param>
/// <returns>true if this provider can create a filter for the current request; otherwise, false.</returns>
bool CanHandle(IServiceProvider serviceProvider);

/// <summary>
/// Creates a test execution filter for the current request.
/// </summary>
/// <param name="serviceProvider">The service provider containing request-specific services.</param>
/// <returns>A test execution filter.</returns>
Task<ITestExecutionFilter> CreateFilterAsync(IServiceProvider serviceProvider);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
// 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.Extensions.Messages;

namespace Microsoft.Testing.Platform.Requests;

/// <summary>
/// Represents a filter for test execution.
/// </summary>
public interface ITestExecutionFilter;
public interface ITestExecutionFilter
{
/// <summary>
/// Determines whether the specified test node matches the filter criteria.
/// </summary>
/// <param name="testNode">The test node to evaluate.</param>
/// <returns>true if the test node matches the filter; otherwise, false.</returns>
bool Matches(TestNode testNode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@

namespace Microsoft.Testing.Platform.Requests;

internal interface ITestExecutionFilterFactory : IExtension
/// <summary>
/// Factory for creating test execution filters in console mode.
/// </summary>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
[SuppressMessage("ApiDesign", "RS0016:Add public types and members to the declared API", Justification = "Experimental API")]
public interface ITestExecutionFilterFactory : IExtension
{
/// <summary>
/// Attempts to create a test execution filter.
/// </summary>
/// <returns>A task containing a tuple with success status and the created filter if successful.</returns>
Task<(bool Success, ITestExecutionFilter? TestExecutionFilter)> TryCreateAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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.Extensions.Messages;

namespace Microsoft.Testing.Platform.Requests;

/// <summary>
/// Provides access to test execution request context for filter providers.
/// Available in the per-request service provider in server mode.
/// </summary>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface ITestExecutionRequestContext
{
/// <summary>
/// Gets the collection of test nodes specified in the request, or null if not filtering by specific nodes.
/// </summary>
ICollection<TestNode>? TestNodes { get; }
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// 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.Extensions.Messages;

namespace Microsoft.Testing.Platform.Requests;

/// <summary>
/// Represents a filter that does nothing.
/// </summary>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
[SuppressMessage("ApiDesign", "RS0016:Add public types and members to the declared API", Justification = "Experimental API")]
public sealed class NopFilter : ITestExecutionFilter;
public sealed class NopFilter : ITestExecutionFilter
{
/// <inheritdoc />
public bool Matches(TestNode testNode) => true;
}
Loading
Loading