Neutralize the runsettings-parsing helpers (Phase 6e-4a) - #9627

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils
Jul 5, 2026
Merged

Neutralize the runsettings-parsing helpers (Phase 6e-4a)#9627
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4a — Neutralize the runsettings-parsing helpers

Part of the initiative to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). Strict byte-for-byte refactor.

What this does

Decouples the four runsettings-XML parsing files (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings, MSTestSettings.RunSettingsXml):

  1. SettingsException → new neutral InvalidRunSettingsException (derives directly from Exception, not from AdapterSettingsException).
  2. Inline the ObjectModel.Constants node names ("RunConfiguration", "TestRunParameters").
  3. Repoint XmlRunSettingsUtilities.ReaderSettings at the neutral RunSettingsUtilities.ReaderSettings.
  4. New neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement), reusing the exact navigation semantics the adapter already vendored privately in RunConfigurationSettings.

Coupling drops 10 → 6 files.

Fidelity — exception-handler trace (the proof)

The adapter has a two-tier settings-exception topology this change preserves. MSTestDiscovererHelpers.InitializeDiscovery catches AdapterSettingsException (invalid MSTest settings values → log + "no tests"); a structural runsettings error escapes that handler to the host. Per-site — which handler catches it, before vs after:

Throw siteSole reachable pathAs VSTest SettingsException (today)As InvalidRunSettingsException (after)
RunSettingsUtilities.ThrowOnHasAttributesTestRunParameters.FromXmlGetTestRunParametersTestExecutionManager.CacheSessionParametersbroad catch (Exception)same broad catch (Exception)
TestRunParameters.FromXmlGetTestRunParametersCacheSessionParametersbroad catch (Exception)same broad catch (Exception)
MSTestAdapterSettings.ToSettings (AssemblyResolution)SettingsProvider.Load (from PopulateSettings, wrapped by InitializeDiscovery's catch (AdapterSettingsException))escapes that catch → DiscoverTests/RunTests (all try/finally, no catch) → hostescapes (distinct type) → host

A distinct type (not AdapterSettingsException) is required for site 3: reusing AdapterSettingsException would make a malformed <AssemblyResolution> newly caught at InitializeDiscovery (log + "no tests") instead of escaping — an observable behavior change.

Cross-process note (site 3 escapes to the host). Perfect type-and-stack fidelity on this path is not achievable while decoupling, because SettingsException is itself an object-model type and must leave PlatformServices before the package-reference drop. The neutral InvalidRunSettingsExceptionpreserves the message, the throw origin/stack, and the escape topology (still escapes InitializeDiscovery and DiscoverTests/RunTests exactly as before); only the exception type name differs, and only on a rare, user-authored malformed-<AssemblyResolution> runsettings that no test exercises. The test platform surfaces adapter-thrown exceptions during Discover/RunTests uniformly as TestMessageLevel.Error (its runsettings-validation special-casing lives in the host's own session-start parsing, not adapter invocation), so this is behavior-neutral on observable output. Adding a dedicated cross-process TRX/error-parity regression test is tracked by #9587. Unifying the two exceptions onto one model is a separate deliberate behavior change, tracked in #9629.

MSTestExecutor/MSTestDiscoverer have no typed settings-exception catch, so there is no boundary catch to update. The …BailOutOnSettingsException tests (which exercise the AdapterSettingsException path) pass unchanged.

Verification

  • Full build.cmd -c Debug green all TFMs, IDE0005 clean.
  • MSTestAdapter.PlatformServices.UnitTests: 897 (net8.0) / 935 (net462), 0 failed.
  • MSTestAdapter.UnitTests: 21, 0 failed (incl. BailOut tests).
  • MSTest.IntegrationTests (cross-proc, net462): 48 total, 0 failed, 1 skipped (pre-existing known-flaky).

Stacking

6c2 #95906d-1 #95916d-2 #96216e-1 #96226e-2 #96236e-3a #96246e-3b #96266e-4a (this). Base = 6e-3b branch. Do not rebase/reset the base.

Decouple the runsettings-XML parsing files from the VSTest object model:
- Replace the VSTest SettingsException thrown during runsettings/test-run-parameter
parsing (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings) with a new
neutral InvalidRunSettingsException. This exception is deliberately DISTINCT from the
existing AdapterSettingsException to preserve behavior byte-for-byte: the only typed
settings-error handler, MSTestDiscovererHelpers.InitializeDiscovery, catches
AdapterSettingsException (invalid MSTest settings values -> report + no tests), while
a structural runsettings error historically threw VSTest SettingsException and
escaped that handler to the host. The malformed <AssemblyResolution> throw site is
reachable through PopulateSettings via SettingsProvider.Load, so reusing
AdapterSettingsException there would have changed the escape semantics; the distinct
InvalidRunSettingsException (unrelated to AdapterSettingsException) preserves them.
The other sites are caught only by a broad catch(Exception) in CacheSessionParameters,
so behavior there is identical either way. Only the direct typed-throw unit
assertions change.
- Inline the VSTest ObjectModel.Constants runsettings node names
(RunConfiguration, TestRunParameters) as neutral constants.
- Repoint XmlRunSettingsUtilities.ReaderSettings at the equivalent neutral
RunSettingsUtilities.ReaderSettings that already existed.
- Add a neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement)
replacing the VSTest ObjectModel.Utilities helpers, reusing the exact navigation
semantics the adapter already vendored privately in RunConfigurationSettings.
Drops the PlatformServices VSTest-ObjectModel coupling from 10 to 6 files (the
remaining are the netfx residuals + AssemblyResolver string literals).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-settings-utils branch from 4b11c0a to fbcafefCompareJuly 5, 2026 12:17
…6e-4b) (#9628)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review July 5, 2026 19:27
@Evangelink
Amaury Levé (Evangelink) merged commit 1fb4b4e into dev/amauryleve/vstest-decoupling-conversionJul 5, 2026
18 of 20 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-settings-utils branch July 5, 2026 19:27

@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. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.


Expert Review — Phase 6e-4a: Neutralize the runsettings-parsing helpers

Verdict: ✅ Informational / COMMENT(PR already merged; no blocking issues found.)


22-Dimension Verdict Table

#DimensionSeverityVerdictNotes
1Algorithmic CorrectnessMAJOR✅ PASSReadToNextElement/SkipToNextElement/ReadToRootNode semantics byte-for-byte match the RunConfigurationSettings private copies they were modelled on.
2Threading & ConcurrencyBLOCKING✅ N/APure synchronous XML parsing, no shared mutable state.
3Security & IPC Contract SafetyBLOCKING✅ PASSXmlReaderSettings default in .NET 4.5+ is DtdProcessing.Prohibit; not setting it explicitly is safe. MSTestAdapterSettings.IsAppDomainCreationDisabled keeps its XmlResolver = null.
4Public API & Binary CompatibilityBLOCKING✅ PASSBoth new types (InvalidRunSettingsException, XmlReaderUtilities) are internal. No PublicAPI.Unshipped.txt changes needed.
5Performance & AllocationsMAJOR✅ PASSGetObjectModelAssembly() scans all loaded assemblies, but it sits on the low-frequency AppDomain setup path (#if NETFRAMEWORK). Acceptable.
6Cross-TFM CompatibilityMAJOR✅ PASSAll AppDomainUtilities changes are properly gated behind #if NETFRAMEWORK. The rest of the diff is TFM-neutral.
7Exception HandlingMAJOR✅ PASSThe two-tier exception topology is preserved. InvalidRunSettingsException intentionally does not derive from AdapterSettingsException; the PR description includes a per-site proof table that is correct and thorough.
8Test Coverage & QualityMAJOR⚠️ SEE BELOWThe diff shows an unchanged context line still asserting Throw<SettingsException>() after the ObjectModel using was removed. Disk state is clean. See inline comment.
9Naming & StyleMINOR✅ PASSInvalidRunSettingsException, XmlReaderUtilities, RunConfigurationSettingsName, TestRunParametersName are all clear and consistent.
10Code DuplicationMINOR⚠️ NOTERunConfigurationSettings retains private ReadToRootNode/ReadToNextElement/SkipToNextElement clones that now mirror the new public XmlReaderUtilities methods. Consolidating would change the exception type on the RunConfiguration path (FormatExceptionInvalidRunSettingsException). Leaving it as-is in this phase is intentional and correct; a later phase can align.
11DocumentationMINOR✅ PASSBoth new types carry XML docs. The InvalidRunSettingsException doc clearly explains the two-tier design rationale.
12Resource ManagementMAJOR✅ N/ANo new IDisposable resources; all existing XmlReader/StringReader uses already have using in their callers.
13Null SafetyMAJOR✅ PASSInvalidRunSettingsException(string? message) is null-safe. GetObjectModelAssembly() either succeeds or throws — never returns null.
14Pattern MatchingMINOR✅ N/ANo opportunities in the changed code.
15LINQ UsageMINOR✅ PASSFirstOrDefault(predicate) in GetObjectModelAssembly() is appropriate.
16LocalizationMAJOR⚠️ MINORXmlReaderUtilities.ReadToRootNode uses a hardcoded interpolated string instead of a Resource.* entry — inconsistent with the rest of the settings-error surface. See inline comment.
17Build & Project FilesMAJOR✅ PASSSDK-style project (MSTestAdapter.PlatformServices.csproj) auto-includes all *.cs files; no explicit <Compile> entries needed for the two new files.
18PR Scope & FocusMAJOR✅ PASSStrictly scoped to the four runsettings-parsing files. The PR description is exemplary in its fidelity analysis.
19Backward CompatibilityBLOCKING✅ PASSThe per-site exception-handler table in the PR description demonstrates that observable behaviour is unchanged. Site 3 (MSTestAdapterSettings.ReadAssemblyResolutionPath) correctly escapes InitializeDiscovery both before and after.
20Error Message QualityMINOR✅ PASSAll messages are actionable. The hardcoded XmlReaderUtilities message is clear even if not resource-backed (see dim 16).
21Design & ArchitectureMAJOR✅ PASSExtension methods on XmlReader is idiomatic; the GetObjectModelAssembly() late-binding approach avoids a hard compile-time dependency on a type that is scheduled to be removed.
22Overarching Principles✅ PASSBackward compatibility is sacred and demonstrably preserved. Scope is disciplined (one phase, one responsibility). No new public API.

Key Findings

⚠️ Test assertion not updated (dim 8, dim 16)

The diff shows a context (unchanged) line inside GetTestRunParametersThrowsOnInvalidSettingsXml that still asserts .Throw<SettingsException>() after the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed. That would be a compilation error. The current disk state is clean (no SettingsException reference), which implies the base branch (6e-3b) already had this method restructured and the git diff's "nearest function" context header is misleading. No action required, but worth an explicit regression note: if the 6e-3b base is ever rebased, this context line should not re-appear.

i️ Hardcoded error message (dim 16)

XmlReaderUtilities.ReadToRootNode throws $"Could not find '...' node..." as an interpolated string. Every peer error in this area (ThrowOnHasAttributes, FromXml, ReadAssemblyResolutionPath) uses Resource.* strings. Low priority, but a resource entry would keep this consistent for any future localization pass.

i️ RunConfigurationSettings duplication (dim 10)

RunConfigurationSettings still carries three private helpers (ReadToRootNode/ReadToNextElement/SkipToNextElement) that are now semantically identical to XmlReaderUtilities. The only observable difference is that RunConfigurationSettings.ReadToRootNode throws FormatException while XmlReaderUtilities.ReadToRootNode throws InvalidRunSettingsException. Consolidating them is a deliberate future-phase decision and is tracked correctly.


Overall: Well-executed, carefully analysed refactoring. The exception-topology proof table in the PR description sets a high bar for this kind of decoupling work. The two non-blocking observations above are worth noting but do not affect correctness or behaviour.

Comments that could not be inline-anchored

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs:33

The diff shows this assertion as an unchanged context line (no - prefix) while the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed in the same hunk. As written after the patch, SettingsException is no longer resolvable — this should be a compilation error.

The disk state does not exhibit the problem (no SettingsException reference remains, and the method was restructured), which suggests it was resolved either within the same commit or via a prior phase…

src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs:29

The error message is a hardcoded interpolated string. Every other settings-parse error in this codebase flows through Resource.* strings (e.g., Resource.InvalidSettingsXmlElement, Resource.InvalidSettingsXmlAttribute). While this path is rare and the message itself is perfectly clear, it is inconsistent with the localization policy of the codebase.

Suggested fix: add a resource entry (e.g. InvalidRunSettingsRootNode) and use `string.Format(CultureInfo.CurrentCulture, Resource.InvalidRu…

Amaury Levé (Evangelink) added a commit that referenced this pull request Jul 5, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink
, '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

Neutralize the runsettings-parsing helpers (Phase 6e-4a) - #9627

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils
Jul 5, 2026
Merged

Neutralize the runsettings-parsing helpers (Phase 6e-4a)#9627
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4a — Neutralize the runsettings-parsing helpers

Part of the initiative to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). Strict byte-for-byte refactor.

What this does

Decouples the four runsettings-XML parsing files (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings, MSTestSettings.RunSettingsXml):

  1. SettingsException → new neutral InvalidRunSettingsException (derives directly from Exception, not from AdapterSettingsException).
  2. Inline the ObjectModel.Constants node names ("RunConfiguration", "TestRunParameters").
  3. Repoint XmlRunSettingsUtilities.ReaderSettings at the neutral RunSettingsUtilities.ReaderSettings.
  4. New neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement), reusing the exact navigation semantics the adapter already vendored privately in RunConfigurationSettings.

Coupling drops 10 → 6 files.

Fidelity — exception-handler trace (the proof)

The adapter has a two-tier settings-exception topology this change preserves. MSTestDiscovererHelpers.InitializeDiscovery catches AdapterSettingsException (invalid MSTest settings values → log + "no tests"); a structural runsettings error escapes that handler to the host. Per-site — which handler catches it, before vs after:

Throw siteSole reachable pathAs VSTest SettingsException (today)As InvalidRunSettingsException (after)
RunSettingsUtilities.ThrowOnHasAttributesTestRunParameters.FromXmlGetTestRunParametersTestExecutionManager.CacheSessionParametersbroad catch (Exception)same broad catch (Exception)
TestRunParameters.FromXmlGetTestRunParametersCacheSessionParametersbroad catch (Exception)same broad catch (Exception)
MSTestAdapterSettings.ToSettings (AssemblyResolution)SettingsProvider.Load (from PopulateSettings, wrapped by InitializeDiscovery's catch (AdapterSettingsException))escapes that catch → DiscoverTests/RunTests (all try/finally, no catch) → hostescapes (distinct type) → host

A distinct type (not AdapterSettingsException) is required for site 3: reusing AdapterSettingsException would make a malformed <AssemblyResolution> newly caught at InitializeDiscovery (log + "no tests") instead of escaping — an observable behavior change.

Cross-process note (site 3 escapes to the host). Perfect type-and-stack fidelity on this path is not achievable while decoupling, because SettingsException is itself an object-model type and must leave PlatformServices before the package-reference drop. The neutral InvalidRunSettingsExceptionpreserves the message, the throw origin/stack, and the escape topology (still escapes InitializeDiscovery and DiscoverTests/RunTests exactly as before); only the exception type name differs, and only on a rare, user-authored malformed-<AssemblyResolution> runsettings that no test exercises. The test platform surfaces adapter-thrown exceptions during Discover/RunTests uniformly as TestMessageLevel.Error (its runsettings-validation special-casing lives in the host's own session-start parsing, not adapter invocation), so this is behavior-neutral on observable output. Adding a dedicated cross-process TRX/error-parity regression test is tracked by #9587. Unifying the two exceptions onto one model is a separate deliberate behavior change, tracked in #9629.

MSTestExecutor/MSTestDiscoverer have no typed settings-exception catch, so there is no boundary catch to update. The …BailOutOnSettingsException tests (which exercise the AdapterSettingsException path) pass unchanged.

Verification

  • Full build.cmd -c Debug green all TFMs, IDE0005 clean.
  • MSTestAdapter.PlatformServices.UnitTests: 897 (net8.0) / 935 (net462), 0 failed.
  • MSTestAdapter.UnitTests: 21, 0 failed (incl. BailOut tests).
  • MSTest.IntegrationTests (cross-proc, net462): 48 total, 0 failed, 1 skipped (pre-existing known-flaky).

Stacking

6c2 #95906d-1 #95916d-2 #96216e-1 #96226e-2 #96236e-3a #96246e-3b #96266e-4a (this). Base = 6e-3b branch. Do not rebase/reset the base.

Decouple the runsettings-XML parsing files from the VSTest object model:
- Replace the VSTest SettingsException thrown during runsettings/test-run-parameter
parsing (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings) with a new
neutral InvalidRunSettingsException. This exception is deliberately DISTINCT from the
existing AdapterSettingsException to preserve behavior byte-for-byte: the only typed
settings-error handler, MSTestDiscovererHelpers.InitializeDiscovery, catches
AdapterSettingsException (invalid MSTest settings values -> report + no tests), while
a structural runsettings error historically threw VSTest SettingsException and
escaped that handler to the host. The malformed <AssemblyResolution> throw site is
reachable through PopulateSettings via SettingsProvider.Load, so reusing
AdapterSettingsException there would have changed the escape semantics; the distinct
InvalidRunSettingsException (unrelated to AdapterSettingsException) preserves them.
The other sites are caught only by a broad catch(Exception) in CacheSessionParameters,
so behavior there is identical either way. Only the direct typed-throw unit
assertions change.
- Inline the VSTest ObjectModel.Constants runsettings node names
(RunConfiguration, TestRunParameters) as neutral constants.
- Repoint XmlRunSettingsUtilities.ReaderSettings at the equivalent neutral
RunSettingsUtilities.ReaderSettings that already existed.
- Add a neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement)
replacing the VSTest ObjectModel.Utilities helpers, reusing the exact navigation
semantics the adapter already vendored privately in RunConfigurationSettings.
Drops the PlatformServices VSTest-ObjectModel coupling from 10 to 6 files (the
remaining are the netfx residuals + AssemblyResolver string literals).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-settings-utils branch from 4b11c0a to fbcafefCompareJuly 5, 2026 12:17
…6e-4b) (#9628)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review July 5, 2026 19:27
@Evangelink
Amaury Levé (Evangelink) merged commit 1fb4b4e into dev/amauryleve/vstest-decoupling-conversionJul 5, 2026
18 of 20 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-settings-utils branch July 5, 2026 19:27

@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. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.


Expert Review — Phase 6e-4a: Neutralize the runsettings-parsing helpers

Verdict: ✅ Informational / COMMENT(PR already merged; no blocking issues found.)


22-Dimension Verdict Table

#DimensionSeverityVerdictNotes
1Algorithmic CorrectnessMAJOR✅ PASSReadToNextElement/SkipToNextElement/ReadToRootNode semantics byte-for-byte match the RunConfigurationSettings private copies they were modelled on.
2Threading & ConcurrencyBLOCKING✅ N/APure synchronous XML parsing, no shared mutable state.
3Security & IPC Contract SafetyBLOCKING✅ PASSXmlReaderSettings default in .NET 4.5+ is DtdProcessing.Prohibit; not setting it explicitly is safe. MSTestAdapterSettings.IsAppDomainCreationDisabled keeps its XmlResolver = null.
4Public API & Binary CompatibilityBLOCKING✅ PASSBoth new types (InvalidRunSettingsException, XmlReaderUtilities) are internal. No PublicAPI.Unshipped.txt changes needed.
5Performance & AllocationsMAJOR✅ PASSGetObjectModelAssembly() scans all loaded assemblies, but it sits on the low-frequency AppDomain setup path (#if NETFRAMEWORK). Acceptable.
6Cross-TFM CompatibilityMAJOR✅ PASSAll AppDomainUtilities changes are properly gated behind #if NETFRAMEWORK. The rest of the diff is TFM-neutral.
7Exception HandlingMAJOR✅ PASSThe two-tier exception topology is preserved. InvalidRunSettingsException intentionally does not derive from AdapterSettingsException; the PR description includes a per-site proof table that is correct and thorough.
8Test Coverage & QualityMAJOR⚠️ SEE BELOWThe diff shows an unchanged context line still asserting Throw<SettingsException>() after the ObjectModel using was removed. Disk state is clean. See inline comment.
9Naming & StyleMINOR✅ PASSInvalidRunSettingsException, XmlReaderUtilities, RunConfigurationSettingsName, TestRunParametersName are all clear and consistent.
10Code DuplicationMINOR⚠️ NOTERunConfigurationSettings retains private ReadToRootNode/ReadToNextElement/SkipToNextElement clones that now mirror the new public XmlReaderUtilities methods. Consolidating would change the exception type on the RunConfiguration path (FormatExceptionInvalidRunSettingsException). Leaving it as-is in this phase is intentional and correct; a later phase can align.
11DocumentationMINOR✅ PASSBoth new types carry XML docs. The InvalidRunSettingsException doc clearly explains the two-tier design rationale.
12Resource ManagementMAJOR✅ N/ANo new IDisposable resources; all existing XmlReader/StringReader uses already have using in their callers.
13Null SafetyMAJOR✅ PASSInvalidRunSettingsException(string? message) is null-safe. GetObjectModelAssembly() either succeeds or throws — never returns null.
14Pattern MatchingMINOR✅ N/ANo opportunities in the changed code.
15LINQ UsageMINOR✅ PASSFirstOrDefault(predicate) in GetObjectModelAssembly() is appropriate.
16LocalizationMAJOR⚠️ MINORXmlReaderUtilities.ReadToRootNode uses a hardcoded interpolated string instead of a Resource.* entry — inconsistent with the rest of the settings-error surface. See inline comment.
17Build & Project FilesMAJOR✅ PASSSDK-style project (MSTestAdapter.PlatformServices.csproj) auto-includes all *.cs files; no explicit <Compile> entries needed for the two new files.
18PR Scope & FocusMAJOR✅ PASSStrictly scoped to the four runsettings-parsing files. The PR description is exemplary in its fidelity analysis.
19Backward CompatibilityBLOCKING✅ PASSThe per-site exception-handler table in the PR description demonstrates that observable behaviour is unchanged. Site 3 (MSTestAdapterSettings.ReadAssemblyResolutionPath) correctly escapes InitializeDiscovery both before and after.
20Error Message QualityMINOR✅ PASSAll messages are actionable. The hardcoded XmlReaderUtilities message is clear even if not resource-backed (see dim 16).
21Design & ArchitectureMAJOR✅ PASSExtension methods on XmlReader is idiomatic; the GetObjectModelAssembly() late-binding approach avoids a hard compile-time dependency on a type that is scheduled to be removed.
22Overarching Principles✅ PASSBackward compatibility is sacred and demonstrably preserved. Scope is disciplined (one phase, one responsibility). No new public API.

Key Findings

⚠️ Test assertion not updated (dim 8, dim 16)

The diff shows a context (unchanged) line inside GetTestRunParametersThrowsOnInvalidSettingsXml that still asserts .Throw<SettingsException>() after the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed. That would be a compilation error. The current disk state is clean (no SettingsException reference), which implies the base branch (6e-3b) already had this method restructured and the git diff's "nearest function" context header is misleading. No action required, but worth an explicit regression note: if the 6e-3b base is ever rebased, this context line should not re-appear.

i️ Hardcoded error message (dim 16)

XmlReaderUtilities.ReadToRootNode throws $"Could not find '...' node..." as an interpolated string. Every peer error in this area (ThrowOnHasAttributes, FromXml, ReadAssemblyResolutionPath) uses Resource.* strings. Low priority, but a resource entry would keep this consistent for any future localization pass.

i️ RunConfigurationSettings duplication (dim 10)

RunConfigurationSettings still carries three private helpers (ReadToRootNode/ReadToNextElement/SkipToNextElement) that are now semantically identical to XmlReaderUtilities. The only observable difference is that RunConfigurationSettings.ReadToRootNode throws FormatException while XmlReaderUtilities.ReadToRootNode throws InvalidRunSettingsException. Consolidating them is a deliberate future-phase decision and is tracked correctly.


Overall: Well-executed, carefully analysed refactoring. The exception-topology proof table in the PR description sets a high bar for this kind of decoupling work. The two non-blocking observations above are worth noting but do not affect correctness or behaviour.

Comments that could not be inline-anchored

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs:33

The diff shows this assertion as an unchanged context line (no - prefix) while the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed in the same hunk. As written after the patch, SettingsException is no longer resolvable — this should be a compilation error.

The disk state does not exhibit the problem (no SettingsException reference remains, and the method was restructured), which suggests it was resolved either within the same commit or via a prior phase…

src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs:29

The error message is a hardcoded interpolated string. Every other settings-parse error in this codebase flows through Resource.* strings (e.g., Resource.InvalidSettingsXmlElement, Resource.InvalidSettingsXmlAttribute). While this path is rare and the message itself is perfectly clear, it is inconsistent with the localization policy of the codebase.

Suggested fix: add a resource entry (e.g. InvalidRunSettingsRootNode) and use `string.Format(CultureInfo.CurrentCulture, Resource.InvalidRu…

Amaury Levé (Evangelink) added a commit that referenced this pull request Jul 5, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink
, '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

Neutralize the runsettings-parsing helpers (Phase 6e-4a) - #9627

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils
Jul 5, 2026
Merged

Neutralize the runsettings-parsing helpers (Phase 6e-4a)#9627
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4a — Neutralize the runsettings-parsing helpers

Part of the initiative to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). Strict byte-for-byte refactor.

What this does

Decouples the four runsettings-XML parsing files (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings, MSTestSettings.RunSettingsXml):

  1. SettingsException → new neutral InvalidRunSettingsException (derives directly from Exception, not from AdapterSettingsException).
  2. Inline the ObjectModel.Constants node names ("RunConfiguration", "TestRunParameters").
  3. Repoint XmlRunSettingsUtilities.ReaderSettings at the neutral RunSettingsUtilities.ReaderSettings.
  4. New neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement), reusing the exact navigation semantics the adapter already vendored privately in RunConfigurationSettings.

Coupling drops 10 → 6 files.

Fidelity — exception-handler trace (the proof)

The adapter has a two-tier settings-exception topology this change preserves. MSTestDiscovererHelpers.InitializeDiscovery catches AdapterSettingsException (invalid MSTest settings values → log + "no tests"); a structural runsettings error escapes that handler to the host. Per-site — which handler catches it, before vs after:

Throw siteSole reachable pathAs VSTest SettingsException (today)As InvalidRunSettingsException (after)
RunSettingsUtilities.ThrowOnHasAttributesTestRunParameters.FromXmlGetTestRunParametersTestExecutionManager.CacheSessionParametersbroad catch (Exception)same broad catch (Exception)
TestRunParameters.FromXmlGetTestRunParametersCacheSessionParametersbroad catch (Exception)same broad catch (Exception)
MSTestAdapterSettings.ToSettings (AssemblyResolution)SettingsProvider.Load (from PopulateSettings, wrapped by InitializeDiscovery's catch (AdapterSettingsException))escapes that catch → DiscoverTests/RunTests (all try/finally, no catch) → hostescapes (distinct type) → host

A distinct type (not AdapterSettingsException) is required for site 3: reusing AdapterSettingsException would make a malformed <AssemblyResolution> newly caught at InitializeDiscovery (log + "no tests") instead of escaping — an observable behavior change.

Cross-process note (site 3 escapes to the host). Perfect type-and-stack fidelity on this path is not achievable while decoupling, because SettingsException is itself an object-model type and must leave PlatformServices before the package-reference drop. The neutral InvalidRunSettingsExceptionpreserves the message, the throw origin/stack, and the escape topology (still escapes InitializeDiscovery and DiscoverTests/RunTests exactly as before); only the exception type name differs, and only on a rare, user-authored malformed-<AssemblyResolution> runsettings that no test exercises. The test platform surfaces adapter-thrown exceptions during Discover/RunTests uniformly as TestMessageLevel.Error (its runsettings-validation special-casing lives in the host's own session-start parsing, not adapter invocation), so this is behavior-neutral on observable output. Adding a dedicated cross-process TRX/error-parity regression test is tracked by #9587. Unifying the two exceptions onto one model is a separate deliberate behavior change, tracked in #9629.

MSTestExecutor/MSTestDiscoverer have no typed settings-exception catch, so there is no boundary catch to update. The …BailOutOnSettingsException tests (which exercise the AdapterSettingsException path) pass unchanged.

Verification

  • Full build.cmd -c Debug green all TFMs, IDE0005 clean.
  • MSTestAdapter.PlatformServices.UnitTests: 897 (net8.0) / 935 (net462), 0 failed.
  • MSTestAdapter.UnitTests: 21, 0 failed (incl. BailOut tests).
  • MSTest.IntegrationTests (cross-proc, net462): 48 total, 0 failed, 1 skipped (pre-existing known-flaky).

Stacking

6c2 #95906d-1 #95916d-2 #96216e-1 #96226e-2 #96236e-3a #96246e-3b #96266e-4a (this). Base = 6e-3b branch. Do not rebase/reset the base.

Decouple the runsettings-XML parsing files from the VSTest object model:
- Replace the VSTest SettingsException thrown during runsettings/test-run-parameter
parsing (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings) with a new
neutral InvalidRunSettingsException. This exception is deliberately DISTINCT from the
existing AdapterSettingsException to preserve behavior byte-for-byte: the only typed
settings-error handler, MSTestDiscovererHelpers.InitializeDiscovery, catches
AdapterSettingsException (invalid MSTest settings values -> report + no tests), while
a structural runsettings error historically threw VSTest SettingsException and
escaped that handler to the host. The malformed <AssemblyResolution> throw site is
reachable through PopulateSettings via SettingsProvider.Load, so reusing
AdapterSettingsException there would have changed the escape semantics; the distinct
InvalidRunSettingsException (unrelated to AdapterSettingsException) preserves them.
The other sites are caught only by a broad catch(Exception) in CacheSessionParameters,
so behavior there is identical either way. Only the direct typed-throw unit
assertions change.
- Inline the VSTest ObjectModel.Constants runsettings node names
(RunConfiguration, TestRunParameters) as neutral constants.
- Repoint XmlRunSettingsUtilities.ReaderSettings at the equivalent neutral
RunSettingsUtilities.ReaderSettings that already existed.
- Add a neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement)
replacing the VSTest ObjectModel.Utilities helpers, reusing the exact navigation
semantics the adapter already vendored privately in RunConfigurationSettings.
Drops the PlatformServices VSTest-ObjectModel coupling from 10 to 6 files (the
remaining are the netfx residuals + AssemblyResolver string literals).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-settings-utils branch from 4b11c0a to fbcafefCompareJuly 5, 2026 12:17
…6e-4b) (#9628)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review July 5, 2026 19:27
@Evangelink
Amaury Levé (Evangelink) merged commit 1fb4b4e into dev/amauryleve/vstest-decoupling-conversionJul 5, 2026
18 of 20 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-settings-utils branch July 5, 2026 19:27

@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. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.


Expert Review — Phase 6e-4a: Neutralize the runsettings-parsing helpers

Verdict: ✅ Informational / COMMENT(PR already merged; no blocking issues found.)


22-Dimension Verdict Table

#DimensionSeverityVerdictNotes
1Algorithmic CorrectnessMAJOR✅ PASSReadToNextElement/SkipToNextElement/ReadToRootNode semantics byte-for-byte match the RunConfigurationSettings private copies they were modelled on.
2Threading & ConcurrencyBLOCKING✅ N/APure synchronous XML parsing, no shared mutable state.
3Security & IPC Contract SafetyBLOCKING✅ PASSXmlReaderSettings default in .NET 4.5+ is DtdProcessing.Prohibit; not setting it explicitly is safe. MSTestAdapterSettings.IsAppDomainCreationDisabled keeps its XmlResolver = null.
4Public API & Binary CompatibilityBLOCKING✅ PASSBoth new types (InvalidRunSettingsException, XmlReaderUtilities) are internal. No PublicAPI.Unshipped.txt changes needed.
5Performance & AllocationsMAJOR✅ PASSGetObjectModelAssembly() scans all loaded assemblies, but it sits on the low-frequency AppDomain setup path (#if NETFRAMEWORK). Acceptable.
6Cross-TFM CompatibilityMAJOR✅ PASSAll AppDomainUtilities changes are properly gated behind #if NETFRAMEWORK. The rest of the diff is TFM-neutral.
7Exception HandlingMAJOR✅ PASSThe two-tier exception topology is preserved. InvalidRunSettingsException intentionally does not derive from AdapterSettingsException; the PR description includes a per-site proof table that is correct and thorough.
8Test Coverage & QualityMAJOR⚠️ SEE BELOWThe diff shows an unchanged context line still asserting Throw<SettingsException>() after the ObjectModel using was removed. Disk state is clean. See inline comment.
9Naming & StyleMINOR✅ PASSInvalidRunSettingsException, XmlReaderUtilities, RunConfigurationSettingsName, TestRunParametersName are all clear and consistent.
10Code DuplicationMINOR⚠️ NOTERunConfigurationSettings retains private ReadToRootNode/ReadToNextElement/SkipToNextElement clones that now mirror the new public XmlReaderUtilities methods. Consolidating would change the exception type on the RunConfiguration path (FormatExceptionInvalidRunSettingsException). Leaving it as-is in this phase is intentional and correct; a later phase can align.
11DocumentationMINOR✅ PASSBoth new types carry XML docs. The InvalidRunSettingsException doc clearly explains the two-tier design rationale.
12Resource ManagementMAJOR✅ N/ANo new IDisposable resources; all existing XmlReader/StringReader uses already have using in their callers.
13Null SafetyMAJOR✅ PASSInvalidRunSettingsException(string? message) is null-safe. GetObjectModelAssembly() either succeeds or throws — never returns null.
14Pattern MatchingMINOR✅ N/ANo opportunities in the changed code.
15LINQ UsageMINOR✅ PASSFirstOrDefault(predicate) in GetObjectModelAssembly() is appropriate.
16LocalizationMAJOR⚠️ MINORXmlReaderUtilities.ReadToRootNode uses a hardcoded interpolated string instead of a Resource.* entry — inconsistent with the rest of the settings-error surface. See inline comment.
17Build & Project FilesMAJOR✅ PASSSDK-style project (MSTestAdapter.PlatformServices.csproj) auto-includes all *.cs files; no explicit <Compile> entries needed for the two new files.
18PR Scope & FocusMAJOR✅ PASSStrictly scoped to the four runsettings-parsing files. The PR description is exemplary in its fidelity analysis.
19Backward CompatibilityBLOCKING✅ PASSThe per-site exception-handler table in the PR description demonstrates that observable behaviour is unchanged. Site 3 (MSTestAdapterSettings.ReadAssemblyResolutionPath) correctly escapes InitializeDiscovery both before and after.
20Error Message QualityMINOR✅ PASSAll messages are actionable. The hardcoded XmlReaderUtilities message is clear even if not resource-backed (see dim 16).
21Design & ArchitectureMAJOR✅ PASSExtension methods on XmlReader is idiomatic; the GetObjectModelAssembly() late-binding approach avoids a hard compile-time dependency on a type that is scheduled to be removed.
22Overarching Principles✅ PASSBackward compatibility is sacred and demonstrably preserved. Scope is disciplined (one phase, one responsibility). No new public API.

Key Findings

⚠️ Test assertion not updated (dim 8, dim 16)

The diff shows a context (unchanged) line inside GetTestRunParametersThrowsOnInvalidSettingsXml that still asserts .Throw<SettingsException>() after the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed. That would be a compilation error. The current disk state is clean (no SettingsException reference), which implies the base branch (6e-3b) already had this method restructured and the git diff's "nearest function" context header is misleading. No action required, but worth an explicit regression note: if the 6e-3b base is ever rebased, this context line should not re-appear.

i️ Hardcoded error message (dim 16)

XmlReaderUtilities.ReadToRootNode throws $"Could not find '...' node..." as an interpolated string. Every peer error in this area (ThrowOnHasAttributes, FromXml, ReadAssemblyResolutionPath) uses Resource.* strings. Low priority, but a resource entry would keep this consistent for any future localization pass.

i️ RunConfigurationSettings duplication (dim 10)

RunConfigurationSettings still carries three private helpers (ReadToRootNode/ReadToNextElement/SkipToNextElement) that are now semantically identical to XmlReaderUtilities. The only observable difference is that RunConfigurationSettings.ReadToRootNode throws FormatException while XmlReaderUtilities.ReadToRootNode throws InvalidRunSettingsException. Consolidating them is a deliberate future-phase decision and is tracked correctly.


Overall: Well-executed, carefully analysed refactoring. The exception-topology proof table in the PR description sets a high bar for this kind of decoupling work. The two non-blocking observations above are worth noting but do not affect correctness or behaviour.

Comments that could not be inline-anchored

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs:33

The diff shows this assertion as an unchanged context line (no - prefix) while the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed in the same hunk. As written after the patch, SettingsException is no longer resolvable — this should be a compilation error.

The disk state does not exhibit the problem (no SettingsException reference remains, and the method was restructured), which suggests it was resolved either within the same commit or via a prior phase…

src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs:29

The error message is a hardcoded interpolated string. Every other settings-parse error in this codebase flows through Resource.* strings (e.g., Resource.InvalidSettingsXmlElement, Resource.InvalidSettingsXmlAttribute). While this path is rare and the message itself is perfectly clear, it is inconsistent with the localization policy of the codebase.

Suggested fix: add a resource entry (e.g. InvalidRunSettingsRootNode) and use `string.Format(CultureInfo.CurrentCulture, Resource.InvalidRu…

Amaury Levé (Evangelink) added a commit that referenced this pull request Jul 5, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink
, '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

Neutralize the runsettings-parsing helpers (Phase 6e-4a) - #9627

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils
Jul 5, 2026
Merged

Neutralize the runsettings-parsing helpers (Phase 6e-4a)#9627
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4a — Neutralize the runsettings-parsing helpers

Part of the initiative to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). Strict byte-for-byte refactor.

What this does

Decouples the four runsettings-XML parsing files (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings, MSTestSettings.RunSettingsXml):

  1. SettingsException → new neutral InvalidRunSettingsException (derives directly from Exception, not from AdapterSettingsException).
  2. Inline the ObjectModel.Constants node names ("RunConfiguration", "TestRunParameters").
  3. Repoint XmlRunSettingsUtilities.ReaderSettings at the neutral RunSettingsUtilities.ReaderSettings.
  4. New neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement), reusing the exact navigation semantics the adapter already vendored privately in RunConfigurationSettings.

Coupling drops 10 → 6 files.

Fidelity — exception-handler trace (the proof)

The adapter has a two-tier settings-exception topology this change preserves. MSTestDiscovererHelpers.InitializeDiscovery catches AdapterSettingsException (invalid MSTest settings values → log + "no tests"); a structural runsettings error escapes that handler to the host. Per-site — which handler catches it, before vs after:

Throw siteSole reachable pathAs VSTest SettingsException (today)As InvalidRunSettingsException (after)
RunSettingsUtilities.ThrowOnHasAttributesTestRunParameters.FromXmlGetTestRunParametersTestExecutionManager.CacheSessionParametersbroad catch (Exception)same broad catch (Exception)
TestRunParameters.FromXmlGetTestRunParametersCacheSessionParametersbroad catch (Exception)same broad catch (Exception)
MSTestAdapterSettings.ToSettings (AssemblyResolution)SettingsProvider.Load (from PopulateSettings, wrapped by InitializeDiscovery's catch (AdapterSettingsException))escapes that catch → DiscoverTests/RunTests (all try/finally, no catch) → hostescapes (distinct type) → host

A distinct type (not AdapterSettingsException) is required for site 3: reusing AdapterSettingsException would make a malformed <AssemblyResolution> newly caught at InitializeDiscovery (log + "no tests") instead of escaping — an observable behavior change.

Cross-process note (site 3 escapes to the host). Perfect type-and-stack fidelity on this path is not achievable while decoupling, because SettingsException is itself an object-model type and must leave PlatformServices before the package-reference drop. The neutral InvalidRunSettingsExceptionpreserves the message, the throw origin/stack, and the escape topology (still escapes InitializeDiscovery and DiscoverTests/RunTests exactly as before); only the exception type name differs, and only on a rare, user-authored malformed-<AssemblyResolution> runsettings that no test exercises. The test platform surfaces adapter-thrown exceptions during Discover/RunTests uniformly as TestMessageLevel.Error (its runsettings-validation special-casing lives in the host's own session-start parsing, not adapter invocation), so this is behavior-neutral on observable output. Adding a dedicated cross-process TRX/error-parity regression test is tracked by #9587. Unifying the two exceptions onto one model is a separate deliberate behavior change, tracked in #9629.

MSTestExecutor/MSTestDiscoverer have no typed settings-exception catch, so there is no boundary catch to update. The …BailOutOnSettingsException tests (which exercise the AdapterSettingsException path) pass unchanged.

Verification

  • Full build.cmd -c Debug green all TFMs, IDE0005 clean.
  • MSTestAdapter.PlatformServices.UnitTests: 897 (net8.0) / 935 (net462), 0 failed.
  • MSTestAdapter.UnitTests: 21, 0 failed (incl. BailOut tests).
  • MSTest.IntegrationTests (cross-proc, net462): 48 total, 0 failed, 1 skipped (pre-existing known-flaky).

Stacking

6c2 #95906d-1 #95916d-2 #96216e-1 #96226e-2 #96236e-3a #96246e-3b #96266e-4a (this). Base = 6e-3b branch. Do not rebase/reset the base.

Decouple the runsettings-XML parsing files from the VSTest object model:
- Replace the VSTest SettingsException thrown during runsettings/test-run-parameter
parsing (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings) with a new
neutral InvalidRunSettingsException. This exception is deliberately DISTINCT from the
existing AdapterSettingsException to preserve behavior byte-for-byte: the only typed
settings-error handler, MSTestDiscovererHelpers.InitializeDiscovery, catches
AdapterSettingsException (invalid MSTest settings values -> report + no tests), while
a structural runsettings error historically threw VSTest SettingsException and
escaped that handler to the host. The malformed <AssemblyResolution> throw site is
reachable through PopulateSettings via SettingsProvider.Load, so reusing
AdapterSettingsException there would have changed the escape semantics; the distinct
InvalidRunSettingsException (unrelated to AdapterSettingsException) preserves them.
The other sites are caught only by a broad catch(Exception) in CacheSessionParameters,
so behavior there is identical either way. Only the direct typed-throw unit
assertions change.
- Inline the VSTest ObjectModel.Constants runsettings node names
(RunConfiguration, TestRunParameters) as neutral constants.
- Repoint XmlRunSettingsUtilities.ReaderSettings at the equivalent neutral
RunSettingsUtilities.ReaderSettings that already existed.
- Add a neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement)
replacing the VSTest ObjectModel.Utilities helpers, reusing the exact navigation
semantics the adapter already vendored privately in RunConfigurationSettings.
Drops the PlatformServices VSTest-ObjectModel coupling from 10 to 6 files (the
remaining are the netfx residuals + AssemblyResolver string literals).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-settings-utils branch from 4b11c0a to fbcafefCompareJuly 5, 2026 12:17
…6e-4b) (#9628)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review July 5, 2026 19:27
@Evangelink
Amaury Levé (Evangelink) merged commit 1fb4b4e into dev/amauryleve/vstest-decoupling-conversionJul 5, 2026
18 of 20 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-settings-utils branch July 5, 2026 19:27

@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. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.


Expert Review — Phase 6e-4a: Neutralize the runsettings-parsing helpers

Verdict: ✅ Informational / COMMENT(PR already merged; no blocking issues found.)


22-Dimension Verdict Table

#DimensionSeverityVerdictNotes
1Algorithmic CorrectnessMAJOR✅ PASSReadToNextElement/SkipToNextElement/ReadToRootNode semantics byte-for-byte match the RunConfigurationSettings private copies they were modelled on.
2Threading & ConcurrencyBLOCKING✅ N/APure synchronous XML parsing, no shared mutable state.
3Security & IPC Contract SafetyBLOCKING✅ PASSXmlReaderSettings default in .NET 4.5+ is DtdProcessing.Prohibit; not setting it explicitly is safe. MSTestAdapterSettings.IsAppDomainCreationDisabled keeps its XmlResolver = null.
4Public API & Binary CompatibilityBLOCKING✅ PASSBoth new types (InvalidRunSettingsException, XmlReaderUtilities) are internal. No PublicAPI.Unshipped.txt changes needed.
5Performance & AllocationsMAJOR✅ PASSGetObjectModelAssembly() scans all loaded assemblies, but it sits on the low-frequency AppDomain setup path (#if NETFRAMEWORK). Acceptable.
6Cross-TFM CompatibilityMAJOR✅ PASSAll AppDomainUtilities changes are properly gated behind #if NETFRAMEWORK. The rest of the diff is TFM-neutral.
7Exception HandlingMAJOR✅ PASSThe two-tier exception topology is preserved. InvalidRunSettingsException intentionally does not derive from AdapterSettingsException; the PR description includes a per-site proof table that is correct and thorough.
8Test Coverage & QualityMAJOR⚠️ SEE BELOWThe diff shows an unchanged context line still asserting Throw<SettingsException>() after the ObjectModel using was removed. Disk state is clean. See inline comment.
9Naming & StyleMINOR✅ PASSInvalidRunSettingsException, XmlReaderUtilities, RunConfigurationSettingsName, TestRunParametersName are all clear and consistent.
10Code DuplicationMINOR⚠️ NOTERunConfigurationSettings retains private ReadToRootNode/ReadToNextElement/SkipToNextElement clones that now mirror the new public XmlReaderUtilities methods. Consolidating would change the exception type on the RunConfiguration path (FormatExceptionInvalidRunSettingsException). Leaving it as-is in this phase is intentional and correct; a later phase can align.
11DocumentationMINOR✅ PASSBoth new types carry XML docs. The InvalidRunSettingsException doc clearly explains the two-tier design rationale.
12Resource ManagementMAJOR✅ N/ANo new IDisposable resources; all existing XmlReader/StringReader uses already have using in their callers.
13Null SafetyMAJOR✅ PASSInvalidRunSettingsException(string? message) is null-safe. GetObjectModelAssembly() either succeeds or throws — never returns null.
14Pattern MatchingMINOR✅ N/ANo opportunities in the changed code.
15LINQ UsageMINOR✅ PASSFirstOrDefault(predicate) in GetObjectModelAssembly() is appropriate.
16LocalizationMAJOR⚠️ MINORXmlReaderUtilities.ReadToRootNode uses a hardcoded interpolated string instead of a Resource.* entry — inconsistent with the rest of the settings-error surface. See inline comment.
17Build & Project FilesMAJOR✅ PASSSDK-style project (MSTestAdapter.PlatformServices.csproj) auto-includes all *.cs files; no explicit <Compile> entries needed for the two new files.
18PR Scope & FocusMAJOR✅ PASSStrictly scoped to the four runsettings-parsing files. The PR description is exemplary in its fidelity analysis.
19Backward CompatibilityBLOCKING✅ PASSThe per-site exception-handler table in the PR description demonstrates that observable behaviour is unchanged. Site 3 (MSTestAdapterSettings.ReadAssemblyResolutionPath) correctly escapes InitializeDiscovery both before and after.
20Error Message QualityMINOR✅ PASSAll messages are actionable. The hardcoded XmlReaderUtilities message is clear even if not resource-backed (see dim 16).
21Design & ArchitectureMAJOR✅ PASSExtension methods on XmlReader is idiomatic; the GetObjectModelAssembly() late-binding approach avoids a hard compile-time dependency on a type that is scheduled to be removed.
22Overarching Principles✅ PASSBackward compatibility is sacred and demonstrably preserved. Scope is disciplined (one phase, one responsibility). No new public API.

Key Findings

⚠️ Test assertion not updated (dim 8, dim 16)

The diff shows a context (unchanged) line inside GetTestRunParametersThrowsOnInvalidSettingsXml that still asserts .Throw<SettingsException>() after the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed. That would be a compilation error. The current disk state is clean (no SettingsException reference), which implies the base branch (6e-3b) already had this method restructured and the git diff's "nearest function" context header is misleading. No action required, but worth an explicit regression note: if the 6e-3b base is ever rebased, this context line should not re-appear.

i️ Hardcoded error message (dim 16)

XmlReaderUtilities.ReadToRootNode throws $"Could not find '...' node..." as an interpolated string. Every peer error in this area (ThrowOnHasAttributes, FromXml, ReadAssemblyResolutionPath) uses Resource.* strings. Low priority, but a resource entry would keep this consistent for any future localization pass.

i️ RunConfigurationSettings duplication (dim 10)

RunConfigurationSettings still carries three private helpers (ReadToRootNode/ReadToNextElement/SkipToNextElement) that are now semantically identical to XmlReaderUtilities. The only observable difference is that RunConfigurationSettings.ReadToRootNode throws FormatException while XmlReaderUtilities.ReadToRootNode throws InvalidRunSettingsException. Consolidating them is a deliberate future-phase decision and is tracked correctly.


Overall: Well-executed, carefully analysed refactoring. The exception-topology proof table in the PR description sets a high bar for this kind of decoupling work. The two non-blocking observations above are worth noting but do not affect correctness or behaviour.

Comments that could not be inline-anchored

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs:33

The diff shows this assertion as an unchanged context line (no - prefix) while the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed in the same hunk. As written after the patch, SettingsException is no longer resolvable — this should be a compilation error.

The disk state does not exhibit the problem (no SettingsException reference remains, and the method was restructured), which suggests it was resolved either within the same commit or via a prior phase…

src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs:29

The error message is a hardcoded interpolated string. Every other settings-parse error in this codebase flows through Resource.* strings (e.g., Resource.InvalidSettingsXmlElement, Resource.InvalidSettingsXmlAttribute). While this path is rare and the message itself is perfectly clear, it is inconsistent with the localization policy of the codebase.

Suggested fix: add a resource entry (e.g. InvalidRunSettingsRootNode) and use `string.Format(CultureInfo.CurrentCulture, Resource.InvalidRu…

Amaury Levé (Evangelink) added a commit that referenced this pull request Jul 5, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink
, '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

Neutralize the runsettings-parsing helpers (Phase 6e-4a) - #9627

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils
Jul 5, 2026
Merged

Neutralize the runsettings-parsing helpers (Phase 6e-4a)#9627
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4a — Neutralize the runsettings-parsing helpers

Part of the initiative to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). Strict byte-for-byte refactor.

What this does

Decouples the four runsettings-XML parsing files (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings, MSTestSettings.RunSettingsXml):

  1. SettingsException → new neutral InvalidRunSettingsException (derives directly from Exception, not from AdapterSettingsException).
  2. Inline the ObjectModel.Constants node names ("RunConfiguration", "TestRunParameters").
  3. Repoint XmlRunSettingsUtilities.ReaderSettings at the neutral RunSettingsUtilities.ReaderSettings.
  4. New neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement), reusing the exact navigation semantics the adapter already vendored privately in RunConfigurationSettings.

Coupling drops 10 → 6 files.

Fidelity — exception-handler trace (the proof)

The adapter has a two-tier settings-exception topology this change preserves. MSTestDiscovererHelpers.InitializeDiscovery catches AdapterSettingsException (invalid MSTest settings values → log + "no tests"); a structural runsettings error escapes that handler to the host. Per-site — which handler catches it, before vs after:

Throw siteSole reachable pathAs VSTest SettingsException (today)As InvalidRunSettingsException (after)
RunSettingsUtilities.ThrowOnHasAttributesTestRunParameters.FromXmlGetTestRunParametersTestExecutionManager.CacheSessionParametersbroad catch (Exception)same broad catch (Exception)
TestRunParameters.FromXmlGetTestRunParametersCacheSessionParametersbroad catch (Exception)same broad catch (Exception)
MSTestAdapterSettings.ToSettings (AssemblyResolution)SettingsProvider.Load (from PopulateSettings, wrapped by InitializeDiscovery's catch (AdapterSettingsException))escapes that catch → DiscoverTests/RunTests (all try/finally, no catch) → hostescapes (distinct type) → host

A distinct type (not AdapterSettingsException) is required for site 3: reusing AdapterSettingsException would make a malformed <AssemblyResolution> newly caught at InitializeDiscovery (log + "no tests") instead of escaping — an observable behavior change.

Cross-process note (site 3 escapes to the host). Perfect type-and-stack fidelity on this path is not achievable while decoupling, because SettingsException is itself an object-model type and must leave PlatformServices before the package-reference drop. The neutral InvalidRunSettingsExceptionpreserves the message, the throw origin/stack, and the escape topology (still escapes InitializeDiscovery and DiscoverTests/RunTests exactly as before); only the exception type name differs, and only on a rare, user-authored malformed-<AssemblyResolution> runsettings that no test exercises. The test platform surfaces adapter-thrown exceptions during Discover/RunTests uniformly as TestMessageLevel.Error (its runsettings-validation special-casing lives in the host's own session-start parsing, not adapter invocation), so this is behavior-neutral on observable output. Adding a dedicated cross-process TRX/error-parity regression test is tracked by #9587. Unifying the two exceptions onto one model is a separate deliberate behavior change, tracked in #9629.

MSTestExecutor/MSTestDiscoverer have no typed settings-exception catch, so there is no boundary catch to update. The …BailOutOnSettingsException tests (which exercise the AdapterSettingsException path) pass unchanged.

Verification

  • Full build.cmd -c Debug green all TFMs, IDE0005 clean.
  • MSTestAdapter.PlatformServices.UnitTests: 897 (net8.0) / 935 (net462), 0 failed.
  • MSTestAdapter.UnitTests: 21, 0 failed (incl. BailOut tests).
  • MSTest.IntegrationTests (cross-proc, net462): 48 total, 0 failed, 1 skipped (pre-existing known-flaky).

Stacking

6c2 #95906d-1 #95916d-2 #96216e-1 #96226e-2 #96236e-3a #96246e-3b #96266e-4a (this). Base = 6e-3b branch. Do not rebase/reset the base.

Decouple the runsettings-XML parsing files from the VSTest object model:
- Replace the VSTest SettingsException thrown during runsettings/test-run-parameter
parsing (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings) with a new
neutral InvalidRunSettingsException. This exception is deliberately DISTINCT from the
existing AdapterSettingsException to preserve behavior byte-for-byte: the only typed
settings-error handler, MSTestDiscovererHelpers.InitializeDiscovery, catches
AdapterSettingsException (invalid MSTest settings values -> report + no tests), while
a structural runsettings error historically threw VSTest SettingsException and
escaped that handler to the host. The malformed <AssemblyResolution> throw site is
reachable through PopulateSettings via SettingsProvider.Load, so reusing
AdapterSettingsException there would have changed the escape semantics; the distinct
InvalidRunSettingsException (unrelated to AdapterSettingsException) preserves them.
The other sites are caught only by a broad catch(Exception) in CacheSessionParameters,
so behavior there is identical either way. Only the direct typed-throw unit
assertions change.
- Inline the VSTest ObjectModel.Constants runsettings node names
(RunConfiguration, TestRunParameters) as neutral constants.
- Repoint XmlRunSettingsUtilities.ReaderSettings at the equivalent neutral
RunSettingsUtilities.ReaderSettings that already existed.
- Add a neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement)
replacing the VSTest ObjectModel.Utilities helpers, reusing the exact navigation
semantics the adapter already vendored privately in RunConfigurationSettings.
Drops the PlatformServices VSTest-ObjectModel coupling from 10 to 6 files (the
remaining are the netfx residuals + AssemblyResolver string literals).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-settings-utils branch from 4b11c0a to fbcafefCompareJuly 5, 2026 12:17
…6e-4b) (#9628)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review July 5, 2026 19:27
@Evangelink
Amaury Levé (Evangelink) merged commit 1fb4b4e into dev/amauryleve/vstest-decoupling-conversionJul 5, 2026
18 of 20 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-settings-utils branch July 5, 2026 19:27

@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. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.


Expert Review — Phase 6e-4a: Neutralize the runsettings-parsing helpers

Verdict: ✅ Informational / COMMENT(PR already merged; no blocking issues found.)


22-Dimension Verdict Table

#DimensionSeverityVerdictNotes
1Algorithmic CorrectnessMAJOR✅ PASSReadToNextElement/SkipToNextElement/ReadToRootNode semantics byte-for-byte match the RunConfigurationSettings private copies they were modelled on.
2Threading & ConcurrencyBLOCKING✅ N/APure synchronous XML parsing, no shared mutable state.
3Security & IPC Contract SafetyBLOCKING✅ PASSXmlReaderSettings default in .NET 4.5+ is DtdProcessing.Prohibit; not setting it explicitly is safe. MSTestAdapterSettings.IsAppDomainCreationDisabled keeps its XmlResolver = null.
4Public API & Binary CompatibilityBLOCKING✅ PASSBoth new types (InvalidRunSettingsException, XmlReaderUtilities) are internal. No PublicAPI.Unshipped.txt changes needed.
5Performance & AllocationsMAJOR✅ PASSGetObjectModelAssembly() scans all loaded assemblies, but it sits on the low-frequency AppDomain setup path (#if NETFRAMEWORK). Acceptable.
6Cross-TFM CompatibilityMAJOR✅ PASSAll AppDomainUtilities changes are properly gated behind #if NETFRAMEWORK. The rest of the diff is TFM-neutral.
7Exception HandlingMAJOR✅ PASSThe two-tier exception topology is preserved. InvalidRunSettingsException intentionally does not derive from AdapterSettingsException; the PR description includes a per-site proof table that is correct and thorough.
8Test Coverage & QualityMAJOR⚠️ SEE BELOWThe diff shows an unchanged context line still asserting Throw<SettingsException>() after the ObjectModel using was removed. Disk state is clean. See inline comment.
9Naming & StyleMINOR✅ PASSInvalidRunSettingsException, XmlReaderUtilities, RunConfigurationSettingsName, TestRunParametersName are all clear and consistent.
10Code DuplicationMINOR⚠️ NOTERunConfigurationSettings retains private ReadToRootNode/ReadToNextElement/SkipToNextElement clones that now mirror the new public XmlReaderUtilities methods. Consolidating would change the exception type on the RunConfiguration path (FormatExceptionInvalidRunSettingsException). Leaving it as-is in this phase is intentional and correct; a later phase can align.
11DocumentationMINOR✅ PASSBoth new types carry XML docs. The InvalidRunSettingsException doc clearly explains the two-tier design rationale.
12Resource ManagementMAJOR✅ N/ANo new IDisposable resources; all existing XmlReader/StringReader uses already have using in their callers.
13Null SafetyMAJOR✅ PASSInvalidRunSettingsException(string? message) is null-safe. GetObjectModelAssembly() either succeeds or throws — never returns null.
14Pattern MatchingMINOR✅ N/ANo opportunities in the changed code.
15LINQ UsageMINOR✅ PASSFirstOrDefault(predicate) in GetObjectModelAssembly() is appropriate.
16LocalizationMAJOR⚠️ MINORXmlReaderUtilities.ReadToRootNode uses a hardcoded interpolated string instead of a Resource.* entry — inconsistent with the rest of the settings-error surface. See inline comment.
17Build & Project FilesMAJOR✅ PASSSDK-style project (MSTestAdapter.PlatformServices.csproj) auto-includes all *.cs files; no explicit <Compile> entries needed for the two new files.
18PR Scope & FocusMAJOR✅ PASSStrictly scoped to the four runsettings-parsing files. The PR description is exemplary in its fidelity analysis.
19Backward CompatibilityBLOCKING✅ PASSThe per-site exception-handler table in the PR description demonstrates that observable behaviour is unchanged. Site 3 (MSTestAdapterSettings.ReadAssemblyResolutionPath) correctly escapes InitializeDiscovery both before and after.
20Error Message QualityMINOR✅ PASSAll messages are actionable. The hardcoded XmlReaderUtilities message is clear even if not resource-backed (see dim 16).
21Design & ArchitectureMAJOR✅ PASSExtension methods on XmlReader is idiomatic; the GetObjectModelAssembly() late-binding approach avoids a hard compile-time dependency on a type that is scheduled to be removed.
22Overarching Principles✅ PASSBackward compatibility is sacred and demonstrably preserved. Scope is disciplined (one phase, one responsibility). No new public API.

Key Findings

⚠️ Test assertion not updated (dim 8, dim 16)

The diff shows a context (unchanged) line inside GetTestRunParametersThrowsOnInvalidSettingsXml that still asserts .Throw<SettingsException>() after the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed. That would be a compilation error. The current disk state is clean (no SettingsException reference), which implies the base branch (6e-3b) already had this method restructured and the git diff's "nearest function" context header is misleading. No action required, but worth an explicit regression note: if the 6e-3b base is ever rebased, this context line should not re-appear.

i️ Hardcoded error message (dim 16)

XmlReaderUtilities.ReadToRootNode throws $"Could not find '...' node..." as an interpolated string. Every peer error in this area (ThrowOnHasAttributes, FromXml, ReadAssemblyResolutionPath) uses Resource.* strings. Low priority, but a resource entry would keep this consistent for any future localization pass.

i️ RunConfigurationSettings duplication (dim 10)

RunConfigurationSettings still carries three private helpers (ReadToRootNode/ReadToNextElement/SkipToNextElement) that are now semantically identical to XmlReaderUtilities. The only observable difference is that RunConfigurationSettings.ReadToRootNode throws FormatException while XmlReaderUtilities.ReadToRootNode throws InvalidRunSettingsException. Consolidating them is a deliberate future-phase decision and is tracked correctly.


Overall: Well-executed, carefully analysed refactoring. The exception-topology proof table in the PR description sets a high bar for this kind of decoupling work. The two non-blocking observations above are worth noting but do not affect correctness or behaviour.

Comments that could not be inline-anchored

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs:33

The diff shows this assertion as an unchanged context line (no - prefix) while the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed in the same hunk. As written after the patch, SettingsException is no longer resolvable — this should be a compilation error.

The disk state does not exhibit the problem (no SettingsException reference remains, and the method was restructured), which suggests it was resolved either within the same commit or via a prior phase…

src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs:29

The error message is a hardcoded interpolated string. Every other settings-parse error in this codebase flows through Resource.* strings (e.g., Resource.InvalidSettingsXmlElement, Resource.InvalidSettingsXmlAttribute). While this path is rare and the message itself is perfectly clear, it is inconsistent with the localization policy of the codebase.

Suggested fix: add a resource entry (e.g. InvalidRunSettingsRootNode) and use `string.Format(CultureInfo.CurrentCulture, Resource.InvalidRu…

Amaury Levé (Evangelink) added a commit that referenced this pull request Jul 5, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink
, '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

Neutralize the runsettings-parsing helpers (Phase 6e-4a) - #9627

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils
Jul 5, 2026
Merged

Neutralize the runsettings-parsing helpers (Phase 6e-4a)#9627
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4a — Neutralize the runsettings-parsing helpers

Part of the initiative to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). Strict byte-for-byte refactor.

What this does

Decouples the four runsettings-XML parsing files (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings, MSTestSettings.RunSettingsXml):

  1. SettingsException → new neutral InvalidRunSettingsException (derives directly from Exception, not from AdapterSettingsException).
  2. Inline the ObjectModel.Constants node names ("RunConfiguration", "TestRunParameters").
  3. Repoint XmlRunSettingsUtilities.ReaderSettings at the neutral RunSettingsUtilities.ReaderSettings.
  4. New neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement), reusing the exact navigation semantics the adapter already vendored privately in RunConfigurationSettings.

Coupling drops 10 → 6 files.

Fidelity — exception-handler trace (the proof)

The adapter has a two-tier settings-exception topology this change preserves. MSTestDiscovererHelpers.InitializeDiscovery catches AdapterSettingsException (invalid MSTest settings values → log + "no tests"); a structural runsettings error escapes that handler to the host. Per-site — which handler catches it, before vs after:

Throw siteSole reachable pathAs VSTest SettingsException (today)As InvalidRunSettingsException (after)
RunSettingsUtilities.ThrowOnHasAttributesTestRunParameters.FromXmlGetTestRunParametersTestExecutionManager.CacheSessionParametersbroad catch (Exception)same broad catch (Exception)
TestRunParameters.FromXmlGetTestRunParametersCacheSessionParametersbroad catch (Exception)same broad catch (Exception)
MSTestAdapterSettings.ToSettings (AssemblyResolution)SettingsProvider.Load (from PopulateSettings, wrapped by InitializeDiscovery's catch (AdapterSettingsException))escapes that catch → DiscoverTests/RunTests (all try/finally, no catch) → hostescapes (distinct type) → host

A distinct type (not AdapterSettingsException) is required for site 3: reusing AdapterSettingsException would make a malformed <AssemblyResolution> newly caught at InitializeDiscovery (log + "no tests") instead of escaping — an observable behavior change.

Cross-process note (site 3 escapes to the host). Perfect type-and-stack fidelity on this path is not achievable while decoupling, because SettingsException is itself an object-model type and must leave PlatformServices before the package-reference drop. The neutral InvalidRunSettingsExceptionpreserves the message, the throw origin/stack, and the escape topology (still escapes InitializeDiscovery and DiscoverTests/RunTests exactly as before); only the exception type name differs, and only on a rare, user-authored malformed-<AssemblyResolution> runsettings that no test exercises. The test platform surfaces adapter-thrown exceptions during Discover/RunTests uniformly as TestMessageLevel.Error (its runsettings-validation special-casing lives in the host's own session-start parsing, not adapter invocation), so this is behavior-neutral on observable output. Adding a dedicated cross-process TRX/error-parity regression test is tracked by #9587. Unifying the two exceptions onto one model is a separate deliberate behavior change, tracked in #9629.

MSTestExecutor/MSTestDiscoverer have no typed settings-exception catch, so there is no boundary catch to update. The …BailOutOnSettingsException tests (which exercise the AdapterSettingsException path) pass unchanged.

Verification

  • Full build.cmd -c Debug green all TFMs, IDE0005 clean.
  • MSTestAdapter.PlatformServices.UnitTests: 897 (net8.0) / 935 (net462), 0 failed.
  • MSTestAdapter.UnitTests: 21, 0 failed (incl. BailOut tests).
  • MSTest.IntegrationTests (cross-proc, net462): 48 total, 0 failed, 1 skipped (pre-existing known-flaky).

Stacking

6c2 #95906d-1 #95916d-2 #96216e-1 #96226e-2 #96236e-3a #96246e-3b #96266e-4a (this). Base = 6e-3b branch. Do not rebase/reset the base.

Decouple the runsettings-XML parsing files from the VSTest object model:
- Replace the VSTest SettingsException thrown during runsettings/test-run-parameter
parsing (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings) with a new
neutral InvalidRunSettingsException. This exception is deliberately DISTINCT from the
existing AdapterSettingsException to preserve behavior byte-for-byte: the only typed
settings-error handler, MSTestDiscovererHelpers.InitializeDiscovery, catches
AdapterSettingsException (invalid MSTest settings values -> report + no tests), while
a structural runsettings error historically threw VSTest SettingsException and
escaped that handler to the host. The malformed <AssemblyResolution> throw site is
reachable through PopulateSettings via SettingsProvider.Load, so reusing
AdapterSettingsException there would have changed the escape semantics; the distinct
InvalidRunSettingsException (unrelated to AdapterSettingsException) preserves them.
The other sites are caught only by a broad catch(Exception) in CacheSessionParameters,
so behavior there is identical either way. Only the direct typed-throw unit
assertions change.
- Inline the VSTest ObjectModel.Constants runsettings node names
(RunConfiguration, TestRunParameters) as neutral constants.
- Repoint XmlRunSettingsUtilities.ReaderSettings at the equivalent neutral
RunSettingsUtilities.ReaderSettings that already existed.
- Add a neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement)
replacing the VSTest ObjectModel.Utilities helpers, reusing the exact navigation
semantics the adapter already vendored privately in RunConfigurationSettings.
Drops the PlatformServices VSTest-ObjectModel coupling from 10 to 6 files (the
remaining are the netfx residuals + AssemblyResolver string literals).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-settings-utils branch from 4b11c0a to fbcafefCompareJuly 5, 2026 12:17
…6e-4b) (#9628)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review July 5, 2026 19:27
@Evangelink
Amaury Levé (Evangelink) merged commit 1fb4b4e into dev/amauryleve/vstest-decoupling-conversionJul 5, 2026
18 of 20 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-settings-utils branch July 5, 2026 19:27

@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. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.


Expert Review — Phase 6e-4a: Neutralize the runsettings-parsing helpers

Verdict: ✅ Informational / COMMENT(PR already merged; no blocking issues found.)


22-Dimension Verdict Table

#DimensionSeverityVerdictNotes
1Algorithmic CorrectnessMAJOR✅ PASSReadToNextElement/SkipToNextElement/ReadToRootNode semantics byte-for-byte match the RunConfigurationSettings private copies they were modelled on.
2Threading & ConcurrencyBLOCKING✅ N/APure synchronous XML parsing, no shared mutable state.
3Security & IPC Contract SafetyBLOCKING✅ PASSXmlReaderSettings default in .NET 4.5+ is DtdProcessing.Prohibit; not setting it explicitly is safe. MSTestAdapterSettings.IsAppDomainCreationDisabled keeps its XmlResolver = null.
4Public API & Binary CompatibilityBLOCKING✅ PASSBoth new types (InvalidRunSettingsException, XmlReaderUtilities) are internal. No PublicAPI.Unshipped.txt changes needed.
5Performance & AllocationsMAJOR✅ PASSGetObjectModelAssembly() scans all loaded assemblies, but it sits on the low-frequency AppDomain setup path (#if NETFRAMEWORK). Acceptable.
6Cross-TFM CompatibilityMAJOR✅ PASSAll AppDomainUtilities changes are properly gated behind #if NETFRAMEWORK. The rest of the diff is TFM-neutral.
7Exception HandlingMAJOR✅ PASSThe two-tier exception topology is preserved. InvalidRunSettingsException intentionally does not derive from AdapterSettingsException; the PR description includes a per-site proof table that is correct and thorough.
8Test Coverage & QualityMAJOR⚠️ SEE BELOWThe diff shows an unchanged context line still asserting Throw<SettingsException>() after the ObjectModel using was removed. Disk state is clean. See inline comment.
9Naming & StyleMINOR✅ PASSInvalidRunSettingsException, XmlReaderUtilities, RunConfigurationSettingsName, TestRunParametersName are all clear and consistent.
10Code DuplicationMINOR⚠️ NOTERunConfigurationSettings retains private ReadToRootNode/ReadToNextElement/SkipToNextElement clones that now mirror the new public XmlReaderUtilities methods. Consolidating would change the exception type on the RunConfiguration path (FormatExceptionInvalidRunSettingsException). Leaving it as-is in this phase is intentional and correct; a later phase can align.
11DocumentationMINOR✅ PASSBoth new types carry XML docs. The InvalidRunSettingsException doc clearly explains the two-tier design rationale.
12Resource ManagementMAJOR✅ N/ANo new IDisposable resources; all existing XmlReader/StringReader uses already have using in their callers.
13Null SafetyMAJOR✅ PASSInvalidRunSettingsException(string? message) is null-safe. GetObjectModelAssembly() either succeeds or throws — never returns null.
14Pattern MatchingMINOR✅ N/ANo opportunities in the changed code.
15LINQ UsageMINOR✅ PASSFirstOrDefault(predicate) in GetObjectModelAssembly() is appropriate.
16LocalizationMAJOR⚠️ MINORXmlReaderUtilities.ReadToRootNode uses a hardcoded interpolated string instead of a Resource.* entry — inconsistent with the rest of the settings-error surface. See inline comment.
17Build & Project FilesMAJOR✅ PASSSDK-style project (MSTestAdapter.PlatformServices.csproj) auto-includes all *.cs files; no explicit <Compile> entries needed for the two new files.
18PR Scope & FocusMAJOR✅ PASSStrictly scoped to the four runsettings-parsing files. The PR description is exemplary in its fidelity analysis.
19Backward CompatibilityBLOCKING✅ PASSThe per-site exception-handler table in the PR description demonstrates that observable behaviour is unchanged. Site 3 (MSTestAdapterSettings.ReadAssemblyResolutionPath) correctly escapes InitializeDiscovery both before and after.
20Error Message QualityMINOR✅ PASSAll messages are actionable. The hardcoded XmlReaderUtilities message is clear even if not resource-backed (see dim 16).
21Design & ArchitectureMAJOR✅ PASSExtension methods on XmlReader is idiomatic; the GetObjectModelAssembly() late-binding approach avoids a hard compile-time dependency on a type that is scheduled to be removed.
22Overarching Principles✅ PASSBackward compatibility is sacred and demonstrably preserved. Scope is disciplined (one phase, one responsibility). No new public API.

Key Findings

⚠️ Test assertion not updated (dim 8, dim 16)

The diff shows a context (unchanged) line inside GetTestRunParametersThrowsOnInvalidSettingsXml that still asserts .Throw<SettingsException>() after the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed. That would be a compilation error. The current disk state is clean (no SettingsException reference), which implies the base branch (6e-3b) already had this method restructured and the git diff's "nearest function" context header is misleading. No action required, but worth an explicit regression note: if the 6e-3b base is ever rebased, this context line should not re-appear.

i️ Hardcoded error message (dim 16)

XmlReaderUtilities.ReadToRootNode throws $"Could not find '...' node..." as an interpolated string. Every peer error in this area (ThrowOnHasAttributes, FromXml, ReadAssemblyResolutionPath) uses Resource.* strings. Low priority, but a resource entry would keep this consistent for any future localization pass.

i️ RunConfigurationSettings duplication (dim 10)

RunConfigurationSettings still carries three private helpers (ReadToRootNode/ReadToNextElement/SkipToNextElement) that are now semantically identical to XmlReaderUtilities. The only observable difference is that RunConfigurationSettings.ReadToRootNode throws FormatException while XmlReaderUtilities.ReadToRootNode throws InvalidRunSettingsException. Consolidating them is a deliberate future-phase decision and is tracked correctly.


Overall: Well-executed, carefully analysed refactoring. The exception-topology proof table in the PR description sets a high bar for this kind of decoupling work. The two non-blocking observations above are worth noting but do not affect correctness or behaviour.

Comments that could not be inline-anchored

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs:33

The diff shows this assertion as an unchanged context line (no - prefix) while the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed in the same hunk. As written after the patch, SettingsException is no longer resolvable — this should be a compilation error.

The disk state does not exhibit the problem (no SettingsException reference remains, and the method was restructured), which suggests it was resolved either within the same commit or via a prior phase…

src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs:29

The error message is a hardcoded interpolated string. Every other settings-parse error in this codebase flows through Resource.* strings (e.g., Resource.InvalidSettingsXmlElement, Resource.InvalidSettingsXmlAttribute). While this path is rare and the message itself is perfectly clear, it is inconsistent with the localization policy of the codebase.

Suggested fix: add a resource entry (e.g. InvalidRunSettingsRootNode) and use `string.Format(CultureInfo.CurrentCulture, Resource.InvalidRu…

Amaury Levé (Evangelink) added a commit that referenced this pull request Jul 5, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink
, '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

Neutralize the runsettings-parsing helpers (Phase 6e-4a) - #9627

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils
Jul 5, 2026
Merged

Neutralize the runsettings-parsing helpers (Phase 6e-4a)#9627
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4a — Neutralize the runsettings-parsing helpers

Part of the initiative to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). Strict byte-for-byte refactor.

What this does

Decouples the four runsettings-XML parsing files (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings, MSTestSettings.RunSettingsXml):

  1. SettingsException → new neutral InvalidRunSettingsException (derives directly from Exception, not from AdapterSettingsException).
  2. Inline the ObjectModel.Constants node names ("RunConfiguration", "TestRunParameters").
  3. Repoint XmlRunSettingsUtilities.ReaderSettings at the neutral RunSettingsUtilities.ReaderSettings.
  4. New neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement), reusing the exact navigation semantics the adapter already vendored privately in RunConfigurationSettings.

Coupling drops 10 → 6 files.

Fidelity — exception-handler trace (the proof)

The adapter has a two-tier settings-exception topology this change preserves. MSTestDiscovererHelpers.InitializeDiscovery catches AdapterSettingsException (invalid MSTest settings values → log + "no tests"); a structural runsettings error escapes that handler to the host. Per-site — which handler catches it, before vs after:

Throw siteSole reachable pathAs VSTest SettingsException (today)As InvalidRunSettingsException (after)
RunSettingsUtilities.ThrowOnHasAttributesTestRunParameters.FromXmlGetTestRunParametersTestExecutionManager.CacheSessionParametersbroad catch (Exception)same broad catch (Exception)
TestRunParameters.FromXmlGetTestRunParametersCacheSessionParametersbroad catch (Exception)same broad catch (Exception)
MSTestAdapterSettings.ToSettings (AssemblyResolution)SettingsProvider.Load (from PopulateSettings, wrapped by InitializeDiscovery's catch (AdapterSettingsException))escapes that catch → DiscoverTests/RunTests (all try/finally, no catch) → hostescapes (distinct type) → host

A distinct type (not AdapterSettingsException) is required for site 3: reusing AdapterSettingsException would make a malformed <AssemblyResolution> newly caught at InitializeDiscovery (log + "no tests") instead of escaping — an observable behavior change.

Cross-process note (site 3 escapes to the host). Perfect type-and-stack fidelity on this path is not achievable while decoupling, because SettingsException is itself an object-model type and must leave PlatformServices before the package-reference drop. The neutral InvalidRunSettingsExceptionpreserves the message, the throw origin/stack, and the escape topology (still escapes InitializeDiscovery and DiscoverTests/RunTests exactly as before); only the exception type name differs, and only on a rare, user-authored malformed-<AssemblyResolution> runsettings that no test exercises. The test platform surfaces adapter-thrown exceptions during Discover/RunTests uniformly as TestMessageLevel.Error (its runsettings-validation special-casing lives in the host's own session-start parsing, not adapter invocation), so this is behavior-neutral on observable output. Adding a dedicated cross-process TRX/error-parity regression test is tracked by #9587. Unifying the two exceptions onto one model is a separate deliberate behavior change, tracked in #9629.

MSTestExecutor/MSTestDiscoverer have no typed settings-exception catch, so there is no boundary catch to update. The …BailOutOnSettingsException tests (which exercise the AdapterSettingsException path) pass unchanged.

Verification

  • Full build.cmd -c Debug green all TFMs, IDE0005 clean.
  • MSTestAdapter.PlatformServices.UnitTests: 897 (net8.0) / 935 (net462), 0 failed.
  • MSTestAdapter.UnitTests: 21, 0 failed (incl. BailOut tests).
  • MSTest.IntegrationTests (cross-proc, net462): 48 total, 0 failed, 1 skipped (pre-existing known-flaky).

Stacking

6c2 #95906d-1 #95916d-2 #96216e-1 #96226e-2 #96236e-3a #96246e-3b #96266e-4a (this). Base = 6e-3b branch. Do not rebase/reset the base.

Decouple the runsettings-XML parsing files from the VSTest object model:
- Replace the VSTest SettingsException thrown during runsettings/test-run-parameter
parsing (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings) with a new
neutral InvalidRunSettingsException. This exception is deliberately DISTINCT from the
existing AdapterSettingsException to preserve behavior byte-for-byte: the only typed
settings-error handler, MSTestDiscovererHelpers.InitializeDiscovery, catches
AdapterSettingsException (invalid MSTest settings values -> report + no tests), while
a structural runsettings error historically threw VSTest SettingsException and
escaped that handler to the host. The malformed <AssemblyResolution> throw site is
reachable through PopulateSettings via SettingsProvider.Load, so reusing
AdapterSettingsException there would have changed the escape semantics; the distinct
InvalidRunSettingsException (unrelated to AdapterSettingsException) preserves them.
The other sites are caught only by a broad catch(Exception) in CacheSessionParameters,
so behavior there is identical either way. Only the direct typed-throw unit
assertions change.
- Inline the VSTest ObjectModel.Constants runsettings node names
(RunConfiguration, TestRunParameters) as neutral constants.
- Repoint XmlRunSettingsUtilities.ReaderSettings at the equivalent neutral
RunSettingsUtilities.ReaderSettings that already existed.
- Add a neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement)
replacing the VSTest ObjectModel.Utilities helpers, reusing the exact navigation
semantics the adapter already vendored privately in RunConfigurationSettings.
Drops the PlatformServices VSTest-ObjectModel coupling from 10 to 6 files (the
remaining are the netfx residuals + AssemblyResolver string literals).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-settings-utils branch from 4b11c0a to fbcafefCompareJuly 5, 2026 12:17
…6e-4b) (#9628)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review July 5, 2026 19:27
@Evangelink
Amaury Levé (Evangelink) merged commit 1fb4b4e into dev/amauryleve/vstest-decoupling-conversionJul 5, 2026
18 of 20 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-settings-utils branch July 5, 2026 19:27

@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. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.


Expert Review — Phase 6e-4a: Neutralize the runsettings-parsing helpers

Verdict: ✅ Informational / COMMENT(PR already merged; no blocking issues found.)


22-Dimension Verdict Table

#DimensionSeverityVerdictNotes
1Algorithmic CorrectnessMAJOR✅ PASSReadToNextElement/SkipToNextElement/ReadToRootNode semantics byte-for-byte match the RunConfigurationSettings private copies they were modelled on.
2Threading & ConcurrencyBLOCKING✅ N/APure synchronous XML parsing, no shared mutable state.
3Security & IPC Contract SafetyBLOCKING✅ PASSXmlReaderSettings default in .NET 4.5+ is DtdProcessing.Prohibit; not setting it explicitly is safe. MSTestAdapterSettings.IsAppDomainCreationDisabled keeps its XmlResolver = null.
4Public API & Binary CompatibilityBLOCKING✅ PASSBoth new types (InvalidRunSettingsException, XmlReaderUtilities) are internal. No PublicAPI.Unshipped.txt changes needed.
5Performance & AllocationsMAJOR✅ PASSGetObjectModelAssembly() scans all loaded assemblies, but it sits on the low-frequency AppDomain setup path (#if NETFRAMEWORK). Acceptable.
6Cross-TFM CompatibilityMAJOR✅ PASSAll AppDomainUtilities changes are properly gated behind #if NETFRAMEWORK. The rest of the diff is TFM-neutral.
7Exception HandlingMAJOR✅ PASSThe two-tier exception topology is preserved. InvalidRunSettingsException intentionally does not derive from AdapterSettingsException; the PR description includes a per-site proof table that is correct and thorough.
8Test Coverage & QualityMAJOR⚠️ SEE BELOWThe diff shows an unchanged context line still asserting Throw<SettingsException>() after the ObjectModel using was removed. Disk state is clean. See inline comment.
9Naming & StyleMINOR✅ PASSInvalidRunSettingsException, XmlReaderUtilities, RunConfigurationSettingsName, TestRunParametersName are all clear and consistent.
10Code DuplicationMINOR⚠️ NOTERunConfigurationSettings retains private ReadToRootNode/ReadToNextElement/SkipToNextElement clones that now mirror the new public XmlReaderUtilities methods. Consolidating would change the exception type on the RunConfiguration path (FormatExceptionInvalidRunSettingsException). Leaving it as-is in this phase is intentional and correct; a later phase can align.
11DocumentationMINOR✅ PASSBoth new types carry XML docs. The InvalidRunSettingsException doc clearly explains the two-tier design rationale.
12Resource ManagementMAJOR✅ N/ANo new IDisposable resources; all existing XmlReader/StringReader uses already have using in their callers.
13Null SafetyMAJOR✅ PASSInvalidRunSettingsException(string? message) is null-safe. GetObjectModelAssembly() either succeeds or throws — never returns null.
14Pattern MatchingMINOR✅ N/ANo opportunities in the changed code.
15LINQ UsageMINOR✅ PASSFirstOrDefault(predicate) in GetObjectModelAssembly() is appropriate.
16LocalizationMAJOR⚠️ MINORXmlReaderUtilities.ReadToRootNode uses a hardcoded interpolated string instead of a Resource.* entry — inconsistent with the rest of the settings-error surface. See inline comment.
17Build & Project FilesMAJOR✅ PASSSDK-style project (MSTestAdapter.PlatformServices.csproj) auto-includes all *.cs files; no explicit <Compile> entries needed for the two new files.
18PR Scope & FocusMAJOR✅ PASSStrictly scoped to the four runsettings-parsing files. The PR description is exemplary in its fidelity analysis.
19Backward CompatibilityBLOCKING✅ PASSThe per-site exception-handler table in the PR description demonstrates that observable behaviour is unchanged. Site 3 (MSTestAdapterSettings.ReadAssemblyResolutionPath) correctly escapes InitializeDiscovery both before and after.
20Error Message QualityMINOR✅ PASSAll messages are actionable. The hardcoded XmlReaderUtilities message is clear even if not resource-backed (see dim 16).
21Design & ArchitectureMAJOR✅ PASSExtension methods on XmlReader is idiomatic; the GetObjectModelAssembly() late-binding approach avoids a hard compile-time dependency on a type that is scheduled to be removed.
22Overarching Principles✅ PASSBackward compatibility is sacred and demonstrably preserved. Scope is disciplined (one phase, one responsibility). No new public API.

Key Findings

⚠️ Test assertion not updated (dim 8, dim 16)

The diff shows a context (unchanged) line inside GetTestRunParametersThrowsOnInvalidSettingsXml that still asserts .Throw<SettingsException>() after the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed. That would be a compilation error. The current disk state is clean (no SettingsException reference), which implies the base branch (6e-3b) already had this method restructured and the git diff's "nearest function" context header is misleading. No action required, but worth an explicit regression note: if the 6e-3b base is ever rebased, this context line should not re-appear.

i️ Hardcoded error message (dim 16)

XmlReaderUtilities.ReadToRootNode throws $"Could not find '...' node..." as an interpolated string. Every peer error in this area (ThrowOnHasAttributes, FromXml, ReadAssemblyResolutionPath) uses Resource.* strings. Low priority, but a resource entry would keep this consistent for any future localization pass.

i️ RunConfigurationSettings duplication (dim 10)

RunConfigurationSettings still carries three private helpers (ReadToRootNode/ReadToNextElement/SkipToNextElement) that are now semantically identical to XmlReaderUtilities. The only observable difference is that RunConfigurationSettings.ReadToRootNode throws FormatException while XmlReaderUtilities.ReadToRootNode throws InvalidRunSettingsException. Consolidating them is a deliberate future-phase decision and is tracked correctly.


Overall: Well-executed, carefully analysed refactoring. The exception-topology proof table in the PR description sets a high bar for this kind of decoupling work. The two non-blocking observations above are worth noting but do not affect correctness or behaviour.

Comments that could not be inline-anchored

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs:33

The diff shows this assertion as an unchanged context line (no - prefix) while the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed in the same hunk. As written after the patch, SettingsException is no longer resolvable — this should be a compilation error.

The disk state does not exhibit the problem (no SettingsException reference remains, and the method was restructured), which suggests it was resolved either within the same commit or via a prior phase…

src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs:29

The error message is a hardcoded interpolated string. Every other settings-parse error in this codebase flows through Resource.* strings (e.g., Resource.InvalidSettingsXmlElement, Resource.InvalidSettingsXmlAttribute). While this path is rare and the message itself is perfectly clear, it is inconsistent with the localization policy of the codebase.

Suggested fix: add a resource entry (e.g. InvalidRunSettingsRootNode) and use `string.Format(CultureInfo.CurrentCulture, Resource.InvalidRu…

Amaury Levé (Evangelink) added a commit that referenced this pull request Jul 5, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink
, '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

Neutralize the runsettings-parsing helpers (Phase 6e-4a) - #9627

Merged
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils
Jul 5, 2026
Merged

Neutralize the runsettings-parsing helpers (Phase 6e-4a)#9627
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-conversionfrom
dev/amauryleve/vstest-decoupling-settings-utils

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4a — Neutralize the runsettings-parsing helpers

Part of the initiative to make MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on the VSTest object model (Microsoft.TestPlatform.ObjectModel). Strict byte-for-byte refactor.

What this does

Decouples the four runsettings-XML parsing files (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings, MSTestSettings.RunSettingsXml):

  1. SettingsException → new neutral InvalidRunSettingsException (derives directly from Exception, not from AdapterSettingsException).
  2. Inline the ObjectModel.Constants node names ("RunConfiguration", "TestRunParameters").
  3. Repoint XmlRunSettingsUtilities.ReaderSettings at the neutral RunSettingsUtilities.ReaderSettings.
  4. New neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement), reusing the exact navigation semantics the adapter already vendored privately in RunConfigurationSettings.

Coupling drops 10 → 6 files.

Fidelity — exception-handler trace (the proof)

The adapter has a two-tier settings-exception topology this change preserves. MSTestDiscovererHelpers.InitializeDiscovery catches AdapterSettingsException (invalid MSTest settings values → log + "no tests"); a structural runsettings error escapes that handler to the host. Per-site — which handler catches it, before vs after:

Throw siteSole reachable pathAs VSTest SettingsException (today)As InvalidRunSettingsException (after)
RunSettingsUtilities.ThrowOnHasAttributesTestRunParameters.FromXmlGetTestRunParametersTestExecutionManager.CacheSessionParametersbroad catch (Exception)same broad catch (Exception)
TestRunParameters.FromXmlGetTestRunParametersCacheSessionParametersbroad catch (Exception)same broad catch (Exception)
MSTestAdapterSettings.ToSettings (AssemblyResolution)SettingsProvider.Load (from PopulateSettings, wrapped by InitializeDiscovery's catch (AdapterSettingsException))escapes that catch → DiscoverTests/RunTests (all try/finally, no catch) → hostescapes (distinct type) → host

A distinct type (not AdapterSettingsException) is required for site 3: reusing AdapterSettingsException would make a malformed <AssemblyResolution> newly caught at InitializeDiscovery (log + "no tests") instead of escaping — an observable behavior change.

Cross-process note (site 3 escapes to the host). Perfect type-and-stack fidelity on this path is not achievable while decoupling, because SettingsException is itself an object-model type and must leave PlatformServices before the package-reference drop. The neutral InvalidRunSettingsExceptionpreserves the message, the throw origin/stack, and the escape topology (still escapes InitializeDiscovery and DiscoverTests/RunTests exactly as before); only the exception type name differs, and only on a rare, user-authored malformed-<AssemblyResolution> runsettings that no test exercises. The test platform surfaces adapter-thrown exceptions during Discover/RunTests uniformly as TestMessageLevel.Error (its runsettings-validation special-casing lives in the host's own session-start parsing, not adapter invocation), so this is behavior-neutral on observable output. Adding a dedicated cross-process TRX/error-parity regression test is tracked by #9587. Unifying the two exceptions onto one model is a separate deliberate behavior change, tracked in #9629.

MSTestExecutor/MSTestDiscoverer have no typed settings-exception catch, so there is no boundary catch to update. The …BailOutOnSettingsException tests (which exercise the AdapterSettingsException path) pass unchanged.

Verification

  • Full build.cmd -c Debug green all TFMs, IDE0005 clean.
  • MSTestAdapter.PlatformServices.UnitTests: 897 (net8.0) / 935 (net462), 0 failed.
  • MSTestAdapter.UnitTests: 21, 0 failed (incl. BailOut tests).
  • MSTest.IntegrationTests (cross-proc, net462): 48 total, 0 failed, 1 skipped (pre-existing known-flaky).

Stacking

6c2 #95906d-1 #95916d-2 #96216e-1 #96226e-2 #96236e-3a #96246e-3b #96266e-4a (this). Base = 6e-3b branch. Do not rebase/reset the base.

Decouple the runsettings-XML parsing files from the VSTest object model:
- Replace the VSTest SettingsException thrown during runsettings/test-run-parameter
parsing (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings) with a new
neutral InvalidRunSettingsException. This exception is deliberately DISTINCT from the
existing AdapterSettingsException to preserve behavior byte-for-byte: the only typed
settings-error handler, MSTestDiscovererHelpers.InitializeDiscovery, catches
AdapterSettingsException (invalid MSTest settings values -> report + no tests), while
a structural runsettings error historically threw VSTest SettingsException and
escaped that handler to the host. The malformed <AssemblyResolution> throw site is
reachable through PopulateSettings via SettingsProvider.Load, so reusing
AdapterSettingsException there would have changed the escape semantics; the distinct
InvalidRunSettingsException (unrelated to AdapterSettingsException) preserves them.
The other sites are caught only by a broad catch(Exception) in CacheSessionParameters,
so behavior there is identical either way. Only the direct typed-throw unit
assertions change.
- Inline the VSTest ObjectModel.Constants runsettings node names
(RunConfiguration, TestRunParameters) as neutral constants.
- Repoint XmlRunSettingsUtilities.ReaderSettings at the equivalent neutral
RunSettingsUtilities.ReaderSettings that already existed.
- Add a neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement)
replacing the VSTest ObjectModel.Utilities helpers, reusing the exact navigation
semantics the adapter already vendored privately in RunConfigurationSettings.
Drops the PlatformServices VSTest-ObjectModel coupling from 10 to 6 files (the
remaining are the netfx residuals + AssemblyResolver string literals).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-settings-utils branch from 4b11c0a to fbcafefCompareJuly 5, 2026 12:17
…6e-4b) (#9628)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review July 5, 2026 19:27
@Evangelink
Amaury Levé (Evangelink) merged commit 1fb4b4e into dev/amauryleve/vstest-decoupling-conversionJul 5, 2026
18 of 20 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-settings-utils branch July 5, 2026 19:27

@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. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.


Expert Review — Phase 6e-4a: Neutralize the runsettings-parsing helpers

Verdict: ✅ Informational / COMMENT(PR already merged; no blocking issues found.)


22-Dimension Verdict Table

#DimensionSeverityVerdictNotes
1Algorithmic CorrectnessMAJOR✅ PASSReadToNextElement/SkipToNextElement/ReadToRootNode semantics byte-for-byte match the RunConfigurationSettings private copies they were modelled on.
2Threading & ConcurrencyBLOCKING✅ N/APure synchronous XML parsing, no shared mutable state.
3Security & IPC Contract SafetyBLOCKING✅ PASSXmlReaderSettings default in .NET 4.5+ is DtdProcessing.Prohibit; not setting it explicitly is safe. MSTestAdapterSettings.IsAppDomainCreationDisabled keeps its XmlResolver = null.
4Public API & Binary CompatibilityBLOCKING✅ PASSBoth new types (InvalidRunSettingsException, XmlReaderUtilities) are internal. No PublicAPI.Unshipped.txt changes needed.
5Performance & AllocationsMAJOR✅ PASSGetObjectModelAssembly() scans all loaded assemblies, but it sits on the low-frequency AppDomain setup path (#if NETFRAMEWORK). Acceptable.
6Cross-TFM CompatibilityMAJOR✅ PASSAll AppDomainUtilities changes are properly gated behind #if NETFRAMEWORK. The rest of the diff is TFM-neutral.
7Exception HandlingMAJOR✅ PASSThe two-tier exception topology is preserved. InvalidRunSettingsException intentionally does not derive from AdapterSettingsException; the PR description includes a per-site proof table that is correct and thorough.
8Test Coverage & QualityMAJOR⚠️ SEE BELOWThe diff shows an unchanged context line still asserting Throw<SettingsException>() after the ObjectModel using was removed. Disk state is clean. See inline comment.
9Naming & StyleMINOR✅ PASSInvalidRunSettingsException, XmlReaderUtilities, RunConfigurationSettingsName, TestRunParametersName are all clear and consistent.
10Code DuplicationMINOR⚠️ NOTERunConfigurationSettings retains private ReadToRootNode/ReadToNextElement/SkipToNextElement clones that now mirror the new public XmlReaderUtilities methods. Consolidating would change the exception type on the RunConfiguration path (FormatExceptionInvalidRunSettingsException). Leaving it as-is in this phase is intentional and correct; a later phase can align.
11DocumentationMINOR✅ PASSBoth new types carry XML docs. The InvalidRunSettingsException doc clearly explains the two-tier design rationale.
12Resource ManagementMAJOR✅ N/ANo new IDisposable resources; all existing XmlReader/StringReader uses already have using in their callers.
13Null SafetyMAJOR✅ PASSInvalidRunSettingsException(string? message) is null-safe. GetObjectModelAssembly() either succeeds or throws — never returns null.
14Pattern MatchingMINOR✅ N/ANo opportunities in the changed code.
15LINQ UsageMINOR✅ PASSFirstOrDefault(predicate) in GetObjectModelAssembly() is appropriate.
16LocalizationMAJOR⚠️ MINORXmlReaderUtilities.ReadToRootNode uses a hardcoded interpolated string instead of a Resource.* entry — inconsistent with the rest of the settings-error surface. See inline comment.
17Build & Project FilesMAJOR✅ PASSSDK-style project (MSTestAdapter.PlatformServices.csproj) auto-includes all *.cs files; no explicit <Compile> entries needed for the two new files.
18PR Scope & FocusMAJOR✅ PASSStrictly scoped to the four runsettings-parsing files. The PR description is exemplary in its fidelity analysis.
19Backward CompatibilityBLOCKING✅ PASSThe per-site exception-handler table in the PR description demonstrates that observable behaviour is unchanged. Site 3 (MSTestAdapterSettings.ReadAssemblyResolutionPath) correctly escapes InitializeDiscovery both before and after.
20Error Message QualityMINOR✅ PASSAll messages are actionable. The hardcoded XmlReaderUtilities message is clear even if not resource-backed (see dim 16).
21Design & ArchitectureMAJOR✅ PASSExtension methods on XmlReader is idiomatic; the GetObjectModelAssembly() late-binding approach avoids a hard compile-time dependency on a type that is scheduled to be removed.
22Overarching Principles✅ PASSBackward compatibility is sacred and demonstrably preserved. Scope is disciplined (one phase, one responsibility). No new public API.

Key Findings

⚠️ Test assertion not updated (dim 8, dim 16)

The diff shows a context (unchanged) line inside GetTestRunParametersThrowsOnInvalidSettingsXml that still asserts .Throw<SettingsException>() after the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed. That would be a compilation error. The current disk state is clean (no SettingsException reference), which implies the base branch (6e-3b) already had this method restructured and the git diff's "nearest function" context header is misleading. No action required, but worth an explicit regression note: if the 6e-3b base is ever rebased, this context line should not re-appear.

i️ Hardcoded error message (dim 16)

XmlReaderUtilities.ReadToRootNode throws $"Could not find '...' node..." as an interpolated string. Every peer error in this area (ThrowOnHasAttributes, FromXml, ReadAssemblyResolutionPath) uses Resource.* strings. Low priority, but a resource entry would keep this consistent for any future localization pass.

i️ RunConfigurationSettings duplication (dim 10)

RunConfigurationSettings still carries three private helpers (ReadToRootNode/ReadToNextElement/SkipToNextElement) that are now semantically identical to XmlReaderUtilities. The only observable difference is that RunConfigurationSettings.ReadToRootNode throws FormatException while XmlReaderUtilities.ReadToRootNode throws InvalidRunSettingsException. Consolidating them is a deliberate future-phase decision and is tracked correctly.


Overall: Well-executed, carefully analysed refactoring. The exception-topology proof table in the PR description sets a high bar for this kind of decoupling work. The two non-blocking observations above are worth noting but do not affect correctness or behaviour.

Comments that could not be inline-anchored

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs:33

The diff shows this assertion as an unchanged context line (no - prefix) while the using Microsoft.VisualStudio.TestPlatform.ObjectModel import was removed in the same hunk. As written after the patch, SettingsException is no longer resolvable — this should be a compilation error.

The disk state does not exhibit the problem (no SettingsException reference remains, and the method was restructured), which suggests it was resolved either within the same commit or via a prior phase…

src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs:29

The error message is a hardcoded interpolated string. Every other settings-parse error in this codebase flows through Resource.* strings (e.g., Resource.InvalidSettingsXmlElement, Resource.InvalidSettingsXmlAttribute). While this path is rare and the message itself is perfectly clear, it is inconsistent with the localization policy of the codebase.

Suggested fix: add a resource entry (e.g. InvalidRunSettingsRootNode) and use `string.Format(CultureInfo.CurrentCulture, Resource.InvalidRu…

Amaury Levé (Evangelink) added a commit that referenced this pull request Jul 5, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink