-
Notifications
You must be signed in to change notification settings - Fork 275
Extensible Filters + AggregateFilter #4200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
@Evangelink Hope you don't mind me having a go at tackling a couple of issues that have been sitting around for months?
|
Same happening with integration tests
Why does it reference packages instead of the local project's code? |
@Evangelink @nohwnd @MarcoRossignoli Providing you were okay with implementing this, how would I go about being unblocked due to private/internal code that I can't edit? |
@thomhurst I wasn't able to play a little with the PR but although I like the concept of the aggregate filter, I don't think that the extensibility as you are doing it is the right way forward as it would allow users to register any kind of filter including filters that are not handled by the framework. As of now, the filters have to be specifically supported by the platform or the framework. |
Hmm that's a good point. The registration code is going to have to be surfaced publicly though in order for frameworks to register it. Might be that filters work in conjunction with capabilities? So filters are only considered active/available if the framework has a capability for it? Kinda like the trx extension. That way we know a framework can handle that filter. |
Another alternative is the filter knows how to perform the logic itself, so that users could actually add filters on the fly without specific support from the framework. This actually makes it freely extensible by the end-user rather than the framework author. E.g.
|
@Evangelink did you have any opinions? I think the filter defining its own logic means users could make their own filters. Way more extensible then |
@Evangelink @nohwnd @MarcoRossignoli @Youssef1313 Here's my latest idea on filtering. Each implementation implements its own This means all frameworks need to do is call: Of course, they still could override the behaviour by doing what they do currently - Casting to the concrete type and implementing the filtering themselves, but I don't envision a lot of scenarios where you'd want to change the logic. This also makes filtering more consistent between frameworks as they can all use centralised logic instead of recreating it themselves, with the risk of introducing bugs. This also means users could create their own filters and register them, and they'd automatically flow through as the ITestExecutionFilter, or inside the AggregateFilter, and the filtering will work with their newly introduced logic without the framework even having to know about the specific type/logic. Keen to know your thoughts. I think this works nicely with the Open/Closed principle - Closed for modification but highly open to extensibility 😄 |
Bump 😇 |
Hey @thomhurst ! Sorry for the delay, the colleagues are in holidays and I was trying to make sure the new version gets released before I also leave but we had many infra issues. I'll try to have a look asap and write my feedback |
Did you ever manage to take a look @Evangelink ? |
Not yet sadly. I am finishing up fixes for 3.7.1 and will be on it. Let me auto-assign the PR. |
@thomhurst I am really sorry but I am not able to get some focus on this for the time being. |
Anyone going to have any capacity at any point? I'd really like to write my own batching filter. |
Thanks for the ping @thomhurst It looks like there are merge conflicts here now, if you can fix them please. |
e1164e8
to
551d8e0
Compare
Thanks! Pushed a newer version 😄 |
Yes @thomhurst, I think this is a very good improvement. I'd like to see some tests that use and demonstrate And very sorry for the delays in reviewing this. |
Added a test for But apart from that there's no logic to test - It's just an interface that serves the purpose of passing data via the parameter. The logic is up to whoever is implementing it. |
… feature/extensible-filters
} | ||
|
||
[TestMethod] | ||
public async Task Batch_ReceivesAllTestNodes_Filter_Example() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Youssef1313 Here's an example of how I'd like to to use IReceivesAllTestNodesExtension
within a filter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume in real-world, you will do something like if ([run|discover]TestExecutionRequest.Filter is IReceivesAllTestNodesExtension receivesTestNodeFilter) receivesTestNodeFilter.ReceiveTestNodes()
?
Isn't that achievable if you define the interface on your side, and have the special handling for AggregateFilter
? Something like:
if (request.Filter is AggregateFilter aggregateFilter)
{
foreach (var inner in aggregateFilter.InnerFilters.OfType<IReceivesAllTestNodes>())
{
inner.ReceiveTestNodes();
}
}
(request.Filter as IReceivesAllTestNodes)?.ReceiveTestNodes();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah you're right actually. I can just define this my side. I'll remove it from this PR!
@@ -24,4 +25,10 @@ private VSTestTestExecutionFilter() | |||
public ImmutableArray<TestCase>? TestCases => null; | |||
|
|||
internal static VSTestTestExecutionFilter Instance { get; } = new(); | |||
|
|||
/// <inheritdoc /> | |||
public bool IsEnabled => false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this filter disabled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an obsolete filter and always returns false for a match
public IReadOnlyList<ITestExecutionFilter> InnerFilters { get; } = innerFilters; | ||
|
||
/// <inheritdoc /> | ||
public bool IsEnabled => true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should check whether any of the subfilters are enabled
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
} | ||
|
||
return ActionResult.Fail<ITestExecutionFilterFactory>(); | ||
ITestExecutionFilter[] requestedFilters = list | ||
.Where(x => x.IsEnabled) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should pass all filters rather than filter them out. this allows us to keep the async nature of the factories and build them at the time that the request is invoked.
especially in case of the server mode, where the request is invoked once JSON-RPC method is invoked and that might not happen immediately. and might impact the filter generated.
for instance, this affects how the RetryFilter operates. with a delayed, async factory, the RetryFilterFactory can check with the retry orchestrator if there's tests to be retried or not and disable the filter on construction. once built, the filter should be immediate, i.e. not contain any async code.
with the non-async approach, either each filter match needs to become async, or all of the filter creation needs to be done at process startup, which the RetryFilter does not have the luxury of doing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each match being async also gives more flexibility to filters. And for most filters, we can just return Task.FromResult(true/false) so it won't be heavy on allocations or create a state machine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed the API to async so you can see what it'd be like
Microsoft.Testing.Platform.Requests.AggregateFilter.IsEnabled.get -> bool | ||
Microsoft.Testing.Platform.Requests.AggregateFilter.MatchesFilter(Microsoft.Testing.Platform.Extensions.Messages.TestNode! testNode) -> bool | ||
Microsoft.Testing.Platform.Requests.ITestExecutionFilter.IsEnabled.get -> bool | ||
Microsoft.Testing.Platform.Requests.ITestExecutionFilter.MatchesFilter(Microsoft.Testing.Platform.Extensions.Messages.TestNode! testNode) -> bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we want to open up the filtering, we should have a discussion on whether this is the right abstraction.
- will it handle the tree filter correctly?
- some test frameworks have a notion of focus tests, where if any test has a focus label only that test is run. is this an example where the filter factory would need to query for all of the test nodes in the dll?
- longer term, can we optimize discovery via the filter? the current
MatchesFilter()
requires the framework to discover all test nodes. would we be able to optimize the discovery (to only discover tests in a class) if the test execution filters can be added externally as a delegate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will it handle the tree filter correctly?
Why wouldn't it?
the current MatchesFilter() requires the framework to discover all test nodes
What do you mean? Currently it just accepts a single node, so it doesn't care about the rest of them.
some test frameworks have a notion of focus tests, where if any test has a focus label only that test is run. is this an example where the filter factory would need to query for all of the test nodes in the dll?
I had an idea for a BatchFilter too. This would need a way of passing it all the test nodes so it has the information it needs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My initial idea was an interface like IReceivesAllTestNodes
with a method that takes testnodes as a parameter, allowing the filter to then store them within a field or something.
This would then be up to the framework to call. Or alternatively, could a DataConsumer of DiscoveredTestNodes exist that aggregates them all and could pass them to the filter if it has that interface? Can a DataConsumer see a filter? 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually - Could a filter itself just also be registered as a data consumer itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@drognanar Can you follow-up here please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added the ability for test filters to also be data consumers. See here: #4200 (comment)
…ers can also be data consumers
public class CompositeExtensionFactory<TExtension> : ICompositeExtensionFactory, ICloneable | ||
where TExtension : class, IExtension | ||
public class CompositeExtensionFactory<TExtension> : ICompositeExtensionFactory | ||
where TExtension : class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why drop the IExtension
constraint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each TestHost method able to take a CompositeExtensionFactory can keep its constraints, so we know we're still registering the appropriate types.
public void AddDataConsumer<T>(CompositeExtensionFactory<T> compositeServiceFactory)
where T : class, IDataConsumer
public void AddTestSessionLifetimeHandle<T>(CompositeExtensionFactory<T> compositeServiceFactory)
where T : class, ITestSessionLifetimeHandler
And the new one:
public void AddTestExecutionFilter<T>(CompositeExtensionFactory<T> compositeServiceFactory)
where T : class, ITestExecutionFilter
Now an ITestExecutionFilter
can be registered using a CompositeExtensionFactory
- Meaning a filter could also be a DataConsumer.
My thinking for this was TestFilters that need the context of all test nodes (Explicit Filter, or Batch Filter) could be both a filter and a data consumer of test nodes. They could collect and aggregate discovered test nodes, and then use those within their filtering logic, and don't then require being passed anything by the test framework, it'll just happen automatically.
TL;DR: A ITestExecutionFilter
isn't an IExtension
so this constraint was preventing me from building a filter with this object type.
} | ||
|
||
private async Task<List<ITestExecutionFilter>> GetEnabledFiltersAsync() | ||
=> _enabledFilters ??= await Task.Run(async () => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why Task.Run instead of just having the logic directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed Task.Run
Fixes #3530
Fixes #3590
Changes:
IsEnabled
property to tell the framework whether it has been requested as a filter for this test run or whether it has the information necessary to perform its filtering.ITestExecutionFilter[]
array insideAggregateFilter
IsMatch
logic - So framework authors don't have to build the logic themselves. This allows filters to be built generically for MTP, and installed on different frameworks, and they should just work. And the behaviour will be consistent.AggregateFilter
The aggregate filter will be built if two or more valid filters are combined when executing the test suite.
The resulting filtering with be a combination of all the combined filtering.
e.g.
Test suite has:
Filter 1 allows:
Filter 2 allows:
The result would be executing:
As they're the only ones that pass all filters.
Custom Filters
Framework or extension authors can now create their own filters and register them.
ITestExecutionFilter
.TestApplicationBuilder.TestHost.RegisterTestExecutionFilter(...)