Skip to content

Resolve the object-model assembly by name in AppDomain wiring (Phase 6e-4b)#9628

Merged
Evangelink merged 1 commit into
dev/amauryleve/vstest-decoupling-settings-utilsfrom
dev/amauryleve/vstest-decoupling-appdomain
Jul 5, 2026
Merged

Resolve the object-model assembly by name in AppDomain wiring (Phase 6e-4b)#9628
Evangelink merged 1 commit into
dev/amauryleve/vstest-decoupling-settings-utilsfrom
dev/amauryleve/vstest-decoupling-appdomain

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Phase 6e-4b — Resolve the object-model assembly by name in the netfx AppDomain wiring

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

What this does

AppDomainUtilities (netfx) 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 applied to the child domain's config.

Both run parent-side during test-source-host setup (GetTargetFrameworkVersionString / SetConfigurationFile), after the adapter has already loaded the object model into the current domain. So the assembly is now resolved by simple name from the current domain:

AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == "Microsoft.VisualStudio.TestPlatform.ObjectModel")
    ?? Assembly.Load("Microsoft.VisualStudio.TestPlatform.ObjectModel");

This returns the same (post-redirect) assembly identity the type reference did, without a compile-time reference — so the resolution path and the generated binding redirect are unchanged. The only remaining mention of the object model in this file is its simple name as a string literal (not an assembly reference — does not block dropping the package).

This removes the real code dependency from AppDomainUtilities. (XmlUtilities.AddAssemblyRedirection here is the adapter's own neutral type, not VSTest's.)

Fidelity proof

Per the review guardrail for this — the last real netfx-isolation risk in the initiative — the swap is proven on the exact scenario the type reference protects:

  • PlatformServices.Desktop.IntegrationTests (net462 AppDomain assembly-resolution-from-runsettings + deployment app-domain paths): 15/15 green.
  • Timing: both call sites run parent-side after the adapter loaded the object model, so there is no child-domain "looked up before loaded → null" hole.

Verification

  • Full build.cmd -c Debug green across all TFMs (net462/net8.0/net9.0/UWP/WinUI), IDE0005 clean.
  • PlatformServices.Desktop.IntegrationTests: 15, 0 failed.
  • MSTestAdapter.PlatformServices.UnitTests: 897 (net8.0) / 935 (net462), 0 failed.
  • MSTestAdapter.UnitTests: 21, 0 failed.

Remaining after this

3 files with real deps: TestDeployment (SuspendCodeCoverage, netfx), TestSourceHost/TestSourceHandler (AssemblyHelper + EqtTrace) — 6e-4c. Then Phase 7 drops the PackageReference + adds the zero-ObjectModel-reference guard test (the AssemblyResolver + this file's string literals don't block it).

Stacking

… #96246e-3b #96266e-4a #96276e-4b (this), onto dev/amauryleve/vstest-decoupling-base. Base = 6e-4a branch (dev/amauryleve/vstest-decoupling-settings-utils). Do not rebase/reset the base.

…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>
@Evangelink Evangelink marked this pull request as ready for review July 5, 2026 19:26
@Evangelink Evangelink merged commit 8e5f3bf into dev/amauryleve/vstest-decoupling-settings-utils Jul 5, 2026
19 checks passed
@Evangelink Evangelink deleted the dev/amauryleve/vstest-decoupling-appdomain branch July 5, 2026 19:26

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

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

# Dimension Verdict
5/15 Performance / Code Structure 🟢 1 NIT

✅ 21/22 dimensions clean.

  • Performance — GetObjectModelAssembly() called twice in SetConfigurationFile; could cache to a local AssemblyName before the if check (see inline comment)

Context: This is a retrospective review of a merged PR. The change itself is a well-executed, strictly-scoped refactor. The assembly-by-name resolution is correct for the caller's lifecycle guarantee (object model always loaded before these methods run), the fallback Assembly.Load is protected by both callers' existing try/catch blocks, and the #if NETFRAMEWORK guard keeps this entirely off modern TFMs. The only actionable note is the cosmetic double-call, which is a carry-forward from the original code structure.

Comment on lines +172 to +175
string currentVersionOfObjectModel = GetObjectModelAssembly().GetName().Version.ToString();
if (!string.Equals(currentVersionOfObjectModel, ObjectModelVersionBuiltAgainst, StringComparison.Ordinal))
{
AssemblyName assemblyName = typeof(TestCase).Assembly.GetName();
AssemblyName assemblyName = GetObjectModelAssembly().GetName();

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.

[NIT] Performance / Code Structure

GetObjectModelAssembly() is called twice within the same try-block. Unlike the old typeof(TestCase).Assembly (a JIT-inlined runtime constant), each call to GetObjectModelAssembly() allocates a LINQ query over AppDomain.CurrentDomain.GetAssemblies(). The practical cost is negligible in a setup path, but the logic can be simplified to a single call:

// Capture once — avoids two linear scans of GetAssemblies()
AssemblyName assemblyName = GetObjectModelAssembly().GetName();
string currentVersionOfObjectModel = assemblyName.Version!.ToString();
if (!string.Equals(currentVersionOfObjectModel, ObjectModelVersionBuiltAgainst, StringComparison.Ordinal))
{
    byte[] configurationBytes =
        XmlUtilities.AddAssemblyRedirection(
            testSourceConfigFile,
            assemblyName,
            ObjectModelVersionBuiltAgainst,
            assemblyName.Version!.ToString());
    appDomainSetup.SetConfigurationBytes(configurationBytes);
}

This also surfaces the Version! null-annotation more explicitly — AssemblyName.Version is nullable, though it will never be null for the VSTest ObjectModel in practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant