Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3) - #9632

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)#9632
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-sourcehostfrom
dev/amauryleve/vstest-decoupling-suspendcoverage

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4c3 — vendor a neutral SuspendCodeCoverage

Part of the initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on Microsoft.TestPlatform.ObjectModel. Strict byte-for-byte, no behavior change.

What this changes

TestDeployment (netfx) wraps 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.

This vendors an internal neutral copy at Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities), reproducing the VSTest behavior exactly:

  • On construction: capture the current value of the process environment variable __VANGUARD_SUSPEND_INSTRUMENT__, then set it to TRUE.
  • On dispose: restore the captured previous value (idempotent).

The environment-variable name and value are the collector IPC contract the dynamic code-coverage (Vanguard) engine reads, so they are preserved byte-identical. The wrapper 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 through its already-present using ...PlatformServices.Utilities;; the VSTest ObjectModel.Utilities using (and its deferral comment) is removed.

Fidelity proof and test-net limitation

The mechanism is a process environment variable (__VANGUARD_SUSPEND_INSTRUMENT__), not a named event / mutex / EventWaitHandle. There is no signal/listener model — the dynamic code-coverage (Vanguard) collector reads this variable from the process environment when deciding whether to instrument a module being loaded. The fidelity of the vendored copy therefore rests entirely on replicating that wire contract byte-identically against the OSS source (vstest v18.4.0 SuspendCodeCoverage.cs).

Source-diff (fidelity proof of record) — OSS original vs vendored copy:

Contract elementOSS (Microsoft.TestPlatform.ObjectModel)Vendored copy
env var name"__VANGUARD_SUSPEND_INSTRUMENT__""__VANGUARD_SUSPEND_INSTRUMENT__"
set value"TRUE""TRUE"
target (all 3 accesses)EnvironmentVariableTarget.ProcessEnvironmentVariableTarget.Process
ctorGetEnvironmentVariable(name, Process) → capture; SetEnvironmentVariable(name, "TRUE", Process)identical
disposeSetEnvironmentVariable(name, prev, Process), guarded by _isDisposedidentical

The only intentional difference is the collapse of the OSS Dispose() / protected virtual Dispose(bool) / GC.SuppressFinalize into a single idempotent Dispose() — behavior-equivalent because the OSS type declares no finalizer (so GC.SuppressFinalize is a no-op and disposing is always true on the public path). The name/value/target/sequence — the entire wire contract the collector keys off — are verbatim.

Note on the test net: PlatformServices.Desktop.IntegrationTests runs with no coverage collector attached, so its green result proves the vendored code does not crash / the deploy path still works — it does not exercise a real collector reading the variable. That is acceptable here precisely because there is no signaling to get wrong: correctness is fully determined by the (source-identical) variable name/value/target above. There is no clean seam to assert the variable mid-deploy without contorting the copy loop, so no brittle probe was added.

Result: PlatformServices is ObjectModel-type-free

After this change, PlatformServices has zero using/type references to the Microsoft.TestPlatform.ObjectModel package — only string-literal assembly names (used for by-name runtime assembly lookup in the AppDomain/source-host wiring) remain. This clears the way to drop the package reference in the capstone (Phase 7).

Verification

  • Builds 0-warning (net462 and all real TFMs; netfx-guarded change).
  • MSTestAdapter.PlatformServices.UnitTests: 935/935 (net462), 897/897 (net8.0).
  • PlatformServices.Desktop.IntegrationTests: 15/15 (net462) — exercises the deployment path that runs inside the SuspendCodeCoverage scope.
  • Expert-reviewer pass.

Stacking

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

…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>
Base automatically changed from dev/amauryleve/vstest-decoupling-sourcehandler 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
@Evangelink
Amaury Levé (Evangelink) merged commit c561276 into dev/amauryleve/vstest-decoupling-sourcehostJul 5, 2026
24 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-suspendcoverage branch July 5, 2026 19:24

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

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

Expert Review — PR #9632 · Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)

Summary

This PR accomplishes two related goals under the ObjectModel-decoupling initiative:

  1. Vendors SuspendCodeCoverage — a faithful internal copy that preserves the Vanguard IPC contract (__VANGUARD_SUSPEND_INSTRUMENT__ / "TRUE" / EnvironmentVariableTarget.Process) byte-identically, gated behind #if NETFRAMEWORK. The PR description's fidelity proof is thorough and accurate.

  2. Vendors DoesSourceReferenceAssembly — replaces the call to AssemblyHelper.DoesReferencesAssembly (vstest ObjectModel) with a local implementation. The original code carried a comment noting this in-AppDomain optimization was acceptable; the new implementation honors that pre-approval.

One minor code-quality issue is noted; no blocking or major findings.


Verdict Table

#DimensionStatusNotes
1Algorithmic Correctness⚠️ MinorArePublicKeyTokensEqual does not handle null from GetPublicKeyToken(); NullReferenceException is silently caught → conservative fallback. See inline comment.
2Threading & Concurrency✅ CleanSuspendCodeCoverage is used in a single-threaded deployment context; plain bool _isDisposed is sufficient. SetEnvironmentVariable concurrency matches original behavior.
3Security & IPC Contract Safety✅ CleanEnv-var name, value, and target are preserved verbatim. No path traversal or injection vectors introduced.
4Public API & Binary Compatibility✅ CleanBoth new types are internal sealed. No public API surface changed. PublicAPI.Unshipped.txt update not needed.
5Performance & Allocations✅ CleanNo LINQ, no unnecessary allocations. The ReflectionOnlyLoadFrom path is equivalent in cost to the original (same assembly load, minus the AppDomain round-trip).
6Cross-TFM Compatibility✅ CleanAll new code is guarded by #if NETFRAMEWORK. Other TFMs are unaffected.
7Error Handling & Resilience✅ CleanBare catch in DoesSourceReferenceAssembly is intentional and carries a comment; it mirrors the original vstest CheckAssemblyReference pattern. Conservative fallback (null → discover anyway) is correct.
8Resource Management✅ CleanSuspendCodeCoverage is IDisposable, idempotent, and used via using. No finalizer is needed (none exists in the original either; GC.SuppressFinalize was a no-op).
9Naming & Code Style✅ CleanAll names follow conventions. _isDisposed, _previousEnvironmentValue, constants are appropriately cased.
10Documentation & Comments✅ CleanBoth new files have XML doc comments. The fidelity comparison table in the PR description is exemplary.
11Test Coverage & Quality✅ CleanFour existing unit tests in DesktopTestSourceTests exercise the refactored IsAssemblyReferenced path, including the null-source/null-assembly, found, and not-found cases. PlatformServices.Desktop.IntegrationTests exercises the deploy path with SuspendCodeCoverage.
12Localization & Resource StringsN/ANo user-facing strings added.
13Logging & DiagnosticsN/ANo logging changes.
14Dependency & Package Hygiene✅ CleanThe Microsoft.TestPlatform.ObjectModel type-reference is eliminated as intended. No new packages introduced.
15Code Complexity & Maintainability✅ CleanBoth new methods are compact and well-commented. DoesSourceReferenceAssembly is ~35 LOC with clear phases.
16Test Infrastructure & Acceptance TestsN/ANo CLI options, output formats, or acceptance-test expectations were changed.
17Configuration & DefaultsN/ANo configuration changes.
18Scope Discipline✅ CleanThe TestSourceHandler.cs refactoring is logically part of the same ObjectModel-decoupling goal and is covered by the PR description, even if the title focuses on SuspendCodeCoverage.
19Build & Project File QualityN/ANo .csproj/.props changes needed; the new .cs file is picked up by the existing glob.
20Invariant/Contract Violations⚠️ Minorbyte[] parameters in ArePublicKeyTokensEqual should be byte[]?; GetPublicKeyToken() returns nullable and the call sites do not null-check. See inline comment.
21Behavioral Regression Risk✅ CleanAppDomain isolation is intentionally dropped (the original code carried a "we can optimize this" comment). ReflectionOnlyLoadFrom in the current AppDomain is safe: it does not execute code from the loaded assembly.
22PowerShell / Script QualityN/ANo scripts modified.

Key Finding

⚠️ Minor — ArePublicKeyTokensEqual null-handling gap (TestSourceHandler.cs line 142)

GetPublicKeyToken() returns byte[]?; both call sites pass the result directly to byte[] parameters. For unsigned assemblies (null token) this throws NullReferenceException, silently caught → null → conservative discovery. The behavior is correct by accident rather than by design. The original vstest CheckAssemblyReference had the same gap, so this is a clean-up opportunity only, not a regression. An inline suggestion is attached.


Positive Notes

  • The fidelity proof table (env-var name / value / target / ctor / dispose sequence) is an excellent record for future maintainers.
  • Collapsing Dispose(bool disposing) + GC.SuppressFinalize to a direct Dispose() is the correct simplification given the absence of a finalizer.
  • The AppDomain drop in DoesSourceReferenceAssembly is safe and was pre-authorized by the inline comment in the original code.

}
}

private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right)

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.

ArePublicKeyTokensEqual declares non-nullable byte[] parameters, but both callers pass AssemblyName.GetPublicKeyToken() which returns byte[]? (null for unsigned assemblies). When either token is null, left.Length on line 144 throws NullReferenceException, caught by the outer try/catch, which returns null → conservative path → discovery proceeds. Functionally safe, but the correctness depends on exception routing rather than explicit logic.

Suggested fix (nullable-aware):

privatestaticboolArePublicKeyTokensEqual(byte[]?left,byte[]?right){if(leftisnull&&rightisnull)returntrue;if(leftisnull||rightisnull)returnfalse;if(left.Length!=right.Length)returnfalse;for(inti=0;i<left.Length;++i){if(left[i]!=right[i])returnfalse;}returntrue;}

This makes "both sides unsigned → match by name" explicit and eliminates the implicit NullReferenceException. Also update referenceAssemblyPublicKeyToken (line 113) to byte[]? to satisfy the nullable annotation.

Not a regression: the original vstestCheckAssemblyReference had the same gap — byte[] publicKeyToken1 = referencedAssembly.GetPublicKeyToken() without null-guard. This is a clean-up opportunity only.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3) - #9632

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)#9632
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-sourcehostfrom
dev/amauryleve/vstest-decoupling-suspendcoverage

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4c3 — vendor a neutral SuspendCodeCoverage

Part of the initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on Microsoft.TestPlatform.ObjectModel. Strict byte-for-byte, no behavior change.

What this changes

TestDeployment (netfx) wraps 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.

This vendors an internal neutral copy at Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities), reproducing the VSTest behavior exactly:

  • On construction: capture the current value of the process environment variable __VANGUARD_SUSPEND_INSTRUMENT__, then set it to TRUE.
  • On dispose: restore the captured previous value (idempotent).

The environment-variable name and value are the collector IPC contract the dynamic code-coverage (Vanguard) engine reads, so they are preserved byte-identical. The wrapper 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 through its already-present using ...PlatformServices.Utilities;; the VSTest ObjectModel.Utilities using (and its deferral comment) is removed.

Fidelity proof and test-net limitation

The mechanism is a process environment variable (__VANGUARD_SUSPEND_INSTRUMENT__), not a named event / mutex / EventWaitHandle. There is no signal/listener model — the dynamic code-coverage (Vanguard) collector reads this variable from the process environment when deciding whether to instrument a module being loaded. The fidelity of the vendored copy therefore rests entirely on replicating that wire contract byte-identically against the OSS source (vstest v18.4.0 SuspendCodeCoverage.cs).

Source-diff (fidelity proof of record) — OSS original vs vendored copy:

Contract elementOSS (Microsoft.TestPlatform.ObjectModel)Vendored copy
env var name"__VANGUARD_SUSPEND_INSTRUMENT__""__VANGUARD_SUSPEND_INSTRUMENT__"
set value"TRUE""TRUE"
target (all 3 accesses)EnvironmentVariableTarget.ProcessEnvironmentVariableTarget.Process
ctorGetEnvironmentVariable(name, Process) → capture; SetEnvironmentVariable(name, "TRUE", Process)identical
disposeSetEnvironmentVariable(name, prev, Process), guarded by _isDisposedidentical

The only intentional difference is the collapse of the OSS Dispose() / protected virtual Dispose(bool) / GC.SuppressFinalize into a single idempotent Dispose() — behavior-equivalent because the OSS type declares no finalizer (so GC.SuppressFinalize is a no-op and disposing is always true on the public path). The name/value/target/sequence — the entire wire contract the collector keys off — are verbatim.

Note on the test net: PlatformServices.Desktop.IntegrationTests runs with no coverage collector attached, so its green result proves the vendored code does not crash / the deploy path still works — it does not exercise a real collector reading the variable. That is acceptable here precisely because there is no signaling to get wrong: correctness is fully determined by the (source-identical) variable name/value/target above. There is no clean seam to assert the variable mid-deploy without contorting the copy loop, so no brittle probe was added.

Result: PlatformServices is ObjectModel-type-free

After this change, PlatformServices has zero using/type references to the Microsoft.TestPlatform.ObjectModel package — only string-literal assembly names (used for by-name runtime assembly lookup in the AppDomain/source-host wiring) remain. This clears the way to drop the package reference in the capstone (Phase 7).

Verification

  • Builds 0-warning (net462 and all real TFMs; netfx-guarded change).
  • MSTestAdapter.PlatformServices.UnitTests: 935/935 (net462), 897/897 (net8.0).
  • PlatformServices.Desktop.IntegrationTests: 15/15 (net462) — exercises the deployment path that runs inside the SuspendCodeCoverage scope.
  • Expert-reviewer pass.

Stacking

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

…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>
Base automatically changed from dev/amauryleve/vstest-decoupling-sourcehandler 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
@Evangelink
Amaury Levé (Evangelink) merged commit c561276 into dev/amauryleve/vstest-decoupling-sourcehostJul 5, 2026
24 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-suspendcoverage branch July 5, 2026 19:24

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

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

Expert Review — PR #9632 · Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)

Summary

This PR accomplishes two related goals under the ObjectModel-decoupling initiative:

  1. Vendors SuspendCodeCoverage — a faithful internal copy that preserves the Vanguard IPC contract (__VANGUARD_SUSPEND_INSTRUMENT__ / "TRUE" / EnvironmentVariableTarget.Process) byte-identically, gated behind #if NETFRAMEWORK. The PR description's fidelity proof is thorough and accurate.

  2. Vendors DoesSourceReferenceAssembly — replaces the call to AssemblyHelper.DoesReferencesAssembly (vstest ObjectModel) with a local implementation. The original code carried a comment noting this in-AppDomain optimization was acceptable; the new implementation honors that pre-approval.

One minor code-quality issue is noted; no blocking or major findings.


Verdict Table

#DimensionStatusNotes
1Algorithmic Correctness⚠️ MinorArePublicKeyTokensEqual does not handle null from GetPublicKeyToken(); NullReferenceException is silently caught → conservative fallback. See inline comment.
2Threading & Concurrency✅ CleanSuspendCodeCoverage is used in a single-threaded deployment context; plain bool _isDisposed is sufficient. SetEnvironmentVariable concurrency matches original behavior.
3Security & IPC Contract Safety✅ CleanEnv-var name, value, and target are preserved verbatim. No path traversal or injection vectors introduced.
4Public API & Binary Compatibility✅ CleanBoth new types are internal sealed. No public API surface changed. PublicAPI.Unshipped.txt update not needed.
5Performance & Allocations✅ CleanNo LINQ, no unnecessary allocations. The ReflectionOnlyLoadFrom path is equivalent in cost to the original (same assembly load, minus the AppDomain round-trip).
6Cross-TFM Compatibility✅ CleanAll new code is guarded by #if NETFRAMEWORK. Other TFMs are unaffected.
7Error Handling & Resilience✅ CleanBare catch in DoesSourceReferenceAssembly is intentional and carries a comment; it mirrors the original vstest CheckAssemblyReference pattern. Conservative fallback (null → discover anyway) is correct.
8Resource Management✅ CleanSuspendCodeCoverage is IDisposable, idempotent, and used via using. No finalizer is needed (none exists in the original either; GC.SuppressFinalize was a no-op).
9Naming & Code Style✅ CleanAll names follow conventions. _isDisposed, _previousEnvironmentValue, constants are appropriately cased.
10Documentation & Comments✅ CleanBoth new files have XML doc comments. The fidelity comparison table in the PR description is exemplary.
11Test Coverage & Quality✅ CleanFour existing unit tests in DesktopTestSourceTests exercise the refactored IsAssemblyReferenced path, including the null-source/null-assembly, found, and not-found cases. PlatformServices.Desktop.IntegrationTests exercises the deploy path with SuspendCodeCoverage.
12Localization & Resource StringsN/ANo user-facing strings added.
13Logging & DiagnosticsN/ANo logging changes.
14Dependency & Package Hygiene✅ CleanThe Microsoft.TestPlatform.ObjectModel type-reference is eliminated as intended. No new packages introduced.
15Code Complexity & Maintainability✅ CleanBoth new methods are compact and well-commented. DoesSourceReferenceAssembly is ~35 LOC with clear phases.
16Test Infrastructure & Acceptance TestsN/ANo CLI options, output formats, or acceptance-test expectations were changed.
17Configuration & DefaultsN/ANo configuration changes.
18Scope Discipline✅ CleanThe TestSourceHandler.cs refactoring is logically part of the same ObjectModel-decoupling goal and is covered by the PR description, even if the title focuses on SuspendCodeCoverage.
19Build & Project File QualityN/ANo .csproj/.props changes needed; the new .cs file is picked up by the existing glob.
20Invariant/Contract Violations⚠️ Minorbyte[] parameters in ArePublicKeyTokensEqual should be byte[]?; GetPublicKeyToken() returns nullable and the call sites do not null-check. See inline comment.
21Behavioral Regression Risk✅ CleanAppDomain isolation is intentionally dropped (the original code carried a "we can optimize this" comment). ReflectionOnlyLoadFrom in the current AppDomain is safe: it does not execute code from the loaded assembly.
22PowerShell / Script QualityN/ANo scripts modified.

Key Finding

⚠️ Minor — ArePublicKeyTokensEqual null-handling gap (TestSourceHandler.cs line 142)

GetPublicKeyToken() returns byte[]?; both call sites pass the result directly to byte[] parameters. For unsigned assemblies (null token) this throws NullReferenceException, silently caught → null → conservative discovery. The behavior is correct by accident rather than by design. The original vstest CheckAssemblyReference had the same gap, so this is a clean-up opportunity only, not a regression. An inline suggestion is attached.


Positive Notes

  • The fidelity proof table (env-var name / value / target / ctor / dispose sequence) is an excellent record for future maintainers.
  • Collapsing Dispose(bool disposing) + GC.SuppressFinalize to a direct Dispose() is the correct simplification given the absence of a finalizer.
  • The AppDomain drop in DoesSourceReferenceAssembly is safe and was pre-authorized by the inline comment in the original code.

}
}

private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right)

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.

ArePublicKeyTokensEqual declares non-nullable byte[] parameters, but both callers pass AssemblyName.GetPublicKeyToken() which returns byte[]? (null for unsigned assemblies). When either token is null, left.Length on line 144 throws NullReferenceException, caught by the outer try/catch, which returns null → conservative path → discovery proceeds. Functionally safe, but the correctness depends on exception routing rather than explicit logic.

Suggested fix (nullable-aware):

privatestaticboolArePublicKeyTokensEqual(byte[]?left,byte[]?right){if(leftisnull&&rightisnull)returntrue;if(leftisnull||rightisnull)returnfalse;if(left.Length!=right.Length)returnfalse;for(inti=0;i<left.Length;++i){if(left[i]!=right[i])returnfalse;}returntrue;}

This makes "both sides unsigned → match by name" explicit and eliminates the implicit NullReferenceException. Also update referenceAssemblyPublicKeyToken (line 113) to byte[]? to satisfy the nullable annotation.

Not a regression: the original vstestCheckAssemblyReference had the same gap — byte[] publicKeyToken1 = referencedAssembly.GetPublicKeyToken() without null-guard. This is a clean-up opportunity only.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3) - #9632

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)#9632
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-sourcehostfrom
dev/amauryleve/vstest-decoupling-suspendcoverage

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4c3 — vendor a neutral SuspendCodeCoverage

Part of the initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on Microsoft.TestPlatform.ObjectModel. Strict byte-for-byte, no behavior change.

What this changes

TestDeployment (netfx) wraps 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.

This vendors an internal neutral copy at Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities), reproducing the VSTest behavior exactly:

  • On construction: capture the current value of the process environment variable __VANGUARD_SUSPEND_INSTRUMENT__, then set it to TRUE.
  • On dispose: restore the captured previous value (idempotent).

The environment-variable name and value are the collector IPC contract the dynamic code-coverage (Vanguard) engine reads, so they are preserved byte-identical. The wrapper 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 through its already-present using ...PlatformServices.Utilities;; the VSTest ObjectModel.Utilities using (and its deferral comment) is removed.

Fidelity proof and test-net limitation

The mechanism is a process environment variable (__VANGUARD_SUSPEND_INSTRUMENT__), not a named event / mutex / EventWaitHandle. There is no signal/listener model — the dynamic code-coverage (Vanguard) collector reads this variable from the process environment when deciding whether to instrument a module being loaded. The fidelity of the vendored copy therefore rests entirely on replicating that wire contract byte-identically against the OSS source (vstest v18.4.0 SuspendCodeCoverage.cs).

Source-diff (fidelity proof of record) — OSS original vs vendored copy:

Contract elementOSS (Microsoft.TestPlatform.ObjectModel)Vendored copy
env var name"__VANGUARD_SUSPEND_INSTRUMENT__""__VANGUARD_SUSPEND_INSTRUMENT__"
set value"TRUE""TRUE"
target (all 3 accesses)EnvironmentVariableTarget.ProcessEnvironmentVariableTarget.Process
ctorGetEnvironmentVariable(name, Process) → capture; SetEnvironmentVariable(name, "TRUE", Process)identical
disposeSetEnvironmentVariable(name, prev, Process), guarded by _isDisposedidentical

The only intentional difference is the collapse of the OSS Dispose() / protected virtual Dispose(bool) / GC.SuppressFinalize into a single idempotent Dispose() — behavior-equivalent because the OSS type declares no finalizer (so GC.SuppressFinalize is a no-op and disposing is always true on the public path). The name/value/target/sequence — the entire wire contract the collector keys off — are verbatim.

Note on the test net: PlatformServices.Desktop.IntegrationTests runs with no coverage collector attached, so its green result proves the vendored code does not crash / the deploy path still works — it does not exercise a real collector reading the variable. That is acceptable here precisely because there is no signaling to get wrong: correctness is fully determined by the (source-identical) variable name/value/target above. There is no clean seam to assert the variable mid-deploy without contorting the copy loop, so no brittle probe was added.

Result: PlatformServices is ObjectModel-type-free

After this change, PlatformServices has zero using/type references to the Microsoft.TestPlatform.ObjectModel package — only string-literal assembly names (used for by-name runtime assembly lookup in the AppDomain/source-host wiring) remain. This clears the way to drop the package reference in the capstone (Phase 7).

Verification

  • Builds 0-warning (net462 and all real TFMs; netfx-guarded change).
  • MSTestAdapter.PlatformServices.UnitTests: 935/935 (net462), 897/897 (net8.0).
  • PlatformServices.Desktop.IntegrationTests: 15/15 (net462) — exercises the deployment path that runs inside the SuspendCodeCoverage scope.
  • Expert-reviewer pass.

Stacking

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

…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>
Base automatically changed from dev/amauryleve/vstest-decoupling-sourcehandler 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
@Evangelink
Amaury Levé (Evangelink) merged commit c561276 into dev/amauryleve/vstest-decoupling-sourcehostJul 5, 2026
24 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-suspendcoverage branch July 5, 2026 19:24

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

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

Expert Review — PR #9632 · Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)

Summary

This PR accomplishes two related goals under the ObjectModel-decoupling initiative:

  1. Vendors SuspendCodeCoverage — a faithful internal copy that preserves the Vanguard IPC contract (__VANGUARD_SUSPEND_INSTRUMENT__ / "TRUE" / EnvironmentVariableTarget.Process) byte-identically, gated behind #if NETFRAMEWORK. The PR description's fidelity proof is thorough and accurate.

  2. Vendors DoesSourceReferenceAssembly — replaces the call to AssemblyHelper.DoesReferencesAssembly (vstest ObjectModel) with a local implementation. The original code carried a comment noting this in-AppDomain optimization was acceptable; the new implementation honors that pre-approval.

One minor code-quality issue is noted; no blocking or major findings.


Verdict Table

#DimensionStatusNotes
1Algorithmic Correctness⚠️ MinorArePublicKeyTokensEqual does not handle null from GetPublicKeyToken(); NullReferenceException is silently caught → conservative fallback. See inline comment.
2Threading & Concurrency✅ CleanSuspendCodeCoverage is used in a single-threaded deployment context; plain bool _isDisposed is sufficient. SetEnvironmentVariable concurrency matches original behavior.
3Security & IPC Contract Safety✅ CleanEnv-var name, value, and target are preserved verbatim. No path traversal or injection vectors introduced.
4Public API & Binary Compatibility✅ CleanBoth new types are internal sealed. No public API surface changed. PublicAPI.Unshipped.txt update not needed.
5Performance & Allocations✅ CleanNo LINQ, no unnecessary allocations. The ReflectionOnlyLoadFrom path is equivalent in cost to the original (same assembly load, minus the AppDomain round-trip).
6Cross-TFM Compatibility✅ CleanAll new code is guarded by #if NETFRAMEWORK. Other TFMs are unaffected.
7Error Handling & Resilience✅ CleanBare catch in DoesSourceReferenceAssembly is intentional and carries a comment; it mirrors the original vstest CheckAssemblyReference pattern. Conservative fallback (null → discover anyway) is correct.
8Resource Management✅ CleanSuspendCodeCoverage is IDisposable, idempotent, and used via using. No finalizer is needed (none exists in the original either; GC.SuppressFinalize was a no-op).
9Naming & Code Style✅ CleanAll names follow conventions. _isDisposed, _previousEnvironmentValue, constants are appropriately cased.
10Documentation & Comments✅ CleanBoth new files have XML doc comments. The fidelity comparison table in the PR description is exemplary.
11Test Coverage & Quality✅ CleanFour existing unit tests in DesktopTestSourceTests exercise the refactored IsAssemblyReferenced path, including the null-source/null-assembly, found, and not-found cases. PlatformServices.Desktop.IntegrationTests exercises the deploy path with SuspendCodeCoverage.
12Localization & Resource StringsN/ANo user-facing strings added.
13Logging & DiagnosticsN/ANo logging changes.
14Dependency & Package Hygiene✅ CleanThe Microsoft.TestPlatform.ObjectModel type-reference is eliminated as intended. No new packages introduced.
15Code Complexity & Maintainability✅ CleanBoth new methods are compact and well-commented. DoesSourceReferenceAssembly is ~35 LOC with clear phases.
16Test Infrastructure & Acceptance TestsN/ANo CLI options, output formats, or acceptance-test expectations were changed.
17Configuration & DefaultsN/ANo configuration changes.
18Scope Discipline✅ CleanThe TestSourceHandler.cs refactoring is logically part of the same ObjectModel-decoupling goal and is covered by the PR description, even if the title focuses on SuspendCodeCoverage.
19Build & Project File QualityN/ANo .csproj/.props changes needed; the new .cs file is picked up by the existing glob.
20Invariant/Contract Violations⚠️ Minorbyte[] parameters in ArePublicKeyTokensEqual should be byte[]?; GetPublicKeyToken() returns nullable and the call sites do not null-check. See inline comment.
21Behavioral Regression Risk✅ CleanAppDomain isolation is intentionally dropped (the original code carried a "we can optimize this" comment). ReflectionOnlyLoadFrom in the current AppDomain is safe: it does not execute code from the loaded assembly.
22PowerShell / Script QualityN/ANo scripts modified.

Key Finding

⚠️ Minor — ArePublicKeyTokensEqual null-handling gap (TestSourceHandler.cs line 142)

GetPublicKeyToken() returns byte[]?; both call sites pass the result directly to byte[] parameters. For unsigned assemblies (null token) this throws NullReferenceException, silently caught → null → conservative discovery. The behavior is correct by accident rather than by design. The original vstest CheckAssemblyReference had the same gap, so this is a clean-up opportunity only, not a regression. An inline suggestion is attached.


Positive Notes

  • The fidelity proof table (env-var name / value / target / ctor / dispose sequence) is an excellent record for future maintainers.
  • Collapsing Dispose(bool disposing) + GC.SuppressFinalize to a direct Dispose() is the correct simplification given the absence of a finalizer.
  • The AppDomain drop in DoesSourceReferenceAssembly is safe and was pre-authorized by the inline comment in the original code.

}
}

private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right)

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.

ArePublicKeyTokensEqual declares non-nullable byte[] parameters, but both callers pass AssemblyName.GetPublicKeyToken() which returns byte[]? (null for unsigned assemblies). When either token is null, left.Length on line 144 throws NullReferenceException, caught by the outer try/catch, which returns null → conservative path → discovery proceeds. Functionally safe, but the correctness depends on exception routing rather than explicit logic.

Suggested fix (nullable-aware):

privatestaticboolArePublicKeyTokensEqual(byte[]?left,byte[]?right){if(leftisnull&&rightisnull)returntrue;if(leftisnull||rightisnull)returnfalse;if(left.Length!=right.Length)returnfalse;for(inti=0;i<left.Length;++i){if(left[i]!=right[i])returnfalse;}returntrue;}

This makes "both sides unsigned → match by name" explicit and eliminates the implicit NullReferenceException. Also update referenceAssemblyPublicKeyToken (line 113) to byte[]? to satisfy the nullable annotation.

Not a regression: the original vstestCheckAssemblyReference had the same gap — byte[] publicKeyToken1 = referencedAssembly.GetPublicKeyToken() without null-guard. This is a clean-up opportunity only.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3) - #9632

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)#9632
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-sourcehostfrom
dev/amauryleve/vstest-decoupling-suspendcoverage

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4c3 — vendor a neutral SuspendCodeCoverage

Part of the initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on Microsoft.TestPlatform.ObjectModel. Strict byte-for-byte, no behavior change.

What this changes

TestDeployment (netfx) wraps 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.

This vendors an internal neutral copy at Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities), reproducing the VSTest behavior exactly:

  • On construction: capture the current value of the process environment variable __VANGUARD_SUSPEND_INSTRUMENT__, then set it to TRUE.
  • On dispose: restore the captured previous value (idempotent).

The environment-variable name and value are the collector IPC contract the dynamic code-coverage (Vanguard) engine reads, so they are preserved byte-identical. The wrapper 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 through its already-present using ...PlatformServices.Utilities;; the VSTest ObjectModel.Utilities using (and its deferral comment) is removed.

Fidelity proof and test-net limitation

The mechanism is a process environment variable (__VANGUARD_SUSPEND_INSTRUMENT__), not a named event / mutex / EventWaitHandle. There is no signal/listener model — the dynamic code-coverage (Vanguard) collector reads this variable from the process environment when deciding whether to instrument a module being loaded. The fidelity of the vendored copy therefore rests entirely on replicating that wire contract byte-identically against the OSS source (vstest v18.4.0 SuspendCodeCoverage.cs).

Source-diff (fidelity proof of record) — OSS original vs vendored copy:

Contract elementOSS (Microsoft.TestPlatform.ObjectModel)Vendored copy
env var name"__VANGUARD_SUSPEND_INSTRUMENT__""__VANGUARD_SUSPEND_INSTRUMENT__"
set value"TRUE""TRUE"
target (all 3 accesses)EnvironmentVariableTarget.ProcessEnvironmentVariableTarget.Process
ctorGetEnvironmentVariable(name, Process) → capture; SetEnvironmentVariable(name, "TRUE", Process)identical
disposeSetEnvironmentVariable(name, prev, Process), guarded by _isDisposedidentical

The only intentional difference is the collapse of the OSS Dispose() / protected virtual Dispose(bool) / GC.SuppressFinalize into a single idempotent Dispose() — behavior-equivalent because the OSS type declares no finalizer (so GC.SuppressFinalize is a no-op and disposing is always true on the public path). The name/value/target/sequence — the entire wire contract the collector keys off — are verbatim.

Note on the test net: PlatformServices.Desktop.IntegrationTests runs with no coverage collector attached, so its green result proves the vendored code does not crash / the deploy path still works — it does not exercise a real collector reading the variable. That is acceptable here precisely because there is no signaling to get wrong: correctness is fully determined by the (source-identical) variable name/value/target above. There is no clean seam to assert the variable mid-deploy without contorting the copy loop, so no brittle probe was added.

Result: PlatformServices is ObjectModel-type-free

After this change, PlatformServices has zero using/type references to the Microsoft.TestPlatform.ObjectModel package — only string-literal assembly names (used for by-name runtime assembly lookup in the AppDomain/source-host wiring) remain. This clears the way to drop the package reference in the capstone (Phase 7).

Verification

  • Builds 0-warning (net462 and all real TFMs; netfx-guarded change).
  • MSTestAdapter.PlatformServices.UnitTests: 935/935 (net462), 897/897 (net8.0).
  • PlatformServices.Desktop.IntegrationTests: 15/15 (net462) — exercises the deployment path that runs inside the SuspendCodeCoverage scope.
  • Expert-reviewer pass.

Stacking

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

…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>
Base automatically changed from dev/amauryleve/vstest-decoupling-sourcehandler 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
@Evangelink
Amaury Levé (Evangelink) merged commit c561276 into dev/amauryleve/vstest-decoupling-sourcehostJul 5, 2026
24 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-suspendcoverage branch July 5, 2026 19:24

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

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

Expert Review — PR #9632 · Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)

Summary

This PR accomplishes two related goals under the ObjectModel-decoupling initiative:

  1. Vendors SuspendCodeCoverage — a faithful internal copy that preserves the Vanguard IPC contract (__VANGUARD_SUSPEND_INSTRUMENT__ / "TRUE" / EnvironmentVariableTarget.Process) byte-identically, gated behind #if NETFRAMEWORK. The PR description's fidelity proof is thorough and accurate.

  2. Vendors DoesSourceReferenceAssembly — replaces the call to AssemblyHelper.DoesReferencesAssembly (vstest ObjectModel) with a local implementation. The original code carried a comment noting this in-AppDomain optimization was acceptable; the new implementation honors that pre-approval.

One minor code-quality issue is noted; no blocking or major findings.


Verdict Table

#DimensionStatusNotes
1Algorithmic Correctness⚠️ MinorArePublicKeyTokensEqual does not handle null from GetPublicKeyToken(); NullReferenceException is silently caught → conservative fallback. See inline comment.
2Threading & Concurrency✅ CleanSuspendCodeCoverage is used in a single-threaded deployment context; plain bool _isDisposed is sufficient. SetEnvironmentVariable concurrency matches original behavior.
3Security & IPC Contract Safety✅ CleanEnv-var name, value, and target are preserved verbatim. No path traversal or injection vectors introduced.
4Public API & Binary Compatibility✅ CleanBoth new types are internal sealed. No public API surface changed. PublicAPI.Unshipped.txt update not needed.
5Performance & Allocations✅ CleanNo LINQ, no unnecessary allocations. The ReflectionOnlyLoadFrom path is equivalent in cost to the original (same assembly load, minus the AppDomain round-trip).
6Cross-TFM Compatibility✅ CleanAll new code is guarded by #if NETFRAMEWORK. Other TFMs are unaffected.
7Error Handling & Resilience✅ CleanBare catch in DoesSourceReferenceAssembly is intentional and carries a comment; it mirrors the original vstest CheckAssemblyReference pattern. Conservative fallback (null → discover anyway) is correct.
8Resource Management✅ CleanSuspendCodeCoverage is IDisposable, idempotent, and used via using. No finalizer is needed (none exists in the original either; GC.SuppressFinalize was a no-op).
9Naming & Code Style✅ CleanAll names follow conventions. _isDisposed, _previousEnvironmentValue, constants are appropriately cased.
10Documentation & Comments✅ CleanBoth new files have XML doc comments. The fidelity comparison table in the PR description is exemplary.
11Test Coverage & Quality✅ CleanFour existing unit tests in DesktopTestSourceTests exercise the refactored IsAssemblyReferenced path, including the null-source/null-assembly, found, and not-found cases. PlatformServices.Desktop.IntegrationTests exercises the deploy path with SuspendCodeCoverage.
12Localization & Resource StringsN/ANo user-facing strings added.
13Logging & DiagnosticsN/ANo logging changes.
14Dependency & Package Hygiene✅ CleanThe Microsoft.TestPlatform.ObjectModel type-reference is eliminated as intended. No new packages introduced.
15Code Complexity & Maintainability✅ CleanBoth new methods are compact and well-commented. DoesSourceReferenceAssembly is ~35 LOC with clear phases.
16Test Infrastructure & Acceptance TestsN/ANo CLI options, output formats, or acceptance-test expectations were changed.
17Configuration & DefaultsN/ANo configuration changes.
18Scope Discipline✅ CleanThe TestSourceHandler.cs refactoring is logically part of the same ObjectModel-decoupling goal and is covered by the PR description, even if the title focuses on SuspendCodeCoverage.
19Build & Project File QualityN/ANo .csproj/.props changes needed; the new .cs file is picked up by the existing glob.
20Invariant/Contract Violations⚠️ Minorbyte[] parameters in ArePublicKeyTokensEqual should be byte[]?; GetPublicKeyToken() returns nullable and the call sites do not null-check. See inline comment.
21Behavioral Regression Risk✅ CleanAppDomain isolation is intentionally dropped (the original code carried a "we can optimize this" comment). ReflectionOnlyLoadFrom in the current AppDomain is safe: it does not execute code from the loaded assembly.
22PowerShell / Script QualityN/ANo scripts modified.

Key Finding

⚠️ Minor — ArePublicKeyTokensEqual null-handling gap (TestSourceHandler.cs line 142)

GetPublicKeyToken() returns byte[]?; both call sites pass the result directly to byte[] parameters. For unsigned assemblies (null token) this throws NullReferenceException, silently caught → null → conservative discovery. The behavior is correct by accident rather than by design. The original vstest CheckAssemblyReference had the same gap, so this is a clean-up opportunity only, not a regression. An inline suggestion is attached.


Positive Notes

  • The fidelity proof table (env-var name / value / target / ctor / dispose sequence) is an excellent record for future maintainers.
  • Collapsing Dispose(bool disposing) + GC.SuppressFinalize to a direct Dispose() is the correct simplification given the absence of a finalizer.
  • The AppDomain drop in DoesSourceReferenceAssembly is safe and was pre-authorized by the inline comment in the original code.

}
}

private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right)

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.

ArePublicKeyTokensEqual declares non-nullable byte[] parameters, but both callers pass AssemblyName.GetPublicKeyToken() which returns byte[]? (null for unsigned assemblies). When either token is null, left.Length on line 144 throws NullReferenceException, caught by the outer try/catch, which returns null → conservative path → discovery proceeds. Functionally safe, but the correctness depends on exception routing rather than explicit logic.

Suggested fix (nullable-aware):

privatestaticboolArePublicKeyTokensEqual(byte[]?left,byte[]?right){if(leftisnull&&rightisnull)returntrue;if(leftisnull||rightisnull)returnfalse;if(left.Length!=right.Length)returnfalse;for(inti=0;i<left.Length;++i){if(left[i]!=right[i])returnfalse;}returntrue;}

This makes "both sides unsigned → match by name" explicit and eliminates the implicit NullReferenceException. Also update referenceAssemblyPublicKeyToken (line 113) to byte[]? to satisfy the nullable annotation.

Not a regression: the original vstestCheckAssemblyReference had the same gap — byte[] publicKeyToken1 = referencedAssembly.GetPublicKeyToken() without null-guard. This is a clean-up opportunity only.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3) - #9632

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)#9632
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-sourcehostfrom
dev/amauryleve/vstest-decoupling-suspendcoverage

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4c3 — vendor a neutral SuspendCodeCoverage

Part of the initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on Microsoft.TestPlatform.ObjectModel. Strict byte-for-byte, no behavior change.

What this changes

TestDeployment (netfx) wraps 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.

This vendors an internal neutral copy at Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities), reproducing the VSTest behavior exactly:

  • On construction: capture the current value of the process environment variable __VANGUARD_SUSPEND_INSTRUMENT__, then set it to TRUE.
  • On dispose: restore the captured previous value (idempotent).

The environment-variable name and value are the collector IPC contract the dynamic code-coverage (Vanguard) engine reads, so they are preserved byte-identical. The wrapper 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 through its already-present using ...PlatformServices.Utilities;; the VSTest ObjectModel.Utilities using (and its deferral comment) is removed.

Fidelity proof and test-net limitation

The mechanism is a process environment variable (__VANGUARD_SUSPEND_INSTRUMENT__), not a named event / mutex / EventWaitHandle. There is no signal/listener model — the dynamic code-coverage (Vanguard) collector reads this variable from the process environment when deciding whether to instrument a module being loaded. The fidelity of the vendored copy therefore rests entirely on replicating that wire contract byte-identically against the OSS source (vstest v18.4.0 SuspendCodeCoverage.cs).

Source-diff (fidelity proof of record) — OSS original vs vendored copy:

Contract elementOSS (Microsoft.TestPlatform.ObjectModel)Vendored copy
env var name"__VANGUARD_SUSPEND_INSTRUMENT__""__VANGUARD_SUSPEND_INSTRUMENT__"
set value"TRUE""TRUE"
target (all 3 accesses)EnvironmentVariableTarget.ProcessEnvironmentVariableTarget.Process
ctorGetEnvironmentVariable(name, Process) → capture; SetEnvironmentVariable(name, "TRUE", Process)identical
disposeSetEnvironmentVariable(name, prev, Process), guarded by _isDisposedidentical

The only intentional difference is the collapse of the OSS Dispose() / protected virtual Dispose(bool) / GC.SuppressFinalize into a single idempotent Dispose() — behavior-equivalent because the OSS type declares no finalizer (so GC.SuppressFinalize is a no-op and disposing is always true on the public path). The name/value/target/sequence — the entire wire contract the collector keys off — are verbatim.

Note on the test net: PlatformServices.Desktop.IntegrationTests runs with no coverage collector attached, so its green result proves the vendored code does not crash / the deploy path still works — it does not exercise a real collector reading the variable. That is acceptable here precisely because there is no signaling to get wrong: correctness is fully determined by the (source-identical) variable name/value/target above. There is no clean seam to assert the variable mid-deploy without contorting the copy loop, so no brittle probe was added.

Result: PlatformServices is ObjectModel-type-free

After this change, PlatformServices has zero using/type references to the Microsoft.TestPlatform.ObjectModel package — only string-literal assembly names (used for by-name runtime assembly lookup in the AppDomain/source-host wiring) remain. This clears the way to drop the package reference in the capstone (Phase 7).

Verification

  • Builds 0-warning (net462 and all real TFMs; netfx-guarded change).
  • MSTestAdapter.PlatformServices.UnitTests: 935/935 (net462), 897/897 (net8.0).
  • PlatformServices.Desktop.IntegrationTests: 15/15 (net462) — exercises the deployment path that runs inside the SuspendCodeCoverage scope.
  • Expert-reviewer pass.

Stacking

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

…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>
Base automatically changed from dev/amauryleve/vstest-decoupling-sourcehandler 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
@Evangelink
Amaury Levé (Evangelink) merged commit c561276 into dev/amauryleve/vstest-decoupling-sourcehostJul 5, 2026
24 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-suspendcoverage branch July 5, 2026 19:24

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

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

Expert Review — PR #9632 · Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)

Summary

This PR accomplishes two related goals under the ObjectModel-decoupling initiative:

  1. Vendors SuspendCodeCoverage — a faithful internal copy that preserves the Vanguard IPC contract (__VANGUARD_SUSPEND_INSTRUMENT__ / "TRUE" / EnvironmentVariableTarget.Process) byte-identically, gated behind #if NETFRAMEWORK. The PR description's fidelity proof is thorough and accurate.

  2. Vendors DoesSourceReferenceAssembly — replaces the call to AssemblyHelper.DoesReferencesAssembly (vstest ObjectModel) with a local implementation. The original code carried a comment noting this in-AppDomain optimization was acceptable; the new implementation honors that pre-approval.

One minor code-quality issue is noted; no blocking or major findings.


Verdict Table

#DimensionStatusNotes
1Algorithmic Correctness⚠️ MinorArePublicKeyTokensEqual does not handle null from GetPublicKeyToken(); NullReferenceException is silently caught → conservative fallback. See inline comment.
2Threading & Concurrency✅ CleanSuspendCodeCoverage is used in a single-threaded deployment context; plain bool _isDisposed is sufficient. SetEnvironmentVariable concurrency matches original behavior.
3Security & IPC Contract Safety✅ CleanEnv-var name, value, and target are preserved verbatim. No path traversal or injection vectors introduced.
4Public API & Binary Compatibility✅ CleanBoth new types are internal sealed. No public API surface changed. PublicAPI.Unshipped.txt update not needed.
5Performance & Allocations✅ CleanNo LINQ, no unnecessary allocations. The ReflectionOnlyLoadFrom path is equivalent in cost to the original (same assembly load, minus the AppDomain round-trip).
6Cross-TFM Compatibility✅ CleanAll new code is guarded by #if NETFRAMEWORK. Other TFMs are unaffected.
7Error Handling & Resilience✅ CleanBare catch in DoesSourceReferenceAssembly is intentional and carries a comment; it mirrors the original vstest CheckAssemblyReference pattern. Conservative fallback (null → discover anyway) is correct.
8Resource Management✅ CleanSuspendCodeCoverage is IDisposable, idempotent, and used via using. No finalizer is needed (none exists in the original either; GC.SuppressFinalize was a no-op).
9Naming & Code Style✅ CleanAll names follow conventions. _isDisposed, _previousEnvironmentValue, constants are appropriately cased.
10Documentation & Comments✅ CleanBoth new files have XML doc comments. The fidelity comparison table in the PR description is exemplary.
11Test Coverage & Quality✅ CleanFour existing unit tests in DesktopTestSourceTests exercise the refactored IsAssemblyReferenced path, including the null-source/null-assembly, found, and not-found cases. PlatformServices.Desktop.IntegrationTests exercises the deploy path with SuspendCodeCoverage.
12Localization & Resource StringsN/ANo user-facing strings added.
13Logging & DiagnosticsN/ANo logging changes.
14Dependency & Package Hygiene✅ CleanThe Microsoft.TestPlatform.ObjectModel type-reference is eliminated as intended. No new packages introduced.
15Code Complexity & Maintainability✅ CleanBoth new methods are compact and well-commented. DoesSourceReferenceAssembly is ~35 LOC with clear phases.
16Test Infrastructure & Acceptance TestsN/ANo CLI options, output formats, or acceptance-test expectations were changed.
17Configuration & DefaultsN/ANo configuration changes.
18Scope Discipline✅ CleanThe TestSourceHandler.cs refactoring is logically part of the same ObjectModel-decoupling goal and is covered by the PR description, even if the title focuses on SuspendCodeCoverage.
19Build & Project File QualityN/ANo .csproj/.props changes needed; the new .cs file is picked up by the existing glob.
20Invariant/Contract Violations⚠️ Minorbyte[] parameters in ArePublicKeyTokensEqual should be byte[]?; GetPublicKeyToken() returns nullable and the call sites do not null-check. See inline comment.
21Behavioral Regression Risk✅ CleanAppDomain isolation is intentionally dropped (the original code carried a "we can optimize this" comment). ReflectionOnlyLoadFrom in the current AppDomain is safe: it does not execute code from the loaded assembly.
22PowerShell / Script QualityN/ANo scripts modified.

Key Finding

⚠️ Minor — ArePublicKeyTokensEqual null-handling gap (TestSourceHandler.cs line 142)

GetPublicKeyToken() returns byte[]?; both call sites pass the result directly to byte[] parameters. For unsigned assemblies (null token) this throws NullReferenceException, silently caught → null → conservative discovery. The behavior is correct by accident rather than by design. The original vstest CheckAssemblyReference had the same gap, so this is a clean-up opportunity only, not a regression. An inline suggestion is attached.


Positive Notes

  • The fidelity proof table (env-var name / value / target / ctor / dispose sequence) is an excellent record for future maintainers.
  • Collapsing Dispose(bool disposing) + GC.SuppressFinalize to a direct Dispose() is the correct simplification given the absence of a finalizer.
  • The AppDomain drop in DoesSourceReferenceAssembly is safe and was pre-authorized by the inline comment in the original code.

}
}

private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right)

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.

ArePublicKeyTokensEqual declares non-nullable byte[] parameters, but both callers pass AssemblyName.GetPublicKeyToken() which returns byte[]? (null for unsigned assemblies). When either token is null, left.Length on line 144 throws NullReferenceException, caught by the outer try/catch, which returns null → conservative path → discovery proceeds. Functionally safe, but the correctness depends on exception routing rather than explicit logic.

Suggested fix (nullable-aware):

privatestaticboolArePublicKeyTokensEqual(byte[]?left,byte[]?right){if(leftisnull&&rightisnull)returntrue;if(leftisnull||rightisnull)returnfalse;if(left.Length!=right.Length)returnfalse;for(inti=0;i<left.Length;++i){if(left[i]!=right[i])returnfalse;}returntrue;}

This makes "both sides unsigned → match by name" explicit and eliminates the implicit NullReferenceException. Also update referenceAssemblyPublicKeyToken (line 113) to byte[]? to satisfy the nullable annotation.

Not a regression: the original vstestCheckAssemblyReference had the same gap — byte[] publicKeyToken1 = referencedAssembly.GetPublicKeyToken() without null-guard. This is a clean-up opportunity only.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3) - #9632

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)#9632
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-sourcehostfrom
dev/amauryleve/vstest-decoupling-suspendcoverage

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4c3 — vendor a neutral SuspendCodeCoverage

Part of the initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on Microsoft.TestPlatform.ObjectModel. Strict byte-for-byte, no behavior change.

What this changes

TestDeployment (netfx) wraps 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.

This vendors an internal neutral copy at Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities), reproducing the VSTest behavior exactly:

  • On construction: capture the current value of the process environment variable __VANGUARD_SUSPEND_INSTRUMENT__, then set it to TRUE.
  • On dispose: restore the captured previous value (idempotent).

The environment-variable name and value are the collector IPC contract the dynamic code-coverage (Vanguard) engine reads, so they are preserved byte-identical. The wrapper 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 through its already-present using ...PlatformServices.Utilities;; the VSTest ObjectModel.Utilities using (and its deferral comment) is removed.

Fidelity proof and test-net limitation

The mechanism is a process environment variable (__VANGUARD_SUSPEND_INSTRUMENT__), not a named event / mutex / EventWaitHandle. There is no signal/listener model — the dynamic code-coverage (Vanguard) collector reads this variable from the process environment when deciding whether to instrument a module being loaded. The fidelity of the vendored copy therefore rests entirely on replicating that wire contract byte-identically against the OSS source (vstest v18.4.0 SuspendCodeCoverage.cs).

Source-diff (fidelity proof of record) — OSS original vs vendored copy:

Contract elementOSS (Microsoft.TestPlatform.ObjectModel)Vendored copy
env var name"__VANGUARD_SUSPEND_INSTRUMENT__""__VANGUARD_SUSPEND_INSTRUMENT__"
set value"TRUE""TRUE"
target (all 3 accesses)EnvironmentVariableTarget.ProcessEnvironmentVariableTarget.Process
ctorGetEnvironmentVariable(name, Process) → capture; SetEnvironmentVariable(name, "TRUE", Process)identical
disposeSetEnvironmentVariable(name, prev, Process), guarded by _isDisposedidentical

The only intentional difference is the collapse of the OSS Dispose() / protected virtual Dispose(bool) / GC.SuppressFinalize into a single idempotent Dispose() — behavior-equivalent because the OSS type declares no finalizer (so GC.SuppressFinalize is a no-op and disposing is always true on the public path). The name/value/target/sequence — the entire wire contract the collector keys off — are verbatim.

Note on the test net: PlatformServices.Desktop.IntegrationTests runs with no coverage collector attached, so its green result proves the vendored code does not crash / the deploy path still works — it does not exercise a real collector reading the variable. That is acceptable here precisely because there is no signaling to get wrong: correctness is fully determined by the (source-identical) variable name/value/target above. There is no clean seam to assert the variable mid-deploy without contorting the copy loop, so no brittle probe was added.

Result: PlatformServices is ObjectModel-type-free

After this change, PlatformServices has zero using/type references to the Microsoft.TestPlatform.ObjectModel package — only string-literal assembly names (used for by-name runtime assembly lookup in the AppDomain/source-host wiring) remain. This clears the way to drop the package reference in the capstone (Phase 7).

Verification

  • Builds 0-warning (net462 and all real TFMs; netfx-guarded change).
  • MSTestAdapter.PlatformServices.UnitTests: 935/935 (net462), 897/897 (net8.0).
  • PlatformServices.Desktop.IntegrationTests: 15/15 (net462) — exercises the deployment path that runs inside the SuspendCodeCoverage scope.
  • Expert-reviewer pass.

Stacking

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

…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>
Base automatically changed from dev/amauryleve/vstest-decoupling-sourcehandler 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
@Evangelink
Amaury Levé (Evangelink) merged commit c561276 into dev/amauryleve/vstest-decoupling-sourcehostJul 5, 2026
24 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-suspendcoverage branch July 5, 2026 19:24

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

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

Expert Review — PR #9632 · Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)

Summary

This PR accomplishes two related goals under the ObjectModel-decoupling initiative:

  1. Vendors SuspendCodeCoverage — a faithful internal copy that preserves the Vanguard IPC contract (__VANGUARD_SUSPEND_INSTRUMENT__ / "TRUE" / EnvironmentVariableTarget.Process) byte-identically, gated behind #if NETFRAMEWORK. The PR description's fidelity proof is thorough and accurate.

  2. Vendors DoesSourceReferenceAssembly — replaces the call to AssemblyHelper.DoesReferencesAssembly (vstest ObjectModel) with a local implementation. The original code carried a comment noting this in-AppDomain optimization was acceptable; the new implementation honors that pre-approval.

One minor code-quality issue is noted; no blocking or major findings.


Verdict Table

#DimensionStatusNotes
1Algorithmic Correctness⚠️ MinorArePublicKeyTokensEqual does not handle null from GetPublicKeyToken(); NullReferenceException is silently caught → conservative fallback. See inline comment.
2Threading & Concurrency✅ CleanSuspendCodeCoverage is used in a single-threaded deployment context; plain bool _isDisposed is sufficient. SetEnvironmentVariable concurrency matches original behavior.
3Security & IPC Contract Safety✅ CleanEnv-var name, value, and target are preserved verbatim. No path traversal or injection vectors introduced.
4Public API & Binary Compatibility✅ CleanBoth new types are internal sealed. No public API surface changed. PublicAPI.Unshipped.txt update not needed.
5Performance & Allocations✅ CleanNo LINQ, no unnecessary allocations. The ReflectionOnlyLoadFrom path is equivalent in cost to the original (same assembly load, minus the AppDomain round-trip).
6Cross-TFM Compatibility✅ CleanAll new code is guarded by #if NETFRAMEWORK. Other TFMs are unaffected.
7Error Handling & Resilience✅ CleanBare catch in DoesSourceReferenceAssembly is intentional and carries a comment; it mirrors the original vstest CheckAssemblyReference pattern. Conservative fallback (null → discover anyway) is correct.
8Resource Management✅ CleanSuspendCodeCoverage is IDisposable, idempotent, and used via using. No finalizer is needed (none exists in the original either; GC.SuppressFinalize was a no-op).
9Naming & Code Style✅ CleanAll names follow conventions. _isDisposed, _previousEnvironmentValue, constants are appropriately cased.
10Documentation & Comments✅ CleanBoth new files have XML doc comments. The fidelity comparison table in the PR description is exemplary.
11Test Coverage & Quality✅ CleanFour existing unit tests in DesktopTestSourceTests exercise the refactored IsAssemblyReferenced path, including the null-source/null-assembly, found, and not-found cases. PlatformServices.Desktop.IntegrationTests exercises the deploy path with SuspendCodeCoverage.
12Localization & Resource StringsN/ANo user-facing strings added.
13Logging & DiagnosticsN/ANo logging changes.
14Dependency & Package Hygiene✅ CleanThe Microsoft.TestPlatform.ObjectModel type-reference is eliminated as intended. No new packages introduced.
15Code Complexity & Maintainability✅ CleanBoth new methods are compact and well-commented. DoesSourceReferenceAssembly is ~35 LOC with clear phases.
16Test Infrastructure & Acceptance TestsN/ANo CLI options, output formats, or acceptance-test expectations were changed.
17Configuration & DefaultsN/ANo configuration changes.
18Scope Discipline✅ CleanThe TestSourceHandler.cs refactoring is logically part of the same ObjectModel-decoupling goal and is covered by the PR description, even if the title focuses on SuspendCodeCoverage.
19Build & Project File QualityN/ANo .csproj/.props changes needed; the new .cs file is picked up by the existing glob.
20Invariant/Contract Violations⚠️ Minorbyte[] parameters in ArePublicKeyTokensEqual should be byte[]?; GetPublicKeyToken() returns nullable and the call sites do not null-check. See inline comment.
21Behavioral Regression Risk✅ CleanAppDomain isolation is intentionally dropped (the original code carried a "we can optimize this" comment). ReflectionOnlyLoadFrom in the current AppDomain is safe: it does not execute code from the loaded assembly.
22PowerShell / Script QualityN/ANo scripts modified.

Key Finding

⚠️ Minor — ArePublicKeyTokensEqual null-handling gap (TestSourceHandler.cs line 142)

GetPublicKeyToken() returns byte[]?; both call sites pass the result directly to byte[] parameters. For unsigned assemblies (null token) this throws NullReferenceException, silently caught → null → conservative discovery. The behavior is correct by accident rather than by design. The original vstest CheckAssemblyReference had the same gap, so this is a clean-up opportunity only, not a regression. An inline suggestion is attached.


Positive Notes

  • The fidelity proof table (env-var name / value / target / ctor / dispose sequence) is an excellent record for future maintainers.
  • Collapsing Dispose(bool disposing) + GC.SuppressFinalize to a direct Dispose() is the correct simplification given the absence of a finalizer.
  • The AppDomain drop in DoesSourceReferenceAssembly is safe and was pre-authorized by the inline comment in the original code.

}
}

private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right)

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.

ArePublicKeyTokensEqual declares non-nullable byte[] parameters, but both callers pass AssemblyName.GetPublicKeyToken() which returns byte[]? (null for unsigned assemblies). When either token is null, left.Length on line 144 throws NullReferenceException, caught by the outer try/catch, which returns null → conservative path → discovery proceeds. Functionally safe, but the correctness depends on exception routing rather than explicit logic.

Suggested fix (nullable-aware):

privatestaticboolArePublicKeyTokensEqual(byte[]?left,byte[]?right){if(leftisnull&&rightisnull)returntrue;if(leftisnull||rightisnull)returnfalse;if(left.Length!=right.Length)returnfalse;for(inti=0;i<left.Length;++i){if(left[i]!=right[i])returnfalse;}returntrue;}

This makes "both sides unsigned → match by name" explicit and eliminates the implicit NullReferenceException. Also update referenceAssemblyPublicKeyToken (line 113) to byte[]? to satisfy the nullable annotation.

Not a regression: the original vstestCheckAssemblyReference had the same gap — byte[] publicKeyToken1 = referencedAssembly.GetPublicKeyToken() without null-guard. This is a clean-up opportunity only.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3) - #9632

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)#9632
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-sourcehostfrom
dev/amauryleve/vstest-decoupling-suspendcoverage

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4c3 — vendor a neutral SuspendCodeCoverage

Part of the initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on Microsoft.TestPlatform.ObjectModel. Strict byte-for-byte, no behavior change.

What this changes

TestDeployment (netfx) wraps 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.

This vendors an internal neutral copy at Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities), reproducing the VSTest behavior exactly:

  • On construction: capture the current value of the process environment variable __VANGUARD_SUSPEND_INSTRUMENT__, then set it to TRUE.
  • On dispose: restore the captured previous value (idempotent).

The environment-variable name and value are the collector IPC contract the dynamic code-coverage (Vanguard) engine reads, so they are preserved byte-identical. The wrapper 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 through its already-present using ...PlatformServices.Utilities;; the VSTest ObjectModel.Utilities using (and its deferral comment) is removed.

Fidelity proof and test-net limitation

The mechanism is a process environment variable (__VANGUARD_SUSPEND_INSTRUMENT__), not a named event / mutex / EventWaitHandle. There is no signal/listener model — the dynamic code-coverage (Vanguard) collector reads this variable from the process environment when deciding whether to instrument a module being loaded. The fidelity of the vendored copy therefore rests entirely on replicating that wire contract byte-identically against the OSS source (vstest v18.4.0 SuspendCodeCoverage.cs).

Source-diff (fidelity proof of record) — OSS original vs vendored copy:

Contract elementOSS (Microsoft.TestPlatform.ObjectModel)Vendored copy
env var name"__VANGUARD_SUSPEND_INSTRUMENT__""__VANGUARD_SUSPEND_INSTRUMENT__"
set value"TRUE""TRUE"
target (all 3 accesses)EnvironmentVariableTarget.ProcessEnvironmentVariableTarget.Process
ctorGetEnvironmentVariable(name, Process) → capture; SetEnvironmentVariable(name, "TRUE", Process)identical
disposeSetEnvironmentVariable(name, prev, Process), guarded by _isDisposedidentical

The only intentional difference is the collapse of the OSS Dispose() / protected virtual Dispose(bool) / GC.SuppressFinalize into a single idempotent Dispose() — behavior-equivalent because the OSS type declares no finalizer (so GC.SuppressFinalize is a no-op and disposing is always true on the public path). The name/value/target/sequence — the entire wire contract the collector keys off — are verbatim.

Note on the test net: PlatformServices.Desktop.IntegrationTests runs with no coverage collector attached, so its green result proves the vendored code does not crash / the deploy path still works — it does not exercise a real collector reading the variable. That is acceptable here precisely because there is no signaling to get wrong: correctness is fully determined by the (source-identical) variable name/value/target above. There is no clean seam to assert the variable mid-deploy without contorting the copy loop, so no brittle probe was added.

Result: PlatformServices is ObjectModel-type-free

After this change, PlatformServices has zero using/type references to the Microsoft.TestPlatform.ObjectModel package — only string-literal assembly names (used for by-name runtime assembly lookup in the AppDomain/source-host wiring) remain. This clears the way to drop the package reference in the capstone (Phase 7).

Verification

  • Builds 0-warning (net462 and all real TFMs; netfx-guarded change).
  • MSTestAdapter.PlatformServices.UnitTests: 935/935 (net462), 897/897 (net8.0).
  • PlatformServices.Desktop.IntegrationTests: 15/15 (net462) — exercises the deployment path that runs inside the SuspendCodeCoverage scope.
  • Expert-reviewer pass.

Stacking

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

…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>
Base automatically changed from dev/amauryleve/vstest-decoupling-sourcehandler 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
@Evangelink
Amaury Levé (Evangelink) merged commit c561276 into dev/amauryleve/vstest-decoupling-sourcehostJul 5, 2026
24 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-suspendcoverage branch July 5, 2026 19:24

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

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

Expert Review — PR #9632 · Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)

Summary

This PR accomplishes two related goals under the ObjectModel-decoupling initiative:

  1. Vendors SuspendCodeCoverage — a faithful internal copy that preserves the Vanguard IPC contract (__VANGUARD_SUSPEND_INSTRUMENT__ / "TRUE" / EnvironmentVariableTarget.Process) byte-identically, gated behind #if NETFRAMEWORK. The PR description's fidelity proof is thorough and accurate.

  2. Vendors DoesSourceReferenceAssembly — replaces the call to AssemblyHelper.DoesReferencesAssembly (vstest ObjectModel) with a local implementation. The original code carried a comment noting this in-AppDomain optimization was acceptable; the new implementation honors that pre-approval.

One minor code-quality issue is noted; no blocking or major findings.


Verdict Table

#DimensionStatusNotes
1Algorithmic Correctness⚠️ MinorArePublicKeyTokensEqual does not handle null from GetPublicKeyToken(); NullReferenceException is silently caught → conservative fallback. See inline comment.
2Threading & Concurrency✅ CleanSuspendCodeCoverage is used in a single-threaded deployment context; plain bool _isDisposed is sufficient. SetEnvironmentVariable concurrency matches original behavior.
3Security & IPC Contract Safety✅ CleanEnv-var name, value, and target are preserved verbatim. No path traversal or injection vectors introduced.
4Public API & Binary Compatibility✅ CleanBoth new types are internal sealed. No public API surface changed. PublicAPI.Unshipped.txt update not needed.
5Performance & Allocations✅ CleanNo LINQ, no unnecessary allocations. The ReflectionOnlyLoadFrom path is equivalent in cost to the original (same assembly load, minus the AppDomain round-trip).
6Cross-TFM Compatibility✅ CleanAll new code is guarded by #if NETFRAMEWORK. Other TFMs are unaffected.
7Error Handling & Resilience✅ CleanBare catch in DoesSourceReferenceAssembly is intentional and carries a comment; it mirrors the original vstest CheckAssemblyReference pattern. Conservative fallback (null → discover anyway) is correct.
8Resource Management✅ CleanSuspendCodeCoverage is IDisposable, idempotent, and used via using. No finalizer is needed (none exists in the original either; GC.SuppressFinalize was a no-op).
9Naming & Code Style✅ CleanAll names follow conventions. _isDisposed, _previousEnvironmentValue, constants are appropriately cased.
10Documentation & Comments✅ CleanBoth new files have XML doc comments. The fidelity comparison table in the PR description is exemplary.
11Test Coverage & Quality✅ CleanFour existing unit tests in DesktopTestSourceTests exercise the refactored IsAssemblyReferenced path, including the null-source/null-assembly, found, and not-found cases. PlatformServices.Desktop.IntegrationTests exercises the deploy path with SuspendCodeCoverage.
12Localization & Resource StringsN/ANo user-facing strings added.
13Logging & DiagnosticsN/ANo logging changes.
14Dependency & Package Hygiene✅ CleanThe Microsoft.TestPlatform.ObjectModel type-reference is eliminated as intended. No new packages introduced.
15Code Complexity & Maintainability✅ CleanBoth new methods are compact and well-commented. DoesSourceReferenceAssembly is ~35 LOC with clear phases.
16Test Infrastructure & Acceptance TestsN/ANo CLI options, output formats, or acceptance-test expectations were changed.
17Configuration & DefaultsN/ANo configuration changes.
18Scope Discipline✅ CleanThe TestSourceHandler.cs refactoring is logically part of the same ObjectModel-decoupling goal and is covered by the PR description, even if the title focuses on SuspendCodeCoverage.
19Build & Project File QualityN/ANo .csproj/.props changes needed; the new .cs file is picked up by the existing glob.
20Invariant/Contract Violations⚠️ Minorbyte[] parameters in ArePublicKeyTokensEqual should be byte[]?; GetPublicKeyToken() returns nullable and the call sites do not null-check. See inline comment.
21Behavioral Regression Risk✅ CleanAppDomain isolation is intentionally dropped (the original code carried a "we can optimize this" comment). ReflectionOnlyLoadFrom in the current AppDomain is safe: it does not execute code from the loaded assembly.
22PowerShell / Script QualityN/ANo scripts modified.

Key Finding

⚠️ Minor — ArePublicKeyTokensEqual null-handling gap (TestSourceHandler.cs line 142)

GetPublicKeyToken() returns byte[]?; both call sites pass the result directly to byte[] parameters. For unsigned assemblies (null token) this throws NullReferenceException, silently caught → null → conservative discovery. The behavior is correct by accident rather than by design. The original vstest CheckAssemblyReference had the same gap, so this is a clean-up opportunity only, not a regression. An inline suggestion is attached.


Positive Notes

  • The fidelity proof table (env-var name / value / target / ctor / dispose sequence) is an excellent record for future maintainers.
  • Collapsing Dispose(bool disposing) + GC.SuppressFinalize to a direct Dispose() is the correct simplification given the absence of a finalizer.
  • The AppDomain drop in DoesSourceReferenceAssembly is safe and was pre-authorized by the inline comment in the original code.

}
}

private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right)

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.

ArePublicKeyTokensEqual declares non-nullable byte[] parameters, but both callers pass AssemblyName.GetPublicKeyToken() which returns byte[]? (null for unsigned assemblies). When either token is null, left.Length on line 144 throws NullReferenceException, caught by the outer try/catch, which returns null → conservative path → discovery proceeds. Functionally safe, but the correctness depends on exception routing rather than explicit logic.

Suggested fix (nullable-aware):

privatestaticboolArePublicKeyTokensEqual(byte[]?left,byte[]?right){if(leftisnull&&rightisnull)returntrue;if(leftisnull||rightisnull)returnfalse;if(left.Length!=right.Length)returnfalse;for(inti=0;i<left.Length;++i){if(left[i]!=right[i])returnfalse;}returntrue;}

This makes "both sides unsigned → match by name" explicit and eliminates the implicit NullReferenceException. Also update referenceAssemblyPublicKeyToken (line 113) to byte[]? to satisfy the nullable annotation.

Not a regression: the original vstestCheckAssemblyReference had the same gap — byte[] publicKeyToken1 = referencedAssembly.GetPublicKeyToken() without null-guard. This is a clean-up opportunity only.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3) - #9632

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

Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)#9632
Amaury Levé (Evangelink) merged 2 commits into
dev/amauryleve/vstest-decoupling-sourcehostfrom
dev/amauryleve/vstest-decoupling-suspendcoverage

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Phase 6e-4c3 — vendor a neutral SuspendCodeCoverage

Part of the initiative to make Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices platform-agnostic by removing its dependency on Microsoft.TestPlatform.ObjectModel. Strict byte-for-byte, no behavior change.

What this changes

TestDeployment (netfx) wraps 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.

This vendors an internal neutral copy at Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities), reproducing the VSTest behavior exactly:

  • On construction: capture the current value of the process environment variable __VANGUARD_SUSPEND_INSTRUMENT__, then set it to TRUE.
  • On dispose: restore the captured previous value (idempotent).

The environment-variable name and value are the collector IPC contract the dynamic code-coverage (Vanguard) engine reads, so they are preserved byte-identical. The wrapper 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 through its already-present using ...PlatformServices.Utilities;; the VSTest ObjectModel.Utilities using (and its deferral comment) is removed.

Fidelity proof and test-net limitation

The mechanism is a process environment variable (__VANGUARD_SUSPEND_INSTRUMENT__), not a named event / mutex / EventWaitHandle. There is no signal/listener model — the dynamic code-coverage (Vanguard) collector reads this variable from the process environment when deciding whether to instrument a module being loaded. The fidelity of the vendored copy therefore rests entirely on replicating that wire contract byte-identically against the OSS source (vstest v18.4.0 SuspendCodeCoverage.cs).

Source-diff (fidelity proof of record) — OSS original vs vendored copy:

Contract elementOSS (Microsoft.TestPlatform.ObjectModel)Vendored copy
env var name"__VANGUARD_SUSPEND_INSTRUMENT__""__VANGUARD_SUSPEND_INSTRUMENT__"
set value"TRUE""TRUE"
target (all 3 accesses)EnvironmentVariableTarget.ProcessEnvironmentVariableTarget.Process
ctorGetEnvironmentVariable(name, Process) → capture; SetEnvironmentVariable(name, "TRUE", Process)identical
disposeSetEnvironmentVariable(name, prev, Process), guarded by _isDisposedidentical

The only intentional difference is the collapse of the OSS Dispose() / protected virtual Dispose(bool) / GC.SuppressFinalize into a single idempotent Dispose() — behavior-equivalent because the OSS type declares no finalizer (so GC.SuppressFinalize is a no-op and disposing is always true on the public path). The name/value/target/sequence — the entire wire contract the collector keys off — are verbatim.

Note on the test net: PlatformServices.Desktop.IntegrationTests runs with no coverage collector attached, so its green result proves the vendored code does not crash / the deploy path still works — it does not exercise a real collector reading the variable. That is acceptable here precisely because there is no signaling to get wrong: correctness is fully determined by the (source-identical) variable name/value/target above. There is no clean seam to assert the variable mid-deploy without contorting the copy loop, so no brittle probe was added.

Result: PlatformServices is ObjectModel-type-free

After this change, PlatformServices has zero using/type references to the Microsoft.TestPlatform.ObjectModel package — only string-literal assembly names (used for by-name runtime assembly lookup in the AppDomain/source-host wiring) remain. This clears the way to drop the package reference in the capstone (Phase 7).

Verification

  • Builds 0-warning (net462 and all real TFMs; netfx-guarded change).
  • MSTestAdapter.PlatformServices.UnitTests: 935/935 (net462), 897/897 (net8.0).
  • PlatformServices.Desktop.IntegrationTests: 15/15 (net462) — exercises the deployment path that runs inside the SuspendCodeCoverage scope.
  • Expert-reviewer pass.

Stacking

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

…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>
Base automatically changed from dev/amauryleve/vstest-decoupling-sourcehandler 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
@Evangelink
Amaury Levé (Evangelink) merged commit c561276 into dev/amauryleve/vstest-decoupling-sourcehostJul 5, 2026
24 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/vstest-decoupling-suspendcoverage branch July 5, 2026 19:24

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

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

Expert Review — PR #9632 · Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3)

Summary

This PR accomplishes two related goals under the ObjectModel-decoupling initiative:

  1. Vendors SuspendCodeCoverage — a faithful internal copy that preserves the Vanguard IPC contract (__VANGUARD_SUSPEND_INSTRUMENT__ / "TRUE" / EnvironmentVariableTarget.Process) byte-identically, gated behind #if NETFRAMEWORK. The PR description's fidelity proof is thorough and accurate.

  2. Vendors DoesSourceReferenceAssembly — replaces the call to AssemblyHelper.DoesReferencesAssembly (vstest ObjectModel) with a local implementation. The original code carried a comment noting this in-AppDomain optimization was acceptable; the new implementation honors that pre-approval.

One minor code-quality issue is noted; no blocking or major findings.


Verdict Table

#DimensionStatusNotes
1Algorithmic Correctness⚠️ MinorArePublicKeyTokensEqual does not handle null from GetPublicKeyToken(); NullReferenceException is silently caught → conservative fallback. See inline comment.
2Threading & Concurrency✅ CleanSuspendCodeCoverage is used in a single-threaded deployment context; plain bool _isDisposed is sufficient. SetEnvironmentVariable concurrency matches original behavior.
3Security & IPC Contract Safety✅ CleanEnv-var name, value, and target are preserved verbatim. No path traversal or injection vectors introduced.
4Public API & Binary Compatibility✅ CleanBoth new types are internal sealed. No public API surface changed. PublicAPI.Unshipped.txt update not needed.
5Performance & Allocations✅ CleanNo LINQ, no unnecessary allocations. The ReflectionOnlyLoadFrom path is equivalent in cost to the original (same assembly load, minus the AppDomain round-trip).
6Cross-TFM Compatibility✅ CleanAll new code is guarded by #if NETFRAMEWORK. Other TFMs are unaffected.
7Error Handling & Resilience✅ CleanBare catch in DoesSourceReferenceAssembly is intentional and carries a comment; it mirrors the original vstest CheckAssemblyReference pattern. Conservative fallback (null → discover anyway) is correct.
8Resource Management✅ CleanSuspendCodeCoverage is IDisposable, idempotent, and used via using. No finalizer is needed (none exists in the original either; GC.SuppressFinalize was a no-op).
9Naming & Code Style✅ CleanAll names follow conventions. _isDisposed, _previousEnvironmentValue, constants are appropriately cased.
10Documentation & Comments✅ CleanBoth new files have XML doc comments. The fidelity comparison table in the PR description is exemplary.
11Test Coverage & Quality✅ CleanFour existing unit tests in DesktopTestSourceTests exercise the refactored IsAssemblyReferenced path, including the null-source/null-assembly, found, and not-found cases. PlatformServices.Desktop.IntegrationTests exercises the deploy path with SuspendCodeCoverage.
12Localization & Resource StringsN/ANo user-facing strings added.
13Logging & DiagnosticsN/ANo logging changes.
14Dependency & Package Hygiene✅ CleanThe Microsoft.TestPlatform.ObjectModel type-reference is eliminated as intended. No new packages introduced.
15Code Complexity & Maintainability✅ CleanBoth new methods are compact and well-commented. DoesSourceReferenceAssembly is ~35 LOC with clear phases.
16Test Infrastructure & Acceptance TestsN/ANo CLI options, output formats, or acceptance-test expectations were changed.
17Configuration & DefaultsN/ANo configuration changes.
18Scope Discipline✅ CleanThe TestSourceHandler.cs refactoring is logically part of the same ObjectModel-decoupling goal and is covered by the PR description, even if the title focuses on SuspendCodeCoverage.
19Build & Project File QualityN/ANo .csproj/.props changes needed; the new .cs file is picked up by the existing glob.
20Invariant/Contract Violations⚠️ Minorbyte[] parameters in ArePublicKeyTokensEqual should be byte[]?; GetPublicKeyToken() returns nullable and the call sites do not null-check. See inline comment.
21Behavioral Regression Risk✅ CleanAppDomain isolation is intentionally dropped (the original code carried a "we can optimize this" comment). ReflectionOnlyLoadFrom in the current AppDomain is safe: it does not execute code from the loaded assembly.
22PowerShell / Script QualityN/ANo scripts modified.

Key Finding

⚠️ Minor — ArePublicKeyTokensEqual null-handling gap (TestSourceHandler.cs line 142)

GetPublicKeyToken() returns byte[]?; both call sites pass the result directly to byte[] parameters. For unsigned assemblies (null token) this throws NullReferenceException, silently caught → null → conservative discovery. The behavior is correct by accident rather than by design. The original vstest CheckAssemblyReference had the same gap, so this is a clean-up opportunity only, not a regression. An inline suggestion is attached.


Positive Notes

  • The fidelity proof table (env-var name / value / target / ctor / dispose sequence) is an excellent record for future maintainers.
  • Collapsing Dispose(bool disposing) + GC.SuppressFinalize to a direct Dispose() is the correct simplification given the absence of a finalizer.
  • The AppDomain drop in DoesSourceReferenceAssembly is safe and was pre-authorized by the inline comment in the original code.

}
}

private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right)

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.

ArePublicKeyTokensEqual declares non-nullable byte[] parameters, but both callers pass AssemblyName.GetPublicKeyToken() which returns byte[]? (null for unsigned assemblies). When either token is null, left.Length on line 144 throws NullReferenceException, caught by the outer try/catch, which returns null → conservative path → discovery proceeds. Functionally safe, but the correctness depends on exception routing rather than explicit logic.

Suggested fix (nullable-aware):

privatestaticboolArePublicKeyTokensEqual(byte[]?left,byte[]?right){if(leftisnull&&rightisnull)returntrue;if(leftisnull||rightisnull)returnfalse;if(left.Length!=right.Length)returnfalse;for(inti=0;i<left.Length;++i){if(left[i]!=right[i])returnfalse;}returntrue;}

This makes "both sides unsigned → match by name" explicit and eliminates the implicit NullReferenceException. Also update referenceAssemblyPublicKeyToken (line 113) to byte[]? to satisfy the nullable annotation.

Not a regression: the original vstestCheckAssemblyReference had the same gap — byte[] publicKeyToken1 = referencedAssembly.GetPublicKeyToken() without null-guard. This is a clean-up opportunity only.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Evangelink