Skip to content

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633

Open
Evangelink wants to merge 25 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel
Open

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Evangelink wants to merge 25 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel

Conversation

@Evangelink

@Evangelink Evangelink commented Jul 5, 2026

Copy link
Copy Markdown
Member

Phase 7 (capstone) — drop the ObjectModel package reference

The final slice of the initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic. After this, PlatformServices no longer references the VSTest object model (Microsoft.TestPlatform.ObjectModel) at all — every VSTest dependency now lives in the MSTest.TestAdapter layer above it. Strict byte-for-byte, no behavior change.

Production changes

  1. Remove the Microsoft.TestPlatform.ObjectModel PackageReference from MSTestAdapter.PlatformServices.csproj.
  2. RunConfigurationSettings was the last consumer of a transitively-provided VSTest package: it used PlatformAbstractions' PlatformApartmentState enum {MTA, STA} to parse ExecutionThreadApartmentState. Replaced with a local internal enum ApartmentStateSetting { MTA, STA } of the same shape (same member names and order — MTA=0, STA=1), preserving the exact Enum.TryParseSTA/MTA → System.Threading.ApartmentState / else-throw behavior byte-for-byte on both the runsettings-XML and config paths. The member order is load-bearing (Enum.TryParse accepts numeric strings "0"/"1", so the numbering must match) and is commented as such. (Parsing directly to System.Threading.ApartmentState would change the handling of the Unknown value — it has that extra member — so a faithful 2-member local enum is required. The property type stays the BCL System.Threading.ApartmentState; only the parse-enum is neutralized.)
  3. Add a direct System.Configuration framework reference on .NET Framework. ConfigurationManager/ConfigurationElementCollection (used by TestDataSource) were previously pulled in transitively via the object-model package; System.Configuration is a framework assembly, so it is now referenced directly.

Result: PlatformServices is fully platform-neutral

The compiled MSTestAdapter.PlatformServices assembly has zero references to any Microsoft.*.TestPlatform.* assembly on every real TFM (net462/net8.0/net9.0 + windows variants), verified via assembly metadata.

Guard test (the finish line)

ObjectModelDecouplingTests asserts the compiled PlatformServices assembly references no assembly whose name contains "TestPlatform" — catching ObjectModel, PlatformAbstractions, CoreUtilities, etc. (MSTest's own framework is MSTest.TestFramework, which doesn't match). This locks the platform-agnostic contract permanently.

Test-project fix

PlatformServices.Desktop.IntegrationTests uses ObjectModel's XmlRunSettingsUtilities directly (previously transitive through the PlatformServices project reference), so it gets its own direct Microsoft.TestPlatform.ObjectModel PackageReference. Test projects may reference the object model; only the production assembly must be neutral.

Verification

  • All real TFMs build 0-warning (UWP builds via full msbuild in CI).
  • Compiled dll: zero TestPlatform references (net462 + net8.0, metadata-verified).
  • MSTestAdapter.PlatformServices.UnitTests: 936/936 (net462), 898/898 (net8.0) — includes the new guard test and the STA/MTA parsing tests on both the runsettings-XML and config paths.
  • PlatformServices.Desktop.IntegrationTests: 15/15.
  • MSTestAdapter.UnitTests: 21/21. MSTest.TestAdapter and MSTest.IntegrationTests build clean.
  • Expert-reviewer pass.

Stacking

Stacks on #9632 (Phase 6e-4c3); base branch dev/amauryleve/vstest-decoupling-suspendcoverage. Review/merge after the earlier PRs in the chain reach the base. Do not squash-rebase the base.

copilot and others added 23 commits July 1, 2026 17:05
Introduce a platform-agnostic IAdapterMessageLogger abstraction (reusing the
existing MessageLevel enum) so the platform services layer no longer depends on
the VSTest IMessageLogger/TestMessageLevel for the standalone message-logger
role. The VSTest bridge (ToAdapterMessageLogger) lives in the adapter-facing
extension and is applied at the MSTestDiscoverer/MSTestExecutor boundary and at
the two execution sites that reuse the framework handle as a logger.

The recorder's dual logger role (IFrameworkHandle/ITestExecutionRecorder) is
intentionally left for the later recorder phase, since logger and recorder are
the same object there.

Tests keep their Mock<IMessageLogger> and wrap with .ToAdapterMessageLogger() at
migrated call sites, so TestMessageLevel Verify assertions are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Introduce a platform-agnostic `ITestResultRecorder` so the execution result
path in PlatformServices no longer constructs VSTest result-side types
(`TestResult`, `TestOutcome`, `TestResultMessage`, `AttachmentSet`,
`UriDataAttachment`). The single VSTest translation point now lives in the
adapter-facing bridge `HostTestResultRecorder`
(`Services/TestResultRecorderExtensions.ToTestResultRecorder`), mirroring the
Phase 1 `IAdapterMessageLogger` + `AdapterMessageLoggerExtensions` pattern.

`TestExecutionManager.Runner.cs` routes start/empty/result reporting through the
neutral recorder. `TestResultExtensions.ToTestResult` and
`UnitTestOutcomeHelper.ToTestOutcome` are unchanged and are now called from the
bridge. This is a pure refactor with no behavior change: the outcome mapping,
assembled `TestResult`, and the trace / `_hasAnyTestFailed` / NotFound+HotReload
branches are preserved.

Independent of and parallel to PR #9548 (Phase 1).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- AdapterMessageLoggerExtensions: validate the logger argument (throw
  ArgumentNullException instead of a later NullReferenceException) and make
  ToAdapterMessageLogger internal to match the containing internal class.
- TestExecutionManager.Parallelization: cache a single IAdapterMessageLogger per
  source instead of allocating a wrapper per call, and route the parallelization
  banner and error SendMessage calls through it (removing the file's remaining
  TestMessageLevel usage).
- MSTestSettingsTests: drop two dead-store local assignments flagged by CodeQL;
  the GetSettings calls remain as statements so logging side effects and Verify
  assertions are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Address PR review: the concrete recorder is provided at the platform boundary by a wrapper over the host's ITestExecutionRecorder (currently TestResultRecorderExtensions in PlatformServices/Services), rather than by the 'adapter layer'. Doc-only change; no behavior change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…esults-platformservices' into dev/amauryleve/vstest-decoupling-base
…rm-agnostic effort) (#9555)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…rm-agnostic effort) (#9566)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…orm-agnostic effort) (#9572)

Co-authored-by: Amaury Leveque <amauryleve@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…-agnostic effort) (#9576)

Co-authored-by: Amaury Leveque <amauryleve@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Amaury Leveque <amauryleve@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…(Phase 6c) (#9585)

Co-authored-by: Amaury Leveque <amauryleve@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the VSTest IRunSettings with a neutral settings-XML string through the
isolation-host layer, removing IRunSettings from MSTestAdapter.PlatformServices.

- IPlatformServiceProvider.CreateTestSourceHost, TestSourceHost (both ctors) and
  AssemblyEnumeratorWrapper.GetTests/GetTestsInIsolation now take string? settingsXml.
- TestExecutionManager.CacheSessionParameters takes the settings-XML string directly.
- Callers extract runContext?.RunSettings?.SettingsXml / discoveryContext?.RunSettings?.SettingsXml
  at the point they already had the (still VSTest) run/discovery context; only .SettingsXml
  (a string) was ever read off IRunSettings, so this is byte-for-byte.

The remaining IRunContext/IDiscoveryContext usage is the test-case filter (deferred to the
filter sub-phase). No behavior change: the appdomain DisableAppDomain decision and the
run-parameter caching read the same settings XML as before.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Move TestMethodFilter (and its nested TestElementFilter) out of MSTestAdapter.PlatformServices
up into MSTest.TestAdapter, and inject the neutral ITestElementFilter into the engine and
discoverer via a new ITestElementFilterProvider abstraction.

- New neutral ITestElementFilterProvider (PlatformServices.Interface): the boundary builds it
  (TestElementFilterProvider, closing over the VSTest IRunContext/IDiscoveryContext) and passes it
  into TestExecutionManager.RunTestsAsync/ExecuteTestsAsync and UnitTestDiscoverer.DiscoverTests.
- The engine/discoverer invoke the provider at the EXACT points they previously built the filter
  (per source), so filter parse-error reporting keeps the same timing and per-source semantics;
  TestElementFilter.Matches still does element.ToTestCase() (byte-for-byte; #9568 deferred).
- This removes ITestCaseFilterExpression / GetTestCaseFilter / MatchTestCase / the VSTest
  TestProperty filter set from PlatformServices code. IRunContext/IDiscoveryContext remain only for
  deployment + settings extraction (removed in a follow-up).

No behavior change: filtered set/order and the discovery/execution filterHasError bail-out are
identical; TestCaseFilteringTests (out-of-proc filter regression net) stays green.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the VSTest IRunContext/IDiscoveryContext with neutral primitives extracted at the
adapter boundary, so MSTestAdapter.PlatformServices no longer references either type.

- Execution: MSTestExecutor builds the neutral DeploymentContext (test-run directory + run
  settings XML) from the host run context and injects it into TestExecutionManager.RunTestsAsync/
  ExecuteTestsAsync/ExecuteTestsInSourceAsync/Deploy (DeploymentContext un-guarded so it is the
  single execution-inputs carrier on all TFMs).
- Discovery: MSTestDiscoverer passes the run settings XML string into UnitTestDiscoverer.
  DiscoverTests/DiscoverTestsInSource; MSTestDiscovererHelpers.InitializeDiscovery and
  MSTestSettings.PopulateSettings take string? settingsXml.
- Only .SettingsXml + .TestRunDirectory were ever read off the contexts, so this is byte-for-byte.

IRunContext/IDiscoveryContext are now absent from PlatformServices code (doc comments only); the
remaining ObjectModel.Adapter surface is the IFrameworkHandle-backed deploy/recorder/logger handles.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…hase 6e-1)

The execution engine used the VSTest IFrameworkHandle exclusively to obtain an
IAdapterMessageLogger via ToAdapterMessageLogger(). Replace the IFrameworkHandle parameter
with the neutral IAdapterMessageLogger throughout TestExecutionManager (RunTestsAsync both
overloads, ExecuteTestsAsync, ExecuteTestsInSourceAsync, Deploy); the adapter boundary
(MSTestExecutor) now calls frameworkHandle.ToAdapterMessageLogger() once and injects the result.

This removes the last VSTest ObjectModel.Adapter reference from the execution engine. No behavior
change: the logger wrapper is stateless, so injecting one instance is identical to building one per
call site.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Move the three remaining VSTest-object-model bridge helpers out of
MSTestAdapter.PlatformServices and into MSTest.TestAdapter:
AdapterMessageLoggerExtensions, MessageLevel (ToTestMessageLevel), and
UnitTestElementSinkExtensions. These are the last code references to
Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging /
ITestCaseDiscoverySink in PlatformServices; only doc comments now mention
the VSTest types. The logical namespace is unchanged so callers at the
adapter boundary and the integration harness are unaffected.

PlatformServices.UnitTests calls the ToAdapterMessageLogger bridge, which
now lives in MSTest.TestAdapter; touching that module runs its
[ModuleInitializer] (MSTestExecutor.SetPlatformLogger), which assigns
PlatformServiceProvider.Instance.AdapterTraceLogger. Make the test double's
setter tolerate the assignment (as the real PlatformServiceProvider does)
instead of throwing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the VSTest object-model Trait type carried on UnitTestElement.Traits
with a neutral, platform-agnostic TestTrait { Name, Value } struct. The
engine-side producers and consumers (ReflectHelper/ReflectionHelper
GetTestPropertiesAsTraits, TypeEnumerator, TestExecutionManager TestContext
building, TestRunInfo, the test-filter context) now operate on TestTrait, so
five files stop referencing Microsoft.VisualStudio.TestPlatform.ObjectModel.
The VSTest Trait only survives at the adapter conversion boundary
(TestCaseExtensions and UnitTestElement.ToTestCase), which convert between
TestTrait and the host trait type.

TestTrait is [Serializable] on .NET Framework because UnitTestElement is
serialized across app domains during isolated discovery/execution; order and
Name/Value are preserved, so trait -> TestContext reporting and the produced
host test case are byte-for-byte identical.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ase 6e-3b)

Move the deep VSTest-object-model conversion out of MSTestAdapter.PlatformServices
and into MSTest.TestAdapter, as a pure relocation (no behavior change):

- UnitTestElement.ToTestCase()/GetOrCreateHostTestCase() and the test-case Id
  hashing (GenerateSerializedDataStrategyTestId / VersionedGuidFromHash) become
  UnitTestElementExtensions in the adapter. The Id hashing moves byte-identical
  (VersionedGuidFromHash verbatim), preserving cross-version discovery->execution
  test-id correlation.
- The EngineConstants '#region Test Property registration' (every TestProperty
  id/label/valueType/attribute, plus the TCM/TFS label constants) moves verbatim
  into a new adapter AdapterTestProperties class. EngineConstants keeps only its
  neutral members (extensions, fixture traits, executor uri) and no longer
  references the VSTest object model.
- TestCaseExtensions and TcmTestPropertiesProvider (already adapter-namespaced)
  move physically into MSTest.TestAdapter.

UnitTestElement, EngineConstants, TestCaseExtensions and TcmTestPropertiesProvider
all stop referencing Microsoft.VisualStudio.TestPlatform.ObjectModel, dropping the
PlatformServices coupling from 13 to 10 files. The conversion is still invoked only
at the adapter boundary (executor/discoverer/recorder/filter). The single ToTestCase
in the test-case filter and CloneWithUpdatedSource are left as-is to keep this change
byte-for-byte (#9568 and #9573 remain open).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
…6e-4b)

Remove the compile-time VSTest object-model dependency from the netfx AppDomain
wiring: AppDomainUtilities used typeof(TestCase).Assembly to (1) add the object-model
assembly's directory to the child app-domain's resolution paths and (2) anchor the
11.0 -> current binding redirect. Both run parent-side during test source host setup,
after the adapter has already loaded the object model, so the assembly is resolved by
simple name from the current domain instead - returning the same (post-redirect)
assembly identity the type reference did, without a compile-time reference.

The only remaining mention of the object model in this file is the assembly's simple
name as a string literal (used for the lookup and, formerly, by the resolver's
skip-list), which is not an assembly reference and does not block dropping the package.

Proven on the netfx AppDomain scenario the type reference protects:
PlatformServices.Desktop.IntegrationTests (assembly-resolution-from-runsettings +
deployment app-domain paths) stays green (15/15).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remove the compile-time VSTest object-model dependency from the netfx source-host
setup. TestSourceHost used two `typeof(...).Assembly` anchors into the VSTest object
model:
- `typeof(EqtTrace).Assembly` (force-loading Microsoft.TestPlatform.CoreUtilities into
  the child app domain to avoid a recursive assembly-resolution cycle), and
- `typeof(AssemblyHelper).Assembly` (locating the test-platform directory for the
  resolution paths).

Both run parent-side (or reflect parent-side loaded assemblies) before the child app
domain resolves anything, so they are resolved by simple name from the current domain
via a small `GetLoadedAssembly(simpleName)` helper - returning the same loaded assembly
identity the type references did. EqtTrace's defining assembly is CoreUtilities (it is
type-forwarded from the object model), so that anchor targets CoreUtilities by name;
AssemblyHelper lives in the object model, so that anchor targets the object model.

The only remaining object-model mention in the file is the assembly simple name as a
string literal (not an assembly reference). Proven on the netfx child-app-domain path:
PlatformServices.Desktop.IntegrationTests stays green (15/15).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…Phase 6e-4c2)

TestSourceHandler.IsAssemblyReferenced (netfx) used
AssemblyHelper.DoesReferencesAssembly from
Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities to decide whether a
source assembly references the test framework before running discovery.

Replace that call with a local neutral DoesSourceReferenceAssembly helper that
reproduces the exact observable behavior of the VSTest implementation:

- ReflectionOnlyLoadFrom(source), then GetReferencedAssemblies().
- Match a referenced assembly by simple name (OrdinalIgnoreCase) plus public
  key token bytes; version is ignored -- identical to
  AssemblyLoadWorker.CheckAssemblyReference.
- Null/empty source or null reference assembly returns null (undeterminable).
- Any exception returns null so discovery proceeds conservatively.

Fidelity note: the VSTest DoesReferencesAssembly created a child AppDomain and an
AssemblyLoadWorker instance, but then called the *static*
AssemblyLoadWorker.CheckAssemblyReference -- the worker instance is never used and
the ReflectionOnlyLoadFrom actually runs in the current domain. The child domain
is therefore dead code for the result, so omitting it is behavior-preserving for
every input where AppDomain creation would have succeeded.

Removes the last real ObjectModel dependency from TestSourceHandler.

Verified: PlatformServices.UnitTests 935 (net462) / 897 (net8.0);
DesktopTestSourceTests IsAssemblyReferenced branches 7/7 (referenced,
not-referenced, null-name, null-source); PlatformServices.Desktop.IntegrationTests
15/15. All real TFMs build 0-warning.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
TestDeployment (netfx) wrapped the deployment file copy in
`using (new SuspendCodeCoverage())` to pause dynamic code-coverage instrumentation
of modules loaded while files are copied. That type came from
Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities.

Vendor an internal neutral copy at
Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities) that
reproduces the VSTest behavior byte-for-byte:

- On construction: read the current value of the process environment variable
  "__VANGUARD_SUSPEND_INSTRUMENT__" and set it to "TRUE".
- On dispose: restore the previously captured value (idempotent).

The environment-variable name and value are the collector IPC contract the
dynamic code-coverage (Vanguard) engine reads, so they are preserved exactly. The
child-object is internal/sealed with a straightforward idempotent Dispose (the
original's Dispose(bool)/GC.SuppressFinalize plumbing has no finalizer to suppress
and is behavior-equivalent to the direct restore).

TestDeployment now resolves SuspendCodeCoverage via the already-imported
PlatformServices.Utilities namespace; the VSTest ObjectModel.Utilities using is
removed.

With this change PlatformServices has zero `using`/type references to the
Microsoft.TestPlatform.ObjectModel package (only string-literal assembly names
used for by-name runtime lookup remain), clearing the way to drop the package
reference in the capstone.

Verified: PlatformServices builds 0-warning (net462 and all real TFMs;
netfx-guarded change); PlatformServices.UnitTests 935 (net462) / 897 (net8.0);
PlatformServices.Desktop.IntegrationTests 15/15 (exercises the deployment path
that runs inside the SuspendCodeCoverage scope).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink Evangelink force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 605ee46 to 37c3973 Compare July 5, 2026 13:57
This is the capstone of the initiative: MSTestAdapter.PlatformServices no longer
references the VSTest object model (Microsoft.TestPlatform.ObjectModel) at all.

Production changes:
- Remove the Microsoft.TestPlatform.ObjectModel PackageReference from
  MSTestAdapter.PlatformServices.csproj.
- The only remaining consumer of a transitively-provided VSTest package was
  RunConfigurationSettings, which used PlatformAbstractions' PlatformApartmentState
  enum {MTA, STA} to parse ExecutionThreadApartmentState. Replace it with a local
  internal enum of the same shape (same member names/order), preserving the exact
  Enum.TryParse-then-map-to-System.Threading.ApartmentState behavior byte-for-byte
  (a 2-member by-name parse is identical; parsing directly to ApartmentState would
  change the handling of the "Unknown" value, so a faithful local enum is required).
- Add a direct framework reference to System.Configuration on .NET Framework.
  ConfigurationManager/ConfigurationElementCollection (used by TestDataSource) were
  previously pulled in transitively via the object-model package; System.Configuration
  is a framework assembly, so it is now referenced directly.

Result: the compiled MSTestAdapter.PlatformServices assembly has ZERO references to
any Microsoft.*.TestPlatform.* assembly on every real target framework
(net462/net8.0/net9.0 + windows variants), verified via assembly metadata. All VSTest
coupling now lives in the MSTest.TestAdapter layer above it.

Guard test:
- ObjectModelDecouplingTests asserts the compiled PlatformServices assembly references
  no assembly whose name contains "TestPlatform" (catches ObjectModel,
  PlatformAbstractions, CoreUtilities, ...; MSTest's own framework is "MSTest.TestFramework",
  which does not match). This locks the platform-agnostic contract permanently.

Test-project fix:
- PlatformServices.Desktop.IntegrationTests uses ObjectModel's XmlRunSettingsUtilities
  directly (previously transitive through the PlatformServices project reference), so it
  gets its own direct Microsoft.TestPlatform.ObjectModel PackageReference. Test projects
  are allowed to reference the object model; only the production assembly must be neutral.

Verified: PlatformServices builds 0-warning on all real TFMs (UWP builds via full msbuild
in CI); PlatformServices.UnitTests 936 (net462) / 898 (net8.0) incl. the new guard test and
the STA/MTA parsing tests on both the runsettings-XML and config paths;
PlatformServices.Desktop.IntegrationTests 15/15; MSTestAdapter.UnitTests 21/21;
MSTest.TestAdapter and MSTest.IntegrationTests build clean.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink Evangelink force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 37c3973 to 16552c6 Compare July 5, 2026 14:08
Base automatically changed from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehost July 5, 2026 19:24
An error occurred while trying to automatically change base from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehost July 5, 2026 19:24
@Evangelink Evangelink marked this pull request as ready for review July 5, 2026 19:24
Base automatically changed from dev/amauryleve/vstest-decoupling-sourcehost to dev/amauryleve/vstest-decoupling-conversion July 5, 2026 19:27

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

🤖 Automated review by GitHub Copilot. 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.

PR #9633 — VSTest ObjectModel decoupling from PlatformServices

# Dimension Verdict
1 Algorithmic Correctness ⚠️ 1 MODERATE
2 Threading & Concurrency ✅ LGTM
3 Security & IPC Contract Safety ✅ LGTM
4 Public API & Binary Compatibility ✅ LGTM
5 Performance & Allocations ✅ LGTM
6 Cross-TFM Compatibility ✅ LGTM
7 Resource & IDisposable Management ✅ LGTM
8 Defensive Coding at Boundaries ✅ LGTM (covered by #1)
9 Localization & Resources ✅ LGTM
10 Test Isolation ✅ LGTM
11 Assertion Quality ✅ LGTM
12 Flakiness Patterns ✅ LGTM
13 Test Completeness ⚠️ 1 MODERATE
14 Data-Driven Test Coverage N/A
15 Code Structure & Simplification ✅ LGTM
16 Naming & Conventions ✅ LGTM
17 Documentation Accuracy ✅ LGTM
18 Analyzer & Code Fix Quality N/A
19 IPC Wire Compatibility N/A
20 Build Infrastructure & Dependencies ✅ LGTM
21 Scope & PR Discipline ✅ LGTM
22 PowerShell Scripting Hygiene N/A

✅ 17/18 applicable dimensions clean.


Findings

  • Algorithmic Correctness (MODERATE)ArePublicKeyTokensEqual(byte[] left, byte[] right) in TestSourceHandler.cs line 142 dereferences both parameters unconditionally. AssemblyName.GetPublicKeyToken() returns null for unsigned assemblies, producing a NullReferenceException that is silently caught and converted to the conservative null → true path (false-positive discovery) rather than the correct false. See inline comment for the fix (annotate byte[]? and add a null guard at the top of the helper).

  • Test Completeness (MODERATE) — The new SuspendCodeCoverage class (Utilities/SuspendCodeCoverage.cs) has no unit tests. Three behaviours are testable and could silently regress on .NET Framework TFMs without coverage: (1) constructor saves the previous env-var value and sets "TRUE", (2) Dispose() restores the previous value (null → delete), (3) the double-dispose guard prevents a second restoration. Suggested location: a new SuspendCodeCoverageTests.cs in MSTestAdapter.PlatformServices.UnitTests, guarded with #if NETFRAMEWORK.


Notable positives

  • The ApartmentStateSetting enum ordering (MTA=0, STA=1) correctly matches the old PlatformApartmentState numeric values, preserving parse compatibility for numeric run-settings strings — and the load-bearing comment explaining this is clear.
  • SuspendCodeCoverage.Dispose correctly passes null (the captured previous value when the env var was absent) to SetEnvironmentVariable, which is the documented way to delete the variable — no resource-leak risk.
  • The System.Configuration explicit reference is correctly scoped to $(NetFrameworkMinimum) only — confirmed that MSTestAdapter.PlatformServices ships exactly one .NET Framework TFM (net462), so no framework TFM is missed.
  • ObjectModelDecouplingTests correctly uses AwesomeAssertions (required by this project's BannedSymbols.txt), uses IndexOf instead of string.Contains(string, StringComparison) for .NET Framework compat, and verifies the compile-time manifest references — the right API for the stated contract.

Comment thread src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs Outdated
@Evangelink Evangelink force-pushed the dev/amauryleve/vstest-decoupling-conversion branch from bb13e25 to 8832952 Compare July 5, 2026 19:44
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Base automatically changed from dev/amauryleve/vstest-decoupling-conversion to main July 5, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant