Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Centralizes access to <see cref="Assembly.Location"/> so that single-file / Native AOT
/// scenarios (where <c>Assembly.Location</c> returns an empty string) are handled once instead
/// of being scattered across many call sites with individual <c>IL3000</c> suppressions.
/// </summary>
/// <remarks>
/// All members tolerate <see cref="Assembly.Location"/> returning an empty string — that is the
/// documented behavior for assemblies embedded in single-file or Native AOT executables.
/// Callers receive <see langword="null"/> (for try-style getters) or fall back to
/// <see cref="AppContext.BaseDirectory"/> / the assembly simple name when appropriate.
/// </remarks>
internal static class AssemblyFileLocator
{
/// <summary>
/// Returns the file path of the given assembly, or <see langword="null"/> when the
/// assembly is embedded in a single-file or Native AOT executable (and therefore has no
/// on-disk location).
/// </summary>
[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;
}

/// <summary>
/// Returns the directory containing the given assembly, falling back to
/// <see cref="AppContext.BaseDirectory"/> when <see cref="Assembly.Location"/> is empty
/// (single-file / Native AOT).
/// </summary>
[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;
}

/// <summary>
/// Returns the file name (with extension) of the given assembly, falling back to the
/// assembly simple name with a <c>.dll</c> suffix when <see cref="Assembly.Location"/>
/// is empty (single-file / Native AOT).
/// </summary>
[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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ internal static class DataSerializationHelper
/// </summary>
/// <param name="data">Data array to serialize.</param>
/// <returns>Serialized array.</returns>
/// <exception cref="NotSupportedException">
/// Thrown when the runtime does not support dynamic code generation
/// (<c>RuntimeFeature.IsDynamicCodeSupported</c> is <see langword="false"/>),
/// 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 <c>ActualData</c> reference is used instead,
/// so this method should never be reached at runtime.
/// </exception>
[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)
Expand All @@ -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.");
}
Comment thread
Evangelink marked this conversation as resolved.
#endif

string?[] serializedData = new string?[data.Length * 2];
for (int i = 0; i < data.Length; i++)
{
Expand Down Expand Up @@ -82,6 +106,14 @@ internal static class DataSerializationHelper
/// </summary>
/// <param name="serializedData">Serialized data array to deserialize.</param>
/// <returns>Deserialized array.</returns>
/// <exception cref="NotSupportedException">
/// Thrown when the runtime does not support dynamic code generation
/// (<c>RuntimeFeature.IsDynamicCodeSupported</c> is <see langword="false"/>),
/// 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 <c>ActualData</c> reference is used instead,
/// so this method should never be reached at runtime.
/// </exception>
[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)
Expand All @@ -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.");
}
Comment thread
Evangelink marked this conversation as resolved.
#endif

int length = serializedData.Length / 2;
object?[] data = new object?[length];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -110,21 +113,32 @@ internal TestSourceHost(string sourceFileName, IRunSettings? runSettings, IAppDo
public void SetupHost()
{
#if NET && !WINDOWS_UWP
List<string> 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<string> 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<string> resolutionPaths = GetResolutionPaths(_sourceFileName, VSInstallationUtilities.IsCurrentProcessRunningInPortableMode());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand All @@ -350,7 +364,6 @@ internal string GetTargetFrameworkVersionString(string sourceFileName)
/// <returns>
/// A list of path.
/// </returns>
[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<string> GetResolutionPaths(string sourceFileName, bool isPortableMode)
{
List<string> resolutionPaths =
Expand Down Expand Up @@ -379,24 +392,22 @@ internal virtual List<string> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -136,7 +137,6 @@ public static string GetTestResultsDirectory(IRunContext? runContext) => !String
/// <param name="deploymentDirectory">The deployment directory.</param>
/// <param name="resultsDirectory">Root results directory.</param>
/// <returns>Returns a list of deployment warnings.</returns>
[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<string> Deploy(IList<DeploymentItem> deploymentItems, string testSourceHandler, string deploymentDirectory, string resultsDirectory)
{
Ensure.NotNullOrWhiteSpace(deploymentDirectory);
Expand Down Expand Up @@ -194,8 +194,9 @@ protected IEnumerable<string> Deploy(IList<DeploymentItem> 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;
Expand Down