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 b848f88591..45f7f92133 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs
@@ -35,6 +35,14 @@ internal static class DataSerializationHelper
///
/// Data array to serialize.
/// Serialized array.
+ ///
+ /// Thrown when the runtime does not support dynamic code generation
+ /// (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,
+ /// so this method should never be reached at runtime.
+ ///
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = DataContractSerializationJustification)]
[UnconditionalSuppressMessage("Aot", "IL3050:Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT", Justification = DataContractSerializationJustification)]
public static string?[]? Serialize(object?[]? data)
@@ -44,6 +52,22 @@ internal static class DataSerializationHelper
return null;
}
+#if NET
+ if (!RuntimeFeature.IsDynamicCodeSupported)
+ {
+ // 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 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
+
string?[] serializedData = new string?[data.Length * 2];
for (int i = 0; i < data.Length; i++)
{
@@ -82,6 +106,14 @@ internal static class DataSerializationHelper
///
/// Serialized data array to deserialize.
/// Deserialized array.
+ ///
+ /// Thrown when the runtime does not support dynamic code generation
+ /// (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,
+ /// so this method should never be reached at runtime.
+ ///
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = DataContractSerializationJustification)]
[UnconditionalSuppressMessage("Aot", "IL3050:Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT", Justification = DataContractSerializationJustification)]
public static object?[]? Deserialize(string?[]? serializedData)
@@ -91,6 +123,20 @@ internal static class DataSerializationHelper
return null;
}
+#if NET
+ if (!RuntimeFeature.IsDynamicCodeSupported)
+ {
+ // 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 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
+
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 60f818f82f..7ff1be7380 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,32 @@ internal TestSourceHost(string sourceFileName, IRunSettings? runSettings, IAppDo
public void SetupHost()
{
#if NET && !WINDOWS_UWP
- List resolutionPaths = GetResolutionPaths(_sourceFileName, false);
-
- if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
+ // 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)
{
- 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 +286,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 +340,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)
@@ -350,7 +364,6 @@ internal string GetTargetFrameworkVersionString(string sourceFileName)
///
/// A list of path.
///
- [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "Assembly.Location is explicitly checked for empty before use; in single-file/Native AOT scenarios it returns empty and the affected blocks are skipped.")]
internal virtual List GetResolutionPaths(string sourceFileName, bool isPortableMode)
{
List resolutionPaths =
@@ -379,24 +392,22 @@ 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.
- if (!string.IsNullOrEmpty(typeof(TestSourceHost).Assembly.Location))
+ // 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))
{
// 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);
}
return resolutionPaths;
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs
index dd6918a934..be6335ba69 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;
@@ -136,7 +137,6 @@ public static string GetTestResultsDirectory(IRunContext? runContext) => !String
/// The deployment directory.
/// Root results directory.
/// Returns a list of deployment warnings.
- [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "Deployment is a reflection-mode/legacy adapter feature; in single-file/Native AOT scenarios Assembly.Location returns an empty string and the comparison falls through without error.")]
protected IEnumerable Deploy(IList deploymentItems, string testSourceHandler, string deploymentDirectory, string resultsDirectory)
{
Ensure.NotNullOrWhiteSpace(deploymentDirectory);
@@ -194,8 +194,9 @@ 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.
- string assemblyName = Path.GetFileName(GetType().Assembly.Location);
+ // 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))
{
continue;