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

Merged
Amaury Levé (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
Amaury Levé (Evangelink) merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.IntegrationTestsHelpInfoTests21/21, HelpInfoAllExtensionsTests9/9
    • MSTest.Acceptance.IntegrationTestsHelpInfoTests6/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>
CopilotAI review requested due to automatic review settings July 9, 2026 09:22

CopilotAI 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
FileDescription
src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.csNew shared helper with the extracted, resource-independent provider logic.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csprojLinks the shared helper into the bridge project.
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojLinks the shared helper into the adapter project.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.csDelegates runsettings load + env-var handling to the shared helper; drops unused using.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.csDelegates to shared helper; drops unused IFileStream alias.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.

Review details

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

@github-actionsgithub-actionsBot 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.

@github-actions

This comment has been minimized.

@github-actionsgithub-actionsBot 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 threadsrc/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
- 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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:29
@Evangelink

Copy link
Copy Markdown
MemberAuthor

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.

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:39

CopilotAI 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

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):

ProjectInternalAPI file needing update
Microsoft.Testing.Extensions.HangDumpsrc/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.MSBuildsrc/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.Retrysrc/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.TrxReportsrc/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)
CodeProjectFile:LineMessage
RS0051HangDumpBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051HangDumpBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051MSBuildBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051MSBuildBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051RetryBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051RetryBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051TrxReportBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051TrxReportBaseSerializer.cs:380WriteListPayload<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-actionsgithub-actionsBot 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>
CopilotAI review requested due to automatic review settings July 9, 2026 12:11

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 17:17

CopilotAI 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

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 · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 9, 2026
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 9, 2026 17:36
@Evangelink
Amaury Levé (Evangelink) merged commit f99f9d3 into mainJul 9, 2026
68 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-fix-9762-dedup-providers branch July 9, 2026 18:55
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.

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

3 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

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

Merged
Amaury Levé (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
Amaury Levé (Evangelink) merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.IntegrationTestsHelpInfoTests21/21, HelpInfoAllExtensionsTests9/9
    • MSTest.Acceptance.IntegrationTestsHelpInfoTests6/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>
CopilotAI review requested due to automatic review settings July 9, 2026 09:22

CopilotAI 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
FileDescription
src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.csNew shared helper with the extracted, resource-independent provider logic.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csprojLinks the shared helper into the bridge project.
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojLinks the shared helper into the adapter project.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.csDelegates runsettings load + env-var handling to the shared helper; drops unused using.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.csDelegates to shared helper; drops unused IFileStream alias.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.

Review details

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

@github-actionsgithub-actionsBot 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.

@github-actions

This comment has been minimized.

@github-actionsgithub-actionsBot 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 threadsrc/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
- 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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:29
@Evangelink

Copy link
Copy Markdown
MemberAuthor

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.

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:39

CopilotAI 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

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):

ProjectInternalAPI file needing update
Microsoft.Testing.Extensions.HangDumpsrc/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.MSBuildsrc/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.Retrysrc/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.TrxReportsrc/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)
CodeProjectFile:LineMessage
RS0051HangDumpBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051HangDumpBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051MSBuildBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051MSBuildBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051RetryBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051RetryBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051TrxReportBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051TrxReportBaseSerializer.cs:380WriteListPayload<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-actionsgithub-actionsBot 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>
CopilotAI review requested due to automatic review settings July 9, 2026 12:11

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 17:17

CopilotAI 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

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 · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 9, 2026
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 9, 2026 17:36
@Evangelink
Amaury Levé (Evangelink) merged commit f99f9d3 into mainJul 9, 2026
68 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-fix-9762-dedup-providers branch July 9, 2026 18:55
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.

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

3 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

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

Merged
Amaury Levé (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
Amaury Levé (Evangelink) merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.IntegrationTestsHelpInfoTests21/21, HelpInfoAllExtensionsTests9/9
    • MSTest.Acceptance.IntegrationTestsHelpInfoTests6/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>
CopilotAI review requested due to automatic review settings July 9, 2026 09:22

CopilotAI 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
FileDescription
src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.csNew shared helper with the extracted, resource-independent provider logic.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csprojLinks the shared helper into the bridge project.
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojLinks the shared helper into the adapter project.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.csDelegates runsettings load + env-var handling to the shared helper; drops unused using.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.csDelegates to shared helper; drops unused IFileStream alias.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.

Review details

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

@github-actionsgithub-actionsBot 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.

@github-actions

This comment has been minimized.

@github-actionsgithub-actionsBot 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 threadsrc/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
- 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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:29
@Evangelink

Copy link
Copy Markdown
MemberAuthor

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.

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:39

CopilotAI 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

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):

ProjectInternalAPI file needing update
Microsoft.Testing.Extensions.HangDumpsrc/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.MSBuildsrc/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.Retrysrc/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.TrxReportsrc/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)
CodeProjectFile:LineMessage
RS0051HangDumpBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051HangDumpBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051MSBuildBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051MSBuildBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051RetryBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051RetryBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051TrxReportBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051TrxReportBaseSerializer.cs:380WriteListPayload<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-actionsgithub-actionsBot 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>
CopilotAI review requested due to automatic review settings July 9, 2026 12:11

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 17:17

CopilotAI 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

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 · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 9, 2026
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 9, 2026 17:36
@Evangelink
Amaury Levé (Evangelink) merged commit f99f9d3 into mainJul 9, 2026
68 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-fix-9762-dedup-providers branch July 9, 2026 18:55
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.

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

3 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

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

Merged
Amaury Levé (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
Amaury Levé (Evangelink) merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.IntegrationTestsHelpInfoTests21/21, HelpInfoAllExtensionsTests9/9
    • MSTest.Acceptance.IntegrationTestsHelpInfoTests6/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>
CopilotAI review requested due to automatic review settings July 9, 2026 09:22

CopilotAI 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
FileDescription
src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.csNew shared helper with the extracted, resource-independent provider logic.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csprojLinks the shared helper into the bridge project.
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojLinks the shared helper into the adapter project.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.csDelegates runsettings load + env-var handling to the shared helper; drops unused using.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.csDelegates to shared helper; drops unused IFileStream alias.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.

Review details

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

@github-actionsgithub-actionsBot 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.

@github-actions

This comment has been minimized.

@github-actionsgithub-actionsBot 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 threadsrc/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
- 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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:29
@Evangelink

Copy link
Copy Markdown
MemberAuthor

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.

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:39

CopilotAI 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

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):

ProjectInternalAPI file needing update
Microsoft.Testing.Extensions.HangDumpsrc/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.MSBuildsrc/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.Retrysrc/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.TrxReportsrc/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)
CodeProjectFile:LineMessage
RS0051HangDumpBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051HangDumpBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051MSBuildBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051MSBuildBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051RetryBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051RetryBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051TrxReportBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051TrxReportBaseSerializer.cs:380WriteListPayload<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-actionsgithub-actionsBot 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>
CopilotAI review requested due to automatic review settings July 9, 2026 12:11

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 17:17

CopilotAI 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

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 · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 9, 2026
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 9, 2026 17:36
@Evangelink
Amaury Levé (Evangelink) merged commit f99f9d3 into mainJul 9, 2026
68 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-fix-9762-dedup-providers branch July 9, 2026 18:55
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.

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

3 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

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

Merged
Amaury Levé (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
Amaury Levé (Evangelink) merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.IntegrationTestsHelpInfoTests21/21, HelpInfoAllExtensionsTests9/9
    • MSTest.Acceptance.IntegrationTestsHelpInfoTests6/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>
CopilotAI review requested due to automatic review settings July 9, 2026 09:22

CopilotAI 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
FileDescription
src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.csNew shared helper with the extracted, resource-independent provider logic.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csprojLinks the shared helper into the bridge project.
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojLinks the shared helper into the adapter project.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.csDelegates runsettings load + env-var handling to the shared helper; drops unused using.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.csDelegates to shared helper; drops unused IFileStream alias.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.

Review details

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

@github-actionsgithub-actionsBot 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.

@github-actions

This comment has been minimized.

@github-actionsgithub-actionsBot 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 threadsrc/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
- 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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:29
@Evangelink

Copy link
Copy Markdown
MemberAuthor

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.

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:39

CopilotAI 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

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):

ProjectInternalAPI file needing update
Microsoft.Testing.Extensions.HangDumpsrc/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.MSBuildsrc/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.Retrysrc/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.TrxReportsrc/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)
CodeProjectFile:LineMessage
RS0051HangDumpBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051HangDumpBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051MSBuildBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051MSBuildBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051RetryBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051RetryBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051TrxReportBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051TrxReportBaseSerializer.cs:380WriteListPayload<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-actionsgithub-actionsBot 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>
CopilotAI review requested due to automatic review settings July 9, 2026 12:11

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 17:17

CopilotAI 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

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 · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 9, 2026
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 9, 2026 17:36
@Evangelink
Amaury Levé (Evangelink) merged commit f99f9d3 into mainJul 9, 2026
68 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-fix-9762-dedup-providers branch July 9, 2026 18:55
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.

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

3 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

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

Merged
Amaury Levé (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
Amaury Levé (Evangelink) merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.IntegrationTestsHelpInfoTests21/21, HelpInfoAllExtensionsTests9/9
    • MSTest.Acceptance.IntegrationTestsHelpInfoTests6/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>
CopilotAI review requested due to automatic review settings July 9, 2026 09:22

CopilotAI 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
FileDescription
src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.csNew shared helper with the extracted, resource-independent provider logic.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csprojLinks the shared helper into the bridge project.
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojLinks the shared helper into the adapter project.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.csDelegates runsettings load + env-var handling to the shared helper; drops unused using.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.csDelegates to shared helper; drops unused IFileStream alias.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.

Review details

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

@github-actionsgithub-actionsBot 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.

@github-actions

This comment has been minimized.

@github-actionsgithub-actionsBot 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 threadsrc/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
- 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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:29
@Evangelink

Copy link
Copy Markdown
MemberAuthor

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.

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:39

CopilotAI 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

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):

ProjectInternalAPI file needing update
Microsoft.Testing.Extensions.HangDumpsrc/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.MSBuildsrc/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.Retrysrc/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.TrxReportsrc/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)
CodeProjectFile:LineMessage
RS0051HangDumpBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051HangDumpBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051MSBuildBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051MSBuildBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051RetryBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051RetryBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051TrxReportBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051TrxReportBaseSerializer.cs:380WriteListPayload<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-actionsgithub-actionsBot 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>
CopilotAI review requested due to automatic review settings July 9, 2026 12:11

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 17:17

CopilotAI 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

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 · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 9, 2026
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 9, 2026 17:36
@Evangelink
Amaury Levé (Evangelink) merged commit f99f9d3 into mainJul 9, 2026
68 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-fix-9762-dedup-providers branch July 9, 2026 18:55
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.

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

3 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

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

Merged
Amaury Levé (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
Amaury Levé (Evangelink) merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.IntegrationTestsHelpInfoTests21/21, HelpInfoAllExtensionsTests9/9
    • MSTest.Acceptance.IntegrationTestsHelpInfoTests6/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>
CopilotAI review requested due to automatic review settings July 9, 2026 09:22

CopilotAI 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
FileDescription
src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.csNew shared helper with the extracted, resource-independent provider logic.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csprojLinks the shared helper into the bridge project.
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojLinks the shared helper into the adapter project.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.csDelegates runsettings load + env-var handling to the shared helper; drops unused using.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.csDelegates to shared helper; drops unused IFileStream alias.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.

Review details

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

@github-actionsgithub-actionsBot 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.

@github-actions

This comment has been minimized.

@github-actionsgithub-actionsBot 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 threadsrc/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
- 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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:29
@Evangelink

Copy link
Copy Markdown
MemberAuthor

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.

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:39

CopilotAI 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

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):

ProjectInternalAPI file needing update
Microsoft.Testing.Extensions.HangDumpsrc/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.MSBuildsrc/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.Retrysrc/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.TrxReportsrc/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)
CodeProjectFile:LineMessage
RS0051HangDumpBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051HangDumpBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051MSBuildBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051MSBuildBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051RetryBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051RetryBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051TrxReportBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051TrxReportBaseSerializer.cs:380WriteListPayload<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-actionsgithub-actionsBot 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>
CopilotAI review requested due to automatic review settings July 9, 2026 12:11

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 17:17

CopilotAI 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

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 · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 9, 2026
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 9, 2026 17:36
@Evangelink
Amaury Levé (Evangelink) merged commit f99f9d3 into mainJul 9, 2026
68 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-fix-9762-dedup-providers branch July 9, 2026 18:55
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.

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

3 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

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

Merged
Amaury Levé (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
Amaury Levé (Evangelink) merged 8 commits into
mainfrom
evangelink-fix-9762-dedup-providers

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.IntegrationTestsHelpInfoTests21/21, HelpInfoAllExtensionsTests9/9
    • MSTest.Acceptance.IntegrationTestsHelpInfoTests6/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>
CopilotAI review requested due to automatic review settings July 9, 2026 09:22

CopilotAI 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
FileDescription
src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.csNew shared helper with the extracted, resource-independent provider logic.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csprojLinks the shared helper into the bridge project.
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojLinks the shared helper into the adapter project.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.csDelegates runsettings load + env-var handling to the shared helper; drops unused using.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.csDelegates to shared helper; drops unused IFileStream alias.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.csUses shared CanReadFile; removes the private copy.
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.csUses shared FindInvalidTestParameter.

Review details

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

@github-actionsgithub-actionsBot 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.

@github-actions

This comment has been minimized.

@github-actionsgithub-actionsBot 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 threadsrc/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
- 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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:29
@Evangelink

Copy link
Copy Markdown
MemberAuthor

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.

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 11:39

CopilotAI 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

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):

ProjectInternalAPI file needing update
Microsoft.Testing.Extensions.HangDumpsrc/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.MSBuildsrc/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.Retrysrc/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt
Microsoft.Testing.Extensions.TrxReportsrc/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)
CodeProjectFile:LineMessage
RS0051HangDumpBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051HangDumpBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051MSBuildBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051MSBuildBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051RetryBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051RetryBaseSerializer.cs:380WriteListPayload<T> is not part of the declared API
RS0051TrxReportBaseSerializer.cs:359ReadFields is not part of the declared API
RS0051TrxReportBaseSerializer.cs:380WriteListPayload<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-actionsgithub-actionsBot 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>
CopilotAI review requested due to automatic review settings July 9, 2026 12:11

CopilotAI 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

…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>
CopilotAI review requested due to automatic review settings July 9, 2026 17:17

CopilotAI 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

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 · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 9, 2026
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 9, 2026 17:36
@Evangelink
Amaury Levé (Evangelink) merged commit f99f9d3 into mainJul 9, 2026
68 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the evangelink-fix-9762-dedup-providers branch July 9, 2026 18:55
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.

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

3 participants

@Evangelink@0101