Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633
Drop the ObjectModel package reference from PlatformServices (Phase 7 capstone)#9633Evangelink wants to merge 25 commits into
Conversation
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>
605ee46 to
37c3973
Compare
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>
37c3973 to
16552c6
Compare
There was a problem hiding this comment.
Note
🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.
PR #9633 — VSTest ObjectModel decoupling from PlatformServices
| # | Dimension | Verdict |
|---|---|---|
| 1 | Algorithmic Correctness | |
| 2 | Threading & Concurrency | ✅ LGTM |
| 3 | Security & IPC Contract Safety | ✅ LGTM |
| 4 | Public API & Binary Compatibility | ✅ LGTM |
| 5 | Performance & Allocations | ✅ LGTM |
| 6 | Cross-TFM Compatibility | ✅ LGTM |
| 7 | Resource & IDisposable Management | ✅ LGTM |
| 8 | Defensive Coding at Boundaries | ✅ LGTM (covered by #1) |
| 9 | Localization & Resources | ✅ LGTM |
| 10 | Test Isolation | ✅ LGTM |
| 11 | Assertion Quality | ✅ LGTM |
| 12 | Flakiness Patterns | ✅ LGTM |
| 13 | Test Completeness | |
| 14 | Data-Driven Test Coverage | N/A |
| 15 | Code Structure & Simplification | ✅ LGTM |
| 16 | Naming & Conventions | ✅ LGTM |
| 17 | Documentation Accuracy | ✅ LGTM |
| 18 | Analyzer & Code Fix Quality | N/A |
| 19 | IPC Wire Compatibility | N/A |
| 20 | Build Infrastructure & Dependencies | ✅ LGTM |
| 21 | Scope & PR Discipline | ✅ LGTM |
| 22 | PowerShell Scripting Hygiene | N/A |
✅ 17/18 applicable dimensions clean.
Findings
-
Algorithmic Correctness (MODERATE) —
ArePublicKeyTokensEqual(byte[] left, byte[] right)inTestSourceHandler.csline 142 dereferences both parameters unconditionally.AssemblyName.GetPublicKeyToken()returnsnullfor unsigned assemblies, producing aNullReferenceExceptionthat is silently caught and converted to the conservativenull → truepath (false-positive discovery) rather than the correctfalse. See inline comment for the fix (annotatebyte[]?and add a null guard at the top of the helper). -
Test Completeness (MODERATE) — The new
SuspendCodeCoverageclass (Utilities/SuspendCodeCoverage.cs) has no unit tests. Three behaviours are testable and could silently regress on.NET FrameworkTFMs 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 newSuspendCodeCoverageTests.csinMSTestAdapter.PlatformServices.UnitTests, guarded with#if NETFRAMEWORK.
Notable positives
- The
ApartmentStateSettingenum ordering (MTA=0, STA=1) correctly matches the oldPlatformApartmentStatenumeric values, preserving parse compatibility for numeric run-settings strings — and the load-bearing comment explaining this is clear. SuspendCodeCoverage.Disposecorrectly passesnull(the captured previous value when the env var was absent) toSetEnvironmentVariable, which is the documented way to delete the variable — no resource-leak risk.- The
System.Configurationexplicit reference is correctly scoped to$(NetFrameworkMinimum)only — confirmed thatMSTestAdapter.PlatformServicesships exactly one.NET FrameworkTFM (net462), so no framework TFM is missed. ObjectModelDecouplingTestscorrectly usesAwesomeAssertions(required by this project'sBannedSymbols.txt), usesIndexOfinstead ofstring.Contains(string, StringComparison)for .NET Framework compat, and verifies the compile-time manifest references — the right API for the stated contract.
bb13e25 to
8832952
Compare
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 7 (capstone) — drop the ObjectModel package reference
The final slice of the initiative to make
Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServicesplatform-agnostic. After this, PlatformServices no longer references the VSTest object model (Microsoft.TestPlatform.ObjectModel) at all — every VSTest dependency now lives in theMSTest.TestAdapterlayer above it. Strict byte-for-byte, no behavior change.Production changes
Microsoft.TestPlatform.ObjectModelPackageReferencefromMSTestAdapter.PlatformServices.csproj.RunConfigurationSettingswas the last consumer of a transitively-provided VSTest package: it usedPlatformAbstractions'PlatformApartmentStateenum{MTA, STA}to parseExecutionThreadApartmentState. Replaced with a localinternal enum ApartmentStateSetting { MTA, STA }of the same shape (same member names and order —MTA=0, STA=1), preserving the exactEnum.TryParse→STA/MTA → System.Threading.ApartmentState / else-throwbehavior byte-for-byte on both the runsettings-XML and config paths. The member order is load-bearing (Enum.TryParseaccepts numeric strings"0"/"1", so the numbering must match) and is commented as such. (Parsing directly toSystem.Threading.ApartmentStatewould change the handling of theUnknownvalue — it has that extra member — so a faithful 2-member local enum is required. The property type stays the BCLSystem.Threading.ApartmentState; only the parse-enum is neutralized.)System.Configurationframework reference on .NET Framework.ConfigurationManager/ConfigurationElementCollection(used byTestDataSource) were previously pulled in transitively via the object-model package;System.Configurationis a framework assembly, so it is now referenced directly.Result: PlatformServices is fully platform-neutral
The compiled
MSTestAdapter.PlatformServicesassembly has zero references to anyMicrosoft.*.TestPlatform.*assembly on every real TFM (net462/net8.0/net9.0 + windows variants), verified via assembly metadata.Guard test (the finish line)
ObjectModelDecouplingTestsasserts the compiled PlatformServices assembly references no assembly whose name contains"TestPlatform"— catching ObjectModel, PlatformAbstractions, CoreUtilities, etc. (MSTest's own framework isMSTest.TestFramework, which doesn't match). This locks the platform-agnostic contract permanently.Test-project fix
PlatformServices.Desktop.IntegrationTestsuses ObjectModel'sXmlRunSettingsUtilitiesdirectly (previously transitive through the PlatformServices project reference), so it gets its own directMicrosoft.TestPlatform.ObjectModelPackageReference. Test projects may reference the object model; only the production assembly must be neutral.Verification
TestPlatformreferences (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.TestAdapterandMSTest.IntegrationTestsbuild clean.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.