From 3eb63bf71163807d5f11142a5876d906e9f55921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 26 Jul 2026 12:49:18 +0200 Subject: [PATCH 1/3] Add composable test execution filter providers Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d7adc271-ccc0-4654-aede-93ac7d83dd40 --- .../020-Test-Execution-Filter-Providers.md | 430 ++++++++++++++++++ .../Resources/PlatformAdapterResources.resx | 4 + .../xlf/PlatformAdapterResources.cs.xlf | 5 + .../xlf/PlatformAdapterResources.de.xlf | 5 + .../xlf/PlatformAdapterResources.es.xlf | 5 + .../xlf/PlatformAdapterResources.fr.xlf | 5 + .../xlf/PlatformAdapterResources.it.xlf | 5 + .../xlf/PlatformAdapterResources.ja.xlf | 5 + .../xlf/PlatformAdapterResources.ko.xlf | 5 + .../xlf/PlatformAdapterResources.pl.xlf | 5 + .../xlf/PlatformAdapterResources.pt-BR.xlf | 5 + .../xlf/PlatformAdapterResources.ru.xlf | 5 + .../xlf/PlatformAdapterResources.tr.xlf | 5 + .../xlf/PlatformAdapterResources.zh-Hans.xlf | 5 + .../xlf/PlatformAdapterResources.zh-Hant.xlf | 5 + .../MSTestFilterContext.cs | 38 +- .../ObjectModel/ContextAdapterBase.cs | 38 +- .../Resources/ExtensionResources.resx | 4 + .../Resources/xlf/ExtensionResources.cs.xlf | 5 + .../Resources/xlf/ExtensionResources.de.xlf | 5 + .../Resources/xlf/ExtensionResources.es.xlf | 5 + .../Resources/xlf/ExtensionResources.fr.xlf | 5 + .../Resources/xlf/ExtensionResources.it.xlf | 5 + .../Resources/xlf/ExtensionResources.ja.xlf | 5 + .../Resources/xlf/ExtensionResources.ko.xlf | 5 + .../Resources/xlf/ExtensionResources.pl.xlf | 5 + .../xlf/ExtensionResources.pt-BR.xlf | 5 + .../Resources/xlf/ExtensionResources.ru.xlf | 5 + .../Resources/xlf/ExtensionResources.tr.xlf | 5 + .../xlf/ExtensionResources.zh-Hans.xlf | 5 + .../xlf/ExtensionResources.zh-Hant.xlf | 5 + .../TestApplicationBuilderExtensions.cs | 12 + .../Hosts/ConsoleTestHost.cs | 7 +- .../Hosts/ServerTestHost.RequestExecution.cs | 26 +- .../Hosts/TestHostBuilder.Modes.cs | 4 + .../InternalAPI/InternalAPI.Unshipped.txt | 15 + .../PublicAPI/PublicAPI.Unshipped.txt | 19 + .../Requests/CompositeTestExecutionFilter.cs | 53 +++ .../ConsoleTestExecutionRequestFactory.cs | 21 +- .../Requests/ITestExecutionFilterProvider.cs | 29 ++ .../Requests/ITestExecutionRequestFactory.cs | 2 +- .../ServerTestExecutionRequestFactory.cs | 8 +- .../Requests/TestExecutionFilterComposer.cs | 157 +++++++ .../Requests/TestExecutionFilterContext.cs | 32 ++ .../Requests/TestExecutionFilterOperator.cs | 16 + .../Requests/TestExecutionRequestKind.cs | 21 + .../Requests/TestExecutionRequestOrigin.cs | 21 + .../Requests/TestHostTestFrameworkInvoker.cs | 2 +- .../Requests/TestNodeUidListFilter.cs | 2 +- .../Resources/PlatformResources.resx | 26 ++ .../Resources/xlf/PlatformResources.cs.xlf | 35 ++ .../Resources/xlf/PlatformResources.de.xlf | 35 ++ .../Resources/xlf/PlatformResources.es.xlf | 35 ++ .../Resources/xlf/PlatformResources.fr.xlf | 35 ++ .../Resources/xlf/PlatformResources.it.xlf | 35 ++ .../Resources/xlf/PlatformResources.ja.xlf | 35 ++ .../Resources/xlf/PlatformResources.ko.xlf | 35 ++ .../Resources/xlf/PlatformResources.pl.xlf | 35 ++ .../Resources/xlf/PlatformResources.pt-BR.xlf | 35 ++ .../Resources/xlf/PlatformResources.ru.xlf | 35 ++ .../Resources/xlf/PlatformResources.tr.xlf | 35 ++ .../xlf/PlatformResources.zh-Hans.xlf | 35 ++ .../xlf/PlatformResources.zh-Hant.xlf | 35 ++ .../TestHost/TestHostManager.cs | 15 + .../ExecutionTests.cs | 183 +++++++- .../MSTestFilterContextTests.cs | 133 ++++++ .../RunContextAdapterFilterTests.cs | 70 ++- .../TestExecutionFilterComposerTests.cs | 341 ++++++++++++++ .../ServerMode/ServerTests.cs | 77 ++++ 69 files changed, 2346 insertions(+), 45 deletions(-) create mode 100644 docs/RFCs/020-Test-Execution-Filter-Providers.md create mode 100644 src/Platform/Microsoft.Testing.Platform/Requests/CompositeTestExecutionFilter.cs create mode 100644 src/Platform/Microsoft.Testing.Platform/Requests/ITestExecutionFilterProvider.cs create mode 100644 src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterComposer.cs create mode 100644 src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterContext.cs create mode 100644 src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterOperator.cs create mode 100644 src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionRequestKind.cs create mode 100644 src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionRequestOrigin.cs create mode 100644 test/UnitTests/MSTestAdapter.UnitTests/MSTestFilterContextTests.cs create mode 100644 test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TestExecutionFilterComposerTests.cs diff --git a/docs/RFCs/020-Test-Execution-Filter-Providers.md b/docs/RFCs/020-Test-Execution-Filter-Providers.md new file mode 100644 index 0000000000..aa79a27d04 --- /dev/null +++ b/docs/RFCs/020-Test-Execution-Filter-Providers.md @@ -0,0 +1,430 @@ +# RFC 020 - Composable test execution filter providers + +- [ ] Approved in principle +- [x] Under discussion +- [x] Implementation +- [ ] Shipped + +## Summary + +Microsoft.Testing.Platform (MTP) currently creates one request filter from the command line or +JSON-RPC payload and passes that filter to the test framework. Extensions cannot add an independent +constraint without replacing or reimplementing the platform's built-in filter creation. + +This RFC introduces an experimental, multi-registration `ITestExecutionFilterProvider` API. +For console requests, MTP creates its built-in filter first, asks every enabled provider for zero or +one additional constraint, and combines all constraints with explicit logical **AND** semantics. +The platform normalizes UID constraints by intersecting them and represents remaining conjunctions +with `CompositeTestExecutionFilter`. + +Providers produce constraints for one request. They do not own discovery/execution orchestration, +batching, sharding, retry scheduling, or any other multi-run plan. + +## Motivation + +The open filtering issues describe several related but distinct needs: + +- [#3590](https://github.com/microsoft/testfx/issues/3590) asks for public filter extensibility. +- [#3530](https://github.com/microsoft/testfx/issues/3530) asks for an aggregate filter. +- [#3528](https://github.com/microsoft/testfx/issues/3528) proposes index-based batching. +- [#4068](https://github.com/microsoft/testfx/issues/4068) asks for test sharding. +- [#4293](https://github.com/microsoft/testfx/issues/4293) shows that a platform-known filter can be + recognized by MTP but unsupported by an adapter. +- [#7160](https://github.com/microsoft/testfx/issues/7160) describes the lack of a coherent, + cross-framework filtering story. + +Previous implementations in [PR #4200](https://github.com/microsoft/testfx/pull/4200) and +[PR #6677](https://github.com/microsoft/testfx/pull/6677) explored self-evaluating custom filters +and whole-filter factories. They exposed too much responsibility at once: + +- A custom filter type can reach a framework that has no way to interpret it. +- A whole-filter factory replaces the built-in CLI filter rather than composing with it. +- Selecting one winning factory makes extension registration order or activation conflicts part of + filter semantics. +- Letting filters own test-suite-wide planning conflates a per-request constraint with orchestration. + +The platform needs a smaller abstraction that composes independently produced constraints without +silently losing any of them. + +## Scope + +This RFC covers: + +- provider registration and lifecycle; +- request kind and request origin; +- platform-known filter representations; +- logical AND composition; +- UID-list intersection; +- unsupported-filter diagnostics; +- console and JSON-RPC boundaries; +- native MSTest and VSTestBridge translation responsibilities; and +- migration from the existing internal single-factory model. + +This RFC does not define: + +- a new command-line option or filter grammar; +- affected-test terminology or source-to-test mapping; +- OR/NOT composition across provider contributions; +- a framework-neutral predicate language; +- discovery-before-run planning; +- sharding, batching, retry scheduling, or multi-process coordination; or +- public orchestration APIs. + +Those planning concerns remain with an orchestrator. An orchestrator can discover tests, partition +the resulting UIDs, and launch one or more requests whose UID filters are ordinary constraints. + +## Terminology + +| Term | Meaning | +| --- | --- | +| Request filter | The filter MTP creates from the current CLI or JSON-RPC request. | +| Provider | An enabled `ITestExecutionFilterProvider` extension that may contribute one constraint. | +| Constraint | A filter that narrows the tests eligible for the current request. | +| Composition | Combining the request filter and provider constraints into one filter. | +| Request kind | Whether the current request performs discovery or execution. | +| Request origin | Whether the request came from the console path or the JSON-RPC server path. | +| Platform-known filter | A filter representation whose semantics MTP defines: `NopFilter`, `TestNodeUidListFilter`, `TreeNodeFilter`, or an AND `CompositeTestExecutionFilter` containing platform-known filters. | + +## Public API + +The initial experimental API is: + +```csharp +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] +public interface ITestExecutionFilterProvider : IExtension +{ + Task GetFilterAsync( + TestExecutionFilterContext context, + CancellationToken cancellationToken); +} + +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] +public sealed class TestExecutionFilterContext +{ + public TestExecutionFilterContext( + TestExecutionRequestKind requestKind, + TestExecutionRequestOrigin origin); + + public TestExecutionRequestKind RequestKind { get; } + public TestExecutionRequestOrigin Origin { get; } +} + +public enum TestExecutionRequestKind +{ + Discovery, + Run, +} + +public enum TestExecutionRequestOrigin +{ + Console, + Server, +} + +public enum TestExecutionFilterOperator +{ + And, +} + +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] +public sealed class CompositeTestExecutionFilter : ITestExecutionFilter +{ + public CompositeTestExecutionFilter( + TestExecutionFilterOperator @operator, + params ITestExecutionFilter[] filters); + + public TestExecutionFilterOperator Operator { get; } + public IReadOnlyList Filters { get; } +} + +builder.AddTestExecutionFilterProvider(serviceProvider => new MyProvider(serviceProvider)); +``` + +MTP creates `TestExecutionFilterContext` for provider calls. Its public constructor lets provider +authors test their implementation without mocking platform internals. + +## Provider lifecycle + +Provider factories are registered while building the test application. Registration is additive; +there is no single winner. + +MTP follows the normal extension lifecycle: + +1. Instantiate each registered provider. +2. Validate extension UID/type uniqueness. +3. Call `IsEnabledAsync`. +4. Initialize enabled providers that implement `IAsyncInitializableExtension`. +5. Retain enabled providers for request creation and dispose them with the application services. +6. For every request, call `GetFilterAsync(context, cancellationToken)` once on each enabled + provider. + +Returning `null` means that the provider has no constraint for that request. Returning `NopFilter` +is accepted but equivalent to `null`. + +The request cancellation token is passed through unchanged. MTP checks cancellation before invoking +each provider. Providers must stop expensive work promptly when cancellation is requested. + +Provider invocation follows registration order for deterministic lifecycle behavior only. +Registration order does not define precedence or filter meaning: every contribution is an AND +constraint. + +## Request kind and origin + +Providers need two independent dimensions: + +- `RequestKind.Discovery` versus `RequestKind.Run`, because a constraint may apply only to execution + or may intentionally constrain both discovery and execution. +- `RequestOrigin.Console` versus `RequestOrigin.Server`, because IDE/Test Explorer requests carry + client-owned selection semantics that must not be silently changed. + +Origin deliberately describes transport/request ownership, not a product name such as "IDE" or an +implementation-specific scenario such as "affected tests". + +## Filter representations + +### No-op + +`NopFilter` contributes no constraint. It is removed during normalization. + +### UID list + +`TestNodeUidListFilter` is a precise set constraint. Its contract is: + +- a non-empty list selects nodes whose UID is in the list; +- duplicate UIDs have no additional meaning; and +- an empty list selects no tests. + +The last rule is important for disjoint intersections and for JSON-RPC requests that explicitly send +an empty selection. + +### Tree/query + +`TreeNodeFilter` remains a platform-known representation. This RFC does not make every adapter +capable of evaluating it. An adapter that cannot translate it must fail with an actionable +unsupported-filter diagnostic; silently treating it as `NopFilter` is forbidden. + +### Composite + +`CompositeTestExecutionFilter` carries an explicit operator and child filters. Version 1 exposes +only `TestExecutionFilterOperator.And`. + +OR is not exposed because combining independently owned constraints with OR broadens selection and +can defeat a security, cost, or correctness boundary imposed by another provider. OR also requires +clear interaction rules with framework-owned filter languages before it can be safe. + +The constructor requires at least two non-null children. MTP flattens nested AND composites before +passing the filter to a framework. + +### Custom filter types + +Although `ITestExecutionFilter` is implementable, public providers in this version may return only +platform-known representations. MTP validates every provider contribution recursively. + +A custom implementation is rejected and the diagnostic names both the provider UID and filter type. +It is never dropped. Supporting custom filter kinds later requires: + +1. a framework capability that declares the kind; +2. negotiation before request execution; +3. a non-silent fallback/rejection policy for mixed frameworks; and +4. adapter translation or a platform-owned evaluation contract. + +## Composition algorithm + +For a console request: + +1. MTP creates the existing built-in request filter from `--filter-uid`, + `--treenode-filter`, or no filter. +2. MTP asks every enabled provider for a contribution. +3. MTP recursively flattens nested AND composites. +4. MTP removes no-op constraints. +5. MTP intersects all `TestNodeUidListFilter` constraints using ordinal UID equality. +6. MTP sorts the resulting UID set for a deterministic representation. +7. MTP returns: + - `NopFilter` for zero constraints; + - the only filter for one constraint; or + - `CompositeTestExecutionFilter(And, ...)` for multiple constraints. + +The intersection is computed in the platform rather than by every adapter. If any UID intersection +is empty, the resulting empty `TestNodeUidListFilter` means match none. + +Examples: + +| Built-in filter | Provider A | Provider B | Result | +| --- | --- | --- | --- | +| `Nop` | `null` | `null` | `Nop` | +| UIDs `{A,B}` | UIDs `{B,C}` | `null` | UIDs `{B}` | +| UIDs `{A}` | UIDs `{B}` | `null` | empty UID list (match none) | +| tree `T` | UIDs `{A,B}` | `null` | `AND(T, UIDs {A,B})` | +| runsettings filter | provider UID constraint | — | adapter evaluates `runsettings AND provider` | + +## Adapter responsibilities + +MTP transports filter representations; the test framework or bridge still owns filtering because it +owns test discovery and execution. + +Native MSTest and VSTestBridge must: + +- traverse `CompositeTestExecutionFilter` recursively; +- implement only `And`; +- translate each UID list to the existing VSTest-compatible expression; +- combine translated constraints with command-line and runsettings filters using AND; +- translate an empty UID list to a valid expression that cannot match any test; and +- throw for `TreeNodeFilter`, unknown operators, and custom filter types they cannot evaluate. + +The empty-list translation uses a contradiction rather than the invalid expression `()`: + +```text +FullyQualifiedName=__MTP_EMPTY_UID_FILTER__ +& +FullyQualifiedName!=__MTP_EMPTY_UID_FILTER__ +``` + +No test can satisfy both clauses, including a test whose actual name happens to equal the sentinel. + +Other test frameworks must add composite handling before opting into provider scenarios that can +produce more than one constraint. + +## Capabilities + +This RFC does not add a public custom-filter capability. Platform-known representations are the +initial compatibility boundary. + +Tree filtering already has framework capability history, but capability declaration is not +consistently available across native frameworks and VSTestBridge. Until capability negotiation is +coherent, adapters must reject unsupported tree filters at translation time rather than ignore +them. + +A future custom-kind capability should be additive to this design: MTP can validate a provider +contribution against the selected framework's capabilities before composition. + +## Diagnostics + +The following are fatal, actionable errors: + +- a provider returns an unknown filter type; +- a composite has an unknown operator; +- a server-origin provider returns a non-no-op constraint; +- an adapter receives a known representation it cannot translate; or +- an adapter receives a custom representation. + +Diagnostics identify the provider or adapter, the filter type/operator, and the supported action +(for example, return `null` for server origin). No unsupported filter is silently ignored. + +## JSON-RPC server and IDE boundary + +JSON-RPC `testing/runTests` and `testing/discoverTests` requests own their selection: + +- `tests` becomes `TestNodeUidListFilter`; +- `filter` becomes `TreeNodeFilter`; and +- neither becomes `NopFilter`. + +Version 1 does not apply provider constraints to server-origin requests. MTP still invokes providers +with `Origin.Server` so an origin-aware provider can explicitly return `null`. If a provider returns +a real constraint, MTP rejects the request with an actionable diagnostic. This is deliberate: + +- existing client selection semantics stay unchanged; +- an extension is not silently ineffective; and +- the API shape can support negotiated server composition later without breaking providers. + +An explicit empty `tests` array remains distinct from an omitted `tests` property and produces an +empty UID filter, which means match none. + +## Compatibility and migration + +The API is additive and experimental. + +- Existing applications with no providers receive the same built-in filter object as before. +- Existing CLI option names and validation do not change. +- Existing JSON-RPC payloads do not change. +- The internal `ITestExecutionFilterFactory` remains the source of the console built-in filter; it + is no longer proposed as the public extensibility point. +- A previous custom factory prototype should migrate to a provider that returns only its additional + constraint. It no longer needs to recreate CLI parsing or replace the built-in filter. +- Frameworks that pattern-match directly on `ITestExecutionFilter` must add recursive composite + support before consuming applications register providers. + +## Relationship to draft PR #8820 + +[Draft PR #8820](https://github.com/microsoft/testfx/pull/8820) proposes a user-facing `--filter` +grammar, filter kinds, and framework registration of those kinds. This RFC is orthogonal: + +- #8820 answers how a user expression becomes a request filter. +- This RFC answers how the request filter composes with independent extension constraints. + +This implementation does not add, rename, claim, or alias any CLI option, so it does not collide +with framework-owned `--filter` options discussed in #8820. If #8820 lands later, its parsed result +can become the built-in request filter at step 1 of the same composition algorithm. + +Custom kinds proposed by #8820 would still need capability negotiation before providers could +contribute them. This RFC does not pre-approve arbitrary custom filter objects. + +## Drawbacks + +- Frameworks must understand a new composite representation. +- Server-origin providers must branch on origin and return `null`. +- AND-only composition is intentionally less expressive than a general Boolean tree. +- UID normalization allocates a set and deterministic result array. +- Publicly implementable `ITestExecutionFilter` remains broader than the provider contract, so + runtime validation is required. + +## Alternatives + +### Public whole-filter factory + +Rejected. Replacing the built-in filter forces extensions to duplicate CLI/request parsing and +creates winner/conflict semantics when multiple factories are enabled. + +### First enabled provider wins + +Rejected. Registration order would define behavior and independent constraints could disappear. + +### Self-evaluating custom filters + +Rejected for the initial API. Frameworks do not necessarily expose every discovered node to the +platform before filtering, and framework-owned metadata/evaluation rules differ. This also cannot +preserve server selection without a capability contract. + +### Let every framework merge providers + +Rejected. It duplicates provider lifecycle, cancellation, composition, and UID intersection in each +adapter and risks semantic drift. + +### General AND/OR/NOT expression tree + +Deferred. AND is the safe operation for independently owned constraints. OR/NOT require a separate +design for ownership, capability negotiation, and framework-language interaction. + +### Batch filter as a platform filter + +Rejected as the solution to sharding. Index ranges assume stable ordering and make a single request +responsible for global planning. Orchestrators should partition discovered UIDs and issue precise +UID constraints. + +## Testing strategy + +Coverage includes: + +- built-in-only behavior; +- disabled, one, and two enabled providers; +- request kind and origin; +- cancellation propagation; +- nested composite flattening; +- UID intersection, empty intersection, and built-in UID plus provider UID; +- tree plus UID composition; +- unsupported custom filters and server-origin contributions; +- native MSTest and VSTestBridge recursive translation; +- runsettings/command-line AND preservation; +- empty UID match-none behavior; +- JSON-RPC empty selection; and +- acceptance assets that register focused fake providers without coupling to a product scenario. + +## Deferred work + +- OR/NOT operators; +- custom filter capability negotiation; +- applying provider constraints to JSON-RPC requests; +- a common cross-framework user filter language; +- the CLI proposal in #8820; +- native TreeNodeFilter support in MSTest; +- public orchestration APIs; and +- sharding, batching, retry planning, and affected-test planning. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/PlatformAdapterResources.resx b/src/Adapter/MSTest.TestAdapter/Resources/PlatformAdapterResources.resx index 078c3d4338..1e8ee94ee0 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/PlatformAdapterResources.resx +++ b/src/Adapter/MSTest.TestAdapter/Resources/PlatformAdapterResources.resx @@ -157,6 +157,10 @@ Runsettings loggers are not supported by Microsoft.Testing.Platform and will be ignored Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + Runsettings datacollectors are not supported by Microsoft.Testing.Platform and will be ignored Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.cs.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.cs.xlf index 035ffb3308..84bcd044f1 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.cs.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.cs.xlf @@ -57,6 +57,11 @@ Microsoft.Testing.Platform nepodporuje protokolovače Runsettings. Bude je proto ignorovat. Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Přemostěná architektura podporuje najednou pouze jednu relaci. Relace s UID {0} je už otevřená. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.de.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.de.xlf index 1217b46e21..7750420603 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.de.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.de.xlf @@ -57,6 +57,11 @@ Runsettings-Protokollierungen werden von Microsoft.Testing.Platform nicht unterstützt und werden ignoriert. Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Das Brückenframework unterstützt jeweils nur eine Sitzung. Eine Sitzung mit UID {0} ist bereits geöffnet. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.es.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.es.xlf index 90cce87680..6c5cefcdb9 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.es.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.es.xlf @@ -57,6 +57,11 @@ Microsoft.Testing.Platform no admite los registradores de Runsettings y se omitirán Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. El marco puente solo admite una sesión a la vez. Ya hay abierta una sesión con {0} UID. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.fr.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.fr.xlf index 4607cf3c0d..7e9d20cfe7 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.fr.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.fr.xlf @@ -57,6 +57,11 @@ Les loggers Runsettings ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Le framework ponté ne prend en charge qu’une seule session à la fois. Une session avec UID {0} est déjà ouverte. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.it.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.it.xlf index 01d9f76e07..5be97e5e5a 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.it.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.it.xlf @@ -57,6 +57,11 @@ I logger Runsettings non sono supportati da Microsoft.Testing.Platform e verranno ignorati Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Il framework bridged supporta solo una sessione alla volta. Una sessione con UID {0} è già aperta. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ja.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ja.xlf index 0bfa1a9fd6..dfe0532d3f 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ja.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ja.xlf @@ -57,6 +57,11 @@ Runsettings loggers は Microsoft.Testing.Platform でサポートされていないため、無視されます Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. ブリッジ されたフレームワークは、一度に 1 つのセッションのみをサポートします。UID{0} のセッションは既に開かれています。 diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ko.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ko.xlf index e5ccb65fa2..4f787cbb7c 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ko.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ko.xlf @@ -57,6 +57,11 @@ Runsettings 로거는 Microsoft.Testing.Platform에서 지원되지 않으며 무시됩니다. Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. 브리지된 프레임워크는 한 번에 하나의 세션만 지원합니다. UID가 {0}인 세션이 이미 열려 있습니다. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.pl.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.pl.xlf index 39ab1e4b10..cd6d2cbd23 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.pl.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.pl.xlf @@ -57,6 +57,11 @@ Rejestratory Runsettings nie są obsługiwane przez element Microsoft.Testing.Platform i będą ignorowane. Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Platforma mostkowania obsługuje tylko jedną sesję naraz. Sesja z identyfikatorem UID {0} jest już otwarta. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.pt-BR.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.pt-BR.xlf index 397ca8e846..df3bae687f 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.pt-BR.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.pt-BR.xlf @@ -57,6 +57,11 @@ Os registradores de arquivos Runsettings não têm suporte no Microsoft.Testing.Platform e serão ignorados Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. A estrutura em ponte dá suporte a apenas uma sessão por vez. Uma sessão com UID {0} já está aberta. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ru.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ru.xlf index 395d984b45..36dc74c775 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ru.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.ru.xlf @@ -57,6 +57,11 @@ Средства ведения журналов Runsettings не поддерживаются в Microsoft.Testing.Platform и будут пропущены Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Интегрированная среда Bridged Framework поддерживает только один сеанс одновременно. Сеанс с UID {0} уже открыт. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.tr.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.tr.xlf index bf846758d7..6697523384 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.tr.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.tr.xlf @@ -57,6 +57,11 @@ Runsettings günlükçüleri Microsoft.Testing.Platform tarafından desteklenmiyor ve yoksayılacak Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Köprülü çerçeve aynı anda yalnızca bir oturumu destekler. UID'si {0} olan bir oturum zaten açık. diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.zh-Hans.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.zh-Hans.xlf index 34e0ef1d64..849c2278e2 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.zh-Hans.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.zh-Hans.xlf @@ -57,6 +57,11 @@ Microsoft.Testing.Platform 不支持 Runsettings 记录器,将被忽略 Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. 桥接框架一次仅支持一个会话。已打开具有 UID {0} 的会话。 diff --git a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.zh-Hant.xlf b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.zh-Hant.xlf index 8c876006e6..463d458bef 100644 --- a/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.zh-Hant.xlf +++ b/src/Adapter/MSTest.TestAdapter/Resources/xlf/PlatformAdapterResources.zh-Hant.xlf @@ -57,6 +57,11 @@ Runsettings 記錄器不受 Microsoft.Testing.Platform 支援,將會予以忽略 Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. 橋接器架構一次只支援一個工作階段。已開啟具有 UID {0} 的工作階段。 diff --git a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestFilterContext.cs b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestFilterContext.cs index 792e9ec06f..9da76e6718 100644 --- a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestFilterContext.cs +++ b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestFilterContext.cs @@ -6,6 +6,7 @@ using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Requests; using Microsoft.VisualStudio.TestPlatform.Common.Filtering; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Resources; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -26,6 +27,8 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.TestingPlatform [SuppressMessage("ApiDesign", "RS0030:Do not use banned APIs", Justification = "We can use MTP from this folder")] internal abstract class MSTestFilterContextBase { + private const string EmptyUidFilterExpression = "FullyQualifiedName=__MTP_EMPTY_UID_FILTER__&FullyQualifiedName!=__MTP_EMPTY_UID_FILTER__"; + // References the native --filter option provider's name. private const string TestCaseFilterOptionName = MSTestTestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName; @@ -101,11 +104,38 @@ private void HandleFilter(ITestExecutionFilter? filter, string? filterFromRunset AppendFilter(filterFromRunsettings, filterBuilder); AppendFilter(filterFromCommandLineOption, filterBuilder); - if (filter is TestNodeUidListFilter testNodeUidListFilter) + if (filter is not null) { - StartFilter(filterBuilder); - BuildFilter(testNodeUidListFilter.TestNodeUids, filterBuilder); - EndFilter(filterBuilder); + foreach (ITestExecutionFilter leafFilter in TestExecutionFilterComposer.GetLeafFilters(filter)) + { + switch (leafFilter) + { + case NopFilter: + break; + + case TestNodeUidListFilter testNodeUidListFilter: + StartFilter(filterBuilder); + if (testNodeUidListFilter.TestNodeUids.Length == 0) + { + filterBuilder.Append(EmptyUidFilterExpression); + } + else + { + BuildFilter(testNodeUidListFilter.TestNodeUids, filterBuilder); + } + + EndFilter(filterBuilder); + break; + + default: + throw new NotSupportedException( + string.Format( + CultureInfo.InvariantCulture, + PlatformAdapterResources.UnsupportedTestExecutionFilter, + leafFilter.GetType().FullName, + "MSTest")); + } + } } if (filterBuilder.Length > 0) diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ContextAdapterBase.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ContextAdapterBase.cs index fc9f682436..43ead93b7c 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ContextAdapterBase.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ContextAdapterBase.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Extensions.VSTestBridge.CommandLine; +using Microsoft.Testing.Extensions.VSTestBridge.Resources; using Microsoft.Testing.Platform; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions.Messages; @@ -14,6 +15,8 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel; internal abstract class ContextAdapterBase { + private const string EmptyUidFilterExpression = "FullyQualifiedName=__MTP_EMPTY_UID_FILTER__&FullyQualifiedName!=__MTP_EMPTY_UID_FILTER__"; + protected ContextAdapterBase(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter) : this(commandLineOptions, runSettings, filter, useFullyQualifiedNameAsUid: false) { @@ -96,11 +99,38 @@ private void HandleFilter(ITestExecutionFilter? filter, string? filterFromRunset AppendFilter(filterFromRunsettings, filterBuilder); AppendFilter(filterFromCommandLineOption, filterBuilder); - if (filter is TestNodeUidListFilter testNodeUidListFilter) + if (filter is not null) { - StartFilter(filterBuilder); - BuildFilter(testNodeUidListFilter.TestNodeUids, filterBuilder, useFullyQualifiedNameAsUid); - EndFilter(filterBuilder); + foreach (ITestExecutionFilter leafFilter in TestExecutionFilterComposer.GetLeafFilters(filter)) + { + switch (leafFilter) + { + case NopFilter: + break; + + case TestNodeUidListFilter testNodeUidListFilter: + StartFilter(filterBuilder); + if (testNodeUidListFilter.TestNodeUids.Length == 0) + { + filterBuilder.Append(EmptyUidFilterExpression); + } + else + { + BuildFilter(testNodeUidListFilter.TestNodeUids, filterBuilder, useFullyQualifiedNameAsUid); + } + + EndFilter(filterBuilder); + break; + + default: + throw new NotSupportedException( + string.Format( + CultureInfo.InvariantCulture, + ExtensionResources.UnsupportedTestExecutionFilter, + leafFilter.GetType().FullName, + "VSTestBridge")); + } + } } if (filterBuilder.Length > 0) diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/ExtensionResources.resx b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/ExtensionResources.resx index 078c3d4338..1e8ee94ee0 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/ExtensionResources.resx +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/ExtensionResources.resx @@ -157,6 +157,10 @@ Runsettings loggers are not supported by Microsoft.Testing.Platform and will be ignored Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + Runsettings datacollectors are not supported by Microsoft.Testing.Platform and will be ignored Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.cs.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.cs.xlf index 982617770e..5f87e1e391 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.cs.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.cs.xlf @@ -57,6 +57,11 @@ Microsoft.Testing.Platform nepodporuje protokolovače Runsettings. Bude je proto ignorovat. Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Přemostěná architektura podporuje najednou pouze jednu relaci. Relace s UID {0} je už otevřená. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.de.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.de.xlf index 8344f07dea..18df8ad162 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.de.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.de.xlf @@ -57,6 +57,11 @@ Runsettings-Protokollierungen werden von Microsoft.Testing.Platform nicht unterstützt und werden ignoriert. Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Das Brückenframework unterstützt jeweils nur eine Sitzung. Eine Sitzung mit UID {0} ist bereits geöffnet. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.es.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.es.xlf index 8f40b6f47f..f781bd9d5b 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.es.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.es.xlf @@ -57,6 +57,11 @@ Microsoft.Testing.Platform no admite los registradores de Runsettings y se omitirán Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. El marco puente solo admite una sesión a la vez. Ya hay abierta una sesión con {0} UID. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.fr.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.fr.xlf index cb7ab2caa6..0f39706415 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.fr.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.fr.xlf @@ -57,6 +57,11 @@ Les loggers Runsettings ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Le framework ponté ne prend en charge qu’une seule session à la fois. Une session avec UID {0} est déjà ouverte. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.it.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.it.xlf index 29412c709c..3410c829b4 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.it.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.it.xlf @@ -57,6 +57,11 @@ I logger Runsettings non sono supportati da Microsoft.Testing.Platform e verranno ignorati Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Il framework bridged supporta solo una sessione alla volta. Una sessione con UID {0} è già aperta. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ja.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ja.xlf index e538c49855..aef0cd1a78 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ja.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ja.xlf @@ -57,6 +57,11 @@ Runsettings loggers は Microsoft.Testing.Platform でサポートされていないため、無視されます Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. ブリッジ されたフレームワークは、一度に 1 つのセッションのみをサポートします。UID{0} のセッションは既に開かれています。 diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ko.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ko.xlf index dd7b342a54..cc9a7789c5 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ko.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ko.xlf @@ -57,6 +57,11 @@ Runsettings 로거는 Microsoft.Testing.Platform에서 지원되지 않으며 무시됩니다. Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. 브리지된 프레임워크는 한 번에 하나의 세션만 지원합니다. UID가 {0}인 세션이 이미 열려 있습니다. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.pl.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.pl.xlf index ecbc48aa95..a90fd7137a 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.pl.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.pl.xlf @@ -57,6 +57,11 @@ Rejestratory Runsettings nie są obsługiwane przez element Microsoft.Testing.Platform i będą ignorowane. Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Platforma mostkowania obsługuje tylko jedną sesję naraz. Sesja z identyfikatorem UID {0} jest już otwarta. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.pt-BR.xlf index ff490a6e8e..d1c75560da 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.pt-BR.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.pt-BR.xlf @@ -57,6 +57,11 @@ Os registradores de arquivos Runsettings não têm suporte no Microsoft.Testing.Platform e serão ignorados Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. A estrutura em ponte dá suporte a apenas uma sessão por vez. Uma sessão com UID {0} já está aberta. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ru.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ru.xlf index 438536918a..9306a4d49c 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ru.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.ru.xlf @@ -57,6 +57,11 @@ Средства ведения журналов Runsettings не поддерживаются в Microsoft.Testing.Platform и будут пропущены Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Интегрированная среда Bridged Framework поддерживает только один сеанс одновременно. Сеанс с UID {0} уже открыт. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.tr.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.tr.xlf index c164a5a8c9..aeb6ee2c83 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.tr.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.tr.xlf @@ -57,6 +57,11 @@ Runsettings günlükçüleri Microsoft.Testing.Platform tarafından desteklenmiyor ve yoksayılacak Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. Köprülü çerçeve aynı anda yalnızca bir oturumu destekler. UID'si {0} olan bir oturum zaten açık. diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.zh-Hans.xlf index 14029ddce0..55d72e0d00 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.zh-Hans.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.zh-Hans.xlf @@ -57,6 +57,11 @@ Microsoft.Testing.Platform 不支持 Runsettings 记录器,将被忽略 Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. 桥接框架一次仅支持一个会话。已打开具有 UID {0} 的会话。 diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.zh-Hant.xlf index 3063971860..1fd1fe9d13 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.zh-Hant.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Resources/xlf/ExtensionResources.zh-Hant.xlf @@ -57,6 +57,11 @@ Runsettings 記錄器不受 Microsoft.Testing.Platform 支援,將會予以忽略 Do not localize {Locked="Runsettings"} or {Locked="Microsoft.Testing.Platform"}. + + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + Test execution filter type '{0}' is not supported by '{1}'. Supported filter types are NopFilter, TestNodeUidListFilter, and an And CompositeTestExecutionFilter. + {0} is the unsupported filter type. {1} is the adapter name. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + The bridged framework supports only one session at a time. A session with UID {0} is already open. 橋接器架構一次只支援一個工作階段。已開啟具有 UID {0} 的工作階段。 diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/TestApplicationBuilderExtensions.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/TestApplicationBuilderExtensions.cs index c7c13a4de6..8819526d0e 100644 --- a/src/Platform/Microsoft.Testing.Platform/Helpers/TestApplicationBuilderExtensions.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/TestApplicationBuilderExtensions.cs @@ -4,6 +4,8 @@ using Microsoft.Testing.Platform.Builder; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Requests; +using Microsoft.Testing.Platform.TestHost; namespace Microsoft.Testing.Platform.Helpers; @@ -21,6 +23,16 @@ public static class TestApplicationBuilderExtensions public static void AddTreeNodeFilterService(this ITestApplicationBuilder testApplicationBuilder, IExtension extension) => testApplicationBuilder.CommandLine.AddProvider(() => new TreeNodeFilterCommandLineOptionsProvider(extension)); + /// + /// Registers a provider that can contribute an additional test execution filter constraint. + /// + /// The test application builder. + /// The factory method for creating the provider. + public static void AddTestExecutionFilterProvider( + this ITestApplicationBuilder builder, + Func providerFactory) + => ((TestHostManager)builder.TestHost).AddTestExecutionFilterProvider(providerFactory); + /// /// Registers the command-line options provider for '--maximum-failed-tests'. /// diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/ConsoleTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/ConsoleTestHost.cs index 1d941b4e08..5f0af5cd75 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/ConsoleTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/ConsoleTestHost.cs @@ -47,6 +47,8 @@ protected override async Task InternalRunAsync(CancellationToken cancellati // Use user provided filter factory or create console default one. ITestExecutionFilterFactory testExecutionFilterFactory = ServiceProvider.GetService() ?? new ConsoleTestExecutionFilterFactory(ServiceProvider.GetCommandLineOptions()); + ITestExecutionFilterProvider[] testExecutionFilterProviders = + [.. ServiceProvider.Services.OfType()]; // Use user provided filter factory or create console default one. ITestFrameworkInvoker testFrameworkInvoker = ServiceProvider.GetService() @@ -58,7 +60,10 @@ protected override async Task InternalRunAsync(CancellationToken cancellati { testFramework = await _buildTestFrameworkAsync(new TestFrameworkBuilderData( ServiceProvider, - new ConsoleTestExecutionRequestFactory(ServiceProvider.GetCommandLineOptions(), testExecutionFilterFactory), + new ConsoleTestExecutionRequestFactory( + ServiceProvider.GetCommandLineOptions(), + testExecutionFilterFactory, + testExecutionFilterProviders), testFrameworkInvoker, testExecutionFilterFactory, ServiceProvider.GetPlatformOutputDevice(), diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.RequestExecution.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.RequestExecution.cs index b2d20f8ad7..93c06c1336 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.RequestExecution.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.RequestExecution.cs @@ -74,9 +74,9 @@ private async Task 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. - ServerTestExecutionRequestFactory requestFactory = new(session => + // The JSON-RPC payload owns server request selection. Providers receive a server-origin + // context so they can explicitly opt out; non-empty contributions are rejected below. + ServerTestExecutionRequestFactory requestFactory = new(async (session, requestCancellationToken) => { ICollection? testNodes = args.TestNodes; string? filter = args.GraphFilter; @@ -86,11 +86,23 @@ private async Task ExecuteRequestAsync(RequestArgsBase args, s ? new TreeNodeFilter(filter) : new NopFilter(); - return method == JsonRpcMethods.TestingRunTests + TestExecutionRequestKind requestKind = method switch + { + JsonRpcMethods.TestingRunTests => TestExecutionRequestKind.Run, + JsonRpcMethods.TestingDiscoverTests => TestExecutionRequestKind.Discovery, + _ => throw new NotImplementedException($"Request not implemented '{method}'"), + }; + + executionFilter = await TestExecutionFilterComposer.ComposeAsync( + executionFilter, + [.. perRequestServiceProvider.Services.OfType()], + new TestExecutionFilterContext(requestKind, TestExecutionRequestOrigin.Server), + allowProviderContributions: false, + requestCancellationToken).ConfigureAwait(false); + + return requestKind == TestExecutionRequestKind.Run ? new RunTestExecutionRequest(session, executionFilter) - : method == JsonRpcMethods.TestingDiscoverTests - ? new DiscoverTestExecutionRequest(session, executionFilter) - : throw new NotImplementedException($"Request not implemented '{method}'"); + : new DiscoverTestExecutionRequest(session, executionFilter); }); // Build the per request objects diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.Modes.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.Modes.cs index f40c423283..f4a1ec5a39 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.Modes.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.Modes.cs @@ -226,6 +226,10 @@ private async Task BuildTestHostModeAsync(BuildContext context) #pragma warning restore CS0618 // Type or member is obsolete context.ServiceProvider.AddServices(testApplicationLifecycleCallback); + ITestExecutionFilterProvider[] testExecutionFilterProviders = + await ((TestHostManager)TestHost).BuildTestExecutionFilterProvidersAsync(context.ServiceProvider).ConfigureAwait(false); + context.ServiceProvider.AddServices(testExecutionFilterProviders); + return context.IsJsonRpcProtocol ? await BuildServerTestHostAsync(context, testControllerConnection).ConfigureAwait(false) : await BuildConsoleTestHostAsync(context, testControllerConnection).ConfigureAwait(false); diff --git a/src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt index d13b39476f..1c15ec3956 100644 --- a/src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt @@ -1,4 +1,19 @@ #nullable enable +Microsoft.Testing.Platform.Requests.ConsoleTestExecutionRequestFactory.ConsoleTestExecutionRequestFactory(Microsoft.Testing.Platform.CommandLine.ICommandLineOptions! commandLineService, Microsoft.Testing.Platform.Requests.ITestExecutionFilterFactory! testExecutionFilterFactory, System.Collections.Generic.IReadOnlyList! testExecutionFilterProviders) -> void +Microsoft.Testing.Platform.Requests.ConsoleTestExecutionRequestFactory.CreateRequestAsync(Microsoft.Testing.Platform.TestHost.TestSessionContext! session, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Testing.Platform.Requests.ITestExecutionRequestFactory.CreateRequestAsync(Microsoft.Testing.Platform.TestHost.TestSessionContext! session, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Testing.Platform.Requests.ServerTestExecutionRequestFactory.CreateRequestAsync(Microsoft.Testing.Platform.TestHost.TestSessionContext! session, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Testing.Platform.Requests.ServerTestExecutionRequestFactory.ServerTestExecutionRequestFactory(System.Func!>! factory) -> void +Microsoft.Testing.Platform.Requests.TestExecutionFilterComposer +Microsoft.Testing.Platform.TestHost.TestHostManager.AddTestExecutionFilterProvider(System.Func! providerFactory) -> void +Microsoft.Testing.Platform.TestHost.TestHostManager.BuildTestExecutionFilterProvidersAsync(Microsoft.Testing.Platform.Services.ServiceProvider! serviceProvider) -> System.Threading.Tasks.Task! +static Microsoft.Testing.Platform.Requests.TestExecutionFilterComposer.ComposeAsync(Microsoft.Testing.Platform.Requests.ITestExecutionFilter! requestFilter, System.Collections.Generic.IReadOnlyList! providers, Microsoft.Testing.Platform.Requests.TestExecutionFilterContext! context, bool allowProviderContributions, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +static Microsoft.Testing.Platform.Requests.TestExecutionFilterComposer.GetLeafFilters(Microsoft.Testing.Platform.Requests.ITestExecutionFilter! filter) -> System.Collections.Generic.IEnumerable! +*REMOVED*Microsoft.Testing.Platform.Requests.ConsoleTestExecutionRequestFactory.ConsoleTestExecutionRequestFactory(Microsoft.Testing.Platform.CommandLine.ICommandLineOptions! commandLineService, Microsoft.Testing.Platform.Requests.ITestExecutionFilterFactory! testExecutionFilterFactory) -> void +*REMOVED*Microsoft.Testing.Platform.Requests.ConsoleTestExecutionRequestFactory.CreateRequestAsync(Microsoft.Testing.Platform.TestHost.TestSessionContext! session) -> System.Threading.Tasks.Task! +*REMOVED*Microsoft.Testing.Platform.Requests.ITestExecutionRequestFactory.CreateRequestAsync(Microsoft.Testing.Platform.TestHost.TestSessionContext! session) -> System.Threading.Tasks.Task! +*REMOVED*Microsoft.Testing.Platform.Requests.ServerTestExecutionRequestFactory.CreateRequestAsync(Microsoft.Testing.Platform.TestHost.TestSessionContext! session) -> System.Threading.Tasks.Task! +*REMOVED*Microsoft.Testing.Platform.Requests.ServerTestExecutionRequestFactory.ServerTestExecutionRequestFactory(System.Func! factory) -> void Microsoft.Testing.Platform.Extensions.ArtifactPostProcessing.ArtifactPostProcessingHandshakeProperties Microsoft.Testing.Platform.Extensions.ArtifactPostProcessing.ArtifactPostProcessingManager Microsoft.Testing.Platform.Extensions.ArtifactPostProcessing.ArtifactPostProcessingManager.AddArtifactPostProcessor(System.Func! factory) -> void diff --git a/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt index c1e4b03f6f..c9605b9ab2 100644 --- a/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,23 @@ #nullable enable +[TPEXP]Microsoft.Testing.Platform.Requests.CompositeTestExecutionFilter +[TPEXP]Microsoft.Testing.Platform.Requests.CompositeTestExecutionFilter.CompositeTestExecutionFilter(Microsoft.Testing.Platform.Requests.TestExecutionFilterOperator operator, params Microsoft.Testing.Platform.Requests.ITestExecutionFilter![]! filters) -> void +[TPEXP]Microsoft.Testing.Platform.Requests.CompositeTestExecutionFilter.Filters.get -> System.Collections.Generic.IReadOnlyList! +[TPEXP]Microsoft.Testing.Platform.Requests.CompositeTestExecutionFilter.Operator.get -> Microsoft.Testing.Platform.Requests.TestExecutionFilterOperator +[TPEXP]Microsoft.Testing.Platform.Requests.ITestExecutionFilterProvider +[TPEXP]Microsoft.Testing.Platform.Requests.ITestExecutionFilterProvider.GetFilterAsync(Microsoft.Testing.Platform.Requests.TestExecutionFilterContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionFilterContext +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionFilterContext.TestExecutionFilterContext(Microsoft.Testing.Platform.Requests.TestExecutionRequestKind requestKind, Microsoft.Testing.Platform.Requests.TestExecutionRequestOrigin origin) -> void +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionFilterContext.Origin.get -> Microsoft.Testing.Platform.Requests.TestExecutionRequestOrigin +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionFilterContext.RequestKind.get -> Microsoft.Testing.Platform.Requests.TestExecutionRequestKind +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionFilterOperator +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionFilterOperator.And = 0 -> Microsoft.Testing.Platform.Requests.TestExecutionFilterOperator +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionRequestKind +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionRequestKind.Discovery = 0 -> Microsoft.Testing.Platform.Requests.TestExecutionRequestKind +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionRequestKind.Run = 1 -> Microsoft.Testing.Platform.Requests.TestExecutionRequestKind +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionRequestOrigin +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionRequestOrigin.Console = 0 -> Microsoft.Testing.Platform.Requests.TestExecutionRequestOrigin +[TPEXP]Microsoft.Testing.Platform.Requests.TestExecutionRequestOrigin.Server = 1 -> Microsoft.Testing.Platform.Requests.TestExecutionRequestOrigin +[TPEXP]static Microsoft.Testing.Platform.Helpers.TestApplicationBuilderExtensions.AddTestExecutionFilterProvider(this Microsoft.Testing.Platform.Builder.ITestApplicationBuilder! builder, System.Func! providerFactory) -> void [TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities [TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities.IsStateful.get -> bool [TPEXP]Microsoft.Testing.Platform.Services.IClientInfo.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities! diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/CompositeTestExecutionFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/CompositeTestExecutionFilter.cs new file mode 100644 index 0000000000..ffd73d1b1a --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Requests/CompositeTestExecutionFilter.cs @@ -0,0 +1,53 @@ +// 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.Resources; + +namespace Microsoft.Testing.Platform.Requests; + +/// +/// Represents an explicit composition of test execution filters. +/// +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] +public sealed class CompositeTestExecutionFilter : ITestExecutionFilter +{ + /// + /// Initializes a new instance of the class. + /// + /// The operator used to combine the child filters. + /// The child filters. + public CompositeTestExecutionFilter(TestExecutionFilterOperator @operator, params ITestExecutionFilter[] filters) + { + if (@operator != TestExecutionFilterOperator.And) + { + throw new ArgumentOutOfRangeException(nameof(@operator), @operator, PlatformResources.UnsupportedTestExecutionFilterOperator); + } + + _ = filters ?? throw new ArgumentNullException(nameof(filters)); + if (filters.Length < 2) + { + throw new ArgumentException(PlatformResources.CompositeTestExecutionFilterRequiresTwoFilters, nameof(filters)); + } + + for (int i = 0; i < filters.Length; i++) + { + if (filters[i] is null) + { + throw new ArgumentException(PlatformResources.CompositeTestExecutionFilterCannotContainNull, nameof(filters)); + } + } + + Operator = @operator; + Filters = Array.AsReadOnly([.. filters]); + } + + /// + /// Gets the operator used to combine the child filters. + /// + public TestExecutionFilterOperator Operator { get; } + + /// + /// Gets the child filters. + /// + public IReadOnlyList Filters { get; } +} diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/ConsoleTestExecutionRequestFactory.cs b/src/Platform/Microsoft.Testing.Platform/Requests/ConsoleTestExecutionRequestFactory.cs index b31feb6d14..f65a69b55c 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/ConsoleTestExecutionRequestFactory.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/ConsoleTestExecutionRequestFactory.cs @@ -8,12 +8,16 @@ namespace Microsoft.Testing.Platform.Requests; -internal sealed class ConsoleTestExecutionRequestFactory(ICommandLineOptions commandLineService, ITestExecutionFilterFactory testExecutionFilterFactory) : ITestExecutionRequestFactory +internal sealed class ConsoleTestExecutionRequestFactory( + ICommandLineOptions commandLineService, + ITestExecutionFilterFactory testExecutionFilterFactory, + IReadOnlyList testExecutionFilterProviders) : ITestExecutionRequestFactory { private readonly ICommandLineOptions _commandLineService = commandLineService; private readonly ITestExecutionFilterFactory _testExecutionFilterFactory = testExecutionFilterFactory; + private readonly IReadOnlyList _testExecutionFilterProviders = testExecutionFilterProviders; - public async Task CreateRequestAsync(TestSessionContext session) + public async Task CreateRequestAsync(TestSessionContext session, CancellationToken cancellationToken) { (bool created, ITestExecutionFilter? testExecutionFilter) = await _testExecutionFilterFactory.TryCreateAsync().ConfigureAwait(false); if (!created) @@ -23,7 +27,18 @@ public async Task CreateRequestAsync(TestSessionContext se ApplicationStateGuard.Ensure(testExecutionFilter is not null); - TestExecutionRequest testExecutionRequest = _commandLineService.IsOptionSet(PlatformCommandLineProvider.DiscoverTestsOptionKey) + bool isDiscovery = _commandLineService.IsOptionSet(PlatformCommandLineProvider.DiscoverTestsOptionKey); + var context = new TestExecutionFilterContext( + isDiscovery ? TestExecutionRequestKind.Discovery : TestExecutionRequestKind.Run, + TestExecutionRequestOrigin.Console); + testExecutionFilter = await TestExecutionFilterComposer.ComposeAsync( + testExecutionFilter, + _testExecutionFilterProviders, + context, + allowProviderContributions: true, + cancellationToken).ConfigureAwait(false); + + TestExecutionRequest testExecutionRequest = isDiscovery ? new DiscoverTestExecutionRequest(session, testExecutionFilter) : new RunTestExecutionRequest(session, testExecutionFilter); diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/ITestExecutionFilterProvider.cs b/src/Platform/Microsoft.Testing.Platform/Requests/ITestExecutionFilterProvider.cs new file mode 100644 index 0000000000..8d284785b0 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Requests/ITestExecutionFilterProvider.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Provides an additional constraint for a test execution request. +/// +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] +public interface ITestExecutionFilterProvider : IExtension +{ + /// + /// Gets the filter constraint for the current request. + /// + /// The test execution filter context. + /// The cancellation token for the request. + /// + /// A task whose result is the filter constraint, or when the provider does + /// not constrain the current request. + /// + /// + /// Provider constraints are not currently applied to JSON-RPC server requests. Return + /// when is + /// . + /// + Task GetFilterAsync(TestExecutionFilterContext context, CancellationToken cancellationToken); +} diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/ITestExecutionRequestFactory.cs b/src/Platform/Microsoft.Testing.Platform/Requests/ITestExecutionRequestFactory.cs index de3b965854..a963beeb44 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/ITestExecutionRequestFactory.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/ITestExecutionRequestFactory.cs @@ -7,5 +7,5 @@ namespace Microsoft.Testing.Platform.Requests; internal interface ITestExecutionRequestFactory { - Task CreateRequestAsync(TestSessionContext session); + Task CreateRequestAsync(TestSessionContext session, CancellationToken cancellationToken); } diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/ServerTestExecutionRequestFactory.cs b/src/Platform/Microsoft.Testing.Platform/Requests/ServerTestExecutionRequestFactory.cs index 7ee4d87129..b406d11585 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/ServerTestExecutionRequestFactory.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/ServerTestExecutionRequestFactory.cs @@ -5,10 +5,10 @@ namespace Microsoft.Testing.Platform.Requests; -internal sealed class ServerTestExecutionRequestFactory(Func factory) : ITestExecutionRequestFactory +internal sealed class ServerTestExecutionRequestFactory(Func> factory) : ITestExecutionRequestFactory { - private readonly Func _factory = factory; + private readonly Func> _factory = factory; - public Task CreateRequestAsync(TestSessionContext session) - => Task.FromResult(_factory(session)); + public Task CreateRequestAsync(TestSessionContext session, CancellationToken cancellationToken) + => _factory(session, cancellationToken); } diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterComposer.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterComposer.cs new file mode 100644 index 0000000000..181380c344 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterComposer.cs @@ -0,0 +1,157 @@ +// 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; +using Microsoft.Testing.Platform.Resources; + +namespace Microsoft.Testing.Platform.Requests; + +internal static class TestExecutionFilterComposer +{ + public static async Task ComposeAsync( + ITestExecutionFilter requestFilter, + IReadOnlyList providers, + TestExecutionFilterContext context, + bool allowProviderContributions, + CancellationToken cancellationToken) + { + List uidFilters = []; + List otherFilters = []; + AddConstraints(requestFilter, providerUid: null, uidFilters, otherFilters); + + foreach (ITestExecutionFilterProvider provider in providers) + { + cancellationToken.ThrowIfCancellationRequested(); + ITestExecutionFilter? providerFilter = await provider.GetFilterAsync(context, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + if (providerFilter is null or NopFilter) + { + continue; + } + + if (!allowProviderContributions) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + PlatformResources.TestExecutionFilterProviderNotSupportedForServer, + provider.Uid, + providerFilter.GetType().FullName)); + } + + AddConstraints(providerFilter, provider.Uid, uidFilters, otherFilters); + } + + if (uidFilters.Count > 0) + { + HashSet? intersection = null; + foreach (TestNodeUidListFilter uidFilter in uidFilters.OrderBy(filter => filter.TestNodeUids.Length)) + { + var currentUids = new HashSet( + uidFilter.TestNodeUids.Select(uid => uid.Value), + StringComparer.Ordinal); + + if (intersection is null) + { + intersection = currentUids; + } + else + { + intersection.IntersectWith(currentUids); + } + + if (intersection.Count == 0) + { + break; + } + } + + RoslynDebug.Assert(intersection is not null); + otherFilters.Add(new TestNodeUidListFilter( + [.. intersection.OrderBy(uid => uid, StringComparer.Ordinal).Select(uid => new TestNodeUid(uid))])); + } + + return otherFilters.Count switch + { + 0 => new NopFilter(), + 1 => otherFilters[0], + _ => new CompositeTestExecutionFilter(TestExecutionFilterOperator.And, [.. otherFilters]), + }; + } + + internal static IEnumerable GetLeafFilters(ITestExecutionFilter filter) + { + if (filter is not CompositeTestExecutionFilter composite) + { + yield return filter; + yield break; + } + + if (composite.Operator != TestExecutionFilterOperator.And) + { + throw new NotSupportedException( + string.Format( + CultureInfo.InvariantCulture, + PlatformResources.UnsupportedTestExecutionFilterOperatorValue, + composite.Operator)); + } + + foreach (ITestExecutionFilter childFilter in composite.Filters) + { + foreach (ITestExecutionFilter leafFilter in GetLeafFilters(childFilter)) + { + yield return leafFilter; + } + } + } + + private static void AddConstraints( + ITestExecutionFilter filter, + string? providerUid, + List uidFilters, + List otherFilters) + { + switch (filter) + { + case NopFilter: + return; + + case TestNodeUidListFilter uidFilter: + uidFilters.Add(uidFilter); + return; + + case TreeNodeFilter treeNodeFilter: + otherFilters.Add(treeNodeFilter); + return; + + case CompositeTestExecutionFilter composite when composite.Operator == TestExecutionFilterOperator.And: + foreach (ITestExecutionFilter childFilter in composite.Filters) + { + AddConstraints(childFilter, providerUid, uidFilters, otherFilters); + } + + return; + + case CompositeTestExecutionFilter composite: + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + PlatformResources.UnsupportedTestExecutionFilterOperatorValue, + composite.Operator)); + + default: + throw providerUid is null + ? new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + PlatformResources.UnsupportedRequestTestExecutionFilter, + filter.GetType().FullName)) + : new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + PlatformResources.UnsupportedProviderTestExecutionFilter, + providerUid, + filter.GetType().FullName)); + } + } +} diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterContext.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterContext.cs new file mode 100644 index 0000000000..039163507f --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterContext.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.Requests; + +/// +/// Describes the request for which a test execution filter provider is being evaluated. +/// +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] +public sealed class TestExecutionFilterContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The request kind. + /// The request origin. + public TestExecutionFilterContext(TestExecutionRequestKind requestKind, TestExecutionRequestOrigin origin) + { + RequestKind = requestKind; + Origin = origin; + } + + /// + /// Gets the request kind. + /// + public TestExecutionRequestKind RequestKind { get; } + + /// + /// Gets the request origin. + /// + public TestExecutionRequestOrigin Origin { get; } +} diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterOperator.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterOperator.cs new file mode 100644 index 0000000000..a935e1a31c --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterOperator.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.Requests; + +/// +/// Specifies how child test execution filters are combined. +/// +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] +public enum TestExecutionFilterOperator +{ + /// + /// Every child filter must match. + /// + And, +} diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionRequestKind.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionRequestKind.cs new file mode 100644 index 0000000000..85f855e7df --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionRequestKind.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.Requests; + +/// +/// Specifies the kind of test execution request. +/// +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] +public enum TestExecutionRequestKind +{ + /// + /// A test discovery request. + /// + Discovery, + + /// + /// A test run request. + /// + Run, +} diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionRequestOrigin.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionRequestOrigin.cs new file mode 100644 index 0000000000..bdbf810ea9 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionRequestOrigin.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.Requests; + +/// +/// Specifies where a test execution request originated. +/// +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] +public enum TestExecutionRequestOrigin +{ + /// + /// A request created by the console test host. + /// + Console, + + /// + /// A request received by the JSON-RPC server. + /// + Server, +} diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs index 248877eb82..606e73d521 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs @@ -76,7 +76,7 @@ public async Task ExecuteAsync(ITestFramework testFramework, ClientInfo client, using (otelService?.StartActivity("CreateTestRequest", tags: [new("SessionUid", sessionId)])) { ITestExecutionRequestFactory testExecutionRequestFactory = ServiceProvider.GetTestExecutionRequestFactory(); - request = await testExecutionRequestFactory.CreateRequestAsync(new(sessionId)).ConfigureAwait(false); + request = await testExecutionRequestFactory.CreateRequestAsync(new(sessionId), cancellationToken).ConfigureAwait(false); } IMessageBus messageBus = ServiceProvider.GetMessageBus(); diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TestNodeUidListFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TestNodeUidListFilter.cs index 43ab5940cc..4ad90df0bc 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TestNodeUidListFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TestNodeUidListFilter.cs @@ -13,7 +13,7 @@ public sealed class TestNodeUidListFilter : ITestExecutionFilter /// /// Initializes a new instance of the class. /// - /// The test node UIDs to filter. + /// The test node UIDs to filter. An empty array selects no tests. public TestNodeUidListFilter(TestNodeUid[] testNodeUids) => TestNodeUids = testNodeUids; /// diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx index f3995b2dce..0bb15604a3 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx +++ b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx @@ -179,6 +179,32 @@ Failed to create a test execution filter + + A composite test execution filter cannot contain a null child filter. + + + A composite test execution filter requires at least two child filters. + + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + The same instance of 'CompositeExtensionFactory' is already registered {Locked="CompositeExtensionFactory"} diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf index 193779c614..0343727262 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf @@ -257,6 +257,16 @@ Už je zaregistrovaná stejná instance CompositeExtensionFactory. {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. Nebyl nalezen konfigurační soubor {0} zadaný pomocí parametru --config-file. @@ -1035,6 +1045,11 @@ Přečtěte si další informace o telemetrii Microsoft Testing Platform: https: Souhrn zjišťování testů: nalezené testy: {0} v(e) {1} sestaveních. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object Pro tento objekt proxy neměla být volána metoda {0}. @@ -1132,6 +1147,26 @@ Přečtěte si další informace o telemetrii Microsoft Testing Platform: https: [ServerTestHost.OnTaskSchedulerUnobservedTaskException] Neošetřená výjimka: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. Čekání na připojení ladicího programu (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) se v prostředích prohlížeče nepodporuje. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf index a6a9db2efb..78bf2fafca 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf @@ -257,6 +257,16 @@ Die gleiche Instanz von „CompositeExtensionFactory“ ist bereits registriert {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. Die mit „--config-file“ angegebene Konfigurationsdatei „{0}“ wurde nicht gefunden. @@ -1035,6 +1045,11 @@ Weitere Informationen zu Microsoft Testing Platform-Telemetriedaten: https://aka Zusammenfassung der Testermittlung: {0} Test(s) in {1} Assemblys gefunden. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object Die Methode "{0}" hätte für dieses Proxyobjekt nicht aufgerufen werden dürfen @@ -1132,6 +1147,26 @@ Weitere Informationen zu Microsoft Testing Platform-Telemetriedaten: https://aka [ServerTestHost.OnTaskSchedulerUnobservedTaskException] Ausnahmefehler: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. Das Warten auf das Anfügen des Debuggers (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) wird in Browserumgebungen nicht unterstützt. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf index ad2ca0f806..daf2ce588e 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf @@ -257,6 +257,16 @@ La misma instancia de "CompositeExtensionFactory" ya está registrada {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. No se ha encontrado el archivo de configuración "{0}" especificado con "--config-file". @@ -1035,6 +1045,11 @@ Más información sobre la telemetría de la Plataforma de pruebas de Microsoft: Resumen de detección de pruebas: se encontraron {0} pruebas en ensamblados {1}. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object No se debería haber llamado al método "{0}" en este objeto proxy @@ -1132,6 +1147,26 @@ Más información sobre la telemetría de la Plataforma de pruebas de Microsoft: [ServerTestHost.OnTaskSchedulerUnobservedTaskException] excepción no controlada: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. La espera para que se adjunte el depurador (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) no es compatible con los entornos de explorador. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf index 23004406fc..3a9f5b2405 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf @@ -257,6 +257,16 @@ La même instance de 'CompositeExtensionFactory' est déjà enregistrée {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. Nous n’avons pas pu trouver le fichier de configuration « {0} » spécifié avec « --config-file ». @@ -1035,6 +1045,11 @@ En savoir plus sur la télémétrie de la plateforme de tests Microsoft : https: Résumé de la découverte de tests : {0} test(s) trouvé(s) dans {1} assemblys. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object La méthode « {0} » n’aurait pas dû être appelée sur cet objet proxy @@ -1132,6 +1147,26 @@ En savoir plus sur la télémétrie de la plateforme de tests Microsoft : https: [ServerTestHost.OnTaskSchedulerUnobservedTaskException] exception non prise en charge : {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. L’attente de la connexion du débogueur (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) n’est pas prise en charge dans les environnements navigateur. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf index ebcddba59e..fdf7c52b8b 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf @@ -257,6 +257,16 @@ L'istanza di 'CompositeExtensionFactory' specificata è già registrata {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. Impossibile trovare il file di configurazione '{0}' specificato con '--config-file'. @@ -1035,6 +1045,11 @@ Altre informazioni sulla telemetria della piattaforma di test Microsoft: https:/ Riepilogo individuazione test: trovati {0} test negli assembly {1}. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object Il metodo '{0}' non avrebbe dovuto essere chiamato su questo oggetto proxy @@ -1132,6 +1147,26 @@ Altre informazioni sulla telemetria della piattaforma di test Microsoft: https:/ [ServerTestHost.OnTaskSchedulerUnobservedTaskException] eccezione non gestita: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. L'attesa per il collegamento di (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) non è supportata negli ambienti browser. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf index 20a909ae98..e13850591e 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf @@ -257,6 +257,16 @@ 'CompositeExtensionFactory' の同じインスタンスが既に登録されています {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. '--config-file' で指定された構成ファイル '{0}' が見つかりませんでした。 @@ -1036,6 +1046,11 @@ Microsoft Testing Platform テレメトリの詳細: https://aka.ms/testingplatf テスト検出の要約: {1} 個のアセンブリで {0} 個のテストが見つかりました。 {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object このプロキシ オブジェクトでメソッド '{0}' を呼び出すべきではありませんでした @@ -1133,6 +1148,26 @@ Microsoft Testing Platform テレメトリの詳細: https://aka.ms/testingplatf ハンドルされない例外 [ServerTestHost.OnTaskSchedulerUnobservedTaskException]: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. デバッガーのアタッチの待機 (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) は、ブラウザー環境ではサポートされていません。 diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf index 05c3a83748..4b0b58a482 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf @@ -257,6 +257,16 @@ 동일한 'CompositeExtensionFactory' 인스턴스가 이미 등록됨 {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. '--config-file'으로 지정된 구성 파일 '{0}' 찾을 수 없습니다. @@ -1035,6 +1045,11 @@ Microsoft 테스트 플랫폼 원격 분석에 대해 자세히 알아보기: ht 테스트 검색 요약: {1}개의 어셈블리에서 {0}개의 테스트를 찾았습니다. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object 이 프록시 개체에서 '{0}' 메서드를 호출하지 않아야 합니다. @@ -1132,6 +1147,26 @@ Microsoft 테스트 플랫폼 원격 분석에 대해 자세히 알아보기: ht 처리되지 않은 예외 [ServerTestHost.OnTaskSchedulerUnobservedTaskException]: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. 디버거 연결 대기(TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1)는 브라우저 환경에서 지원되지 않습니다. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf index e760ace9a7..5f51526ddd 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf @@ -257,6 +257,16 @@ To samo wystąpienie elementu „CompositeExtensionFactory” jest już zarejestrowane {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. Nie można odnaleźć pliku konfiguracji „{0}” określonego za pomocą parametru „--config-file”. @@ -1035,6 +1045,11 @@ Więcej informacji o telemetrii platformy testowej firmy Microsoft: https://aka. Podsumowanie odnajdywania testów: znaleziono testy ({0}) w {1} zestawach. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object Metoda „{0}” nie powinna być wywoływana w tym obiekcie proxy @@ -1132,6 +1147,26 @@ Więcej informacji o telemetrii platformy testowej firmy Microsoft: https://aka. [ServerTestHost.OnTaskSchedulerUnobservedTaskException] Nieobsługiwany wyjątek: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. Oczekiwanie na dołączenie debugera (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) nie jest obsługiwane w środowiskach przeglądarki. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf index 58f1ffe7a7..35d49c7431 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf @@ -257,6 +257,16 @@ A mesma instância de 'CompositeExtensionFactory' já está registrada {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. Não foi possível encontrar o arquivo de configuração '{0}' especificado com '--config-file'. @@ -1035,6 +1045,11 @@ Leia mais sobre a telemetria da Plataforma de Testes da Microsoft: https://aka.m Resumo da descoberta de teste: encontrado {0} teste(s) em {1} assemblies. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object O método ''{0}'' não deve ter sido chamado neste objeto proxy @@ -1132,6 +1147,26 @@ Leia mais sobre a telemetria da Plataforma de Testes da Microsoft: https://aka.m [ServerTestHost.OnTaskSchedulerUnobservedTaskException] exceção sem tratamento: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. Não há suporte para aguardar a anexação do depurador (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) em ambientes de navegador. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf index c28d15635a..58551c3e17 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf @@ -257,6 +257,16 @@ Такой же экземпляр "CompositeExtensionFactory" уже зарегистрирован {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. Не удалось найти файл конфигурации "{0}", указанный с параметром "--config-file". @@ -1035,6 +1045,11 @@ Read more about Microsoft Testing Platform telemetry: https://aka.ms/testingplat Сводка по обнаружению тестов: найдены тесты ({0}) в {1} сборках. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object Метод "{0}" не должен был вызываться для этого прокси-объекта @@ -1132,6 +1147,26 @@ Read more about Microsoft Testing Platform telemetry: https://aka.ms/testingplat [ServerTestHost.OnTaskSchedulerUnobservedTaskException] необработанное исключение: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. Ожидание подключения отладчика (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) не поддерживается в браузерных средах. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf index 8176ff2acd..25afe94b69 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf @@ -257,6 +257,16 @@ Aynı 'CompositeExtensionFactory' örneği zaten kayıtlı {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. '--config-file' ile belirtilen '{0}' yapılandırma dosyası bulunamadı. @@ -1035,6 +1045,11 @@ Microsoft Test Platformu telemetrisi hakkında daha fazla bilgi edinin: https:// Test bulma özeti: {1} derlemede {0} test bulundu. {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object '{0}' yöntemi bu ara sunucu nesnesinde çağrılmamalıdır @@ -1132,6 +1147,26 @@ Microsoft Test Platformu telemetrisi hakkında daha fazla bilgi edinin: https:// [ServerTestHost.OnTaskSchedulerUnobservedTaskException] özel durum: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. Hata ayıklayıcısının eklenmesini bekleme (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) tarayıcı ortamlarında desteklenmez. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf index bd0ff021fd..89c40322c6 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf @@ -257,6 +257,16 @@ "CompositeExtensionFactory" 的同一实例已注册 {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. 找不到使用 ‘--config-file’ 指定的配置文件“{0}”。 @@ -1035,6 +1045,11 @@ Microsoft 测试平台会收集使用数据,帮助我们改善体验。该数 测试发现摘要: 在 {1} 个程序集中找到 {0} 个测试。 {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object 不应对此代理对象调用方法 '{0}' @@ -1132,6 +1147,26 @@ Microsoft 测试平台会收集使用数据,帮助我们改善体验。该数 [ServerTestHost.OnTaskSchedulerUnobservedTaskException] 未经处理的异常: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. 浏览器环境不支持等待调试器附加(TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1)。 diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf index 0e021c5fcd..af7778e80b 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf @@ -257,6 +257,16 @@ 已註冊 'CompositeExtensionFactory' 的同一執行個體 {Locked="CompositeExtensionFactory"} + + A composite test execution filter cannot contain a null child filter. + A composite test execution filter cannot contain a null child filter. + + + + A composite test execution filter requires at least two child filters. + A composite test execution filter requires at least two child filters. + + The configuration file '{0}' specified with '--config-file' could not be found. 找不到以 '--config-file' 指定的設定檔 '{0}'。 @@ -1035,6 +1045,11 @@ Microsoft 測試平台會收集使用量資料,以協助我們改善您的體 測試探索摘要: 在 {1} 個組件中找到 {0} 個測試。 {0} is the number of discovered tests. {1} is the count of assemblies. + + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + Test execution filter provider '{0}' returned filter type '{1}' for a JSON-RPC server request. Provider constraints are not applied to server requests; return null when TestExecutionFilterContext.Origin is Server. + {0} is the provider UID. {1} is the filter type. {Locked="JSON-RPC"}{Locked="null"}{Locked="TestExecutionFilterContext.Origin"}{Locked="Server"} + Method '{0}' should not have been called on this proxy object 不應在此 Proxy 物件上呼叫方法 '{0}'。 @@ -1132,6 +1147,26 @@ Microsoft 測試平台會收集使用量資料,以協助我們改善您的體 [ServerTestHost.OnTaskSchedulerUnobservedTaskException] 未處理的例外狀況: {0} {0} is the exception that was unhandled/unobserved. {Locked="ServerTestHost.OnTaskSchedulerUnobservedTaskException"} + + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + Test execution filter provider '{0}' returned unsupported filter type '{1}'. Providers can return only NopFilter, TestNodeUidListFilter, TreeNodeFilter, or an And CompositeTestExecutionFilter. + {0} is the provider UID. {1} is the unsupported filter type. {Locked="NopFilter"}{Locked="TestNodeUidListFilter"}{Locked="TreeNodeFilter"}{Locked="And"}{Locked="CompositeTestExecutionFilter"} + + + The request contains unsupported test execution filter type '{0}'. + The request contains unsupported test execution filter type '{0}'. + {0} is the unsupported filter type. + + + Only the And test execution filter operator is supported. + Only the And test execution filter operator is supported. + {Locked="And"} + + + Test execution filter operator '{0}' is not supported. Only And is supported. + Test execution filter operator '{0}' is not supported. Only And is supported. + {0} is the unsupported operator value. {Locked="And"} + Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. 瀏覽器環境不支援等待除錯器附加 (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1)。 diff --git a/src/Platform/Microsoft.Testing.Platform/TestHost/TestHostManager.cs b/src/Platform/Microsoft.Testing.Platform/TestHost/TestHostManager.cs index fd2e3c93cf..1684728426 100644 --- a/src/Platform/Microsoft.Testing.Platform/TestHost/TestHostManager.cs +++ b/src/Platform/Microsoft.Testing.Platform/TestHost/TestHostManager.cs @@ -21,6 +21,7 @@ internal sealed class TestHostManager : ITestHostManager private readonly List> _testApplicationLifecycleCallbacksFactories = []; private readonly List> _dataConsumerFactories = []; private readonly List> _testSessionLifetimeHandlerFactories = []; + private readonly List> _testExecutionFilterProviderFactories = []; private readonly List _dataConsumersCompositeServiceFactories = []; private readonly List _testSessionLifetimeHandlerCompositeFactories = []; @@ -54,6 +55,20 @@ public void AddTestExecutionFilterFactory(Func> TryBuildTestExecutionFilterFactoryAsync(ServiceProvider serviceProvider) => TryBuildSingletonExtensionAsync(_testExecutionFilterFactory, serviceProvider); + internal void AddTestExecutionFilterProvider(Func providerFactory) + => _testExecutionFilterProviderFactories.Add(providerFactory ?? throw new ArgumentNullException(nameof(providerFactory))); + + internal async Task BuildTestExecutionFilterProvidersAsync(ServiceProvider serviceProvider) + { + List providers = []; + await ExtensionBuilderHelper.BuildAndRegisterExtensionsAsync( + _testExecutionFilterProviderFactories, + serviceProvider, + providers).ConfigureAwait(false); + + return [.. providers]; + } + private static async Task> TryBuildSingletonExtensionAsync(Func? factory, ServiceProvider serviceProvider) where T : class, IExtension { diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionTests.cs index fe14840d44..183c915c53 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionTests.cs @@ -7,6 +7,8 @@ namespace Microsoft.Testing.Platform.Acceptance.IntegrationTests; public class ExecutionTests : AcceptanceTestBase { private const string AssetName = "ExecutionTests"; + private const string FilterProviderAEnvironmentVariable = "MTP_TEST_FILTER_PROVIDER_A"; + private const string FilterProviderBEnvironmentVariable = "MTP_TEST_FILTER_PROVIDER_B"; [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] [TestMethod] @@ -195,6 +197,101 @@ public async Task Exec_WhenListTestsAndMinimumExpectedTestsAreSpecified_Discover testHostResult.AssertOutputContains("Error: '--list-tests' and '--minimum-expected-tests' are incompatible options"); } + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + [TestMethod] + public async Task Exec_WhenFilterProviderIsEnabled_OnlyContributedUidRuns(string tfm) + { + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); + + TestHostResult testHostResult = await testHost.ExecuteAsync( + environmentVariables: new Dictionary + { + [FilterProviderAEnvironmentVariable] = "0", + }, + cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCode.Success); + testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); + } + + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + [TestMethod] + public async Task Exec_WhenTwoProvidersContributeDisjointUids_RunsNoTests(string tfm) + { + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); + + TestHostResult testHostResult = await testHost.ExecuteAsync( + environmentVariables: new Dictionary + { + [FilterProviderAEnvironmentVariable] = "0", + [FilterProviderBEnvironmentVariable] = "1", + }, + cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); + } + + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + [TestMethod] + public async Task Exec_WhenBuiltInAndProviderUidFiltersAreSpecified_UsesIntersection(string tfm) + { + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); + + TestHostResult testHostResult = await testHost.ExecuteAsync( + "--filter-uid 0 --filter-uid 1", + environmentVariables: new Dictionary + { + [FilterProviderAEnvironmentVariable] = "1", + }, + cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCode.Success); + testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); + } + + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + [TestMethod] + public async Task Exec_WhenProviderOnlyConstrainsRun_DiscoveryRemainsUnfiltered(string tfm) + { + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); + + TestHostResult discoveryResult = await testHost.ExecuteAsync( + "--list-tests", + environmentVariables: new Dictionary + { + [FilterProviderAEnvironmentVariable] = "run:0", + }, + cancellationToken: TestContext.CancellationToken); + TestHostResult runResult = await testHost.ExecuteAsync( + environmentVariables: new Dictionary + { + [FilterProviderAEnvironmentVariable] = "run:0", + }, + cancellationToken: TestContext.CancellationToken); + + discoveryResult.AssertExitCodeIs(ExitCode.Success); + discoveryResult.AssertOutputMatchesRegex(@"Test discovery summary: found 2 test\(s\)"); + runResult.AssertExitCodeIs(ExitCode.Success); + runResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); + } + + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + [TestMethod] + public async Task Exec_WhenTreeAndProviderConstraintsAreDisjoint_RunsNoTests(string tfm) + { + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); + + TestHostResult testHostResult = await testHost.ExecuteAsync( + "--treenode-filter \"\"", + environmentVariables: new Dictionary + { + [FilterProviderAEnvironmentVariable] = "1", + }, + cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); + } + public sealed class TestAssetFixture() : TestAssetFixtureBase() { private const string TestCode = """ @@ -232,8 +329,10 @@ public static async Task Main(string[] args) MyExtension myExtension = new(); builder.RegisterTestFramework( sp => new TestFrameworkCapabilities(), - (_,sp) => new DummyTestFramework(sp, myExtension)); + (_, _) => new DummyTestFramework(myExtension)); builder.AddTreeNodeFilterService(myExtension); + builder.AddTestExecutionFilterProvider(_ => new EnvironmentFilterProvider("filter-provider-a", "MTP_TEST_FILTER_PROVIDER_A")); + builder.AddTestExecutionFilterProvider(_ => new EnvironmentFilterProvider("filter-provider-b", "MTP_TEST_FILTER_PROVIDER_B")); using ITestApplication app = await builder.BuildAsync(); return await app.RunAsync(); } @@ -248,14 +347,64 @@ public class MyExtension : IExtension public Task IsEnabledAsync() => Task.FromResult(true); } +public sealed class EnvironmentFilterProvider(string uid, string environmentVariable) : ITestExecutionFilterProvider +{ + public string Uid => uid; + + public string Version => "1.0.0"; + + public string DisplayName => uid; + + public string Description => uid; + + public Task IsEnabledAsync() + => Task.FromResult(Environment.GetEnvironmentVariable(environmentVariable) is not null); + + public Task GetFilterAsync( + TestExecutionFilterContext context, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + string? value = Environment.GetEnvironmentVariable(environmentVariable); + if (value is null) + { + return Task.FromResult(null); + } + + string[] requestSpecificValue = value.Split(new[] { ':' }, 2); + if (requestSpecificValue.Length == 2) + { + TestExecutionRequestKind requestedKind = requestSpecificValue[0] switch + { + "discovery" => TestExecutionRequestKind.Discovery, + "run" => TestExecutionRequestKind.Run, + _ => throw new InvalidOperationException($"Unknown request kind '{requestSpecificValue[0]}'."), + }; + + if (context.RequestKind != requestedKind) + { + return Task.FromResult(null); + } + + value = requestSpecificValue[1]; + } + + TestNodeUid[] uids = + [ + .. value + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(uidValue => new TestNodeUid(uidValue.Trim())), + ]; + return Task.FromResult(new TestNodeUidListFilter(uids)); + } +} + public class DummyTestFramework : ITestFramework, IDataProducer { - private IServiceProvider _sp; - private MyExtension _myExtension; + private readonly MyExtension _myExtension; - public DummyTestFramework(IServiceProvider sp, MyExtension myExtension) + public DummyTestFramework(MyExtension myExtension) { - _sp = sp; _myExtension = myExtension; } @@ -281,13 +430,10 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) { // Test runner should be able to return discovery events during discovery, and also run and discovery events during run. // Simulate that here. - bool isDiscovery = _sp.GetCommandLineOptions().TryGetOptionArgumentList("--list-tests", out _); - - var uidListFilter = ((TestExecutionRequest)context.Request).Filter as TestNodeUidListFilter; - - // If --filter-uid is used, but it doesn't contain a given Uid, then don't publish TestNodeUpdateMessage for that Uid. - var excludeUid0 = uidListFilter is not null && !uidListFilter.TestNodeUids.Any(n => n.Value == "0"); - var excludeUid1 = uidListFilter is not null && !uidListFilter.TestNodeUids.Any(n => n.Value == "1"); + TestExecutionRequest request = (TestExecutionRequest)context.Request; + bool isDiscovery = request is DiscoverTestExecutionRequest; + bool excludeUid0 = !MatchesFilter(request.Filter, "0", "/"); + bool excludeUid1 = !MatchesFilter(request.Filter, "1", "/other"); if (!excludeUid0) { @@ -301,7 +447,7 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) new TestNode() { Uid = "0", DisplayName = "Test1", Properties = new(PassedTestNodeStateProperty.CachedInstance) })); } - if (!_sp.GetCommandLineOptions().TryGetOptionArgumentList("--treenode-filter", out _) && !excludeUid1) + if (!excludeUid1) { await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() { Uid = "1", DisplayName = "Test2", Properties = new(DiscoveredTestNodeStateProperty.CachedInstance) })); @@ -315,6 +461,17 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) context.Complete(); } + + private static bool MatchesFilter(ITestExecutionFilter filter, string uid, string path) + => filter switch + { + NopFilter => true, + TestNodeUidListFilter uidFilter => uidFilter.TestNodeUids.Any(nodeUid => nodeUid.Value == uid), + TreeNodeFilter treeFilter => treeFilter.MatchesFilter(path, new PropertyBag()), + CompositeTestExecutionFilter { Operator: TestExecutionFilterOperator.And } composite => + composite.Filters.All(childFilter => MatchesFilter(childFilter, uid, path)), + _ => throw new NotSupportedException($"Unsupported filter '{filter.GetType()}'."), + }; } """; diff --git a/test/UnitTests/MSTestAdapter.UnitTests/MSTestFilterContextTests.cs b/test/UnitTests/MSTestAdapter.UnitTests/MSTestFilterContextTests.cs new file mode 100644 index 0000000000..7b97defbc7 --- /dev/null +++ b/test/UnitTests/MSTestAdapter.UnitTests/MSTestFilterContextTests.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Requests; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.TestingPlatformAdapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +using Moq; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests; + +public sealed class MSTestFilterContextTests : TestContainer +{ + private const string EmptyRunSettings = + """ + + + + + """; + + public void EmptyUidListBuildsMatchNoneFilter() + { + MSTestRunContext context = CreateContext(EmptyRunSettings, CreateUidFilter()); + + GetFilterValue(context).Should().Be( + "(FullyQualifiedName=__MTP_EMPTY_UID_FILTER__&FullyQualifiedName!=__MTP_EMPTY_UID_FILTER__)"); + MatchesFullyQualifiedName(context, "__MTP_EMPTY_UID_FILTER__").Should().BeFalse(); + MatchesFullyQualifiedName(context, "A.B.Test").Should().BeFalse(); + } + + public void AndCompositeTranslatesChildrenRecursively() + { + CompositeTestExecutionFilter filter = new( + TestExecutionFilterOperator.And, + CreateUidFilter("A.B.Test1", "A.B.Test2"), + CreateUidFilter("A.B.Test2", "A.B.Test3")); + + MSTestRunContext context = CreateContext(EmptyRunSettings, filter); + + GetFilterValue(context).Should().Be( + "(FullyQualifiedName=A.B.Test1|FullyQualifiedName=A.B.Test2) & (FullyQualifiedName=A.B.Test2|FullyQualifiedName=A.B.Test3)"); + } + + public void RunSettingsCommandLineAndCompositePreserveAndSemantics() + { + const string RunSettings = + """ + + + Category=Fast + + + """; + CompositeTestExecutionFilter filter = new( + TestExecutionFilterOperator.And, + new NopFilter(), + CreateUidFilter("A.B.Test")); + + MSTestRunContext context = CreateContext(RunSettings, filter, commandLineFilter: "Priority=1"); + + GetFilterValue(context).Should().Be( + "(Category=Fast) & (Priority=1) & (FullyQualifiedName=A.B.Test)"); + } + + public void TreeNodeFilterThrowsActionableError() + { + Action action = () => _ = CreateContext(EmptyRunSettings, new TreeNodeFilter("/Tests/**")); + + action.Should().Throw() + .Which.Message.Should().Contain(nameof(TreeNodeFilter)) + .And.Contain("MSTest"); + } + + public void CustomFilterThrowsActionableError() + { + Action action = () => _ = CreateContext(EmptyRunSettings, new CustomFilter()); + + action.Should().Throw() + .Which.Message.Should().Contain(nameof(CustomFilter)) + .And.Contain("MSTest"); + } + + private static TestNodeUidListFilter CreateUidFilter(params string[] uids) + => new([.. uids.Select(uid => new TestNodeUid(uid))]); + + private static string GetFilterValue(MSTestRunContext context) + { + ITestCaseFilterExpression? filterExpression = context.GetTestCaseFilter(null, _ => null); + filterExpression.Should().NotBeNull(); + return filterExpression!.TestCaseFilterValue; + } + + private static bool MatchesFullyQualifiedName(MSTestRunContext context, string fullyQualifiedName) + { + ITestCaseFilterExpression? filterExpression = context.GetTestCaseFilter(null, _ => null); + filterExpression.Should().NotBeNull(); + var testCase = new TestCase(fullyQualifiedName, new Uri("executor://mstest"), "source.dll"); + return filterExpression!.MatchTestCase( + testCase, + propertyName => string.Equals(propertyName, "FullyQualifiedName", StringComparison.OrdinalIgnoreCase) + ? fullyQualifiedName + : null); + } + + private static MSTestRunContext CreateContext( + string runSettingsXml, + ITestExecutionFilter filter, + string? commandLineFilter = null) + { + var runSettings = new Mock(); + runSettings.Setup(settings => settings.SettingsXml).Returns(runSettingsXml); + + var commandLineOptions = new Mock(); + string[]? commandLineFilterArguments = commandLineFilter is null ? null : [commandLineFilter]; + commandLineOptions + .Setup(options => options.TryGetOptionArgumentList( + MSTestTestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName, + out commandLineFilterArguments)) + .Returns(commandLineFilter is not null); + + return new MSTestRunContext(commandLineOptions.Object, runSettings.Object, filter); + } + + private sealed class CustomFilter : ITestExecutionFilter; +} diff --git a/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/RunContextAdapterFilterTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/RunContextAdapterFilterTests.cs index fa8e06317d..2f80ebff66 100644 --- a/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/RunContextAdapterFilterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/RunContextAdapterFilterTests.cs @@ -59,15 +59,15 @@ public void GetTestCaseFilter_WithSingleFullyQualifiedNameNode_BuildsFullyQualif } [TestMethod] - public void GetTestCaseFilter_WithEmptyNodeList_ThrowsOnEmptyGroup() + public void GetTestCaseFilter_WithEmptyNodeList_BuildsMatchNoneFilter() { - // An empty UID list is constructible and builds the empty group "()", which the VSTest filter - // parser rejects, so GetTestCaseFilter surfaces a format error. Pinning this guards the edge - // case now that this suite owns the filter-building coverage. RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter()); - TestPlatformFormatException exception = Assert.ThrowsExactly(() => adapter.GetTestCaseFilter(null, _ => null)); - Assert.AreEqual("()", exception.FilterValue); + Assert.AreEqual( + "(FullyQualifiedName=__MTP_EMPTY_UID_FILTER__&FullyQualifiedName!=__MTP_EMPTY_UID_FILTER__)", + GetFilterValue(adapter)); + Assert.IsFalse(MatchesFullyQualifiedName(adapter, "__MTP_EMPTY_UID_FILTER__")); + Assert.IsFalse(MatchesFullyQualifiedName(adapter, "A.B.Test")); } [TestMethod] @@ -168,6 +168,62 @@ public void GetTestCaseFilter_WithRunSettingsAndNodeFilter_CombinesWithAndOperat Assert.AreEqual("(Category=Fast) & (FullyQualifiedName=A.B.Test)", GetFilterValue(adapter)); } + [TestMethod] + public void GetTestCaseFilter_WithAndComposite_TranslatesChildrenRecursively() + { + CompositeTestExecutionFilter filter = new( + TestExecutionFilterOperator.And, + CreateUidFilter("A.B.Test1", "A.B.Test2"), + CreateUidFilter("A.B.Test2", "A.B.Test3")); + + RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, filter); + + Assert.AreEqual( + "(FullyQualifiedName=A.B.Test1|FullyQualifiedName=A.B.Test2) & (FullyQualifiedName=A.B.Test2|FullyQualifiedName=A.B.Test3)", + GetFilterValue(adapter)); + } + + [TestMethod] + public void GetTestCaseFilter_WithRunSettingsAndComposite_PreservesAndSemantics() + { + string runSettings = + """ + + + Category=Fast + + + """; + CompositeTestExecutionFilter filter = new( + TestExecutionFilterOperator.And, + new NopFilter(), + CreateUidFilter("A.B.Test")); + + RunContextAdapter adapter = CreateAdapter(runSettings, filter); + + Assert.AreEqual("(Category=Fast) & (FullyQualifiedName=A.B.Test)", GetFilterValue(adapter)); + } + + [TestMethod] + public void Constructor_WithTreeNodeFilter_ThrowsActionableError() + { + NotSupportedException exception = Assert.ThrowsExactly( + () => CreateAdapter(EmptyRunSettings, new TreeNodeFilter("/Tests/**"))); + + Assert.Contains(nameof(TreeNodeFilter), exception.Message); + Assert.Contains("VSTestBridge", exception.Message); + } + + [TestMethod] + public void Constructor_WithCustomFilter_ThrowsActionableError() + { + NotSupportedException exception = Assert.ThrowsExactly( + () => CreateAdapter(EmptyRunSettings, new CustomFilter())); + + Assert.Contains(nameof(CustomFilter), exception.Message); + Assert.Contains("VSTestBridge", exception.Message); + } + [TestMethod] public void GetTestCaseFilter_WithRunSettingsAndCommandLineFilter_CombinesWithAndOperator() { @@ -304,4 +360,6 @@ private static RunContextAdapter CreateAdapter(string runSettingsXml, ITestExecu return new RunContextAdapter(commandLineOptions.Object, runSettings.Object, filter, useFullyQualifiedNameAsUid); } + + private sealed class CustomFilter : ITestExecutionFilter; } diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TestExecutionFilterComposerTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TestExecutionFilterComposerTests.cs new file mode 100644 index 0000000000..a76da2b91e --- /dev/null +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TestExecutionFilterComposerTests.cs @@ -0,0 +1,341 @@ +// 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; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Requests; +using Microsoft.Testing.Platform.Services; +using Microsoft.Testing.Platform.TestHost; + +namespace Microsoft.Testing.Platform.UnitTests; + +[TestClass] +public sealed class TestExecutionFilterComposerTests +{ + private static readonly TestExecutionFilterContext RunConsoleContext = + new(TestExecutionRequestKind.Run, TestExecutionRequestOrigin.Console); + + [TestMethod] + public async Task ComposeAsync_WithBuiltInFilterOnly_PreservesConstraint() + { + TreeNodeFilter builtInFilter = new("/Tests/**"); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + builtInFilter, + [], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + Assert.AreSame(builtInFilter, result); + } + + [TestMethod] + public async Task ComposeAsync_WithOneProvider_UsesProviderConstraint() + { + StubFilterProvider provider = new("provider-a", new TestNodeUidListFilter([new("B"), new("A")])); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + new NopFilter(), + [provider], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + AssertUidFilter(result, "A", "B"); + } + + [TestMethod] + public async Task ComposeAsync_WithTwoUidProviders_IntersectsIndependentlyOfRegistrationOrder() + { + StubFilterProvider providerA = new("provider-a", new TestNodeUidListFilter([new("A"), new("B")])); + StubFilterProvider providerB = new("provider-b", new TestNodeUidListFilter([new("B"), new("C")])); + + ITestExecutionFilter resultAB = await TestExecutionFilterComposer.ComposeAsync( + new NopFilter(), + [providerA, providerB], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + ITestExecutionFilter resultBA = await TestExecutionFilterComposer.ComposeAsync( + new NopFilter(), + [providerB, providerA], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + AssertUidFilter(resultAB, "B"); + AssertUidFilter(resultBA, "B"); + } + + [TestMethod] + public async Task ComposeAsync_WithDisjointUidProviders_ReturnsEmptyUidFilter() + { + StubFilterProvider providerA = new("provider-a", new TestNodeUidListFilter([new("A")])); + StubFilterProvider providerB = new("provider-b", new TestNodeUidListFilter([new("B")])); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + new NopFilter(), + [providerA, providerB], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + AssertUidFilter(result); + } + + [TestMethod] + public async Task ComposeAsync_WithBuiltInAndProviderUidFilters_IntersectsConstraints() + { + TestNodeUidListFilter builtInFilter = new([new("A"), new("B")]); + StubFilterProvider provider = new("provider-a", new TestNodeUidListFilter([new("B"), new("C")])); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + builtInFilter, + [provider], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + AssertUidFilter(result, "B"); + } + + [TestMethod] + public async Task ComposeAsync_WithTreeAndUidConstraints_ReturnsAndComposite() + { + TreeNodeFilter treeFilter = new("/Tests/**"); + StubFilterProvider provider = new("provider-a", new TestNodeUidListFilter([new("A")])); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + treeFilter, + [provider], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + CompositeTestExecutionFilter composite = Assert.IsInstanceOfType(result); + Assert.AreEqual(TestExecutionFilterOperator.And, composite.Operator); + Assert.HasCount(2, composite.Filters); + Assert.AreSame(treeFilter, Assert.ContainsSingle(composite.Filters.OfType())); + _ = Assert.ContainsSingle(composite.Filters.OfType()); + } + + [TestMethod] + public async Task ComposeAsync_WithNestedAndComposite_FlattensAndIntersectsUidConstraints() + { + TreeNodeFilter treeFilter = new("/Tests/**"); + CompositeTestExecutionFilter providerFilter = new( + TestExecutionFilterOperator.And, + treeFilter, + new TestNodeUidListFilter([new("A"), new("B")])); + StubFilterProvider provider = new("provider-a", providerFilter); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + new TestNodeUidListFilter([new("B"), new("C")]), + [provider], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + CompositeTestExecutionFilter composite = Assert.IsInstanceOfType(result); + Assert.HasCount(2, composite.Filters); + Assert.AreSame(treeFilter, Assert.ContainsSingle(composite.Filters.OfType())); + AssertUidFilter(Assert.ContainsSingle(composite.Filters.OfType()), "B"); + } + + [TestMethod] + public async Task ComposeAsync_WithUnsupportedProviderFilter_ThrowsActionableError() + { + StubFilterProvider provider = new("provider-a", new CustomFilter()); + + InvalidOperationException exception = await Assert.ThrowsExactlyAsync( + () => TestExecutionFilterComposer.ComposeAsync( + new NopFilter(), + [provider], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None)); + + Assert.Contains("provider-a", exception.Message); + Assert.Contains(typeof(CustomFilter).FullName!, exception.Message); + } + + [TestMethod] + public async Task ComposeAsync_ForServerWithNullContribution_PreservesRequestFilterAndContext() + { + TestExecutionFilterContext? observedContext = null; + StubFilterProvider provider = new( + "provider-a", + getFilter: (context, _) => + { + observedContext = context; + return Task.FromResult(null); + }); + TestNodeUidListFilter requestFilter = new([new("A")]); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + requestFilter, + [provider], + new(TestExecutionRequestKind.Discovery, TestExecutionRequestOrigin.Server), + allowProviderContributions: false, + CancellationToken.None); + + AssertUidFilter(result, "A"); + Assert.IsNotNull(observedContext); + Assert.AreEqual(TestExecutionRequestKind.Discovery, observedContext.RequestKind); + Assert.AreEqual(TestExecutionRequestOrigin.Server, observedContext.Origin); + } + + [TestMethod] + public async Task ComposeAsync_ForServerWithProviderConstraint_ThrowsActionableError() + { + StubFilterProvider provider = new("provider-a", new TestNodeUidListFilter([new("A")])); + + InvalidOperationException exception = await Assert.ThrowsExactlyAsync( + () => TestExecutionFilterComposer.ComposeAsync( + new NopFilter(), + [provider], + new(TestExecutionRequestKind.Run, TestExecutionRequestOrigin.Server), + allowProviderContributions: false, + CancellationToken.None)); + + Assert.Contains("provider-a", exception.Message); + Assert.Contains("return null", exception.Message); + } + + [TestMethod] + public async Task ComposeAsync_PassesRequestContextAndCancellationToken() + { + using CancellationTokenSource cancellationTokenSource = new(); + TestExecutionFilterContext? observedContext = null; + CancellationToken observedCancellationToken = default; + StubFilterProvider provider = new( + "provider-a", + getFilter: (context, cancellationToken) => + { + observedContext = context; + observedCancellationToken = cancellationToken; + return Task.FromResult(null); + }); + TestExecutionFilterContext expectedContext = new(TestExecutionRequestKind.Discovery, TestExecutionRequestOrigin.Console); + + _ = await TestExecutionFilterComposer.ComposeAsync( + new NopFilter(), + [provider], + expectedContext, + allowProviderContributions: true, + cancellationTokenSource.Token); + + Assert.AreSame(expectedContext, observedContext); + Assert.AreEqual(cancellationTokenSource.Token, observedCancellationToken); + } + + [TestMethod] + public async Task ComposeAsync_WithCanceledToken_DoesNotInvokeProvider() + { + bool wasInvoked = false; + StubFilterProvider provider = new( + "provider-a", + getFilter: (_, _) => + { + wasInvoked = true; + return Task.FromResult(null); + }); + CancellationToken canceledToken = new(canceled: true); + + _ = await Assert.ThrowsExactlyAsync( + () => TestExecutionFilterComposer.ComposeAsync( + new NopFilter(), + [provider], + RunConsoleContext, + allowProviderContributions: true, + canceledToken)); + + Assert.IsFalse(wasInvoked); + } + + [TestMethod] + public async Task BuildTestExecutionFilterProvidersAsync_SkipsDisabledAndInitializesAllEnabledProviders() + { + TestHostManager manager = new(); + StubFilterProvider enabledA = new("enabled-a", new NopFilter()); + StubFilterProvider disabled = new("disabled", new NopFilter(), isEnabled: false); + StubFilterProvider enabledB = new("enabled-b", new NopFilter()); + manager.AddTestExecutionFilterProvider(_ => enabledA); + manager.AddTestExecutionFilterProvider(_ => disabled); + manager.AddTestExecutionFilterProvider(_ => enabledB); + + ITestExecutionFilterProvider[] providers = await manager.BuildTestExecutionFilterProvidersAsync(new ServiceProvider()); + + Assert.HasCount(2, providers); + Assert.AreSame(enabledA, providers[0]); + Assert.AreSame(enabledB, providers[1]); + Assert.IsTrue(enabledA.IsInitialized); + Assert.IsFalse(disabled.IsInitialized); + Assert.IsTrue(enabledB.IsInitialized); + } + + [TestMethod] + public void CompositeFilter_WithFewerThanTwoChildren_Throws() + => Assert.ThrowsExactly( + () => new CompositeTestExecutionFilter(TestExecutionFilterOperator.And, new NopFilter())); + + [TestMethod] + public void CompositeFilter_WithNullChild_Throws() + => Assert.ThrowsExactly( + () => new CompositeTestExecutionFilter(TestExecutionFilterOperator.And, new NopFilter(), null!)); + + private static void AssertUidFilter(ITestExecutionFilter filter, params string[] expectedUids) + { + TestNodeUidListFilter uidFilter = Assert.IsInstanceOfType(filter); + Assert.HasCount(expectedUids.Length, uidFilter.TestNodeUids); + for (int i = 0; i < expectedUids.Length; i++) + { + Assert.AreEqual(expectedUids[i], uidFilter.TestNodeUids[i].Value); + } + } + + private sealed class CustomFilter : ITestExecutionFilter; + + private sealed class StubFilterProvider : ITestExecutionFilterProvider, IAsyncInitializableExtension + { + private readonly bool _isEnabled; + private readonly Func> _getFilter; + + public StubFilterProvider(string uid, ITestExecutionFilter filter, bool isEnabled = true) + : this(uid, (_, _) => Task.FromResult(filter), isEnabled) + { + } + + public StubFilterProvider( + string uid, + Func> getFilter, + bool isEnabled = true) + { + Uid = uid; + _getFilter = getFilter; + _isEnabled = isEnabled; + } + + public string Uid { get; } + + public string Version => "1.0.0"; + + public string DisplayName => Uid; + + public string Description => Uid; + + public bool IsInitialized { get; private set; } + + public Task GetFilterAsync(TestExecutionFilterContext context, CancellationToken cancellationToken) + => _getFilter(context, cancellationToken); + + public Task InitializeAsync() + { + IsInitialized = true; + return Task.CompletedTask; + } + + public Task IsEnabledAsync() => Task.FromResult(_isEnabled); + } +} diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs index 154cfc8738..6cd647aee2 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs @@ -9,6 +9,7 @@ using Microsoft.Testing.Platform.Extensions.TestFramework; using Microsoft.Testing.Platform.Extensions.TestHost; using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.Requests; using Microsoft.Testing.Platform.ServerMode; using Microsoft.Testing.Platform.Services; @@ -127,6 +128,82 @@ await WriteMessageAsync( Assert.AreEqual(0, result); } + [TestMethod] + public async Task RunRequestWithEmptyTests_PreservesEmptyUidSelection() + { + using var server = TcpServer.Create(); + TaskCompletionSource requestCaptured = new(TaskCreationOptions.RunContinuationsAsynchronously); + + string[] args = ["--no-banner", "--server", "--client-port", $"{server.Port}", "--internal-testingplatform-skipbuildercheck"]; + ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + builder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_, __) => new MockTestAdapter + { + DiscoveryAction = context => + { + context.Complete(); + requestCaptured.TrySetResult((TestExecutionRequest)context.Request); + return Task.CompletedTask; + }, + }); + var testApplication = (TestApplication)await builder.BuildAsync(); + testApplication.ServiceProvider.GetRequiredService().SuppressOutput(); + Task serverTask = Task.Run(testApplication.RunAsync); + + using CancellationTokenSource timeout = new(TimeoutHelper.DefaultHangTimeSpanTimeout); + using TcpClient client = await server.WaitForConnectionAsync(timeout.Token); + using NetworkStream stream = client.GetStream(); + using StreamWriter writer = new(stream, Encoding.UTF8); + TcpMessageHandler messageHandler = new( + client, + clientToServerStream: client.GetStream(), + serverToClientStream: client.GetStream(), + FormatterUtilities.CreateFormatter()); + + const string InitializeMessage = """ + { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "processId": 32, + "clientInfo": { "name": "testingplatform-unittests", "version": "1.0.0" }, + "capabilities": { + "testing": { + "debuggerProvider": true + } + } + } + } + """; + await WriteMessageAsync(writer, InitializeMessage); + _ = await WaitForMessage( + messageHandler, + rpcMessage => rpcMessage is ResponseMessage response && response.Id == 1, + "Wait initialize", + timeout.Token); + + const string RunTestsMessage = """ + { + "jsonrpc": "2.0", + "id": 2, + "method": "testing/runTests", + "params": { + "runId": "00000000-0000-0000-0000-000000000001", + "tests": [] + } + } + """; + await WriteMessageAsync(writer, RunTestsMessage); + await requestCaptured.Task.TimeoutAfterAsync(TimeoutHelper.DefaultHangTimeSpanTimeout); + RunTestExecutionRequest runRequest = Assert.IsInstanceOfType(await requestCaptured.Task); + TestNodeUidListFilter uidFilter = Assert.IsInstanceOfType(runRequest.Filter); + Assert.IsEmpty(uidFilter.TestNodeUids); + + await WriteMessageAsync(writer, """{ "jsonrpc": "2.0", "method": "exit", "params": { } }"""); + + Assert.AreEqual(0, await serverTask); + } + [TestMethod] public async Task DiscoveryRequestCanBeCanceled() { From 32380756a9ca002ae6d1ab92566ee65b0ea9d7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 26 Jul 2026 12:56:01 +0200 Subject: [PATCH 2/3] Document affected-test filter provider migration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d7adc271-ccc0-4654-aede-93ac7d83dd40 --- .../020-Test-Execution-Filter-Providers.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/docs/RFCs/020-Test-Execution-Filter-Providers.md b/docs/RFCs/020-Test-Execution-Filter-Providers.md index aa79a27d04..f16d89e738 100644 --- a/docs/RFCs/020-Test-Execution-Filter-Providers.md +++ b/docs/RFCs/020-Test-Execution-Filter-Providers.md @@ -257,6 +257,17 @@ Examples: | tree `T` | UIDs `{A,B}` | `null` | `AND(T, UIDs {A,B})` | | runsettings filter | provider UID constraint | — | adapter evaluates `runsettings AND provider` | +Response files and large UID selections do not require another filter representation. Command-line +processing materializes built-in `--filter-uid` values before composition, including values supplied +through response files. The composer sees the resulting `TestNodeUidListFilter` and intersects it +with provider UID constraints in the same way as an inline value. Response-file parsing remains a +launcher/command-line concern rather than becoming provider API surface. + +An in-process provider can return a large `TestNodeUidListFilter` directly, so its selection is not +limited by operating-system command-line length. An orchestrator that must cross a process boundary +can continue to use response files for the built-in option; this RFC does not remove or reinterpret +that fallback. + ## Adapter responsibilities MTP transports filter representations; the test framework or bridge still owns filtering because it @@ -343,6 +354,72 @@ The API is additive and experimental. - Frameworks that pattern-match directly on `ITestExecutionFilter` must add recursive composite support before consuming applications register providers. +### Concrete migration: affected-test selection + +A private affected-test extension provides a concrete migration example without making affected-test +terminology part of MTP. Its current implementation has two ways to select tests: + +1. The compatibility path uses a `RunAffectedTestsOrchestrator`. It computes the selected UIDs, + removes its parent activation option to prevent recursive activation, and launches the test host + with the built-in `--filter-uid` option. Large selections are written to response files, recursive + response files are supported, and the child execution remains connected to `dotnet test` + reporting. +2. A transitional direct path can conditionally register the single-winner + `ITestExecutionFilterFactory` when an `AffectedTestsFilterApiAvailable` build switch is enabled. + That avoids a child launch, but it replaces the platform factory and therefore cannot safely + coexist with the built-in request filter or another extension factory. + +When the provider API is available, only the second path changes. The extension registers an +`ITestExecutionFilterProvider` and contributes the selected UIDs as an additional constraint: + +```csharp +builder.AddTestExecutionFilterProvider( + serviceProvider => new AffectedTestsFilterProvider(serviceProvider)); + +public Task GetFilterAsync( + TestExecutionFilterContext context, + CancellationToken cancellationToken) +{ + if (context.Origin == TestExecutionRequestOrigin.Server + || context.RequestKind == TestExecutionRequestKind.Discovery) + { + return Task.FromResult(null); + } + + TestNodeUid[] affectedTestUids = GetAffectedTestUids(cancellationToken); + return Task.FromResult( + new TestNodeUidListFilter(affectedTestUids)); +} +``` + +The example name and UID-selection algorithm belong to the consumer. The platform sees only a +provider and a platform-known UID constraint. The provider's `IsEnabledAsync` remains gated by the +consumer's run-affected activation option; `AffectedTestsFilterApiAvailable` chooses the available +integration path rather than changing request semantics. + +This migration has the following behavior: + +- The provider no longer strips the activation option, relaunches the host, or recreates the + built-in filter. MTP ANDs its UID set with any user/request constraint. +- An explicit user `--filter-uid` selection and the provider selection are intersected. Neither can + override the other, and an empty intersection runs no tests. +- A large direct selection stays in memory and therefore has no command-line-length limit. +- When the provider API is unavailable, the existing orchestrator fallback remains unchanged, + including recursive response-file handling and long UID lists. +- `dotnet test` reporting is preserved in both modes: the provider runs in the original test host, + while the compatibility orchestrator keeps its existing child-reporting connection. +- The transitional single-winner factory registration is removed once the provider path is used; + the factory must not remain as a second activation mechanism. + +The extension's refresh operation remains an orchestrator. It performs managed x64 profiler +batching and owns a multi-run/instrumentation plan, so converting it to a filter provider would +violate the constraint-versus-planning boundary in this RFC. + +Consumer migration tests should prove that the provider and orchestrator fallback select the same +UIDs, that an explicit built-in UID filter intersects correctly, that direct large selections are +not truncated, and that recursive response-file fallback and `dotnet test` reporting remain +unchanged. + ## Relationship to draft PR #8820 [Draft PR #8820](https://github.com/microsoft/testfx/pull/8820) proposes a user-facing `--filter` From 3e59355d5eba754e49b4c0202b9cc82d86d3d51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 27 Jul 2026 14:08:02 +0200 Subject: [PATCH 3/3] Short-circuit filter composition when no provider contributes Address PR review feedback: - ComposeAsync now returns the built-in request filter instance untouched when no enabled provider contributes a constraint, so NopFilter/TestNodeUidListFilter are not rebuilt, UID order is not changed, and a framework-specific request filter is no longer rejected. This makes the RFC compatibility guarantee literally true. - Document the short-circuit in the composition algorithm, examples, and compatibility sections. - Add the missing [Experimental("TPEXP")] attribute to the three enums in the RFC public API listing so it matches the real declarations. - State explicitly that this RFC does not resolve #3590 (custom filter implementations), which stays open pending capability negotiation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b9d32942-cfb3-453e-a488-7e6aed7c42fd --- .../020-Test-Execution-Filter-Providers.md | 45 +++++++++-- .../Requests/TestExecutionFilterComposer.cs | 23 ++++-- .../TestExecutionFilterComposerTests.cs | 79 +++++++++++++++++++ 3 files changed, 135 insertions(+), 12 deletions(-) diff --git a/docs/RFCs/020-Test-Execution-Filter-Providers.md b/docs/RFCs/020-Test-Execution-Filter-Providers.md index f16d89e738..13457baf2a 100644 --- a/docs/RFCs/020-Test-Execution-Filter-Providers.md +++ b/docs/RFCs/020-Test-Execution-Filter-Providers.md @@ -25,6 +25,8 @@ batching, sharding, retry scheduling, or any other multi-run plan. The open filtering issues describe several related but distinct needs: - [#3590](https://github.com/microsoft/testfx/issues/3590) asks for public filter extensibility. + This RFC delivers the composable extension point but not custom filter implementations, so the + issue stays open. - [#3530](https://github.com/microsoft/testfx/issues/3530) asks for an aggregate filter. - [#3528](https://github.com/microsoft/testfx/issues/3528) proposes index-based batching. - [#4068](https://github.com/microsoft/testfx/issues/4068) asks for test sharding. @@ -63,6 +65,8 @@ This RFC covers: This RFC does not define: - a new command-line option or filter grammar; +- public custom `ITestExecutionFilter` implementations or the capability negotiation they need + ([#3590](https://github.com/microsoft/testfx/issues/3590) stays open); - affected-test terminology or source-to-test mapping; - OR/NOT composition across provider contributions; - a framework-neutral predicate language; @@ -109,18 +113,21 @@ public sealed class TestExecutionFilterContext public TestExecutionRequestOrigin Origin { get; } } +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] public enum TestExecutionRequestKind { Discovery, Run, } +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] public enum TestExecutionRequestOrigin { Console, Server, } +[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] public enum TestExecutionFilterOperator { And, @@ -228,6 +235,17 @@ It is never dropped. Supporting custom filter kinds later requires: 3. a non-silent fallback/rejection policy for mixed frameworks; and 4. adapter translation or a platform-owned evaluation contract. +Because this RFC keeps custom filter kinds out of the composable surface, it does not resolve the +"open up filters for custom implementations" request tracked in +[#3590](https://github.com/microsoft/testfx/issues/3590). It is a prerequisite: composition and +recursive validation exist, so a later capability-negotiation RFC only has to decide which custom +kinds a framework can declare and accept. + +A custom filter is only rejected when it takes part in composition. If no provider contributes a +constraint, the built-in request filter is returned untouched (see the composition algorithm below), +so an application whose internal factory produces a framework-specific representation is not broken +by this RFC. + ## Composition algorithm For a console request: @@ -235,15 +253,23 @@ For a console request: 1. MTP creates the existing built-in request filter from `--filter-uid`, `--treenode-filter`, or no filter. 2. MTP asks every enabled provider for a contribution. -3. MTP recursively flattens nested AND composites. -4. MTP removes no-op constraints. -5. MTP intersects all `TestNodeUidListFilter` constraints using ordinal UID equality. -6. MTP sorts the resulting UID set for a deterministic representation. -7. MTP returns: +3. If no provider contributed a constraint, MTP returns the built-in request filter unchanged and + the remaining steps are skipped. +4. MTP recursively flattens nested AND composites. +5. MTP removes no-op constraints. +6. MTP intersects all `TestNodeUidListFilter` constraints using ordinal UID equality. +7. MTP sorts the resulting UID set for a deterministic representation. +8. MTP returns: - `NopFilter` for zero constraints; - the only filter for one constraint; or - `CompositeTestExecutionFilter(And, ...)` for multiple constraints. +Step 3 is what makes composition strictly additive. Normalization and validation of the built-in +request filter only happen when at least one provider actually contributes, so an application with +no provider — or one whose providers all return `null`/`NopFilter` — observes the same filter +instance and the same behavior as before this RFC, even if its framework uses a filter +representation the composer does not know. + The intersection is computed in the platform rather than by every adapter. If any UID intersection is empty, the resulting empty `TestNodeUidListFilter` means match none. @@ -251,7 +277,8 @@ Examples: | Built-in filter | Provider A | Provider B | Result | | --- | --- | --- | --- | -| `Nop` | `null` | `null` | `Nop` | +| `Nop` | `null` | `null` | the same `Nop` instance | +| UIDs `{B,A}` | — | — | the same UIDs `{B,A}` instance (unsorted, no providers) | | UIDs `{A,B}` | UIDs `{B,C}` | `null` | UIDs `{B}` | | UIDs `{A}` | UIDs `{B}` | `null` | empty UID list (match none) | | tree `T` | UIDs `{A,B}` | `null` | `AND(T, UIDs {A,B})` | @@ -344,7 +371,11 @@ empty UID filter, which means match none. The API is additive and experimental. -- Existing applications with no providers receive the same built-in filter object as before. +- Existing applications with no providers receive the same built-in filter object as before: the + composer short-circuits before any normalization or validation, so `NopFilter` and + `TestNodeUidListFilter` instances are not rebuilt and UID order is not changed. +- The same short-circuit applies when providers are registered but all of them return `null` or + `NopFilter` for a request, which is the expected server-origin path in this version. - Existing CLI option names and validation do not change. - Existing JSON-RPC payloads do not change. - The internal `ITestExecutionFilterFactory` remains the source of the console built-in filter; it diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterComposer.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterComposer.cs index 181380c344..7608d0081c 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterComposer.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TestExecutionFilterComposer.cs @@ -15,10 +15,7 @@ public static async Task ComposeAsync( bool allowProviderContributions, CancellationToken cancellationToken) { - List uidFilters = []; - List otherFilters = []; - AddConstraints(requestFilter, providerUid: null, uidFilters, otherFilters); - + List<(string ProviderUid, ITestExecutionFilter Filter)>? providerFilters = null; foreach (ITestExecutionFilterProvider provider in providers) { cancellationToken.ThrowIfCancellationRequested(); @@ -39,7 +36,23 @@ public static async Task ComposeAsync( providerFilter.GetType().FullName)); } - AddConstraints(providerFilter, provider.Uid, uidFilters, otherFilters); + (providerFilters ??= []).Add((provider.Uid, providerFilter)); + } + + // No provider contributed a constraint, so there is nothing to compose. Return the built-in request + // filter as-is: applications without providers keep the exact object and semantics they had before + // composition existed, including filter representations only their own framework understands. + if (providerFilters is null) + { + return requestFilter; + } + + List uidFilters = []; + List otherFilters = []; + AddConstraints(requestFilter, providerUid: null, uidFilters, otherFilters); + foreach ((string providerUid, ITestExecutionFilter providerFilter) in providerFilters) + { + AddConstraints(providerFilter, providerUid, uidFilters, otherFilters); } if (uidFilters.Count > 0) diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TestExecutionFilterComposerTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TestExecutionFilterComposerTests.cs index a76da2b91e..9d557247e4 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TestExecutionFilterComposerTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TestExecutionFilterComposerTests.cs @@ -30,6 +30,84 @@ public async Task ComposeAsync_WithBuiltInFilterOnly_PreservesConstraint() Assert.AreSame(builtInFilter, result); } + [TestMethod] + public async Task ComposeAsync_WithoutProviders_ReturnsSameNopFilterInstance() + { + NopFilter builtInFilter = new(); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + builtInFilter, + [], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + Assert.AreSame(builtInFilter, result); + } + + [TestMethod] + public async Task ComposeAsync_WithoutProviders_DoesNotNormalizeUidFilter() + { + TestNodeUidListFilter builtInFilter = new([new("B"), new("A"), new("B")]); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + builtInFilter, + [], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + Assert.AreSame(builtInFilter, result); + } + + [TestMethod] + public async Task ComposeAsync_WithoutProviders_DoesNotRejectCustomRequestFilter() + { + CustomFilter builtInFilter = new(); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + builtInFilter, + [], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + Assert.AreSame(builtInFilter, result); + } + + [TestMethod] + public async Task ComposeAsync_WhenAllProvidersOptOut_ReturnsSameRequestFilterInstance() + { + StubFilterProvider nullProvider = new("provider-a", getFilter: (_, _) => Task.FromResult(null)); + StubFilterProvider nopProvider = new("provider-b", new NopFilter()); + TestNodeUidListFilter builtInFilter = new([new("B"), new("A")]); + + ITestExecutionFilter result = await TestExecutionFilterComposer.ComposeAsync( + builtInFilter, + [nullProvider, nopProvider], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None); + + Assert.AreSame(builtInFilter, result); + } + + [TestMethod] + public async Task ComposeAsync_WithProviderContribution_RejectsCustomRequestFilter() + { + StubFilterProvider provider = new("provider-a", new TestNodeUidListFilter([new("A")])); + + InvalidOperationException exception = await Assert.ThrowsExactlyAsync( + () => TestExecutionFilterComposer.ComposeAsync( + new CustomFilter(), + [provider], + RunConsoleContext, + allowProviderContributions: true, + CancellationToken.None)); + + Assert.Contains(typeof(CustomFilter).FullName!, exception.Message); + } + [TestMethod] public async Task ComposeAsync_WithOneProvider_UsesProviderConstraint() { @@ -181,6 +259,7 @@ public async Task ComposeAsync_ForServerWithNullContribution_PreservesRequestFil CancellationToken.None); AssertUidFilter(result, "A"); + Assert.AreSame(requestFilter, result); Assert.IsNotNull(observedContext); Assert.AreEqual(TestExecutionRequestKind.Discovery, observedContext.RequestKind); Assert.AreEqual(TestExecutionRequestOrigin.Server, observedContext.Origin);