From 4f54c3c5a085461e1ae674e4d034774b0761661e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 29 May 2026 20:11:22 +0200 Subject: [PATCH 1/4] Make AOT-incompatible code paths a no-op at runtime in MTP/Native AOT mode This change introduces two targeted improvements that reduce the trim/AOT warning surface for the MSTest adapter when consumers enable `PublishTrimmed=true` or `PublishAot=true` and pull MSTest in via MTP. 1. Centralize `Assembly.Location` handling in a new internal `AssemblyFileLocator` helper. Previously, every call site that needed the path or directory of an on-disk assembly carried its own `#pragma warning disable IL3000` (or relied on an IL3000 attribute suppression) and re-implemented the single-file/Native AOT fallback to `AppContext.BaseDirectory` or the assembly simple name. The helper gives one place to suppress IL3000 with a meaningful justification and one place to maintain the fallback logic. Updated call sites: - `TestSourceHost.SetContext` (WIN_UI branch), - `TestSourceHost.GetAppBaseAsPerPlatform` (NETFRAMEWORK), - `TestSourceHost.GetResolutionPaths` (NET, !WINDOWS_UWP and NETFRAMEWORK), - `DeploymentUtilityBase.Deploy` (filtering the adapter assembly out of the deployment loop). 2. Skip the AssemblyResolver registration and `DataSerializationHelper` work at runtime in Native AOT. `RuntimeFeature.IsDynamicCodeSupported` is the established AOT-detection signal in this codebase (already used by `TestApplication` and `AppInsightsTelemetryProviderExtensions`). When it is `false`: - `TestSourceHost.SetupHost` no longer constructs `AssemblyResolver` (its `AssemblyLoadContext.Resolving` hook can never fire under NAOT). This also lets the trimmer drop `GetResolutionPaths` and the resolver class from the published binary. - `DataSerializationHelper.Serialize` / `Deserialize` throw a descriptive `NotSupportedException` instead of attempting `DataContractJsonSerializer`-based serialization (which requires dynamic code). In MTP mode the in-process `TestMethod.ActualData` is used for parameterized arguments and these methods are not reached at runtime; the throw is a safety net plus a trimmer hint. The trimmer treats `RuntimeFeature.IsDynamicCodeSupported` as a constant `false` substitution during NAOT publish, so the gated branches (and the unreferenced `AssemblyResolver` / DataContract code paths) are statically removed from the published binary. This PR is independent of #8686 (which is the pragma-to-attribute conversion for the same warnings) but the two changes overlap on `TestSourceHost.cs` and `DataSerializationHelper.cs`; expect a merge conflict between them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/AssemblyFileLocator.cs | 71 +++++++++++++++++++ .../Helpers/DataSerializationHelper.cs | 36 ++++++++++ .../Services/TestSourceHost.cs | 60 ++++++++-------- .../Utilities/DeploymentUtilityBase.cs | 5 +- 4 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Helpers/AssemblyFileLocator.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AssemblyFileLocator.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AssemblyFileLocator.cs new file mode 100644 index 0000000000..0939fe644b --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AssemblyFileLocator.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; + +/// +/// Centralizes access to so that single-file / Native AOT +/// scenarios (where Assembly.Location returns an empty string) are handled once instead +/// of being scattered across many call sites with individual IL3000 suppressions. +/// +/// +/// All members tolerate returning an empty string — that is the +/// documented behavior for assemblies embedded in single-file or Native AOT executables. +/// Callers receive (for try-style getters) or fall back to +/// / the assembly simple name when appropriate. +/// +internal static class AssemblyFileLocator +{ + /// + /// Returns the file path of the given assembly, or when the + /// assembly is embedded in a single-file or Native AOT executable (and therefore has no + /// on-disk location). + /// + [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "Empty return is the documented contract; callers are expected to handle null.")] + public static string? TryGetLocation(Assembly assembly) + { + string location = assembly.Location; + return string.IsNullOrEmpty(location) ? null : location; + } + + /// + /// Returns the directory containing the given assembly, falling back to + /// when is empty + /// (single-file / Native AOT). + /// + [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "Falls back to AppContext.BaseDirectory when Assembly.Location is empty (single-file/Native AOT case).")] + public static string GetDirectoryOrAppContextBase(Assembly assembly) + { + string location = assembly.Location; + if (!string.IsNullOrEmpty(location)) + { + string? directory = Path.GetDirectoryName(location); + if (!string.IsNullOrEmpty(directory)) + { + return directory; + } + } + + return AppContext.BaseDirectory; + } + + /// + /// Returns the file name (with extension) of the given assembly, falling back to the + /// assembly simple name with a .dll suffix when + /// is empty (single-file / Native AOT). + /// + [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "Falls back to assembly simple name when Assembly.Location is empty (single-file/Native AOT case).")] + public static string GetFileNameOrSimpleName(Assembly assembly) + { + string location = assembly.Location; + if (!string.IsNullOrEmpty(location)) + { + return Path.GetFileName(location); + } + + string? simpleName = assembly.GetName().Name; + return string.IsNullOrEmpty(simpleName) + ? string.Empty + : simpleName + ".dll"; + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs index c7c980e9fd..b08c0ff377 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs @@ -30,6 +30,12 @@ internal static class DataSerializationHelper /// /// Data array to serialize. /// Serialized array. + /// + /// Thrown when running under Native AOT. DataContract-based serialization requires + /// dynamic code generation, which is unavailable in AOT. In AOT/MTP scenarios the + /// in-process ActualData reference is used instead, so this method should + /// never be reached at runtime. + /// public static string?[]? Serialize(object?[]? data) { if (data == null) @@ -37,6 +43,19 @@ internal static class DataSerializationHelper return null; } +#if NET + if (!RuntimeFeature.IsDynamicCodeSupported) + { + // Cross-process data serialization is not used in MTP / Native AOT scenarios; + // discovery sets ActualData (in-process reference) instead. If we reach here + // it means a vstest-style code path is being exercised in an AOT build, which + // is unsupported. + throw new NotSupportedException( + "MSTest data-source argument serialization is not supported when running as Native AOT. " + + "Use Microsoft.Testing.Platform (MTP) mode, where parameterized test arguments are passed in-process."); + } +#endif + string?[] serializedData = new string?[data.Length * 2]; for (int i = 0; i < data.Length; i++) { @@ -82,6 +101,12 @@ internal static class DataSerializationHelper /// /// Serialized data array to deserialize. /// Deserialized array. + /// + /// Thrown when running under Native AOT. DataContract-based deserialization requires + /// dynamic code generation, which is unavailable in AOT. In AOT/MTP scenarios the + /// in-process ActualData reference is used instead, so this method should + /// never be reached at runtime. + /// public static object?[]? Deserialize(string?[]? serializedData) { if (serializedData == null || serializedData.Length % 2 != 0) @@ -89,6 +114,17 @@ internal static class DataSerializationHelper return null; } +#if NET + if (!RuntimeFeature.IsDynamicCodeSupported) + { + // See note on Serialize: the AOT/MTP execution path uses TestMethod.ActualData, + // so this branch should be unreachable at runtime. + throw new NotSupportedException( + "MSTest data-source argument deserialization is not supported when running as Native AOT. " + + "Use Microsoft.Testing.Platform (MTP) mode, where parameterized test arguments are passed in-process."); + } +#endif + int length = serializedData.Length / 2; object?[] data = new object?[length]; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs index 0e8c0c8be6..c67468aeea 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs @@ -18,6 +18,9 @@ #if !WINDOWS_UWP using Microsoft.VisualStudio.TestTools.UnitTesting; #endif +#if NETFRAMEWORK || (NET && !WINDOWS_UWP) +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; +#endif namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -110,21 +113,28 @@ internal TestSourceHost(string sourceFileName, IRunSettings? runSettings, IAppDo public void SetupHost() { #if NET && !WINDOWS_UWP - List resolutionPaths = GetResolutionPaths(_sourceFileName, false); - - if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) + // In Native AOT, dynamic assembly loading is not supported, so the AssemblyResolver + // (which hooks AssemblyLoadContext.Resolving) can never fire. Skip its creation + // entirely; this also lets the trimmer drop GetResolutionPaths and the resolver + // itself from the published binary. + if (RuntimeFeature.IsDynamicCodeSupported) { - PlatformServiceProvider.Instance.AdapterTraceLogger.Info("DesktopTestSourceHost.SetupHost(): Creating assembly resolver with resolution paths {0}.", string.Join(',', resolutionPaths)); - } + List resolutionPaths = GetResolutionPaths(_sourceFileName, false); - var assemblyResolver = new AssemblyResolver(resolutionPaths); - if (TryAddSearchDirectoriesSpecifiedInRunSettingsToAssemblyResolver(assemblyResolver, Path.GetDirectoryName(_sourceFileName)!)) - { - _parentDomainAssemblyResolver = assemblyResolver; - } - else - { - assemblyResolver.Dispose(); + if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) + { + PlatformServiceProvider.Instance.AdapterTraceLogger.Info("DesktopTestSourceHost.SetupHost(): Creating assembly resolver with resolution paths {0}.", string.Join(',', resolutionPaths)); + } + + var assemblyResolver = new AssemblyResolver(resolutionPaths); + if (TryAddSearchDirectoriesSpecifiedInRunSettingsToAssemblyResolver(assemblyResolver, Path.GetDirectoryName(_sourceFileName)!)) + { + _parentDomainAssemblyResolver = assemblyResolver; + } + else + { + assemblyResolver.Dispose(); + } } #elif NETFRAMEWORK List resolutionPaths = GetResolutionPaths(_sourceFileName, VSInstallationUtilities.IsCurrentProcessRunningInPortableMode()); @@ -272,7 +282,7 @@ private void SetContext(string? source) #if WIN_UI if (StringEx.IsNullOrEmpty(dirName)) { - dirName = Path.GetDirectoryName(typeof(TestSourceHost).Assembly.Location)!; + dirName = AssemblyFileLocator.GetDirectoryOrAppContextBase(typeof(TestSourceHost).Assembly); } Directory.SetCurrentDirectory(dirName); @@ -326,8 +336,8 @@ internal string GetAppBaseAsPerPlatform() // there would be a mismatch of platform service assemblies during discovery. DebugEx.Assert(_targetFrameworkVersion is not null, "Target framework version is null."); return _targetFrameworkVersion.Contains(EngineConstants.DotNetFrameWorkStringPrefix) - ? Path.GetDirectoryName(_sourceFileName) ?? Path.GetDirectoryName(typeof(TestSourceHost).Assembly.Location) - : Path.GetDirectoryName(typeof(TestSourceHost).Assembly.Location); + ? Path.GetDirectoryName(_sourceFileName) ?? AssemblyFileLocator.GetDirectoryOrAppContextBase(typeof(TestSourceHost).Assembly) + : AssemblyFileLocator.GetDirectoryOrAppContextBase(typeof(TestSourceHost).Assembly); } internal string GetTargetFrameworkVersionString(string sourceFileName) @@ -380,25 +390,19 @@ internal virtual List GetResolutionPaths(string sourceFileName, bool isP // We check for the empty path, and in single file mode, or on source gen mode we don't allow // loading dependencies than from the current folder, which is what the default loader handles by itself. -#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file - if (!string.IsNullOrEmpty(typeof(TestSourceHost).Assembly.Location)) + string? adapterDirectory = Path.GetDirectoryName(AssemblyFileLocator.TryGetLocation(typeof(TestSourceHost).Assembly)); + if (!string.IsNullOrEmpty(adapterDirectory) && !resolutionPaths.Contains(adapterDirectory)) { // Adding adapter folder to resolution paths - if (!resolutionPaths.Contains(Path.GetDirectoryName(typeof(TestSourceHost).Assembly.Location)!)) - { - resolutionPaths.Add(Path.GetDirectoryName(typeof(TestSourceHost).Assembly.Location)!); - } + resolutionPaths.Add(adapterDirectory); } - if (!string.IsNullOrEmpty(typeof(AssemblyHelper).Assembly.Location)) + string? testPlatformDirectory = Path.GetDirectoryName(AssemblyFileLocator.TryGetLocation(typeof(AssemblyHelper).Assembly)); + if (!string.IsNullOrEmpty(testPlatformDirectory) && !resolutionPaths.Contains(testPlatformDirectory)) { // Adding TestPlatform folder to resolution paths - if (!resolutionPaths.Contains(Path.GetDirectoryName(typeof(AssemblyHelper).Assembly.Location)!)) - { - resolutionPaths.Add(Path.GetDirectoryName(typeof(AssemblyHelper).Assembly.Location)!); - } + resolutionPaths.Add(testPlatformDirectory); } -#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file return resolutionPaths; } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs index f86755558b..d05d1b5da5 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs @@ -6,6 +6,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -194,9 +195,7 @@ protected IEnumerable Deploy(IList deploymentItems, stri // Ignore the test platform files. string tempFile = Path.GetFileName(fileToDeploy); // We throw when we run in source gen mode. -#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file - string assemblyName = Path.GetFileName(GetType().Assembly.Location); -#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file + string assemblyName = AssemblyFileLocator.GetFileNameOrSimpleName(GetType().Assembly); if (tempFile.Equals(assemblyName, StringComparison.OrdinalIgnoreCase)) { continue; From 6b53b02958610001425a4a6832ec367a809ff36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 29 May 2026 21:18:40 +0200 Subject: [PATCH 2/4] =?UTF-8?q?Address=20review:=20clarify=20wording=20?= =?UTF-8?q?=E2=80=94=20dynamic=20code=20generation,=20not=20"Native=20AOT"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RuntimeFeature.IsDynamicCodeSupported is false for any AOT-like runtime without a JIT or interpreter (Native AOT, Mono iOS AOT, Blazor WebAssembly AOT, …), not exclusively Native AOT. Update doc comments, exception messages, and the in-source rationale in TestSourceHost to describe the actual condition being checked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/DataSerializationHelper.cs | 46 +++++++++++-------- .../Services/TestSourceHost.cs | 12 +++-- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs index b08c0ff377..08dc11d798 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs @@ -31,10 +31,12 @@ internal static class DataSerializationHelper /// Data array to serialize. /// Serialized array. /// - /// Thrown when running under Native AOT. DataContract-based serialization requires - /// dynamic code generation, which is unavailable in AOT. In AOT/MTP scenarios the - /// in-process ActualData reference is used instead, so this method should - /// never be reached at runtime. + /// Thrown when the runtime does not support dynamic code generation + /// ( is ), + /// for example under Native AOT, Mono AOT on iOS, or Blazor WebAssembly AOT. + /// DataContract-based serialization relies on dynamic code generation, and in such + /// AOT/MTP scenarios the in-process ActualData reference is used instead, + /// so this method should never be reached at runtime. /// public static string?[]? Serialize(object?[]? data) { @@ -46,13 +48,16 @@ internal static class DataSerializationHelper #if NET if (!RuntimeFeature.IsDynamicCodeSupported) { - // Cross-process data serialization is not used in MTP / Native AOT scenarios; - // discovery sets ActualData (in-process reference) instead. If we reach here - // it means a vstest-style code path is being exercised in an AOT build, which - // is unsupported. + // Cross-process data serialization is not used when dynamic code generation is + // unavailable (Native AOT, Mono iOS AOT, Blazor WASM AOT, ...). In MTP mode, + // discovery sets ActualData (in-process reference) instead. Reaching this code + // means a vstest-style code path is being exercised in an AOT build, which is + // unsupported because DataContractJsonSerializer requires runtime code generation. throw new NotSupportedException( - "MSTest data-source argument serialization is not supported when running as Native AOT. " + - "Use Microsoft.Testing.Platform (MTP) mode, where parameterized test arguments are passed in-process."); + "MSTest data-source argument serialization is not supported when the runtime " + + "does not support dynamic code generation (Native AOT, Mono iOS AOT, Blazor " + + "WebAssembly AOT, ...). Use Microsoft.Testing.Platform (MTP) mode, where " + + "parameterized test arguments are passed in-process."); } #endif @@ -102,10 +107,12 @@ internal static class DataSerializationHelper /// Serialized data array to deserialize. /// Deserialized array. /// - /// Thrown when running under Native AOT. DataContract-based deserialization requires - /// dynamic code generation, which is unavailable in AOT. In AOT/MTP scenarios the - /// in-process ActualData reference is used instead, so this method should - /// never be reached at runtime. + /// Thrown when the runtime does not support dynamic code generation + /// ( is ), + /// for example under Native AOT, Mono AOT on iOS, or Blazor WebAssembly AOT. + /// DataContract-based deserialization relies on dynamic code generation, and in such + /// AOT/MTP scenarios the in-process ActualData reference is used instead, + /// so this method should never be reached at runtime. /// public static object?[]? Deserialize(string?[]? serializedData) { @@ -117,11 +124,14 @@ internal static class DataSerializationHelper #if NET if (!RuntimeFeature.IsDynamicCodeSupported) { - // See note on Serialize: the AOT/MTP execution path uses TestMethod.ActualData, - // so this branch should be unreachable at runtime. + // See note on Serialize: when dynamic code generation is unavailable + // (Native AOT, Mono iOS AOT, Blazor WASM AOT, ...), the execution path uses + // TestMethod.ActualData in MTP mode, so this branch should be unreachable. throw new NotSupportedException( - "MSTest data-source argument deserialization is not supported when running as Native AOT. " + - "Use Microsoft.Testing.Platform (MTP) mode, where parameterized test arguments are passed in-process."); + "MSTest data-source argument deserialization is not supported when the runtime " + + "does not support dynamic code generation (Native AOT, Mono iOS AOT, Blazor " + + "WebAssembly AOT, ...). Use Microsoft.Testing.Platform (MTP) mode, where " + + "parameterized test arguments are passed in-process."); } #endif diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs index c67468aeea..30afa0b901 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs @@ -113,10 +113,14 @@ internal TestSourceHost(string sourceFileName, IRunSettings? runSettings, IAppDo public void SetupHost() { #if NET && !WINDOWS_UWP - // In Native AOT, dynamic assembly loading is not supported, so the AssemblyResolver - // (which hooks AssemblyLoadContext.Resolving) can never fire. Skip its creation - // entirely; this also lets the trimmer drop GetResolutionPaths and the resolver - // itself from the published binary. + // When the runtime does not support dynamic code generation + // (RuntimeFeature.IsDynamicCodeSupported is false — Native AOT, Mono iOS AOT, + // Blazor WebAssembly AOT, ...), the host cannot load assemblies through + // reflection-style fallbacks, so the AssemblyResolver (which hooks + // AssemblyLoadContext.Resolving to load assemblies on demand) can never + // contribute anything useful. Skip its creation entirely; this also lets the + // trimmer drop GetResolutionPaths and the resolver itself from the published + // binary. if (RuntimeFeature.IsDynamicCodeSupported) { List resolutionPaths = GetResolutionPaths(_sourceFileName, false); From 51a44a9c4b48c4599c2cba36bc2a689a1575c24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 29 May 2026 22:02:45 +0200 Subject: [PATCH 3/4] Fix CS1574: `RuntimeFeature.IsDynamicCodeSupported` cref unresolved on netfx/uap `RuntimeFeature.IsDynamicCodeSupported` is .NET Core/5+ only, so the `` references in the XML docs fail to resolve when the project builds for net462 and uap10.0.16299. Switch to `...` literals to keep the documentation readable on all target frameworks without requiring conditional compilation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/DataSerializationHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs index 08dc11d798..33afa6f6a2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs @@ -32,7 +32,7 @@ internal static class DataSerializationHelper /// Serialized array. /// /// Thrown when the runtime does not support dynamic code generation - /// ( is ), + /// (RuntimeFeature.IsDynamicCodeSupported is ), /// for example under Native AOT, Mono AOT on iOS, or Blazor WebAssembly AOT. /// DataContract-based serialization relies on dynamic code generation, and in such /// AOT/MTP scenarios the in-process ActualData reference is used instead, @@ -108,7 +108,7 @@ internal static class DataSerializationHelper /// Deserialized array. /// /// Thrown when the runtime does not support dynamic code generation - /// ( is ), + /// (RuntimeFeature.IsDynamicCodeSupported is ), /// for example under Native AOT, Mono AOT on iOS, or Blazor WebAssembly AOT. /// DataContract-based deserialization relies on dynamic code generation, and in such /// AOT/MTP scenarios the in-process ActualData reference is used instead, From faba143c54e9f52b2402ca79d256e3e7e6855646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 29 May 2026 22:58:07 +0200 Subject: [PATCH 4/4] Address review: update stale 'source gen mode' comments Both comments referenced 'source gen mode', but the actual code paths check for Assembly.Location being empty (single-file / Native AOT scenarios). Reworded to describe the real behavior to avoid confusing future maintainers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/TestSourceHost.cs | 6 ++++-- .../Utilities/DeploymentUtilityBase.cs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs index 30afa0b901..7ff1be7380 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs @@ -392,8 +392,10 @@ internal virtual List GetResolutionPaths(string sourceFileName, bool isP #endif } - // We check for the empty path, and in single file mode, or on source gen mode we don't allow - // loading dependencies than from the current folder, which is what the default loader handles by itself. + // AssemblyFileLocator.TryGetLocation returns null when Assembly.Location is empty + // (single-file / Native AOT). In those scenarios we don't add the adapter or test platform + // directories to the resolution paths and rely on the default loader resolving dependencies + // from the current folder, which is what it handles by itself in single-file mode. string? adapterDirectory = Path.GetDirectoryName(AssemblyFileLocator.TryGetLocation(typeof(TestSourceHost).Assembly)); if (!string.IsNullOrEmpty(adapterDirectory) && !resolutionPaths.Contains(adapterDirectory)) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs index d05d1b5da5..be6335ba69 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs @@ -194,7 +194,8 @@ protected IEnumerable Deploy(IList deploymentItems, stri // Ignore the test platform files. string tempFile = Path.GetFileName(fileToDeploy); - // We throw when we run in source gen mode. + // Use AssemblyFileLocator to safely obtain the file name even when Assembly.Location + // is empty (single-file / Native AOT scenarios) by falling back to the simple name. string assemblyName = AssemblyFileLocator.GetFileNameOrSimpleName(GetType().Assembly); if (tempFile.Equals(assemblyName, StringComparison.OrdinalIgnoreCase)) {