Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort) - #9555

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic
Jul 3, 2026
Merged

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort)#9555
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jul 2, 2026

Copy link
Copy Markdown
Member

Why

Part of the multi-PR effort to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). This is Phase 2: test filtering.

Today the PlatformServices discovery and execution pipelines filter tests using the VSTest filter object model directly: ITestCaseFilterExpression, TestProperty, IRunContext.GetTestCaseFilter, and ITestCaseFilterExpression.MatchTestCase. This phase decouples the filtering consumers from those types so filtering is expressed as a neutral predicate over the neutral UnitTestElement model.

What

  • New neutral abstractionITestElementFilter { bool Matches(UnitTestElement testElement); } in Interfaces/ITestElementFilter.cs (namespace …PlatformServices.Interface). A null filter means "no filter" → every element is included. The name is deliberately functional and carries no VSTest/TestCase/ObjectModel naming.
  • TestMethodFilter stays as the single VSTest translation bridge (mirroring how Phase 1/5 left their AdapterMessageLoggerExtensions / TestResultRecorderExtensions bridges in PlatformServices). It gains GetTestElementFilter(IDiscoveryContext?, IAdapterMessageLogger, out bool filterHasError), which parses the VSTest filter exactly as before via the existing GetFilterExpression and wraps the result in a private TestElementFilter : ITestElementFilter. That wrapper is the lone place that translates the neutral element to a VSTest TestCase (element.ToTestCase()) and reuses ITestCaseFilterExpression.MatchTestCase + PropertyValueProvider.
  • Consumers routed through the neutral filter:
    • Discovery: UnitTestDiscoverer.SendTestCases now uses ITestElementFilter? + filter.Matches(element).
    • Execution: TestExecutionManager.MatchTestFilter / ExecuteTestsInSourceAsync now use ITestElementFilter? + filter.Matches(test.ToUnitTestElementWithUpdatedSource(source)).
      Neither consumer references ITestCaseFilterExpression / MatchTestCase / TestProperty anymore.
  • Unit tests added in TestMethodFilterTests.cs for the new wrapper (null context, no-filter context, match/non-match, parse-error → filterHasError).

No behavior change

Pure refactor. The supported-property set, the discovery-context vs run-context code paths, the parse-error handling, and the filterHasError bail-out semantics are preserved exactly:

  • Discovery still enumerates the assembly and logs warnings before bailing on a filter parse error.
  • Execution still early-bails before creating the isolation UnitTestRunner.

One transparent nuance (surfaced during review): in the out-of-proc VSTest path, execution now evaluates the filter against TestCase → UnitTestElement → TestCase, i.e. the same reconstruction that produces the executed element, instead of the raw incoming TestCase. For every test discovered by this adapter the round-trip is identical for all supported filter properties (FullyQualifiedName, DisplayName, Id, TestCategory, Priority, ClassName, traits), so results are unchanged in practice — the only theoretical divergence would be a foreign/older-hash Id or a non-standard fully-qualified name, which the same-adapter discover→execute flow does not produce. Reusing ToTestCase() as the single translation point was pre-approved, and the behavior is now locked by the out-of-proc --filter regression guard described under Verification.

Remaining work (future phase)

  • Physically move TestMethodFilter (the VSTest parser) up into the adapter layer (MSTest.TestAdapter), building the filter at the adapter boundary and passing ITestElementFilter? into the PlatformServices entry points. That was intentionally deferred here because preserving the different per-side filterHasError semantics byte-for-byte would require threading a nullable filter + error flag through many discovery/execution signatures — an awkward change for a transitional step.
  • Let the filter operate on UnitTestElement end-to-end to avoid the transitional double ToTestCase() conversion in filtered discovery.

Verification

  • .\build.cmd -c Debug — green across all TFMs (net462, net8.0, net9.0, UWP, WinUI), 0 warnings / 0 errors.
  • MSTestAdapter.PlatformServices.UnitTests (net8.0): 893 passed.
  • MSTestAdapter.UnitTests (net8.0): 21 passed.
  • Out-of-proc --filter regression guard added — new MSTest.IntegrationTests/TestCaseFilteringTests discovers then runs the DiscoverInternalsProject asset and asserts that filtering by both FullyQualifiedName and Id selects exactly the target test, in discovery and execution. The execution tests clear TestCase.LocalExtensionData to force the out-of-proc TestCase → UnitTestElement → TestCase reconstruction path this PR re-routes, so a future change cannot silently break --filter. All 4 pass; full MSTest.IntegrationTests suite green (47 passed, 1 pre-existing skip).
  • Ran the expert-reviewer agent on both the refactor and the regression guard; no blocking/major findings.

⚠️ Stacked PR

This is a stacked PR on top of:

Its base branch is dev/amauryleve/vstest-decoupling-base, an integration branch that combines #9548 + #9550 (filtering touches TestMethodFilter.cs from Phase 1 and TestExecutionManager.Runner.cs from Phase 5). Until those merge, this PR's diff will include their changes. Please review/merge this after #9548 and #9550.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…rm-agnostic effort)
Introduce a neutral ITestElementFilter abstraction so the PlatformServices
discovery and execution pipelines filter over the neutral UnitTestElement model
instead of the VSTest filter object model (ITestCaseFilterExpression,
TestProperty, IRunContext.GetTestCaseFilter, MatchTestCase).
TestMethodFilter remains the single VSTest translation bridge: it now also
exposes GetTestElementFilter, wrapping the parsed ITestCaseFilterExpression in a
private ITestElementFilter that reuses UnitTestElement.ToTestCase() +
MatchTestCase + PropertyValueProvider. UnitTestDiscoverer.SendTestCases and
TestExecutionManager (MatchTestFilter / ExecuteTestsInSourceAsync) route through
the neutral filter; the filterHasError bail-out semantics are preserved exactly.
Pure refactor: supported-property set, discovery-vs-run-context paths, parse
error handling, and every match result are unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds integration tests (MSTest.IntegrationTests/TestCaseFilteringTests) that lock
the end-to-end discover-then-run behavior of the neutral ITestElementFilter for
FullyQualifiedName and Id filters. The execution tests clear TestCase
LocalExtensionData to force the out-of-proc reconstruction path
(TestCase -> UnitTestElement -> TestCase) that the filtering refactor re-routes,
then assert exactly the target test is selected and executed.
Adds a RunTestsAsync(testCases, testCaseFilter) overload to CLITestBase that runs
execution through a filter-carrying IRunContext, and promotes the shared
InternalRunSettings nested type.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 3, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort) - #9555

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic
Jul 3, 2026
Merged

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort)#9555
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jul 2, 2026

Copy link
Copy Markdown
Member

Why

Part of the multi-PR effort to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). This is Phase 2: test filtering.

Today the PlatformServices discovery and execution pipelines filter tests using the VSTest filter object model directly: ITestCaseFilterExpression, TestProperty, IRunContext.GetTestCaseFilter, and ITestCaseFilterExpression.MatchTestCase. This phase decouples the filtering consumers from those types so filtering is expressed as a neutral predicate over the neutral UnitTestElement model.

What

  • New neutral abstractionITestElementFilter { bool Matches(UnitTestElement testElement); } in Interfaces/ITestElementFilter.cs (namespace …PlatformServices.Interface). A null filter means "no filter" → every element is included. The name is deliberately functional and carries no VSTest/TestCase/ObjectModel naming.
  • TestMethodFilter stays as the single VSTest translation bridge (mirroring how Phase 1/5 left their AdapterMessageLoggerExtensions / TestResultRecorderExtensions bridges in PlatformServices). It gains GetTestElementFilter(IDiscoveryContext?, IAdapterMessageLogger, out bool filterHasError), which parses the VSTest filter exactly as before via the existing GetFilterExpression and wraps the result in a private TestElementFilter : ITestElementFilter. That wrapper is the lone place that translates the neutral element to a VSTest TestCase (element.ToTestCase()) and reuses ITestCaseFilterExpression.MatchTestCase + PropertyValueProvider.
  • Consumers routed through the neutral filter:
    • Discovery: UnitTestDiscoverer.SendTestCases now uses ITestElementFilter? + filter.Matches(element).
    • Execution: TestExecutionManager.MatchTestFilter / ExecuteTestsInSourceAsync now use ITestElementFilter? + filter.Matches(test.ToUnitTestElementWithUpdatedSource(source)).
      Neither consumer references ITestCaseFilterExpression / MatchTestCase / TestProperty anymore.
  • Unit tests added in TestMethodFilterTests.cs for the new wrapper (null context, no-filter context, match/non-match, parse-error → filterHasError).

No behavior change

Pure refactor. The supported-property set, the discovery-context vs run-context code paths, the parse-error handling, and the filterHasError bail-out semantics are preserved exactly:

  • Discovery still enumerates the assembly and logs warnings before bailing on a filter parse error.
  • Execution still early-bails before creating the isolation UnitTestRunner.

One transparent nuance (surfaced during review): in the out-of-proc VSTest path, execution now evaluates the filter against TestCase → UnitTestElement → TestCase, i.e. the same reconstruction that produces the executed element, instead of the raw incoming TestCase. For every test discovered by this adapter the round-trip is identical for all supported filter properties (FullyQualifiedName, DisplayName, Id, TestCategory, Priority, ClassName, traits), so results are unchanged in practice — the only theoretical divergence would be a foreign/older-hash Id or a non-standard fully-qualified name, which the same-adapter discover→execute flow does not produce. Reusing ToTestCase() as the single translation point was pre-approved, and the behavior is now locked by the out-of-proc --filter regression guard described under Verification.

Remaining work (future phase)

  • Physically move TestMethodFilter (the VSTest parser) up into the adapter layer (MSTest.TestAdapter), building the filter at the adapter boundary and passing ITestElementFilter? into the PlatformServices entry points. That was intentionally deferred here because preserving the different per-side filterHasError semantics byte-for-byte would require threading a nullable filter + error flag through many discovery/execution signatures — an awkward change for a transitional step.
  • Let the filter operate on UnitTestElement end-to-end to avoid the transitional double ToTestCase() conversion in filtered discovery.

Verification

  • .\build.cmd -c Debug — green across all TFMs (net462, net8.0, net9.0, UWP, WinUI), 0 warnings / 0 errors.
  • MSTestAdapter.PlatformServices.UnitTests (net8.0): 893 passed.
  • MSTestAdapter.UnitTests (net8.0): 21 passed.
  • Out-of-proc --filter regression guard added — new MSTest.IntegrationTests/TestCaseFilteringTests discovers then runs the DiscoverInternalsProject asset and asserts that filtering by both FullyQualifiedName and Id selects exactly the target test, in discovery and execution. The execution tests clear TestCase.LocalExtensionData to force the out-of-proc TestCase → UnitTestElement → TestCase reconstruction path this PR re-routes, so a future change cannot silently break --filter. All 4 pass; full MSTest.IntegrationTests suite green (47 passed, 1 pre-existing skip).
  • Ran the expert-reviewer agent on both the refactor and the regression guard; no blocking/major findings.

⚠️ Stacked PR

This is a stacked PR on top of:

Its base branch is dev/amauryleve/vstest-decoupling-base, an integration branch that combines #9548 + #9550 (filtering touches TestMethodFilter.cs from Phase 1 and TestExecutionManager.Runner.cs from Phase 5). Until those merge, this PR's diff will include their changes. Please review/merge this after #9548 and #9550.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…rm-agnostic effort)
Introduce a neutral ITestElementFilter abstraction so the PlatformServices
discovery and execution pipelines filter over the neutral UnitTestElement model
instead of the VSTest filter object model (ITestCaseFilterExpression,
TestProperty, IRunContext.GetTestCaseFilter, MatchTestCase).
TestMethodFilter remains the single VSTest translation bridge: it now also
exposes GetTestElementFilter, wrapping the parsed ITestCaseFilterExpression in a
private ITestElementFilter that reuses UnitTestElement.ToTestCase() +
MatchTestCase + PropertyValueProvider. UnitTestDiscoverer.SendTestCases and
TestExecutionManager (MatchTestFilter / ExecuteTestsInSourceAsync) route through
the neutral filter; the filterHasError bail-out semantics are preserved exactly.
Pure refactor: supported-property set, discovery-vs-run-context paths, parse
error handling, and every match result are unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds integration tests (MSTest.IntegrationTests/TestCaseFilteringTests) that lock
the end-to-end discover-then-run behavior of the neutral ITestElementFilter for
FullyQualifiedName and Id filters. The execution tests clear TestCase
LocalExtensionData to force the out-of-proc reconstruction path
(TestCase -> UnitTestElement -> TestCase) that the filtering refactor re-routes,
then assert exactly the target test is selected and executed.
Adds a RunTestsAsync(testCases, testCaseFilter) overload to CLITestBase that runs
execution through a filter-carrying IRunContext, and promotes the shared
InternalRunSettings nested type.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 3, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort) - #9555

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic
Jul 3, 2026
Merged

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort)#9555
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jul 2, 2026

Copy link
Copy Markdown
Member

Why

Part of the multi-PR effort to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). This is Phase 2: test filtering.

Today the PlatformServices discovery and execution pipelines filter tests using the VSTest filter object model directly: ITestCaseFilterExpression, TestProperty, IRunContext.GetTestCaseFilter, and ITestCaseFilterExpression.MatchTestCase. This phase decouples the filtering consumers from those types so filtering is expressed as a neutral predicate over the neutral UnitTestElement model.

What

  • New neutral abstractionITestElementFilter { bool Matches(UnitTestElement testElement); } in Interfaces/ITestElementFilter.cs (namespace …PlatformServices.Interface). A null filter means "no filter" → every element is included. The name is deliberately functional and carries no VSTest/TestCase/ObjectModel naming.
  • TestMethodFilter stays as the single VSTest translation bridge (mirroring how Phase 1/5 left their AdapterMessageLoggerExtensions / TestResultRecorderExtensions bridges in PlatformServices). It gains GetTestElementFilter(IDiscoveryContext?, IAdapterMessageLogger, out bool filterHasError), which parses the VSTest filter exactly as before via the existing GetFilterExpression and wraps the result in a private TestElementFilter : ITestElementFilter. That wrapper is the lone place that translates the neutral element to a VSTest TestCase (element.ToTestCase()) and reuses ITestCaseFilterExpression.MatchTestCase + PropertyValueProvider.
  • Consumers routed through the neutral filter:
    • Discovery: UnitTestDiscoverer.SendTestCases now uses ITestElementFilter? + filter.Matches(element).
    • Execution: TestExecutionManager.MatchTestFilter / ExecuteTestsInSourceAsync now use ITestElementFilter? + filter.Matches(test.ToUnitTestElementWithUpdatedSource(source)).
      Neither consumer references ITestCaseFilterExpression / MatchTestCase / TestProperty anymore.
  • Unit tests added in TestMethodFilterTests.cs for the new wrapper (null context, no-filter context, match/non-match, parse-error → filterHasError).

No behavior change

Pure refactor. The supported-property set, the discovery-context vs run-context code paths, the parse-error handling, and the filterHasError bail-out semantics are preserved exactly:

  • Discovery still enumerates the assembly and logs warnings before bailing on a filter parse error.
  • Execution still early-bails before creating the isolation UnitTestRunner.

One transparent nuance (surfaced during review): in the out-of-proc VSTest path, execution now evaluates the filter against TestCase → UnitTestElement → TestCase, i.e. the same reconstruction that produces the executed element, instead of the raw incoming TestCase. For every test discovered by this adapter the round-trip is identical for all supported filter properties (FullyQualifiedName, DisplayName, Id, TestCategory, Priority, ClassName, traits), so results are unchanged in practice — the only theoretical divergence would be a foreign/older-hash Id or a non-standard fully-qualified name, which the same-adapter discover→execute flow does not produce. Reusing ToTestCase() as the single translation point was pre-approved, and the behavior is now locked by the out-of-proc --filter regression guard described under Verification.

Remaining work (future phase)

  • Physically move TestMethodFilter (the VSTest parser) up into the adapter layer (MSTest.TestAdapter), building the filter at the adapter boundary and passing ITestElementFilter? into the PlatformServices entry points. That was intentionally deferred here because preserving the different per-side filterHasError semantics byte-for-byte would require threading a nullable filter + error flag through many discovery/execution signatures — an awkward change for a transitional step.
  • Let the filter operate on UnitTestElement end-to-end to avoid the transitional double ToTestCase() conversion in filtered discovery.

Verification

  • .\build.cmd -c Debug — green across all TFMs (net462, net8.0, net9.0, UWP, WinUI), 0 warnings / 0 errors.
  • MSTestAdapter.PlatformServices.UnitTests (net8.0): 893 passed.
  • MSTestAdapter.UnitTests (net8.0): 21 passed.
  • Out-of-proc --filter regression guard added — new MSTest.IntegrationTests/TestCaseFilteringTests discovers then runs the DiscoverInternalsProject asset and asserts that filtering by both FullyQualifiedName and Id selects exactly the target test, in discovery and execution. The execution tests clear TestCase.LocalExtensionData to force the out-of-proc TestCase → UnitTestElement → TestCase reconstruction path this PR re-routes, so a future change cannot silently break --filter. All 4 pass; full MSTest.IntegrationTests suite green (47 passed, 1 pre-existing skip).
  • Ran the expert-reviewer agent on both the refactor and the regression guard; no blocking/major findings.

⚠️ Stacked PR

This is a stacked PR on top of:

Its base branch is dev/amauryleve/vstest-decoupling-base, an integration branch that combines #9548 + #9550 (filtering touches TestMethodFilter.cs from Phase 1 and TestExecutionManager.Runner.cs from Phase 5). Until those merge, this PR's diff will include their changes. Please review/merge this after #9548 and #9550.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…rm-agnostic effort)
Introduce a neutral ITestElementFilter abstraction so the PlatformServices
discovery and execution pipelines filter over the neutral UnitTestElement model
instead of the VSTest filter object model (ITestCaseFilterExpression,
TestProperty, IRunContext.GetTestCaseFilter, MatchTestCase).
TestMethodFilter remains the single VSTest translation bridge: it now also
exposes GetTestElementFilter, wrapping the parsed ITestCaseFilterExpression in a
private ITestElementFilter that reuses UnitTestElement.ToTestCase() +
MatchTestCase + PropertyValueProvider. UnitTestDiscoverer.SendTestCases and
TestExecutionManager (MatchTestFilter / ExecuteTestsInSourceAsync) route through
the neutral filter; the filterHasError bail-out semantics are preserved exactly.
Pure refactor: supported-property set, discovery-vs-run-context paths, parse
error handling, and every match result are unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds integration tests (MSTest.IntegrationTests/TestCaseFilteringTests) that lock
the end-to-end discover-then-run behavior of the neutral ITestElementFilter for
FullyQualifiedName and Id filters. The execution tests clear TestCase
LocalExtensionData to force the out-of-proc reconstruction path
(TestCase -> UnitTestElement -> TestCase) that the filtering refactor re-routes,
then assert exactly the target test is selected and executed.
Adds a RunTestsAsync(testCases, testCaseFilter) overload to CLITestBase that runs
execution through a filter-carrying IRunContext, and promotes the shared
InternalRunSettings nested type.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 3, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort) - #9555

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic
Jul 3, 2026
Merged

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort)#9555
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jul 2, 2026

Copy link
Copy Markdown
Member

Why

Part of the multi-PR effort to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). This is Phase 2: test filtering.

Today the PlatformServices discovery and execution pipelines filter tests using the VSTest filter object model directly: ITestCaseFilterExpression, TestProperty, IRunContext.GetTestCaseFilter, and ITestCaseFilterExpression.MatchTestCase. This phase decouples the filtering consumers from those types so filtering is expressed as a neutral predicate over the neutral UnitTestElement model.

What

  • New neutral abstractionITestElementFilter { bool Matches(UnitTestElement testElement); } in Interfaces/ITestElementFilter.cs (namespace …PlatformServices.Interface). A null filter means "no filter" → every element is included. The name is deliberately functional and carries no VSTest/TestCase/ObjectModel naming.
  • TestMethodFilter stays as the single VSTest translation bridge (mirroring how Phase 1/5 left their AdapterMessageLoggerExtensions / TestResultRecorderExtensions bridges in PlatformServices). It gains GetTestElementFilter(IDiscoveryContext?, IAdapterMessageLogger, out bool filterHasError), which parses the VSTest filter exactly as before via the existing GetFilterExpression and wraps the result in a private TestElementFilter : ITestElementFilter. That wrapper is the lone place that translates the neutral element to a VSTest TestCase (element.ToTestCase()) and reuses ITestCaseFilterExpression.MatchTestCase + PropertyValueProvider.
  • Consumers routed through the neutral filter:
    • Discovery: UnitTestDiscoverer.SendTestCases now uses ITestElementFilter? + filter.Matches(element).
    • Execution: TestExecutionManager.MatchTestFilter / ExecuteTestsInSourceAsync now use ITestElementFilter? + filter.Matches(test.ToUnitTestElementWithUpdatedSource(source)).
      Neither consumer references ITestCaseFilterExpression / MatchTestCase / TestProperty anymore.
  • Unit tests added in TestMethodFilterTests.cs for the new wrapper (null context, no-filter context, match/non-match, parse-error → filterHasError).

No behavior change

Pure refactor. The supported-property set, the discovery-context vs run-context code paths, the parse-error handling, and the filterHasError bail-out semantics are preserved exactly:

  • Discovery still enumerates the assembly and logs warnings before bailing on a filter parse error.
  • Execution still early-bails before creating the isolation UnitTestRunner.

One transparent nuance (surfaced during review): in the out-of-proc VSTest path, execution now evaluates the filter against TestCase → UnitTestElement → TestCase, i.e. the same reconstruction that produces the executed element, instead of the raw incoming TestCase. For every test discovered by this adapter the round-trip is identical for all supported filter properties (FullyQualifiedName, DisplayName, Id, TestCategory, Priority, ClassName, traits), so results are unchanged in practice — the only theoretical divergence would be a foreign/older-hash Id or a non-standard fully-qualified name, which the same-adapter discover→execute flow does not produce. Reusing ToTestCase() as the single translation point was pre-approved, and the behavior is now locked by the out-of-proc --filter regression guard described under Verification.

Remaining work (future phase)

  • Physically move TestMethodFilter (the VSTest parser) up into the adapter layer (MSTest.TestAdapter), building the filter at the adapter boundary and passing ITestElementFilter? into the PlatformServices entry points. That was intentionally deferred here because preserving the different per-side filterHasError semantics byte-for-byte would require threading a nullable filter + error flag through many discovery/execution signatures — an awkward change for a transitional step.
  • Let the filter operate on UnitTestElement end-to-end to avoid the transitional double ToTestCase() conversion in filtered discovery.

Verification

  • .\build.cmd -c Debug — green across all TFMs (net462, net8.0, net9.0, UWP, WinUI), 0 warnings / 0 errors.
  • MSTestAdapter.PlatformServices.UnitTests (net8.0): 893 passed.
  • MSTestAdapter.UnitTests (net8.0): 21 passed.
  • Out-of-proc --filter regression guard added — new MSTest.IntegrationTests/TestCaseFilteringTests discovers then runs the DiscoverInternalsProject asset and asserts that filtering by both FullyQualifiedName and Id selects exactly the target test, in discovery and execution. The execution tests clear TestCase.LocalExtensionData to force the out-of-proc TestCase → UnitTestElement → TestCase reconstruction path this PR re-routes, so a future change cannot silently break --filter. All 4 pass; full MSTest.IntegrationTests suite green (47 passed, 1 pre-existing skip).
  • Ran the expert-reviewer agent on both the refactor and the regression guard; no blocking/major findings.

⚠️ Stacked PR

This is a stacked PR on top of:

Its base branch is dev/amauryleve/vstest-decoupling-base, an integration branch that combines #9548 + #9550 (filtering touches TestMethodFilter.cs from Phase 1 and TestExecutionManager.Runner.cs from Phase 5). Until those merge, this PR's diff will include their changes. Please review/merge this after #9548 and #9550.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…rm-agnostic effort)
Introduce a neutral ITestElementFilter abstraction so the PlatformServices
discovery and execution pipelines filter over the neutral UnitTestElement model
instead of the VSTest filter object model (ITestCaseFilterExpression,
TestProperty, IRunContext.GetTestCaseFilter, MatchTestCase).
TestMethodFilter remains the single VSTest translation bridge: it now also
exposes GetTestElementFilter, wrapping the parsed ITestCaseFilterExpression in a
private ITestElementFilter that reuses UnitTestElement.ToTestCase() +
MatchTestCase + PropertyValueProvider. UnitTestDiscoverer.SendTestCases and
TestExecutionManager (MatchTestFilter / ExecuteTestsInSourceAsync) route through
the neutral filter; the filterHasError bail-out semantics are preserved exactly.
Pure refactor: supported-property set, discovery-vs-run-context paths, parse
error handling, and every match result are unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds integration tests (MSTest.IntegrationTests/TestCaseFilteringTests) that lock
the end-to-end discover-then-run behavior of the neutral ITestElementFilter for
FullyQualifiedName and Id filters. The execution tests clear TestCase
LocalExtensionData to force the out-of-proc reconstruction path
(TestCase -> UnitTestElement -> TestCase) that the filtering refactor re-routes,
then assert exactly the target test is selected and executed.
Adds a RunTestsAsync(testCases, testCaseFilter) overload to CLITestBase that runs
execution through a filter-carrying IRunContext, and promotes the shared
InternalRunSettings nested type.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 3, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort) - #9555

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic
Jul 3, 2026
Merged

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort)#9555
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jul 2, 2026

Copy link
Copy Markdown
Member

Why

Part of the multi-PR effort to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). This is Phase 2: test filtering.

Today the PlatformServices discovery and execution pipelines filter tests using the VSTest filter object model directly: ITestCaseFilterExpression, TestProperty, IRunContext.GetTestCaseFilter, and ITestCaseFilterExpression.MatchTestCase. This phase decouples the filtering consumers from those types so filtering is expressed as a neutral predicate over the neutral UnitTestElement model.

What

  • New neutral abstractionITestElementFilter { bool Matches(UnitTestElement testElement); } in Interfaces/ITestElementFilter.cs (namespace …PlatformServices.Interface). A null filter means "no filter" → every element is included. The name is deliberately functional and carries no VSTest/TestCase/ObjectModel naming.
  • TestMethodFilter stays as the single VSTest translation bridge (mirroring how Phase 1/5 left their AdapterMessageLoggerExtensions / TestResultRecorderExtensions bridges in PlatformServices). It gains GetTestElementFilter(IDiscoveryContext?, IAdapterMessageLogger, out bool filterHasError), which parses the VSTest filter exactly as before via the existing GetFilterExpression and wraps the result in a private TestElementFilter : ITestElementFilter. That wrapper is the lone place that translates the neutral element to a VSTest TestCase (element.ToTestCase()) and reuses ITestCaseFilterExpression.MatchTestCase + PropertyValueProvider.
  • Consumers routed through the neutral filter:
    • Discovery: UnitTestDiscoverer.SendTestCases now uses ITestElementFilter? + filter.Matches(element).
    • Execution: TestExecutionManager.MatchTestFilter / ExecuteTestsInSourceAsync now use ITestElementFilter? + filter.Matches(test.ToUnitTestElementWithUpdatedSource(source)).
      Neither consumer references ITestCaseFilterExpression / MatchTestCase / TestProperty anymore.
  • Unit tests added in TestMethodFilterTests.cs for the new wrapper (null context, no-filter context, match/non-match, parse-error → filterHasError).

No behavior change

Pure refactor. The supported-property set, the discovery-context vs run-context code paths, the parse-error handling, and the filterHasError bail-out semantics are preserved exactly:

  • Discovery still enumerates the assembly and logs warnings before bailing on a filter parse error.
  • Execution still early-bails before creating the isolation UnitTestRunner.

One transparent nuance (surfaced during review): in the out-of-proc VSTest path, execution now evaluates the filter against TestCase → UnitTestElement → TestCase, i.e. the same reconstruction that produces the executed element, instead of the raw incoming TestCase. For every test discovered by this adapter the round-trip is identical for all supported filter properties (FullyQualifiedName, DisplayName, Id, TestCategory, Priority, ClassName, traits), so results are unchanged in practice — the only theoretical divergence would be a foreign/older-hash Id or a non-standard fully-qualified name, which the same-adapter discover→execute flow does not produce. Reusing ToTestCase() as the single translation point was pre-approved, and the behavior is now locked by the out-of-proc --filter regression guard described under Verification.

Remaining work (future phase)

  • Physically move TestMethodFilter (the VSTest parser) up into the adapter layer (MSTest.TestAdapter), building the filter at the adapter boundary and passing ITestElementFilter? into the PlatformServices entry points. That was intentionally deferred here because preserving the different per-side filterHasError semantics byte-for-byte would require threading a nullable filter + error flag through many discovery/execution signatures — an awkward change for a transitional step.
  • Let the filter operate on UnitTestElement end-to-end to avoid the transitional double ToTestCase() conversion in filtered discovery.

Verification

  • .\build.cmd -c Debug — green across all TFMs (net462, net8.0, net9.0, UWP, WinUI), 0 warnings / 0 errors.
  • MSTestAdapter.PlatformServices.UnitTests (net8.0): 893 passed.
  • MSTestAdapter.UnitTests (net8.0): 21 passed.
  • Out-of-proc --filter regression guard added — new MSTest.IntegrationTests/TestCaseFilteringTests discovers then runs the DiscoverInternalsProject asset and asserts that filtering by both FullyQualifiedName and Id selects exactly the target test, in discovery and execution. The execution tests clear TestCase.LocalExtensionData to force the out-of-proc TestCase → UnitTestElement → TestCase reconstruction path this PR re-routes, so a future change cannot silently break --filter. All 4 pass; full MSTest.IntegrationTests suite green (47 passed, 1 pre-existing skip).
  • Ran the expert-reviewer agent on both the refactor and the regression guard; no blocking/major findings.

⚠️ Stacked PR

This is a stacked PR on top of:

Its base branch is dev/amauryleve/vstest-decoupling-base, an integration branch that combines #9548 + #9550 (filtering touches TestMethodFilter.cs from Phase 1 and TestExecutionManager.Runner.cs from Phase 5). Until those merge, this PR's diff will include their changes. Please review/merge this after #9548 and #9550.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…rm-agnostic effort)
Introduce a neutral ITestElementFilter abstraction so the PlatformServices
discovery and execution pipelines filter over the neutral UnitTestElement model
instead of the VSTest filter object model (ITestCaseFilterExpression,
TestProperty, IRunContext.GetTestCaseFilter, MatchTestCase).
TestMethodFilter remains the single VSTest translation bridge: it now also
exposes GetTestElementFilter, wrapping the parsed ITestCaseFilterExpression in a
private ITestElementFilter that reuses UnitTestElement.ToTestCase() +
MatchTestCase + PropertyValueProvider. UnitTestDiscoverer.SendTestCases and
TestExecutionManager (MatchTestFilter / ExecuteTestsInSourceAsync) route through
the neutral filter; the filterHasError bail-out semantics are preserved exactly.
Pure refactor: supported-property set, discovery-vs-run-context paths, parse
error handling, and every match result are unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds integration tests (MSTest.IntegrationTests/TestCaseFilteringTests) that lock
the end-to-end discover-then-run behavior of the neutral ITestElementFilter for
FullyQualifiedName and Id filters. The execution tests clear TestCase
LocalExtensionData to force the out-of-proc reconstruction path
(TestCase -> UnitTestElement -> TestCase) that the filtering refactor re-routes,
then assert exactly the target test is selected and executed.
Adds a RunTestsAsync(testCases, testCaseFilter) overload to CLITestBase that runs
execution through a filter-carrying IRunContext, and promotes the shared
InternalRunSettings nested type.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 3, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort) - #9555

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic
Jul 3, 2026
Merged

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort)#9555
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jul 2, 2026

Copy link
Copy Markdown
Member

Why

Part of the multi-PR effort to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). This is Phase 2: test filtering.

Today the PlatformServices discovery and execution pipelines filter tests using the VSTest filter object model directly: ITestCaseFilterExpression, TestProperty, IRunContext.GetTestCaseFilter, and ITestCaseFilterExpression.MatchTestCase. This phase decouples the filtering consumers from those types so filtering is expressed as a neutral predicate over the neutral UnitTestElement model.

What

  • New neutral abstractionITestElementFilter { bool Matches(UnitTestElement testElement); } in Interfaces/ITestElementFilter.cs (namespace …PlatformServices.Interface). A null filter means "no filter" → every element is included. The name is deliberately functional and carries no VSTest/TestCase/ObjectModel naming.
  • TestMethodFilter stays as the single VSTest translation bridge (mirroring how Phase 1/5 left their AdapterMessageLoggerExtensions / TestResultRecorderExtensions bridges in PlatformServices). It gains GetTestElementFilter(IDiscoveryContext?, IAdapterMessageLogger, out bool filterHasError), which parses the VSTest filter exactly as before via the existing GetFilterExpression and wraps the result in a private TestElementFilter : ITestElementFilter. That wrapper is the lone place that translates the neutral element to a VSTest TestCase (element.ToTestCase()) and reuses ITestCaseFilterExpression.MatchTestCase + PropertyValueProvider.
  • Consumers routed through the neutral filter:
    • Discovery: UnitTestDiscoverer.SendTestCases now uses ITestElementFilter? + filter.Matches(element).
    • Execution: TestExecutionManager.MatchTestFilter / ExecuteTestsInSourceAsync now use ITestElementFilter? + filter.Matches(test.ToUnitTestElementWithUpdatedSource(source)).
      Neither consumer references ITestCaseFilterExpression / MatchTestCase / TestProperty anymore.
  • Unit tests added in TestMethodFilterTests.cs for the new wrapper (null context, no-filter context, match/non-match, parse-error → filterHasError).

No behavior change

Pure refactor. The supported-property set, the discovery-context vs run-context code paths, the parse-error handling, and the filterHasError bail-out semantics are preserved exactly:

  • Discovery still enumerates the assembly and logs warnings before bailing on a filter parse error.
  • Execution still early-bails before creating the isolation UnitTestRunner.

One transparent nuance (surfaced during review): in the out-of-proc VSTest path, execution now evaluates the filter against TestCase → UnitTestElement → TestCase, i.e. the same reconstruction that produces the executed element, instead of the raw incoming TestCase. For every test discovered by this adapter the round-trip is identical for all supported filter properties (FullyQualifiedName, DisplayName, Id, TestCategory, Priority, ClassName, traits), so results are unchanged in practice — the only theoretical divergence would be a foreign/older-hash Id or a non-standard fully-qualified name, which the same-adapter discover→execute flow does not produce. Reusing ToTestCase() as the single translation point was pre-approved, and the behavior is now locked by the out-of-proc --filter regression guard described under Verification.

Remaining work (future phase)

  • Physically move TestMethodFilter (the VSTest parser) up into the adapter layer (MSTest.TestAdapter), building the filter at the adapter boundary and passing ITestElementFilter? into the PlatformServices entry points. That was intentionally deferred here because preserving the different per-side filterHasError semantics byte-for-byte would require threading a nullable filter + error flag through many discovery/execution signatures — an awkward change for a transitional step.
  • Let the filter operate on UnitTestElement end-to-end to avoid the transitional double ToTestCase() conversion in filtered discovery.

Verification

  • .\build.cmd -c Debug — green across all TFMs (net462, net8.0, net9.0, UWP, WinUI), 0 warnings / 0 errors.
  • MSTestAdapter.PlatformServices.UnitTests (net8.0): 893 passed.
  • MSTestAdapter.UnitTests (net8.0): 21 passed.
  • Out-of-proc --filter regression guard added — new MSTest.IntegrationTests/TestCaseFilteringTests discovers then runs the DiscoverInternalsProject asset and asserts that filtering by both FullyQualifiedName and Id selects exactly the target test, in discovery and execution. The execution tests clear TestCase.LocalExtensionData to force the out-of-proc TestCase → UnitTestElement → TestCase reconstruction path this PR re-routes, so a future change cannot silently break --filter. All 4 pass; full MSTest.IntegrationTests suite green (47 passed, 1 pre-existing skip).
  • Ran the expert-reviewer agent on both the refactor and the regression guard; no blocking/major findings.

⚠️ Stacked PR

This is a stacked PR on top of:

Its base branch is dev/amauryleve/vstest-decoupling-base, an integration branch that combines #9548 + #9550 (filtering touches TestMethodFilter.cs from Phase 1 and TestExecutionManager.Runner.cs from Phase 5). Until those merge, this PR's diff will include their changes. Please review/merge this after #9548 and #9550.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…rm-agnostic effort)
Introduce a neutral ITestElementFilter abstraction so the PlatformServices
discovery and execution pipelines filter over the neutral UnitTestElement model
instead of the VSTest filter object model (ITestCaseFilterExpression,
TestProperty, IRunContext.GetTestCaseFilter, MatchTestCase).
TestMethodFilter remains the single VSTest translation bridge: it now also
exposes GetTestElementFilter, wrapping the parsed ITestCaseFilterExpression in a
private ITestElementFilter that reuses UnitTestElement.ToTestCase() +
MatchTestCase + PropertyValueProvider. UnitTestDiscoverer.SendTestCases and
TestExecutionManager (MatchTestFilter / ExecuteTestsInSourceAsync) route through
the neutral filter; the filterHasError bail-out semantics are preserved exactly.
Pure refactor: supported-property set, discovery-vs-run-context paths, parse
error handling, and every match result are unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds integration tests (MSTest.IntegrationTests/TestCaseFilteringTests) that lock
the end-to-end discover-then-run behavior of the neutral ITestElementFilter for
FullyQualifiedName and Id filters. The execution tests clear TestCase
LocalExtensionData to force the out-of-proc reconstruction path
(TestCase -> UnitTestElement -> TestCase) that the filtering refactor re-routes,
then assert exactly the target test is selected and executed.
Adds a RunTestsAsync(testCases, testCaseFilter) overload to CLITestBase that runs
execution through a filter-carrying IRunContext, and promotes the shared
InternalRunSettings nested type.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 3, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort) - #9555

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic
Jul 3, 2026
Merged

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort)#9555
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jul 2, 2026

Copy link
Copy Markdown
Member

Why

Part of the multi-PR effort to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). This is Phase 2: test filtering.

Today the PlatformServices discovery and execution pipelines filter tests using the VSTest filter object model directly: ITestCaseFilterExpression, TestProperty, IRunContext.GetTestCaseFilter, and ITestCaseFilterExpression.MatchTestCase. This phase decouples the filtering consumers from those types so filtering is expressed as a neutral predicate over the neutral UnitTestElement model.

What

  • New neutral abstractionITestElementFilter { bool Matches(UnitTestElement testElement); } in Interfaces/ITestElementFilter.cs (namespace …PlatformServices.Interface). A null filter means "no filter" → every element is included. The name is deliberately functional and carries no VSTest/TestCase/ObjectModel naming.
  • TestMethodFilter stays as the single VSTest translation bridge (mirroring how Phase 1/5 left their AdapterMessageLoggerExtensions / TestResultRecorderExtensions bridges in PlatformServices). It gains GetTestElementFilter(IDiscoveryContext?, IAdapterMessageLogger, out bool filterHasError), which parses the VSTest filter exactly as before via the existing GetFilterExpression and wraps the result in a private TestElementFilter : ITestElementFilter. That wrapper is the lone place that translates the neutral element to a VSTest TestCase (element.ToTestCase()) and reuses ITestCaseFilterExpression.MatchTestCase + PropertyValueProvider.
  • Consumers routed through the neutral filter:
    • Discovery: UnitTestDiscoverer.SendTestCases now uses ITestElementFilter? + filter.Matches(element).
    • Execution: TestExecutionManager.MatchTestFilter / ExecuteTestsInSourceAsync now use ITestElementFilter? + filter.Matches(test.ToUnitTestElementWithUpdatedSource(source)).
      Neither consumer references ITestCaseFilterExpression / MatchTestCase / TestProperty anymore.
  • Unit tests added in TestMethodFilterTests.cs for the new wrapper (null context, no-filter context, match/non-match, parse-error → filterHasError).

No behavior change

Pure refactor. The supported-property set, the discovery-context vs run-context code paths, the parse-error handling, and the filterHasError bail-out semantics are preserved exactly:

  • Discovery still enumerates the assembly and logs warnings before bailing on a filter parse error.
  • Execution still early-bails before creating the isolation UnitTestRunner.

One transparent nuance (surfaced during review): in the out-of-proc VSTest path, execution now evaluates the filter against TestCase → UnitTestElement → TestCase, i.e. the same reconstruction that produces the executed element, instead of the raw incoming TestCase. For every test discovered by this adapter the round-trip is identical for all supported filter properties (FullyQualifiedName, DisplayName, Id, TestCategory, Priority, ClassName, traits), so results are unchanged in practice — the only theoretical divergence would be a foreign/older-hash Id or a non-standard fully-qualified name, which the same-adapter discover→execute flow does not produce. Reusing ToTestCase() as the single translation point was pre-approved, and the behavior is now locked by the out-of-proc --filter regression guard described under Verification.

Remaining work (future phase)

  • Physically move TestMethodFilter (the VSTest parser) up into the adapter layer (MSTest.TestAdapter), building the filter at the adapter boundary and passing ITestElementFilter? into the PlatformServices entry points. That was intentionally deferred here because preserving the different per-side filterHasError semantics byte-for-byte would require threading a nullable filter + error flag through many discovery/execution signatures — an awkward change for a transitional step.
  • Let the filter operate on UnitTestElement end-to-end to avoid the transitional double ToTestCase() conversion in filtered discovery.

Verification

  • .\build.cmd -c Debug — green across all TFMs (net462, net8.0, net9.0, UWP, WinUI), 0 warnings / 0 errors.
  • MSTestAdapter.PlatformServices.UnitTests (net8.0): 893 passed.
  • MSTestAdapter.UnitTests (net8.0): 21 passed.
  • Out-of-proc --filter regression guard added — new MSTest.IntegrationTests/TestCaseFilteringTests discovers then runs the DiscoverInternalsProject asset and asserts that filtering by both FullyQualifiedName and Id selects exactly the target test, in discovery and execution. The execution tests clear TestCase.LocalExtensionData to force the out-of-proc TestCase → UnitTestElement → TestCase reconstruction path this PR re-routes, so a future change cannot silently break --filter. All 4 pass; full MSTest.IntegrationTests suite green (47 passed, 1 pre-existing skip).
  • Ran the expert-reviewer agent on both the refactor and the regression guard; no blocking/major findings.

⚠️ Stacked PR

This is a stacked PR on top of:

Its base branch is dev/amauryleve/vstest-decoupling-base, an integration branch that combines #9548 + #9550 (filtering touches TestMethodFilter.cs from Phase 1 and TestExecutionManager.Runner.cs from Phase 5). Until those merge, this PR's diff will include their changes. Please review/merge this after #9548 and #9550.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…rm-agnostic effort)
Introduce a neutral ITestElementFilter abstraction so the PlatformServices
discovery and execution pipelines filter over the neutral UnitTestElement model
instead of the VSTest filter object model (ITestCaseFilterExpression,
TestProperty, IRunContext.GetTestCaseFilter, MatchTestCase).
TestMethodFilter remains the single VSTest translation bridge: it now also
exposes GetTestElementFilter, wrapping the parsed ITestCaseFilterExpression in a
private ITestElementFilter that reuses UnitTestElement.ToTestCase() +
MatchTestCase + PropertyValueProvider. UnitTestDiscoverer.SendTestCases and
TestExecutionManager (MatchTestFilter / ExecuteTestsInSourceAsync) route through
the neutral filter; the filterHasError bail-out semantics are preserved exactly.
Pure refactor: supported-property set, discovery-vs-run-context paths, parse
error handling, and every match result are unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds integration tests (MSTest.IntegrationTests/TestCaseFilteringTests) that lock
the end-to-end discover-then-run behavior of the neutral ITestElementFilter for
FullyQualifiedName and Id filters. The execution tests clear TestCase
LocalExtensionData to force the out-of-proc reconstruction path
(TestCase -> UnitTestElement -> TestCase) that the filtering refactor re-routes,
then assert exactly the target test is selected and executed.
Adds a RunTestsAsync(testCases, testCaseFilter) overload to CLITestBase that runs
execution through a filter-carrying IRunContext, and promotes the shared
InternalRunSettings nested type.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 3, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort) - #9555

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic
Jul 3, 2026
Merged

Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort)#9555
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-basefrom
dev/amauryleve/abstract-vstest-filtering-platformservic

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jul 2, 2026

Copy link
Copy Markdown
Member

Why

Part of the multi-PR effort to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). This is Phase 2: test filtering.

Today the PlatformServices discovery and execution pipelines filter tests using the VSTest filter object model directly: ITestCaseFilterExpression, TestProperty, IRunContext.GetTestCaseFilter, and ITestCaseFilterExpression.MatchTestCase. This phase decouples the filtering consumers from those types so filtering is expressed as a neutral predicate over the neutral UnitTestElement model.

What

  • New neutral abstractionITestElementFilter { bool Matches(UnitTestElement testElement); } in Interfaces/ITestElementFilter.cs (namespace …PlatformServices.Interface). A null filter means "no filter" → every element is included. The name is deliberately functional and carries no VSTest/TestCase/ObjectModel naming.
  • TestMethodFilter stays as the single VSTest translation bridge (mirroring how Phase 1/5 left their AdapterMessageLoggerExtensions / TestResultRecorderExtensions bridges in PlatformServices). It gains GetTestElementFilter(IDiscoveryContext?, IAdapterMessageLogger, out bool filterHasError), which parses the VSTest filter exactly as before via the existing GetFilterExpression and wraps the result in a private TestElementFilter : ITestElementFilter. That wrapper is the lone place that translates the neutral element to a VSTest TestCase (element.ToTestCase()) and reuses ITestCaseFilterExpression.MatchTestCase + PropertyValueProvider.
  • Consumers routed through the neutral filter:
    • Discovery: UnitTestDiscoverer.SendTestCases now uses ITestElementFilter? + filter.Matches(element).
    • Execution: TestExecutionManager.MatchTestFilter / ExecuteTestsInSourceAsync now use ITestElementFilter? + filter.Matches(test.ToUnitTestElementWithUpdatedSource(source)).
      Neither consumer references ITestCaseFilterExpression / MatchTestCase / TestProperty anymore.
  • Unit tests added in TestMethodFilterTests.cs for the new wrapper (null context, no-filter context, match/non-match, parse-error → filterHasError).

No behavior change

Pure refactor. The supported-property set, the discovery-context vs run-context code paths, the parse-error handling, and the filterHasError bail-out semantics are preserved exactly:

  • Discovery still enumerates the assembly and logs warnings before bailing on a filter parse error.
  • Execution still early-bails before creating the isolation UnitTestRunner.

One transparent nuance (surfaced during review): in the out-of-proc VSTest path, execution now evaluates the filter against TestCase → UnitTestElement → TestCase, i.e. the same reconstruction that produces the executed element, instead of the raw incoming TestCase. For every test discovered by this adapter the round-trip is identical for all supported filter properties (FullyQualifiedName, DisplayName, Id, TestCategory, Priority, ClassName, traits), so results are unchanged in practice — the only theoretical divergence would be a foreign/older-hash Id or a non-standard fully-qualified name, which the same-adapter discover→execute flow does not produce. Reusing ToTestCase() as the single translation point was pre-approved, and the behavior is now locked by the out-of-proc --filter regression guard described under Verification.

Remaining work (future phase)

  • Physically move TestMethodFilter (the VSTest parser) up into the adapter layer (MSTest.TestAdapter), building the filter at the adapter boundary and passing ITestElementFilter? into the PlatformServices entry points. That was intentionally deferred here because preserving the different per-side filterHasError semantics byte-for-byte would require threading a nullable filter + error flag through many discovery/execution signatures — an awkward change for a transitional step.
  • Let the filter operate on UnitTestElement end-to-end to avoid the transitional double ToTestCase() conversion in filtered discovery.

Verification

  • .\build.cmd -c Debug — green across all TFMs (net462, net8.0, net9.0, UWP, WinUI), 0 warnings / 0 errors.
  • MSTestAdapter.PlatformServices.UnitTests (net8.0): 893 passed.
  • MSTestAdapter.UnitTests (net8.0): 21 passed.
  • Out-of-proc --filter regression guard added — new MSTest.IntegrationTests/TestCaseFilteringTests discovers then runs the DiscoverInternalsProject asset and asserts that filtering by both FullyQualifiedName and Id selects exactly the target test, in discovery and execution. The execution tests clear TestCase.LocalExtensionData to force the out-of-proc TestCase → UnitTestElement → TestCase reconstruction path this PR re-routes, so a future change cannot silently break --filter. All 4 pass; full MSTest.IntegrationTests suite green (47 passed, 1 pre-existing skip).
  • Ran the expert-reviewer agent on both the refactor and the regression guard; no blocking/major findings.

⚠️ Stacked PR

This is a stacked PR on top of:

Its base branch is dev/amauryleve/vstest-decoupling-base, an integration branch that combines #9548 + #9550 (filtering touches TestMethodFilter.cs from Phase 1 and TestExecutionManager.Runner.cs from Phase 5). Until those merge, this PR's diff will include their changes. Please review/merge this after #9548 and #9550.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…rm-agnostic effort)
Introduce a neutral ITestElementFilter abstraction so the PlatformServices
discovery and execution pipelines filter over the neutral UnitTestElement model
instead of the VSTest filter object model (ITestCaseFilterExpression,
TestProperty, IRunContext.GetTestCaseFilter, MatchTestCase).
TestMethodFilter remains the single VSTest translation bridge: it now also
exposes GetTestElementFilter, wrapping the parsed ITestCaseFilterExpression in a
private ITestElementFilter that reuses UnitTestElement.ToTestCase() +
MatchTestCase + PropertyValueProvider. UnitTestDiscoverer.SendTestCases and
TestExecutionManager (MatchTestFilter / ExecuteTestsInSourceAsync) route through
the neutral filter; the filterHasError bail-out semantics are preserved exactly.
Pure refactor: supported-property set, discovery-vs-run-context paths, parse
error handling, and every match result are unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds integration tests (MSTest.IntegrationTests/TestCaseFilteringTests) that lock
the end-to-end discover-then-run behavior of the neutral ITestElementFilter for
FullyQualifiedName and Id filters. The execution tests clear TestCase
LocalExtensionData to force the out-of-proc reconstruction path
(TestCase -> UnitTestElement -> TestCase) that the filtering refactor re-routes,
then assert exactly the target test is selected and executed.
Adds a RunTestsAsync(testCases, testCaseFilter) overload to CLITestBase that runs
execution through a filter-carrying IRunContext, and promotes the shared
InternalRunSettings nested type.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 3, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink@0101