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

Merged
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel
Jul 7, 2026
Merged

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.ObjectModelPackageReference 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.Configurationframework 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.ObjectModelPackageReference. 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: zeroTestPlatform 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.

copilotand 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 605ee46 to 37c3973CompareJuly 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 37c3973 to 16552c6CompareJuly 5, 2026 14:08
Base automatically changed from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
An error occurred while trying to automatically change base from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
@Evangelink
Amaury Levé (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-conversionJuly 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.

PR #9633 — VSTest ObjectModel decoupling from PlatformServices

#DimensionVerdict
1Algorithmic Correctness⚠️ 1 MODERATE
2Threading & Concurrency✅ LGTM
3Security & IPC Contract Safety✅ LGTM
4Public API & Binary Compatibility✅ LGTM
5Performance & Allocations✅ LGTM
6Cross-TFM Compatibility✅ LGTM
7Resource & IDisposable Management✅ LGTM
8Defensive Coding at Boundaries✅ LGTM (covered by #1)
9Localization & Resources✅ LGTM
10Test Isolation✅ LGTM
11Assertion Quality✅ LGTM
12Flakiness Patterns✅ LGTM
13Test Completeness⚠️ 1 MODERATE
14Data-Driven Test CoverageN/A
15Code Structure & Simplification✅ LGTM
16Naming & Conventions✅ LGTM
17Documentation Accuracy✅ LGTM
18Analyzer & Code Fix QualityN/A
19IPC Wire CompatibilityN/A
20Build Infrastructure & Dependencies✅ LGTM
21Scope & PR Discipline✅ LGTM
22PowerShell Scripting HygieneN/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 threadsrc/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs Outdated
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-conversion branch from bb13e25 to 8832952CompareJuly 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 mainJuly 5, 2026 20:42
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 6, 2026 12:54
…-decoupling-drop-objectmodel
# Conflicts:
#	src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs
- Remove duplicate CloneWithSource method in TestMethod.cs that the
auto-merge concatenated from both branches (CS0111).
- Add a direct Microsoft.TestPlatform.ObjectModel reference to
MSTest.TestAdapter for the UWP (uap10.0.16299) TFM. The adapter's
VSTest-facing code needs the object model; on other TFMs it flows in
via VSTestBridge, but that reference is excluded for UWP and
PlatformServices no longer references the object model.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings July 7, 2026 04:02
@github-actions

This comment has been minimized.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is the final slice of a multi-PR initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic. It removes the Microsoft.TestPlatform.ObjectModel package reference from the production assembly, moving all remaining VSTest coupling up into the MSTest.TestAdapter layer. The work is a faithful, no-behavior-change decoupling, locked in by a new guard test.

I verified the key correctness claims: the new local ApartmentStateSetting { MTA, STA } enum exactly mirrors VSTest's PlatformApartmentState (MTA=0, STA=1, confirmed from the vstest source), so Enum.TryParse numeric-string compatibility is preserved on both the runsettings-XML and config paths; System.Configuration usage is entirely #if NETFRAMEWORK-guarded with net462 being the only netfx TFM; the UWP ObjectModel reference is consistent with VSTestBridge being excluded for UwpMinimum; and the guard test's assumption holds (MSTest's framework assemblies are named MSTest.TestFramework*, which don't contain "TestPlatform").

Changes:

  • Remove the Microsoft.TestPlatform.ObjectModel package reference from PlatformServices; replace the last VSTest enum consumer with a local neutral ApartmentStateSetting, and add a direct System.Configuration framework reference on .NET Framework.
  • Add explicit Microsoft.TestPlatform.ObjectModel references where the transitive path is now gone (MSTest.TestAdapter for UWP; the Desktop integration test project which uses XmlRunSettingsUtilities directly).
  • Add ObjectModelDecouplingTests guard asserting the compiled PlatformServices assembly references no *TestPlatform* assembly; harden TestSourceHandler public-key-token comparison against null/empty tokens.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.csNew neutral enum replacing VSTest PlatformApartmentState, with load-bearing member order documented
src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.csParse apartment state via local enum on both XML and config paths
src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csprojRemove ObjectModel package ref; add netfx-only System.Configuration reference
src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.csMake public-key-token comparison null/empty-safe
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojAdd explicit ObjectModel reference for UWP (VSTestBridge excluded there)
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.csNew guard test enforcing the platform-neutral contract
test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csprojAdd direct ObjectModel package ref for XmlRunSettingsUtilities

Review details

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

Cover the new public-key-token comparison branches in
TestSourceHandler: a same-named reference with a missing token
(signed-vs-unsigned) and one with a differing token both correctly
return false. The missing-token case is a regression guard for the
null-handling fix (previously it NRE'd and was swallowed into a
false-positive true).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9633

GradeTestNotes
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenDiffers
Boundary test: differing non-null token; byte-array magic explained by comment; clean AAA. No issues found.
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenIsMissing
Boundary test: null public-key-token case; .Should().BeFalse() is complete for a bool return. No issues found.
A (90–100)new ObjectModelDecouplingTests.
PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly
Contract guard via reflection with a well-messaged .Should().BeEmpty(). No issues found.

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

🤖 Automated content by GitHub Copilot. 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 Grade Tests on PR (on open / sync) workflow. · 61.4 AIC · ⌖ 11.1 AIC · ⊞ 9.5K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 8f1b01c into mainJul 7, 2026
45 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-drop-objectmodel branch July 7, 2026 05:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} 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

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

Merged
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel
Jul 7, 2026
Merged

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.ObjectModelPackageReference 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.Configurationframework 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.ObjectModelPackageReference. 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: zeroTestPlatform 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.

copilotand 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 605ee46 to 37c3973CompareJuly 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 37c3973 to 16552c6CompareJuly 5, 2026 14:08
Base automatically changed from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
An error occurred while trying to automatically change base from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
@Evangelink
Amaury Levé (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-conversionJuly 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.

PR #9633 — VSTest ObjectModel decoupling from PlatformServices

#DimensionVerdict
1Algorithmic Correctness⚠️ 1 MODERATE
2Threading & Concurrency✅ LGTM
3Security & IPC Contract Safety✅ LGTM
4Public API & Binary Compatibility✅ LGTM
5Performance & Allocations✅ LGTM
6Cross-TFM Compatibility✅ LGTM
7Resource & IDisposable Management✅ LGTM
8Defensive Coding at Boundaries✅ LGTM (covered by #1)
9Localization & Resources✅ LGTM
10Test Isolation✅ LGTM
11Assertion Quality✅ LGTM
12Flakiness Patterns✅ LGTM
13Test Completeness⚠️ 1 MODERATE
14Data-Driven Test CoverageN/A
15Code Structure & Simplification✅ LGTM
16Naming & Conventions✅ LGTM
17Documentation Accuracy✅ LGTM
18Analyzer & Code Fix QualityN/A
19IPC Wire CompatibilityN/A
20Build Infrastructure & Dependencies✅ LGTM
21Scope & PR Discipline✅ LGTM
22PowerShell Scripting HygieneN/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 threadsrc/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs Outdated
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-conversion branch from bb13e25 to 8832952CompareJuly 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 mainJuly 5, 2026 20:42
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 6, 2026 12:54
…-decoupling-drop-objectmodel
# Conflicts:
#	src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs
- Remove duplicate CloneWithSource method in TestMethod.cs that the
auto-merge concatenated from both branches (CS0111).
- Add a direct Microsoft.TestPlatform.ObjectModel reference to
MSTest.TestAdapter for the UWP (uap10.0.16299) TFM. The adapter's
VSTest-facing code needs the object model; on other TFMs it flows in
via VSTestBridge, but that reference is excluded for UWP and
PlatformServices no longer references the object model.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings July 7, 2026 04:02
@github-actions

This comment has been minimized.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is the final slice of a multi-PR initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic. It removes the Microsoft.TestPlatform.ObjectModel package reference from the production assembly, moving all remaining VSTest coupling up into the MSTest.TestAdapter layer. The work is a faithful, no-behavior-change decoupling, locked in by a new guard test.

I verified the key correctness claims: the new local ApartmentStateSetting { MTA, STA } enum exactly mirrors VSTest's PlatformApartmentState (MTA=0, STA=1, confirmed from the vstest source), so Enum.TryParse numeric-string compatibility is preserved on both the runsettings-XML and config paths; System.Configuration usage is entirely #if NETFRAMEWORK-guarded with net462 being the only netfx TFM; the UWP ObjectModel reference is consistent with VSTestBridge being excluded for UwpMinimum; and the guard test's assumption holds (MSTest's framework assemblies are named MSTest.TestFramework*, which don't contain "TestPlatform").

Changes:

  • Remove the Microsoft.TestPlatform.ObjectModel package reference from PlatformServices; replace the last VSTest enum consumer with a local neutral ApartmentStateSetting, and add a direct System.Configuration framework reference on .NET Framework.
  • Add explicit Microsoft.TestPlatform.ObjectModel references where the transitive path is now gone (MSTest.TestAdapter for UWP; the Desktop integration test project which uses XmlRunSettingsUtilities directly).
  • Add ObjectModelDecouplingTests guard asserting the compiled PlatformServices assembly references no *TestPlatform* assembly; harden TestSourceHandler public-key-token comparison against null/empty tokens.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.csNew neutral enum replacing VSTest PlatformApartmentState, with load-bearing member order documented
src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.csParse apartment state via local enum on both XML and config paths
src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csprojRemove ObjectModel package ref; add netfx-only System.Configuration reference
src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.csMake public-key-token comparison null/empty-safe
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojAdd explicit ObjectModel reference for UWP (VSTestBridge excluded there)
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.csNew guard test enforcing the platform-neutral contract
test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csprojAdd direct ObjectModel package ref for XmlRunSettingsUtilities

Review details

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

Cover the new public-key-token comparison branches in
TestSourceHandler: a same-named reference with a missing token
(signed-vs-unsigned) and one with a differing token both correctly
return false. The missing-token case is a regression guard for the
null-handling fix (previously it NRE'd and was swallowed into a
false-positive true).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9633

GradeTestNotes
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenDiffers
Boundary test: differing non-null token; byte-array magic explained by comment; clean AAA. No issues found.
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenIsMissing
Boundary test: null public-key-token case; .Should().BeFalse() is complete for a bool return. No issues found.
A (90–100)new ObjectModelDecouplingTests.
PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly
Contract guard via reflection with a well-messaged .Should().BeEmpty(). No issues found.

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

🤖 Automated content by GitHub Copilot. 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 Grade Tests on PR (on open / sync) workflow. · 61.4 AIC · ⌖ 11.1 AIC · ⊞ 9.5K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 8f1b01c into mainJul 7, 2026
45 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-drop-objectmodel branch July 7, 2026 05:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel
Jul 7, 2026
Merged

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.ObjectModelPackageReference 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.Configurationframework 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.ObjectModelPackageReference. 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: zeroTestPlatform 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.

copilotand 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 605ee46 to 37c3973CompareJuly 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 37c3973 to 16552c6CompareJuly 5, 2026 14:08
Base automatically changed from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
An error occurred while trying to automatically change base from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
@Evangelink
Amaury Levé (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-conversionJuly 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.

PR #9633 — VSTest ObjectModel decoupling from PlatformServices

#DimensionVerdict
1Algorithmic Correctness⚠️ 1 MODERATE
2Threading & Concurrency✅ LGTM
3Security & IPC Contract Safety✅ LGTM
4Public API & Binary Compatibility✅ LGTM
5Performance & Allocations✅ LGTM
6Cross-TFM Compatibility✅ LGTM
7Resource & IDisposable Management✅ LGTM
8Defensive Coding at Boundaries✅ LGTM (covered by #1)
9Localization & Resources✅ LGTM
10Test Isolation✅ LGTM
11Assertion Quality✅ LGTM
12Flakiness Patterns✅ LGTM
13Test Completeness⚠️ 1 MODERATE
14Data-Driven Test CoverageN/A
15Code Structure & Simplification✅ LGTM
16Naming & Conventions✅ LGTM
17Documentation Accuracy✅ LGTM
18Analyzer & Code Fix QualityN/A
19IPC Wire CompatibilityN/A
20Build Infrastructure & Dependencies✅ LGTM
21Scope & PR Discipline✅ LGTM
22PowerShell Scripting HygieneN/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 threadsrc/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs Outdated
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-conversion branch from bb13e25 to 8832952CompareJuly 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 mainJuly 5, 2026 20:42
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 6, 2026 12:54
…-decoupling-drop-objectmodel
# Conflicts:
#	src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs
- Remove duplicate CloneWithSource method in TestMethod.cs that the
auto-merge concatenated from both branches (CS0111).
- Add a direct Microsoft.TestPlatform.ObjectModel reference to
MSTest.TestAdapter for the UWP (uap10.0.16299) TFM. The adapter's
VSTest-facing code needs the object model; on other TFMs it flows in
via VSTestBridge, but that reference is excluded for UWP and
PlatformServices no longer references the object model.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings July 7, 2026 04:02
@github-actions

This comment has been minimized.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is the final slice of a multi-PR initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic. It removes the Microsoft.TestPlatform.ObjectModel package reference from the production assembly, moving all remaining VSTest coupling up into the MSTest.TestAdapter layer. The work is a faithful, no-behavior-change decoupling, locked in by a new guard test.

I verified the key correctness claims: the new local ApartmentStateSetting { MTA, STA } enum exactly mirrors VSTest's PlatformApartmentState (MTA=0, STA=1, confirmed from the vstest source), so Enum.TryParse numeric-string compatibility is preserved on both the runsettings-XML and config paths; System.Configuration usage is entirely #if NETFRAMEWORK-guarded with net462 being the only netfx TFM; the UWP ObjectModel reference is consistent with VSTestBridge being excluded for UwpMinimum; and the guard test's assumption holds (MSTest's framework assemblies are named MSTest.TestFramework*, which don't contain "TestPlatform").

Changes:

  • Remove the Microsoft.TestPlatform.ObjectModel package reference from PlatformServices; replace the last VSTest enum consumer with a local neutral ApartmentStateSetting, and add a direct System.Configuration framework reference on .NET Framework.
  • Add explicit Microsoft.TestPlatform.ObjectModel references where the transitive path is now gone (MSTest.TestAdapter for UWP; the Desktop integration test project which uses XmlRunSettingsUtilities directly).
  • Add ObjectModelDecouplingTests guard asserting the compiled PlatformServices assembly references no *TestPlatform* assembly; harden TestSourceHandler public-key-token comparison against null/empty tokens.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.csNew neutral enum replacing VSTest PlatformApartmentState, with load-bearing member order documented
src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.csParse apartment state via local enum on both XML and config paths
src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csprojRemove ObjectModel package ref; add netfx-only System.Configuration reference
src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.csMake public-key-token comparison null/empty-safe
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojAdd explicit ObjectModel reference for UWP (VSTestBridge excluded there)
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.csNew guard test enforcing the platform-neutral contract
test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csprojAdd direct ObjectModel package ref for XmlRunSettingsUtilities

Review details

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

Cover the new public-key-token comparison branches in
TestSourceHandler: a same-named reference with a missing token
(signed-vs-unsigned) and one with a differing token both correctly
return false. The missing-token case is a regression guard for the
null-handling fix (previously it NRE'd and was swallowed into a
false-positive true).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9633

GradeTestNotes
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenDiffers
Boundary test: differing non-null token; byte-array magic explained by comment; clean AAA. No issues found.
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenIsMissing
Boundary test: null public-key-token case; .Should().BeFalse() is complete for a bool return. No issues found.
A (90–100)new ObjectModelDecouplingTests.
PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly
Contract guard via reflection with a well-messaged .Should().BeEmpty(). No issues found.

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

🤖 Automated content by GitHub Copilot. 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 Grade Tests on PR (on open / sync) workflow. · 61.4 AIC · ⌖ 11.1 AIC · ⊞ 9.5K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 8f1b01c into mainJul 7, 2026
45 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-drop-objectmodel branch July 7, 2026 05:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel
Jul 7, 2026
Merged

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.ObjectModelPackageReference 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.Configurationframework 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.ObjectModelPackageReference. 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: zeroTestPlatform 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.

copilotand 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 605ee46 to 37c3973CompareJuly 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 37c3973 to 16552c6CompareJuly 5, 2026 14:08
Base automatically changed from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
An error occurred while trying to automatically change base from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
@Evangelink
Amaury Levé (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-conversionJuly 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.

PR #9633 — VSTest ObjectModel decoupling from PlatformServices

#DimensionVerdict
1Algorithmic Correctness⚠️ 1 MODERATE
2Threading & Concurrency✅ LGTM
3Security & IPC Contract Safety✅ LGTM
4Public API & Binary Compatibility✅ LGTM
5Performance & Allocations✅ LGTM
6Cross-TFM Compatibility✅ LGTM
7Resource & IDisposable Management✅ LGTM
8Defensive Coding at Boundaries✅ LGTM (covered by #1)
9Localization & Resources✅ LGTM
10Test Isolation✅ LGTM
11Assertion Quality✅ LGTM
12Flakiness Patterns✅ LGTM
13Test Completeness⚠️ 1 MODERATE
14Data-Driven Test CoverageN/A
15Code Structure & Simplification✅ LGTM
16Naming & Conventions✅ LGTM
17Documentation Accuracy✅ LGTM
18Analyzer & Code Fix QualityN/A
19IPC Wire CompatibilityN/A
20Build Infrastructure & Dependencies✅ LGTM
21Scope & PR Discipline✅ LGTM
22PowerShell Scripting HygieneN/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 threadsrc/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs Outdated
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-conversion branch from bb13e25 to 8832952CompareJuly 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 mainJuly 5, 2026 20:42
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 6, 2026 12:54
…-decoupling-drop-objectmodel
# Conflicts:
#	src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs
- Remove duplicate CloneWithSource method in TestMethod.cs that the
auto-merge concatenated from both branches (CS0111).
- Add a direct Microsoft.TestPlatform.ObjectModel reference to
MSTest.TestAdapter for the UWP (uap10.0.16299) TFM. The adapter's
VSTest-facing code needs the object model; on other TFMs it flows in
via VSTestBridge, but that reference is excluded for UWP and
PlatformServices no longer references the object model.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings July 7, 2026 04:02
@github-actions

This comment has been minimized.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is the final slice of a multi-PR initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic. It removes the Microsoft.TestPlatform.ObjectModel package reference from the production assembly, moving all remaining VSTest coupling up into the MSTest.TestAdapter layer. The work is a faithful, no-behavior-change decoupling, locked in by a new guard test.

I verified the key correctness claims: the new local ApartmentStateSetting { MTA, STA } enum exactly mirrors VSTest's PlatformApartmentState (MTA=0, STA=1, confirmed from the vstest source), so Enum.TryParse numeric-string compatibility is preserved on both the runsettings-XML and config paths; System.Configuration usage is entirely #if NETFRAMEWORK-guarded with net462 being the only netfx TFM; the UWP ObjectModel reference is consistent with VSTestBridge being excluded for UwpMinimum; and the guard test's assumption holds (MSTest's framework assemblies are named MSTest.TestFramework*, which don't contain "TestPlatform").

Changes:

  • Remove the Microsoft.TestPlatform.ObjectModel package reference from PlatformServices; replace the last VSTest enum consumer with a local neutral ApartmentStateSetting, and add a direct System.Configuration framework reference on .NET Framework.
  • Add explicit Microsoft.TestPlatform.ObjectModel references where the transitive path is now gone (MSTest.TestAdapter for UWP; the Desktop integration test project which uses XmlRunSettingsUtilities directly).
  • Add ObjectModelDecouplingTests guard asserting the compiled PlatformServices assembly references no *TestPlatform* assembly; harden TestSourceHandler public-key-token comparison against null/empty tokens.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.csNew neutral enum replacing VSTest PlatformApartmentState, with load-bearing member order documented
src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.csParse apartment state via local enum on both XML and config paths
src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csprojRemove ObjectModel package ref; add netfx-only System.Configuration reference
src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.csMake public-key-token comparison null/empty-safe
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojAdd explicit ObjectModel reference for UWP (VSTestBridge excluded there)
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.csNew guard test enforcing the platform-neutral contract
test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csprojAdd direct ObjectModel package ref for XmlRunSettingsUtilities

Review details

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

Cover the new public-key-token comparison branches in
TestSourceHandler: a same-named reference with a missing token
(signed-vs-unsigned) and one with a differing token both correctly
return false. The missing-token case is a regression guard for the
null-handling fix (previously it NRE'd and was swallowed into a
false-positive true).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9633

GradeTestNotes
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenDiffers
Boundary test: differing non-null token; byte-array magic explained by comment; clean AAA. No issues found.
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenIsMissing
Boundary test: null public-key-token case; .Should().BeFalse() is complete for a bool return. No issues found.
A (90–100)new ObjectModelDecouplingTests.
PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly
Contract guard via reflection with a well-messaged .Should().BeEmpty(). No issues found.

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

🤖 Automated content by GitHub Copilot. 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 Grade Tests on PR (on open / sync) workflow. · 61.4 AIC · ⌖ 11.1 AIC · ⊞ 9.5K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 8f1b01c into mainJul 7, 2026
45 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-drop-objectmodel branch July 7, 2026 05:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } 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

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

Merged
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel
Jul 7, 2026
Merged

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.ObjectModelPackageReference 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.Configurationframework 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.ObjectModelPackageReference. 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: zeroTestPlatform 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.

copilotand 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 605ee46 to 37c3973CompareJuly 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 37c3973 to 16552c6CompareJuly 5, 2026 14:08
Base automatically changed from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
An error occurred while trying to automatically change base from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
@Evangelink
Amaury Levé (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-conversionJuly 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.

PR #9633 — VSTest ObjectModel decoupling from PlatformServices

#DimensionVerdict
1Algorithmic Correctness⚠️ 1 MODERATE
2Threading & Concurrency✅ LGTM
3Security & IPC Contract Safety✅ LGTM
4Public API & Binary Compatibility✅ LGTM
5Performance & Allocations✅ LGTM
6Cross-TFM Compatibility✅ LGTM
7Resource & IDisposable Management✅ LGTM
8Defensive Coding at Boundaries✅ LGTM (covered by #1)
9Localization & Resources✅ LGTM
10Test Isolation✅ LGTM
11Assertion Quality✅ LGTM
12Flakiness Patterns✅ LGTM
13Test Completeness⚠️ 1 MODERATE
14Data-Driven Test CoverageN/A
15Code Structure & Simplification✅ LGTM
16Naming & Conventions✅ LGTM
17Documentation Accuracy✅ LGTM
18Analyzer & Code Fix QualityN/A
19IPC Wire CompatibilityN/A
20Build Infrastructure & Dependencies✅ LGTM
21Scope & PR Discipline✅ LGTM
22PowerShell Scripting HygieneN/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 threadsrc/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs Outdated
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-conversion branch from bb13e25 to 8832952CompareJuly 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 mainJuly 5, 2026 20:42
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 6, 2026 12:54
…-decoupling-drop-objectmodel
# Conflicts:
#	src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs
- Remove duplicate CloneWithSource method in TestMethod.cs that the
auto-merge concatenated from both branches (CS0111).
- Add a direct Microsoft.TestPlatform.ObjectModel reference to
MSTest.TestAdapter for the UWP (uap10.0.16299) TFM. The adapter's
VSTest-facing code needs the object model; on other TFMs it flows in
via VSTestBridge, but that reference is excluded for UWP and
PlatformServices no longer references the object model.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings July 7, 2026 04:02
@github-actions

This comment has been minimized.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is the final slice of a multi-PR initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic. It removes the Microsoft.TestPlatform.ObjectModel package reference from the production assembly, moving all remaining VSTest coupling up into the MSTest.TestAdapter layer. The work is a faithful, no-behavior-change decoupling, locked in by a new guard test.

I verified the key correctness claims: the new local ApartmentStateSetting { MTA, STA } enum exactly mirrors VSTest's PlatformApartmentState (MTA=0, STA=1, confirmed from the vstest source), so Enum.TryParse numeric-string compatibility is preserved on both the runsettings-XML and config paths; System.Configuration usage is entirely #if NETFRAMEWORK-guarded with net462 being the only netfx TFM; the UWP ObjectModel reference is consistent with VSTestBridge being excluded for UwpMinimum; and the guard test's assumption holds (MSTest's framework assemblies are named MSTest.TestFramework*, which don't contain "TestPlatform").

Changes:

  • Remove the Microsoft.TestPlatform.ObjectModel package reference from PlatformServices; replace the last VSTest enum consumer with a local neutral ApartmentStateSetting, and add a direct System.Configuration framework reference on .NET Framework.
  • Add explicit Microsoft.TestPlatform.ObjectModel references where the transitive path is now gone (MSTest.TestAdapter for UWP; the Desktop integration test project which uses XmlRunSettingsUtilities directly).
  • Add ObjectModelDecouplingTests guard asserting the compiled PlatformServices assembly references no *TestPlatform* assembly; harden TestSourceHandler public-key-token comparison against null/empty tokens.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.csNew neutral enum replacing VSTest PlatformApartmentState, with load-bearing member order documented
src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.csParse apartment state via local enum on both XML and config paths
src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csprojRemove ObjectModel package ref; add netfx-only System.Configuration reference
src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.csMake public-key-token comparison null/empty-safe
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojAdd explicit ObjectModel reference for UWP (VSTestBridge excluded there)
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.csNew guard test enforcing the platform-neutral contract
test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csprojAdd direct ObjectModel package ref for XmlRunSettingsUtilities

Review details

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

Cover the new public-key-token comparison branches in
TestSourceHandler: a same-named reference with a missing token
(signed-vs-unsigned) and one with a differing token both correctly
return false. The missing-token case is a regression guard for the
null-handling fix (previously it NRE'd and was swallowed into a
false-positive true).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9633

GradeTestNotes
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenDiffers
Boundary test: differing non-null token; byte-array magic explained by comment; clean AAA. No issues found.
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenIsMissing
Boundary test: null public-key-token case; .Should().BeFalse() is complete for a bool return. No issues found.
A (90–100)new ObjectModelDecouplingTests.
PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly
Contract guard via reflection with a well-messaged .Should().BeEmpty(). No issues found.

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

🤖 Automated content by GitHub Copilot. 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 Grade Tests on PR (on open / sync) workflow. · 61.4 AIC · ⌖ 11.1 AIC · ⊞ 9.5K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 8f1b01c into mainJul 7, 2026
45 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-drop-objectmodel branch July 7, 2026 05:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel
Jul 7, 2026
Merged

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.ObjectModelPackageReference 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.Configurationframework 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.ObjectModelPackageReference. 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: zeroTestPlatform 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.

copilotand 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 605ee46 to 37c3973CompareJuly 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 37c3973 to 16552c6CompareJuly 5, 2026 14:08
Base automatically changed from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
An error occurred while trying to automatically change base from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
@Evangelink
Amaury Levé (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-conversionJuly 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.

PR #9633 — VSTest ObjectModel decoupling from PlatformServices

#DimensionVerdict
1Algorithmic Correctness⚠️ 1 MODERATE
2Threading & Concurrency✅ LGTM
3Security & IPC Contract Safety✅ LGTM
4Public API & Binary Compatibility✅ LGTM
5Performance & Allocations✅ LGTM
6Cross-TFM Compatibility✅ LGTM
7Resource & IDisposable Management✅ LGTM
8Defensive Coding at Boundaries✅ LGTM (covered by #1)
9Localization & Resources✅ LGTM
10Test Isolation✅ LGTM
11Assertion Quality✅ LGTM
12Flakiness Patterns✅ LGTM
13Test Completeness⚠️ 1 MODERATE
14Data-Driven Test CoverageN/A
15Code Structure & Simplification✅ LGTM
16Naming & Conventions✅ LGTM
17Documentation Accuracy✅ LGTM
18Analyzer & Code Fix QualityN/A
19IPC Wire CompatibilityN/A
20Build Infrastructure & Dependencies✅ LGTM
21Scope & PR Discipline✅ LGTM
22PowerShell Scripting HygieneN/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 threadsrc/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs Outdated
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-conversion branch from bb13e25 to 8832952CompareJuly 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 mainJuly 5, 2026 20:42
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 6, 2026 12:54
…-decoupling-drop-objectmodel
# Conflicts:
#	src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs
- Remove duplicate CloneWithSource method in TestMethod.cs that the
auto-merge concatenated from both branches (CS0111).
- Add a direct Microsoft.TestPlatform.ObjectModel reference to
MSTest.TestAdapter for the UWP (uap10.0.16299) TFM. The adapter's
VSTest-facing code needs the object model; on other TFMs it flows in
via VSTestBridge, but that reference is excluded for UWP and
PlatformServices no longer references the object model.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings July 7, 2026 04:02
@github-actions

This comment has been minimized.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is the final slice of a multi-PR initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic. It removes the Microsoft.TestPlatform.ObjectModel package reference from the production assembly, moving all remaining VSTest coupling up into the MSTest.TestAdapter layer. The work is a faithful, no-behavior-change decoupling, locked in by a new guard test.

I verified the key correctness claims: the new local ApartmentStateSetting { MTA, STA } enum exactly mirrors VSTest's PlatformApartmentState (MTA=0, STA=1, confirmed from the vstest source), so Enum.TryParse numeric-string compatibility is preserved on both the runsettings-XML and config paths; System.Configuration usage is entirely #if NETFRAMEWORK-guarded with net462 being the only netfx TFM; the UWP ObjectModel reference is consistent with VSTestBridge being excluded for UwpMinimum; and the guard test's assumption holds (MSTest's framework assemblies are named MSTest.TestFramework*, which don't contain "TestPlatform").

Changes:

  • Remove the Microsoft.TestPlatform.ObjectModel package reference from PlatformServices; replace the last VSTest enum consumer with a local neutral ApartmentStateSetting, and add a direct System.Configuration framework reference on .NET Framework.
  • Add explicit Microsoft.TestPlatform.ObjectModel references where the transitive path is now gone (MSTest.TestAdapter for UWP; the Desktop integration test project which uses XmlRunSettingsUtilities directly).
  • Add ObjectModelDecouplingTests guard asserting the compiled PlatformServices assembly references no *TestPlatform* assembly; harden TestSourceHandler public-key-token comparison against null/empty tokens.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.csNew neutral enum replacing VSTest PlatformApartmentState, with load-bearing member order documented
src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.csParse apartment state via local enum on both XML and config paths
src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csprojRemove ObjectModel package ref; add netfx-only System.Configuration reference
src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.csMake public-key-token comparison null/empty-safe
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojAdd explicit ObjectModel reference for UWP (VSTestBridge excluded there)
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.csNew guard test enforcing the platform-neutral contract
test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csprojAdd direct ObjectModel package ref for XmlRunSettingsUtilities

Review details

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

Cover the new public-key-token comparison branches in
TestSourceHandler: a same-named reference with a missing token
(signed-vs-unsigned) and one with a differing token both correctly
return false. The missing-token case is a regression guard for the
null-handling fix (previously it NRE'd and was swallowed into a
false-positive true).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9633

GradeTestNotes
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenDiffers
Boundary test: differing non-null token; byte-array magic explained by comment; clean AAA. No issues found.
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenIsMissing
Boundary test: null public-key-token case; .Should().BeFalse() is complete for a bool return. No issues found.
A (90–100)new ObjectModelDecouplingTests.
PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly
Contract guard via reflection with a well-messaged .Should().BeEmpty(). No issues found.

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

🤖 Automated content by GitHub Copilot. 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 Grade Tests on PR (on open / sync) workflow. · 61.4 AIC · ⌖ 11.1 AIC · ⊞ 9.5K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 8f1b01c into mainJul 7, 2026
45 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-drop-objectmodel branch July 7, 2026 05:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel
Jul 7, 2026
Merged

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.ObjectModelPackageReference 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.Configurationframework 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.ObjectModelPackageReference. 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: zeroTestPlatform 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.

copilotand 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 605ee46 to 37c3973CompareJuly 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 37c3973 to 16552c6CompareJuly 5, 2026 14:08
Base automatically changed from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
An error occurred while trying to automatically change base from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
@Evangelink
Amaury Levé (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-conversionJuly 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.

PR #9633 — VSTest ObjectModel decoupling from PlatformServices

#DimensionVerdict
1Algorithmic Correctness⚠️ 1 MODERATE
2Threading & Concurrency✅ LGTM
3Security & IPC Contract Safety✅ LGTM
4Public API & Binary Compatibility✅ LGTM
5Performance & Allocations✅ LGTM
6Cross-TFM Compatibility✅ LGTM
7Resource & IDisposable Management✅ LGTM
8Defensive Coding at Boundaries✅ LGTM (covered by #1)
9Localization & Resources✅ LGTM
10Test Isolation✅ LGTM
11Assertion Quality✅ LGTM
12Flakiness Patterns✅ LGTM
13Test Completeness⚠️ 1 MODERATE
14Data-Driven Test CoverageN/A
15Code Structure & Simplification✅ LGTM
16Naming & Conventions✅ LGTM
17Documentation Accuracy✅ LGTM
18Analyzer & Code Fix QualityN/A
19IPC Wire CompatibilityN/A
20Build Infrastructure & Dependencies✅ LGTM
21Scope & PR Discipline✅ LGTM
22PowerShell Scripting HygieneN/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 threadsrc/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs Outdated
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-conversion branch from bb13e25 to 8832952CompareJuly 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 mainJuly 5, 2026 20:42
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 6, 2026 12:54
…-decoupling-drop-objectmodel
# Conflicts:
#	src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs
- Remove duplicate CloneWithSource method in TestMethod.cs that the
auto-merge concatenated from both branches (CS0111).
- Add a direct Microsoft.TestPlatform.ObjectModel reference to
MSTest.TestAdapter for the UWP (uap10.0.16299) TFM. The adapter's
VSTest-facing code needs the object model; on other TFMs it flows in
via VSTestBridge, but that reference is excluded for UWP and
PlatformServices no longer references the object model.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings July 7, 2026 04:02
@github-actions

This comment has been minimized.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is the final slice of a multi-PR initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic. It removes the Microsoft.TestPlatform.ObjectModel package reference from the production assembly, moving all remaining VSTest coupling up into the MSTest.TestAdapter layer. The work is a faithful, no-behavior-change decoupling, locked in by a new guard test.

I verified the key correctness claims: the new local ApartmentStateSetting { MTA, STA } enum exactly mirrors VSTest's PlatformApartmentState (MTA=0, STA=1, confirmed from the vstest source), so Enum.TryParse numeric-string compatibility is preserved on both the runsettings-XML and config paths; System.Configuration usage is entirely #if NETFRAMEWORK-guarded with net462 being the only netfx TFM; the UWP ObjectModel reference is consistent with VSTestBridge being excluded for UwpMinimum; and the guard test's assumption holds (MSTest's framework assemblies are named MSTest.TestFramework*, which don't contain "TestPlatform").

Changes:

  • Remove the Microsoft.TestPlatform.ObjectModel package reference from PlatformServices; replace the last VSTest enum consumer with a local neutral ApartmentStateSetting, and add a direct System.Configuration framework reference on .NET Framework.
  • Add explicit Microsoft.TestPlatform.ObjectModel references where the transitive path is now gone (MSTest.TestAdapter for UWP; the Desktop integration test project which uses XmlRunSettingsUtilities directly).
  • Add ObjectModelDecouplingTests guard asserting the compiled PlatformServices assembly references no *TestPlatform* assembly; harden TestSourceHandler public-key-token comparison against null/empty tokens.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.csNew neutral enum replacing VSTest PlatformApartmentState, with load-bearing member order documented
src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.csParse apartment state via local enum on both XML and config paths
src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csprojRemove ObjectModel package ref; add netfx-only System.Configuration reference
src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.csMake public-key-token comparison null/empty-safe
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojAdd explicit ObjectModel reference for UWP (VSTestBridge excluded there)
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.csNew guard test enforcing the platform-neutral contract
test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csprojAdd direct ObjectModel package ref for XmlRunSettingsUtilities

Review details

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

Cover the new public-key-token comparison branches in
TestSourceHandler: a same-named reference with a missing token
(signed-vs-unsigned) and one with a differing token both correctly
return false. The missing-token case is a regression guard for the
null-handling fix (previously it NRE'd and was swallowed into a
false-positive true).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9633

GradeTestNotes
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenDiffers
Boundary test: differing non-null token; byte-array magic explained by comment; clean AAA. No issues found.
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenIsMissing
Boundary test: null public-key-token case; .Should().BeFalse() is complete for a bool return. No issues found.
A (90–100)new ObjectModelDecouplingTests.
PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly
Contract guard via reflection with a well-messaged .Should().BeEmpty(). No issues found.

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

🤖 Automated content by GitHub Copilot. 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 Grade Tests on PR (on open / sync) workflow. · 61.4 AIC · ⌖ 11.1 AIC · ⊞ 9.5K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 8f1b01c into mainJul 7, 2026
45 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-drop-objectmodel branch July 7, 2026 05:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel
Jul 7, 2026
Merged

Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Amaury Levé (Evangelink) merged 28 commits into
mainfrom
dev/amauryleve/vstest-decoupling-drop-objectmodel

Conversation

@Evangelink

@EvangelinkAmaury Levé (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.ObjectModelPackageReference 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.Configurationframework 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.ObjectModelPackageReference. 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: zeroTestPlatform 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.

copilotand 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 605ee46 to 37c3973CompareJuly 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
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-drop-objectmodel branch from 37c3973 to 16552c6CompareJuly 5, 2026 14:08
Base automatically changed from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
An error occurred while trying to automatically change base from dev/amauryleve/vstest-decoupling-suspendcoverage to dev/amauryleve/vstest-decoupling-sourcehostJuly 5, 2026 19:24
@Evangelink
Amaury Levé (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-conversionJuly 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.

PR #9633 — VSTest ObjectModel decoupling from PlatformServices

#DimensionVerdict
1Algorithmic Correctness⚠️ 1 MODERATE
2Threading & Concurrency✅ LGTM
3Security & IPC Contract Safety✅ LGTM
4Public API & Binary Compatibility✅ LGTM
5Performance & Allocations✅ LGTM
6Cross-TFM Compatibility✅ LGTM
7Resource & IDisposable Management✅ LGTM
8Defensive Coding at Boundaries✅ LGTM (covered by #1)
9Localization & Resources✅ LGTM
10Test Isolation✅ LGTM
11Assertion Quality✅ LGTM
12Flakiness Patterns✅ LGTM
13Test Completeness⚠️ 1 MODERATE
14Data-Driven Test CoverageN/A
15Code Structure & Simplification✅ LGTM
16Naming & Conventions✅ LGTM
17Documentation Accuracy✅ LGTM
18Analyzer & Code Fix QualityN/A
19IPC Wire CompatibilityN/A
20Build Infrastructure & Dependencies✅ LGTM
21Scope & PR Discipline✅ LGTM
22PowerShell Scripting HygieneN/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 threadsrc/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs Outdated
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/vstest-decoupling-conversion branch from bb13e25 to 8832952CompareJuly 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 mainJuly 5, 2026 20:42
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) July 6, 2026 12:54
…-decoupling-drop-objectmodel
# Conflicts:
#	src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs
- Remove duplicate CloneWithSource method in TestMethod.cs that the
auto-merge concatenated from both branches (CS0111).
- Add a direct Microsoft.TestPlatform.ObjectModel reference to
MSTest.TestAdapter for the UWP (uap10.0.16299) TFM. The adapter's
VSTest-facing code needs the object model; on other TFMs it flows in
via VSTestBridge, but that reference is excluded for UWP and
PlatformServices no longer references the object model.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings July 7, 2026 04:02
@github-actions

This comment has been minimized.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is the final slice of a multi-PR initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic. It removes the Microsoft.TestPlatform.ObjectModel package reference from the production assembly, moving all remaining VSTest coupling up into the MSTest.TestAdapter layer. The work is a faithful, no-behavior-change decoupling, locked in by a new guard test.

I verified the key correctness claims: the new local ApartmentStateSetting { MTA, STA } enum exactly mirrors VSTest's PlatformApartmentState (MTA=0, STA=1, confirmed from the vstest source), so Enum.TryParse numeric-string compatibility is preserved on both the runsettings-XML and config paths; System.Configuration usage is entirely #if NETFRAMEWORK-guarded with net462 being the only netfx TFM; the UWP ObjectModel reference is consistent with VSTestBridge being excluded for UwpMinimum; and the guard test's assumption holds (MSTest's framework assemblies are named MSTest.TestFramework*, which don't contain "TestPlatform").

Changes:

  • Remove the Microsoft.TestPlatform.ObjectModel package reference from PlatformServices; replace the last VSTest enum consumer with a local neutral ApartmentStateSetting, and add a direct System.Configuration framework reference on .NET Framework.
  • Add explicit Microsoft.TestPlatform.ObjectModel references where the transitive path is now gone (MSTest.TestAdapter for UWP; the Desktop integration test project which uses XmlRunSettingsUtilities directly).
  • Add ObjectModelDecouplingTests guard asserting the compiled PlatformServices assembly references no *TestPlatform* assembly; harden TestSourceHandler public-key-token comparison against null/empty tokens.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.csNew neutral enum replacing VSTest PlatformApartmentState, with load-bearing member order documented
src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.csParse apartment state via local enum on both XML and config paths
src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csprojRemove ObjectModel package ref; add netfx-only System.Configuration reference
src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.csMake public-key-token comparison null/empty-safe
src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csprojAdd explicit ObjectModel reference for UWP (VSTestBridge excluded there)
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.csNew guard test enforcing the platform-neutral contract
test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csprojAdd direct ObjectModel package ref for XmlRunSettingsUtilities

Review details

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

Cover the new public-key-token comparison branches in
TestSourceHandler: a same-named reference with a missing token
(signed-vs-unsigned) and one with a differing token both correctly
return false. The missing-token case is a regression guard for the
null-handling fix (previously it NRE'd and was swallowed into a
false-positive true).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9633

GradeTestNotes
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenDiffers
Boundary test: differing non-null token; byte-array magic explained by comment; clean AAA. No issues found.
A (90–100)mod DesktopTestSourceTests.
IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenIsMissing
Boundary test: null public-key-token case; .Should().BeFalse() is complete for a bool return. No issues found.
A (90–100)new ObjectModelDecouplingTests.
PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly
Contract guard via reflection with a well-messaged .Should().BeEmpty(). No issues found.

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

🤖 Automated content by GitHub Copilot. 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 Grade Tests on PR (on open / sync) workflow. · 61.4 AIC · ⌖ 11.1 AIC · ⊞ 9.5K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 8f1b01c into mainJul 7, 2026
45 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-drop-objectmodel branch July 7, 2026 05:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101