Skip to content

Fix #9762: share RunSettings/TestRunParameters provider logic between MSTest adapter and VSTestBridge via dependency-free linked source#9775

Merged
Evangelink merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers
Jul 9, 2026
Merged

Fix #9762: share RunSettings/TestRunParameters provider logic between MSTest adapter and VSTestBridge via dependency-free linked source#9775
Evangelink merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers

Conversation

@Evangelink

@Evangelink Evangelink commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

The MSTest adapter's native Microsoft.Testing.Platform providers (src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/) contained near-identical copies of four VSTestBridge providers. Those copies were created deliberately during the "Native MTP integration" work (#9706/#9743/#9748) to remove the adapter's dependency on the VSTestBridge package, so re-adding a bridge reference is off the table.

This PR removes the duplicated logic the dependency-free way (issue recommendation #1): a single linked shared source file that both projects compile, with no new project dependency.

What changed

  • New shared file: src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs — an internal static class (namespace Microsoft.Testing.Extensions, matching the sibling shared helpers) holding the resource-independent logic:
    • CanReadFile(IFileSystem, string)
    • TryLoadRunSettingsAsync(ICommandLineOptions, IFileSystem, IEnvironment, string) — CLI option → TESTINGPLATFORM_EXPERIMENTAL_VSTEST_RUNSETTINGS content → TESTINGPLATFORM_VSTESTBRIDGE_RUNSETTINGS_FILE path resolution, with the existing NETCOREAPP vs non-core XDocument load branching preserved
    • HasEnvironmentVariables(XDocument)
    • ApplyEnvironmentVariables(XDocument, IEnvironmentVariables)
    • FindInvalidTestParameter(string[])
    • The whole type is wrapped in #if !WINDOWS_UWP (MTP types aren't available on the adapter's UWP TFMs, which is why the adapter providers are UWP-guarded) and carries [SuppressMessage("ApiDesign", "RS0030")].
  • Linked into both csprojs with explicit Link items (VSTestBridge: Helpers\...; adapter: Shared\...).
  • Refactored the providers to call the shared helper:
    • Both RunSettings CLI providers (RunSettingsCommandLineOptionsProvider / MSTestRunSettingsCommandLineOptionsProvider) — shared CanReadFile; each keeps its own ExistFile check and resource messages.
    • Both env-var providers (RunSettingsEnvironmentVariableProvider / MSTestRunSettingsEnvironmentVariableProvider) — shared TryLoadRunSettingsAsync + HasEnvironmentVariables + ApplyEnvironmentVariables, each passing its own option-name constant.
    • Both TestRunParameters providers — shared FindInvalidTestParameter, each formatting with its own resource string.
    • The TestCaseFilter pair has no real logic and was left untouched (no churn).

Decoupling preserved

No dependency on Microsoft.Testing.Extensions.VSTestBridge is reintroduced. The adapter and the bridge simply compile the same source file from src/Platform/SharedExtensionHelpers, exactly like the existing shared helpers in that folder. Each package still owns its own resource strings (PlatformAdapterResources vs ExtensionResources), option registration and UWP guards.

Drift resolved

The env-var providers had diverged: the adapter used string.IsNullOrEmpty while the bridge used RoslynString.IsNullOrEmpty. The logic now lives in one place and is unified on string.IsNullOrEmpty.

Behavior

No option-surface change: option names (settings, filter, test-parameter), descriptions, arities, validation messages and registration are unchanged. No resx/xlf/PublicAPI edits (text unchanged; new type is internal).

One intentional behavioral improvement in the shared CanReadFile: it now catches UnauthorizedAccessException in addition to IOException. Previously a --settings file that exists but is unreadable due to permissions would let the exception propagate out of ValidateOptionArgumentsAsync; it now surfaces the friendly RunsettingsFileCannotBeRead validation message. This is a strict superset of the prior behavior and is covered by a new unit test (RunSettingsOption_WhenFileCannotBeReadDueToPermissions_IsNotValid).

Validation

  • .\build.cmd -packsucceeded, 0 warnings / 0 errors; all shipping NuGets (incl. MSTest.TestAdapter and Microsoft.Testing.Extensions.VSTestBridge) produced.
  • Compiles cleanly on all target frameworks: VSTestBridge (net8.0, net9.0, netstandard2.0) and MSTest.TestAdapter (net462, net8.0, net9.0, net8.0-windows, net9.0-windows, uap10.0.16299).
  • Unit tests: Microsoft.Testing.Extensions.VSTestBridge.UnitTests45/45 passing (covers the RunSettings CLI — including the new permission-error branch — env-var and TestRunParameters providers).
  • Help/info acceptance tests — all pass with no expectation edits (confirms option text/behavior is unchanged):
    • Microsoft.Testing.Platform.Acceptance.IntegrationTestsHelpInfoTests 21/21, HelpInfoAllExtensionsTests 9/9
    • MSTest.Acceptance.IntegrationTestsHelpInfoTests 6/6

Fixes #9762.

…ked source

Extract the resource-independent logic duplicated between the MSTest
adapter's native Microsoft.Testing.Platform providers and the VSTestBridge
providers into a new dependency-free linked shared file
(src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs), linked
into both projects. This removes the near-identical copies without
reintroducing a dependency on Microsoft.Testing.Extensions.VSTestBridge.

- CanReadFile, TryLoadRunSettingsAsync, HasEnvironmentVariables,
  ApplyEnvironmentVariables and FindInvalidTestParameter now live once.
- Unifies the string.IsNullOrEmpty vs RoslynString.IsNullOrEmpty drift on
  string.IsNullOrEmpty.
- Each package keeps its own resource strings, option registration and
  UWP guards; the shared type is fully guarded by #if !WINDOWS_UWP.
- No public/behavioral change: option names, descriptions, arities and
  validation messages are unchanged. TestCaseFilter pair left untouched.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 09: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

This PR resolves issue #9762 (duplicate-code) by de-duplicating the RunSettings / TestRunParameters provider logic that the MSTest adapter's native Microsoft.Testing.Platform (MTP) providers had copied from the VSTestBridge. Rather than re-introducing a dependency on Microsoft.Testing.Extensions.VSTestBridge (which the RFC-018 native-MTP work #9706/#9743/#9748 deliberately removed), it follows the issue's recommended dependency-free approach: a single internal static helper in src/Platform/SharedExtensionHelpers that both projects compile via linked Compile items, exactly like the existing shared helpers in that folder. Each package keeps its own resource strings, option registration, and UWP guards.

Changes:

  • Adds RunSettingsProviderHelper (namespace Microsoft.Testing.Extensions, #if !WINDOWS_UWP) holding the resource-independent logic: CanReadFile, TryLoadRunSettingsAsync, HasEnvironmentVariables, ApplyEnvironmentVariables, FindInvalidTestParameter.
  • Links the shared file into both Microsoft.Testing.Extensions.VSTestBridge.csproj (as Helpers\…) and MSTest.TestAdapter.csproj (as Shared\…) with no new project reference.
  • Refactors the two RunSettings CLI providers, two env-var providers, and two TestRunParameters providers to call the shared helper; unifies the previously drifted string.IsNullOrEmpty vs RoslynString.IsNullOrEmpty on string.IsNullOrEmpty (removing the now-unused using Microsoft.Testing.Platform; in the bridge env-var provider).

I verified the behavior is preserved (identical resolution ordering, NETCOREAPP/non-core branch, and env-var enablement gate), all types resolve via repo-wide global usings and enclosing-namespace lookup, and netstandard2.0 string.Contains(char) is satisfied by the linked Polyfills. No concrete defects were found.

Show a summary per file
File Description
src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs New shared helper with the extracted, resource-independent provider logic.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csproj Links the shared helper into the bridge project.
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj Links the shared helper into the adapter project.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.cs Delegates runsettings load + env-var handling to the shared helper; drops unused using.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.cs Uses shared CanReadFile; removes the private copy.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.cs Uses shared FindInvalidTestParameter.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.cs Delegates to shared helper; drops unused IFileStream alias.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.cs Uses shared CanReadFile; removes the private copy.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.cs Uses shared FindInvalidTestParameter.

Review details

  • Files reviewed: 9/9 changed files
  • Comments generated: 0
  • Review effort level: Medium

@github-actions github-actions Bot 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.

Note

🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Summary

Clean deduplication PR. Reviewed across correctness, resource management, threading, cross-TFM compatibility, and MSBuild authoring dimensions.

No blocking issues found. One low-severity suggestion posted inline about broadening the exception catch in CanReadFile to handle UnauthorizedAccessException (preserves existing behavior, so non-blocking).

Verified clean:

  • Namespace visibility: Microsoft.Testing.Extensions is reachable from Microsoft.Testing.Extensions.VSTestBridge.* via C# parent-namespace resolution; adapter files correctly add the explicit using.
  • Null safety: All ! operators are guarded by prior checks or call-site preconditions (HasEnvironmentVariablesApplyEnvironmentVariables).
  • Thread safety: _runSettings assignment/read follows MTP's guaranteed sequential IsEnabledAsyncUpdateAsync call order.
  • Cross-TFM: #if !WINDOWS_UWP and #if NETCOREAPP guards are correctly placed.
  • Implicit usings: Directory.Build.props provides System.Xml.Linq, System.Diagnostics.CodeAnalysis, etc. — no fragile assumptions.
  • MSBuild linking: <Compile Include="..." Link="..." /> follows the established SharedExtensionHelpers pattern with clear issue-referencing comments.
  • Drift resolution: Unification on string.IsNullOrEmpty over RoslynString.IsNullOrEmpty is a sound simplification for the shared helper.

Comment thread src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs
@github-actions

This comment has been minimized.

@github-actions github-actions Bot 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.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · 217 AIC · ⌖ 9.54 AIC · ⊞ 7.3K ·

Comment thread src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
Evangelink and others added 3 commits July 9, 2026 13:07
- Track RunSettingsProviderHelper in InternalAPI.Unshipped.txt for both the
  VSTestBridge and MSTest.TestAdapter projects (RS0051). Both projects use
  InternalsVisibleTo, so the internal-API analyzer tracks internal symbols;
  entries go in InternalAPI.Unshipped.txt, not PublicAPI.Unshipped.txt.
- Also add the pre-existing untracked PlatformServicesConfigurationAdapter
  (non-UWP) to the adapter InternalAPI.Unshipped.txt to unblock RS0051.
- Broaden CanReadFile catch to also handle UnauthorizedAccessException per
  review, so a permission error yields the friendly 'cannot be read'
  validation message instead of an unhandled exception.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 11:29
@Evangelink

Copy link
Copy Markdown
Member Author

Addressed the review feedback and the red pipeline:

RS0051 build failure (fixed). The failure is the InternalAPI analyzer (declared-API file InternalAPI.Unshipped.txt), whose enforcement was newly introduced by the InternalAPI-tracking work that landed on main after this PR opened. RunSettingsProviderHelper is internal, so its entries belong in InternalAPI.Unshipped.txt (not PublicAPI.Unshipped.txt):

  • Microsoft.Testing.Extensions.VSTestBridge/InternalAPI.Unshipped.txt — 6 RunSettingsProviderHelper entries.
  • MSTest.TestAdapter/InternalAPI/InternalAPI.Unshipped.txt (non-UWP) — 6 RunSettingsProviderHelper entries + 3 PlatformServicesConfigurationAdapter entries (a pre-existing untracked type surfaced by the tracking merge; added here so this build goes green). The uwp file stays empty since both types are #if !WINDOWS_UWP.

Review nit (applied). CanReadFile now catches UnauthorizedAccessException alongside IOException.

Merged current origin/main (conflict-free). Verified locally with CI-style flags (/p:ContinuousIntegrationBuild=true, --no-incremental): both projects build with 0 RS0051/RS0016/RS0017 across all TFMs incl. uap10.0; Microsoft.Testing.Extensions.VSTestBridge.UnitTests 44/44.

Heads-up (out of scope, main-side): the Microsoft.Testing.Platform project has pre-existing RS0051 warnings for BaseSerializer.ReadFields / WriteListPayload (from #9774) that are untracked even on current main — not touched by this PR.

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.

Review details

  • Files reviewed: 11/11 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs
…RS0051

Microsoft.Testing.Platform's BaseSerializer.ReadFields and WriteListPayload<T>
(added by #9774) were never added to InternalAPI.Unshipped.txt, so once #9752
enabled RS0051 internal-API enforcement both landed on main and left main red.
This foundational project's failure cascades and blocks the whole build, so
track the two methods in the base InternalAPI.Unshipped.txt (the diagnostic
fires on netstandard2.0 too, so it belongs in the base file, not net/).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 11:39

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.

Review details

  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Low

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — All 25 build errors are RS0051 ("Symbol is not part of the declared API") for two newly-visible protected static methods on BaseSerializer, reported across 4 extension projects that compile the file as linked source.

Root cause: BaseSerializer API entries missing from extension projects' InternalAPI.Unshipped.txt

Commit fdaa831 correctly added the two symbols to src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt. However, BaseSerializer.cs is also compiled as a linked source file (via <Compile Include="..." Link="..." />) in 4 other projects. Each of those projects has its own InternalAPI.Unshipped.txt that the Public API Analyzer checks independently — and none of them declare the new methods.

Affected projects (all reporting the same 2 symbols × multiple TFMs):

Project InternalAPI file needing update
Microsoft.Testing.Extensions.HangDump src/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.MSBuild src/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.Retry src/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.TrxReport src/Platform/Microsoft.Testing.Extensions.TrxReport/InternalAPI/InternalAPI.Unshipped.txt

Proposed fix — Append the same two lines to each of the 4 files above:

static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void

Build overview
  • Build status: FAILED
  • Duration: 229.8s
  • MSBuild version: 18.8.0-preview-26302-115
  • Total projects: 49 | Errors: 25 | Warnings: 0
  • Failed projects: Microsoft.Testing.Extensions.HangDump, Microsoft.Testing.Extensions.MSBuild, Microsoft.Testing.Extensions.Retry, Microsoft.Testing.Extensions.TrxReport, NonWindowsTests.slnf, Build.proj
All MSBuild errors (25 — 2 unique, repeated across 4 projects × multiple TFMs)
Code Project File:Line Message
RS0051 HangDump BaseSerializer.cs:359 ReadFields is not part of the declared API
RS0051 HangDump BaseSerializer.cs:380 WriteListPayload<T> is not part of the declared API
RS0051 MSBuild BaseSerializer.cs:359 ReadFields is not part of the declared API
RS0051 MSBuild BaseSerializer.cs:380 WriteListPayload<T> is not part of the declared API
RS0051 Retry BaseSerializer.cs:359 ReadFields is not part of the declared API
RS0051 Retry BaseSerializer.cs:380 WriteListPayload<T> is not part of the declared API
RS0051 TrxReport BaseSerializer.cs:359 ReadFields is not part of the declared API
RS0051 TrxReport BaseSerializer.cs:380 WriteListPayload<T> is not part of the declared API

(Each row repeats per target framework — 3 TFMs for most projects = 24 errors + 1 "Build failed" sentinel.)


🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit fdaa831

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · 161.3 AIC · ⌖ 6.76 AIC · ⊞ 7.3K · [◷]( · )

@github-actions github-actions Bot 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.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · 161.3 AIC · ⌖ 6.76 AIC · ⊞ 7.3K ·

BaseSerializer.cs is compiled as linked shared source into HangDump, MSBuild,
TrxReport and Retry (in addition to Microsoft.Testing.Platform), and each of
those projects does its own RS0051 internal-API tracking. My previous commit
only tracked ReadFields/WriteListPayload<T> in Microsoft.Testing.Platform, so
the four linking projects still failed RS0051 under CI's -warnaserror (locally
these surface as warnings, which is why an earlier per-project build looked
green). Add the same two entries to each project's InternalAPI.Unshipped.txt.

Verified by reproduce-then-clear with CI-style flags
(/p:ContinuousIntegrationBuild=true /p:RunAnalyzers=true --no-incremental):
removing the lines re-emits RS0051; with the lines all five projects report
zero RS0051/RS0016/RS0017.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 12:11

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.

Review details

  • Files reviewed: 16/16 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs
Evangelink and others added 2 commits July 9, 2026 19:14
…edup-providers

# Conflicts:
#	src/Adapter/MSTest.TestAdapter/InternalAPI/InternalAPI.Unshipped.txt
The shared RunSettingsProviderHelper.CanReadFile intentionally broadens the
caught exceptions to include UnauthorizedAccessException (a permission error on
an existing .runsettings file now surfaces the friendly 'cannot be read'
validation message). Add a unit test exercising that branch.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 17:17

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.

Review details

  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9775

GradeTestNotes
A (90–100) new RunSettingsCommandLineOptionsProviderTests.
RunSettingsOption_
WhenFileCannotBeReadDueToPermissions_
IsNotValid
Clear AAA structure; verifies both IsValid=false and the exact error message for the new UnauthorizedAccessException handling path.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · 56.8 AIC · ⌖ 7.7 AIC · ⊞ 9.5K · [◷]( · )

@Evangelink Evangelink added the state/needs-review Awaiting review from the team. label Jul 9, 2026
@Evangelink
Evangelink enabled auto-merge (squash) July 9, 2026 17:36
@Evangelink
Evangelink merged commit f99f9d3 into main Jul 9, 2026
68 checks passed
@Evangelink
Evangelink deleted the evangelink-fix-9762-dedup-providers branch July 9, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-review Awaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[duplicate-code] Duplicate RunSettings/TestRunParameters/TestCaseFilter Providers: MSTest Adapter Mirrors VSTestBridge

3 participants