From e6dc77cb079a77ca62c00f9e96b063c6d7854b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 15:18:50 +0200 Subject: [PATCH] Reimplement the source assembly-reference check without ObjectModel (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> --- .../Services/TestSourceHandler.cs | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs index 4255641119..a2ce548535 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs @@ -5,9 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.AppContainer; #endif using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -#if NETFRAMEWORK -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -#endif namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -79,7 +76,7 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) { #if NETFRAMEWORK // This loads the dll in a different app domain. We can optimize this to load in the current domain since this code could be run in a new app domain anyway. - bool? utfReference = AssemblyHelper.DoesReferencesAssembly(source, assemblyName); + bool? utfReference = DoesSourceReferenceAssembly(source, assemblyName); // If no reference to UTF don't run discovery. Take conservative approach. If not able to find proceed with discovery. return !utfReference.HasValue || utfReference.Value; @@ -94,6 +91,73 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) #endif } +#if NETFRAMEWORK + /// + /// Checks whether the source assembly directly references the given assembly. + /// Only the assembly simple name and public key token are matched; version is ignored. + /// Returns if the reference could not be determined. + /// + /// The path to the source assembly to inspect. + /// The assembly to look for in the source's references. + /// if referenced, if not, if undeterminable. + private static bool? DoesSourceReferenceAssembly(string source, AssemblyName referenceAssembly) + { + if (string.IsNullOrEmpty(source) || referenceAssembly is null) + { + return null; + } + + try + { + string? referenceAssemblyName = referenceAssembly.Name; + byte[] referenceAssemblyPublicKeyToken = referenceAssembly.GetPublicKeyToken(); + + // ReflectionOnlyLoadFrom loads from the specified path only (no probing) and does not + // execute any code from the loaded assembly. + var assembly = Assembly.ReflectionOnlyLoadFrom(source); + + foreach (AssemblyName referencedAssembly in assembly.GetReferencedAssemblies()) + { + // Match without version: only the simple name and public key token. + if (!string.Equals(referencedAssembly.Name, referenceAssemblyName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (ArePublicKeyTokensEqual(referencedAssembly.GetPublicKeyToken(), referenceAssemblyPublicKeyToken)) + { + return true; + } + } + + return false; + } + catch + { + // Return null if we are not able to check. + return null; + } + } + + private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right) + { + if (left.Length != right.Length) + { + return false; + } + + for (int i = 0; i < left.Length; ++i) + { + if (left[i] != right[i]) + { + return false; + } + } + + return true; + } +#endif + /// /// Gets the set of sources (dll's/exe's) that contain tests. If a source is a package (appx), return the file (dll/exe) that contains tests from it. ///