Skip to content

Honor /TestCaseFilter on the MTP execution path - #16282

Draft
Azat Mukhametshin (azat-msft) wants to merge 7 commits into
microsoft:mainfrom
azat-msft:fix/mtp-testcasefilter
Draft

Honor /TestCaseFilter on the MTP execution path#16282
Azat Mukhametshin (azat-msft) wants to merge 7 commits into
microsoft:mainfrom
azat-msft:fix/mtp-testcasefilter

Conversation

@azat-msft

Copy link
Copy Markdown
Member

Problem

On the vstest.console path that runs a Microsoft.Testing.Platform (MTP) app as its own testhost (#16201), /TestCaseFilter:<expr> was silently ignored during execution — every test ran regardless of the filter. /Tests (specific names) worked, but any filter expression was dropped.

Root cause

A /TestCaseFilter run reaches MtpProxyExecutionManager as sources with no specific tests. BuildWork then yields the source with null tests and RunSource runs everything. MTP has no notion of the vstest filter expression, and nothing translated it, so the filter had no effect.

Fix

When a TestCaseFilter is present and there are no specific tests, discover the source over MTP, evaluate the expression against the discovered tests with TestCaseFilterExpression (traits + boolean operators, same semantics as the classic path), and run only the matching test-node uids. A non-matching filter now runs zero tests instead of the whole suite. /Tests is unchanged.

Test

New acceptance test RunMtpApplicationHonorsTestCaseFilter runs the MTP app with /TestCaseFilter:"DisplayName~TestPasses" and asserts only the two matching tests run (2/0/0) instead of the full 2/1/1 suite.

Validation

  • RunMtpApplicationHonorsTestCaseFilter: 2/2 matrix cases pass (fails on main — filter ignored, whole suite runs).
  • Release build of Microsoft.TestPlatform.CrossPlatEngine and the acceptance project: clean.
  • Verified end-to-end against a real TUnit MTP app: ~BasicTests -> 2 (was 33); non-matching -> 0.

On the MTP-as-testhost path a /TestCaseFilter run arrives as sources with no
specific tests, and MtpProxyExecutionManager.StartTestRun ran every test because
MTP has no notion of the vstest filter expression. Now, when a TestCaseFilter is
present, the source is discovered over MTP, the expression is evaluated against
the discovered tests (traits and boolean operators included via
TestCaseFilterExpression) and only the matching test-node uids are run. A
non-matching filter runs no tests instead of the whole suite.

Adds acceptance test RunMtpApplicationHonorsTestCaseFilter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fb7de7a6-b8a1-4ebb-8266-5c5a948f245c
Copilot AI review requested due to automatic review settings July 14, 2026 10:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the Microsoft.Testing.Platform (MTP) execution path in vstest.console so /TestCaseFilter:<expr> is honored during execution (previously it was silently ignored and all tests ran). It does so by performing an MTP discovery pass when a filter is present (and no explicit /Tests list is provided), evaluating the vstest filter expression against discovered tests, and then executing only the matching test-node UIDs.

Changes:

  • Add a filtered execution path in MtpProxyExecutionManager that discovers tests and applies TestCaseFilterExpression to select which tests to run.
  • Add an acceptance integration test validating that an MTP run honors /TestCaseFilter and only executes matching tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
test/Microsoft.TestPlatform.Acceptance.IntegrationTests/MtpUnderVstestTests.cs Adds an acceptance test verifying /TestCaseFilter limits executed tests for an MTP app.
src/Microsoft.TestPlatform.CrossPlatEngine/Client/MTP/MtpProxyExecutionManager.cs Implements discovery + filter evaluation for MTP execution when /TestCaseFilter is provided without specific tests.

Comment thread src/Microsoft.TestPlatform.CrossPlatEngine/Client/MTP/MtpProxyExecutionManager.cs Outdated
Comment thread src/Microsoft.TestPlatform.CrossPlatEngine/Client/MTP/MtpProxyExecutionManager.cs Outdated
Comment thread src/Microsoft.TestPlatform.CrossPlatEngine/Client/MTP/MtpProxyExecutionManager.cs Outdated
- BuildPropertyProvider now exposes every property carried on the test case (by its
  filter label) plus the Name/DisplayName aliases and traits, instead of a hard-coded
  FullyQualifiedName/DisplayName/Name subset. Filters on other properties (e.g.
  Source) previously evaluated to "no value" and silently matched nothing; they now
  behave like the classic path.
- DiscoverSourceTests waits for the discovery-completion sentinel honoring the
  cancellation token and logs a warning on timeout, so a /TestCaseFilter is not
  silently evaluated against an incomplete discovery set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fb7de7a6-b8a1-4ebb-8266-5c5a948f245c
@azat-msft

Copy link
Copy Markdown
Member Author

Addressed the review feedback (commit 140a078):

  • BuildPropertyProvider now exposes every property carried on the test case (by its filter label) plus the Name/DisplayName aliases and traits, instead of a hard-coded FullyQualifiedName/DisplayName/Name subset. Filters on other properties (e.g. Source) previously evaluated to "no value" and silently matched nothing; they are now honored like the classic path (verified: Source~MtpMSTestProject matches all tests, Source~NoSuchSource matches none).
  • DiscoverSourceTests now waits for the discovery-completion sentinel honoring the cancellation token and logs a warning on timeout, so a /TestCaseFilter is not silently evaluated against an incomplete discovery set.

Verified: RunMtpApplicationHonorsTestCaseFilter passes 2/2 matrix cases.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

Comment on lines +105 to +106
// The filter matched nothing for this source: run no tests (rather than the whole
// suite). An empty, non-null list flows through as "run exactly these zero tests".
}
};

connection.Start(source, EnvironmentVariables, MtpClientHelpers.GetConnectionTimeout());
Comment on lines +463 to +465
// Wait for the discovery-completion sentinel, honoring cancellation. If it does not arrive within
// the window the discovered set may be incomplete, which would make a /TestCaseFilter run execute
// the wrong tests, so make that explicit in the logs instead of failing silently.
Comment on lines +468 to +470
EqtTrace.Warning(
"MtpProxyExecutionManager.DiscoverSourceTests: discovery for '{0}' did not signal completion within the wait window; a /TestCaseFilter may be evaluated against an incomplete discovery set.",
source);
Copilot AI review requested due to automatic review settings July 14, 2026 12:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment on lines +113 to +115
// The filter matched nothing for this source: run no tests (rather than the whole
// suite). An empty, non-null list flows through as "run exactly these zero tests".
if (testsToRun.Count == 0)
Comment on lines +436 to +438
var discovered = new List<TestCase>();
var completed = new ManualResetEventSlim(false);

Comment on lines +471 to +475
// Wait for the discovery-completion sentinel, honoring cancellation. If it does not arrive within
// the window the discovered set may be incomplete, which would make a /TestCaseFilter run execute
// the wrong tests, so make that explicit in the logs instead of failing silently.
if (!completed.Wait(TimeSpan.FromSeconds(3), _cancellationTokenSource.Token))
{
The previous comment claimed an empty, non-null test list would flow through as
"run exactly these zero tests". That is misleading: the continue skips RunSource,
and RunSource cannot express an empty selection (it omits the MTP tests filter when
the list is empty, which MTP treats as "run every test"). Reword the comment so a
future change does not remove the continue and accidentally run the whole suite for
a non-matching filter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fb7de7a6-b8a1-4ebb-8266-5c5a948f245c
Copilot AI review requested due to automatic review settings July 14, 2026 12:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

private List<TestCase> DiscoverSourceTests(string source, IInternalTestRunEventsHandler eventHandler)
{
var discovered = new List<TestCase>();
var completed = new ManualResetEventSlim(false);
Comment on lines +564 to +565
}

@azat-msft

Copy link
Copy Markdown
Member Author

No more reviews are needed, the PR is a WIP

- Start the discovery pass with no environment variables so the
  data-collector profiler variables (execution-only) are not injected,
  mirroring MtpProxyDiscoveryManager.
- Dispose the ManualResetEventSlim in DiscoverSourceTests via a using
  declaration to avoid leaking a wait handle.
- Reword the discovery-completion comment/warning to reflect the ordered
  stream guarantee: the discovered set is complete once the DiscoverTests
  response returns; the sentinel wait is a non-critical drain.
- Add an acceptance test asserting a non-matching /TestCaseFilter runs
  zero tests on the MTP path instead of the whole suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61d9a1c3-3c9f-4dab-8ba3-5baba8d55ed6
Copilot AI review requested due to automatic review settings July 14, 2026 13:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +432 to +436
/// <summary>
/// Runs an MTP discovery pass against <paramref name="source"/> and returns the discovered tests. Used
/// to resolve a <c>/TestCaseFilter</c> into a concrete set of tests to run by uid.
/// </summary>
private List<TestCase> DiscoverSourceTests(string source, IInternalTestRunEventsHandler eventHandler)
- RunMtpApplicationSurfacesPerTestStandardOutput asserted the old 4-test
  summary (2,1,1); the shared MtpMSTestProject asset gained a 5th test
  (RunSettingsEnvironmentVariableIsInjected), so the run is (3,1,1).
  This is a semantic collision between microsoft#16283 and microsoft#16284 that only
  surfaces when both are on main. Update the expected summary.
- Address review feedback: extract the near-identical MTP discovery
  routine from MtpProxyDiscoveryManager and MtpProxyExecutionManager into
  a single MtpClientHelpers.DiscoverSourceTests helper so the two paths
  cannot drift (timeouts, cancellation, sentinel drain, env-var policy).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61d9a1c3-3c9f-4dab-8ba3-5baba8d55ed6
Copilot AI review requested due to automatic review settings July 14, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment on lines +201 to +205
// /TestCaseFilter must scope an MTP run just like it does on the classic path. MTP has no notion of the
// vstest filter expression, so vstest.console discovers the app, evaluates the expression against the
// discovered tests and runs only the matching test-node uids. Before this the filter was silently
// ignored and the whole suite ran (2/1/1). The filter here selects only the two passing tests.
[TestMatrix(testHost: Target.Net)]
Comment on lines +226 to +229
// A /TestCaseFilter that matches nothing must run zero tests on the MTP path, not fall back to running
// the whole suite. This guards the regression the feature targets: before, a non-matching filter was
// silently ignored and every test ran (2/1/1). Here the filter matches no test, so nothing runs.
[TestMatrix(testHost: Target.Net)]
Comment on lines +504 to +505
return name => properties.TryGetValue(name, out List<string>? values) ? values.ToArray() : null;
}
BuildPropertyProvider now returns the single value directly (instead of a
one-element array) when a property has exactly one value, matching how the
classic TestCaseFilterExpression provider behaves. Also generalize the two
TestCaseFilter test comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61d9a1c3-3c9f-4dab-8ba3-5baba8d55ed6
Copilot AI review requested due to automatic review settings July 14, 2026 19:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment on lines +113 to +120
// The filter matched nothing for this source. Skip the source entirely: RunSource
// cannot express "run zero tests" — it only sends the MTP tests filter when the list
// has entries and otherwise omits it, which MTP treats as "run every test". So calling
// RunSource with an empty list would run the whole suite; the continue avoids that.
if (testsToRun.Count == 0)
{
continue;
}
Comment on lines +416 to +420
var filterExpression = new TestCaseFilterExpression(filterWrapper);

List<TestCase> discovered = MtpClientHelpers.DiscoverSourceTests(source, eventHandler.HandleLogMessage, _cancellationTokenSource.Token);

var matched = new List<TestCase>();
@nohwnd

Copy link
Copy Markdown
Member

feels like we are adding bunch of overhead by doing the discovery and then filtering, lets hold on on this one, ideally we would get official mtp client from MTP (by extracting the one in VS) and use it here, so we don't have to keep fixing the client over an over, working on that one.

@azat-msft
Azat Mukhametshin (azat-msft) marked this pull request as draft July 17, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants