Skip to content
Closed
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
Expand Up @@ -7,13 +7,34 @@

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Bridges the MSTest adapter trace logging interface to the Microsoft Testing Platform logger.
/// </summary>
/// <remarks>
/// On .NET Framework, this class inherits from <see cref="MarshalByRefObject"/> to support
/// cross-AppDomain logging. When the logger is set on objects in child AppDomains (like
/// <see cref="TestPlatform.MSTestAdapter.PlatformServices.AssemblyResolver"/>), calls are
/// proxied back to the parent domain where the actual <see cref="ILogger"/> instance resides.
/// </remarks>
[SuppressMessage("ApiDesign", "RS0030:Do not use banned APIs", Justification = "MTP logger bridge")]
internal sealed class BridgedTraceLogger : IAdapterTraceLogger
internal sealed class BridgedTraceLogger :
#if NETFRAMEWORK
MarshalByRefObject,
#endif
IAdapterTraceLogger
{
private readonly ILogger _logger;

public bool IsInfoEnabled => _logger.IsEnabled(LogLevel.Information);

public bool IsWarningEnabled => _logger.IsEnabled(LogLevel.Warning);

public bool IsErrorEnabled => _logger.IsEnabled(LogLevel.Error);

public bool IsVerboseEnabled => _logger.IsEnabled(LogLevel.Debug);

public BridgedTraceLogger(ILogger logger)
=> _logger = logger;
=> _logger = logger ?? throw new ArgumentNullException(nameof(logger));

public void LogError(string format, params object?[] args)
{
Expand All @@ -38,5 +59,20 @@ public void LogWarning(string format, params object?[] args)
_logger.LogWarning(string.Format(CultureInfo.CurrentCulture, format, args));
}
}

public void LogVerbose(string format, params object?[] args)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(string.Format(CultureInfo.CurrentCulture, format, args));
}
}

#if NETFRAMEWORK
public void PrepareRemoteAppDomain(AppDomain appDomain)
// Force loading Microsoft.Testing.Platform in the child AppDomain to ensure the ILogger
// type can be resolved when creating a transparent proxy for this MarshalByRefObject.
=> appDomain.Load(typeof(ILogger).Assembly.GetName());
#endif
}
#endif
210 changes: 104 additions & 106 deletions src/Adapter/MSTestAdapter.PlatformServices/AssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using System.Security.Permissions;
#endif

using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
Expand Down Expand Up @@ -87,22 +87,46 @@ class AssemblyResolver :
private static List<string>? s_currentlyLoading;
private bool _disposed;

#if NETFRAMEWORK
/// <summary>
/// Gets or sets the logger to use for tracing.
/// </summary>
/// <remarks>
/// This property allows setting the logger after construction, which is necessary when creating
/// instances in child AppDomains. The logger cannot be passed as a constructor argument because
/// the CLR needs to marshal arguments before the instance is created, but the AssemblyResolver
/// (which enables type resolution in the child domain) doesn't exist yet at that point.
/// By setting the logger after construction, the assembly is already loaded and the logger type
/// can be properly resolved for creating a transparent proxy.
/// </remarks>
#else
/// <summary>
/// Gets or sets the logger to use for tracing.
/// </summary>
#endif
public IAdapterTraceLogger? Logger { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="AssemblyResolver"/> class.
/// </summary>
/// <param name="directories">
/// A list of directories for resolution path.
/// </param>
/// <param name="logger">
/// The logger to use for tracing. Can be null if logging is not needed, or if the logger
/// will be set later via the <see cref="Logger"/> property (necessary for cross-AppDomain scenarios).
/// </param>
/// <remarks>
/// If there are additional paths where a recursive search is required
/// call AddSearchDirectoryFromRunSetting method with that list.
/// </remarks>
public AssemblyResolver(IList<string> directories)
public AssemblyResolver(IList<string> directories, IAdapterTraceLogger? logger)
{
Ensure.NotNullOrEmpty(directories);

_searchDirectories = [.. directories];
_directoryList = new Queue<RecursiveDirectoryPath>();
Logger = logger;

AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(OnResolve);
#if NETFRAMEWORK
Expand Down Expand Up @@ -342,18 +366,11 @@ protected virtual
}
catch (Exception ex)
{
SafeLog(
SafeLogInfo(
name,
() =>
{
if (EqtTrace.IsInfoEnabled)
{
EqtTrace.Info(
"MSTest.AssemblyResolver.OnResolve: Failed to create assemblyName '{0}'. Reason: {1} ",
name,
ex);
}
});
"MSTest.AssemblyResolver.OnResolve: Failed to create assemblyName '{0}'. Reason: {1} ",
name,
ex);

return null;
}
Expand All @@ -367,15 +384,11 @@ protected virtual
continue;
}

SafeLog(
SafeLogVerbose(
name,
() =>
{
if (EqtTrace.IsVerboseEnabled)
{
EqtTrace.Verbose("MSTest.AssemblyResolver.OnResolve: Searching assembly '{0}' in the directory '{1}'", requestedName.Name, dir);
}
});
"MSTest.AssemblyResolver.OnResolve: Searching assembly '{0}' in the directory '{1}'",
requestedName.Name,
dir);

foreach (string extension in new string[] { ".dll", ".exe" })
{
Expand All @@ -389,15 +402,11 @@ protected virtual
// the ResourceHelper's currentlyLoading stack to null if an exception occurs.
if (s_currentlyLoading != null && s_currentlyLoading.Count > 0 && s_currentlyLoading.LastIndexOf(assemblyPath) != -1)
{
SafeLog(
SafeLogInfo(
name,
"MSTest.AssemblyResolver.OnResolve: Assembly '{0}' is searching for itself recursively '{1}', returning as not found.",
name,
() =>
{
if (EqtTrace.IsInfoEnabled)
{
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Assembly '{0}' is searching for itself recursively '{1}', returning as not found.", name, assemblyPath);
}
});
assemblyPath);
_resolvedAssemblies[name] = null;
return null;
}
Expand Down Expand Up @@ -497,27 +506,17 @@ private void WindowsRuntimeMetadataReflectionOnlyNamespaceResolve(object sender,
return null;
}

SafeLog(
SafeLogInfo(
args.Name,
() =>
{
if (EqtTrace.IsInfoEnabled)
{
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Resolving assembly '{0}'", args.Name);
}
});
"MSTest.AssemblyResolver.OnResolve: Resolving assembly '{0}'",
args.Name);

string assemblyNameToLoad = AppDomain.CurrentDomain.ApplyPolicy(args.Name);

SafeLog(
SafeLogInfo(
assemblyNameToLoad,
() =>
{
if (EqtTrace.IsInfoEnabled)
{
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Resolving assembly after applying policy '{0}'", assemblyNameToLoad);
}
});
"MSTest.AssemblyResolver.OnResolve: Resolving assembly after applying policy '{0}'",
assemblyNameToLoad);

lock (_syncLock)
{
Expand Down Expand Up @@ -561,17 +560,10 @@ private void WindowsRuntimeMetadataReflectionOnlyNamespaceResolve(object sender,
else
{
// generate warning that path does not exist.
SafeLog(
SafeLogWarning(
assemblyNameToLoad,
() =>
{
if (EqtTrace.IsWarningEnabled)
{
EqtTrace.Warning(
"MSTest.AssemblyResolver.OnResolve: the directory '{0}', does not exist",
currentNode.DirectoryPath);
}
});
"MSTest.AssemblyResolver.OnResolve: the directory '{0}', does not exist",
currentNode.DirectoryPath);
}
}

Expand Down Expand Up @@ -610,15 +602,11 @@ private void WindowsRuntimeMetadataReflectionOnlyNamespaceResolve(object sender,
}
catch (Exception ex)
{
SafeLog(
SafeLogInfo(
args.Name,
() =>
{
if (EqtTrace.IsInfoEnabled)
{
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Failed to load assembly '{0}'. Reason: {1}", assemblyNameToLoad, ex);
}
});
"MSTest.AssemblyResolver.OnResolve: Failed to load assembly '{0}'. Reason: {1}",
assemblyNameToLoad,
ex);
}

return assembly;
Expand All @@ -639,38 +627,61 @@ private bool TryLoadFromCache(string assemblyName, bool isReflectionOnly, out As
: _resolvedAssemblies.TryGetValue(assemblyName, out assembly);
if (isFoundInCache)
{
SafeLog(
SafeLogInfo(
assemblyName,
() =>
{
if (EqtTrace.IsInfoEnabled)
{
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Resolved '{0}'", assemblyName);
}
});
"MSTest.AssemblyResolver.OnResolve: Resolved '{0}'",
assemblyName);
return true;
}

return false;
}

/// <summary>
/// Call logger APIs safely. We do not want a stackoverflow when objectmodel assembly itself
/// is being resolved and an EqtTrace message prompts the load of the same dll again.
/// CLR does not trigger a load when the EqtTrace messages are in a lambda expression. Leaving it that way
/// to preserve readability instead of creating wrapper functions.
/// Checks if it's safe to log for the given assembly name.
/// We do not want a stack overflow when objectmodel assembly itself
/// is being resolved and logging prompts the load of the same dll again.
/// </summary>
/// <param name="assemblyName">The assembly being resolved.</param>
/// <param name="loggerAction">The logger function.</param>
private static void SafeLog(string? assemblyName, Action loggerAction)
{
// Logger assembly was in `Microsoft.VisualStudio.TestPlatform.ObjectModel` assembly in legacy versions and we need to omit it as well.
if (!StringEx.IsNullOrEmpty(assemblyName)
/// <returns><c>true</c> if it's safe to log; <c>false</c> otherwise.</returns>
// Logger assembly was in `Microsoft.VisualStudio.TestPlatform.ObjectModel` assembly in legacy versions and we need to omit it as well.
private bool CanSafelyLog(string? assemblyName) =>
Logger is not null
&& !StringEx.IsNullOrEmpty(assemblyName)
&& !assemblyName.StartsWith(LoggerAssemblyName, StringComparison.Ordinal)
&& !assemblyName.StartsWith(LoggerAssemblyNameLegacy, StringComparison.Ordinal)
&& !assemblyName.StartsWith(PlatformServicesResourcesName, StringComparison.Ordinal))
&& !assemblyName.StartsWith(PlatformServicesResourcesName, StringComparison.Ordinal);

/// <summary>
/// Safely logs an info message if logging is available and safe.
/// </summary>
private void SafeLogInfo(string? assemblyName, string format, params object?[] args)
{
if (CanSafelyLog(assemblyName) && Logger!.IsInfoEnabled)
{
loggerAction.Invoke();
Logger.LogInfo(format, args);
}
}

/// <summary>
/// Safely logs a warning message if logging is available and safe.
/// </summary>
private void SafeLogWarning(string? assemblyName, string format, params object?[] args)
{
if (CanSafelyLog(assemblyName) && Logger!.IsWarningEnabled)
{
Logger.LogWarning(format, args);
}
}

/// <summary>
/// Safely logs a verbose message if logging is available and safe.
/// </summary>
private void SafeLogVerbose(string? assemblyName, string format, params object?[] args)
{
if (CanSafelyLog(assemblyName) && Logger!.IsVerboseEnabled)
{
Logger.LogVerbose(format, args);
}
}

Expand Down Expand Up @@ -715,29 +726,20 @@ private static void SafeLog(string? assemblyName, Action loggerAction)
_resolvedAssemblies[assemblyName] = assembly;
}

SafeLog(
SafeLogInfo(
assemblyName,
() =>
{
if (EqtTrace.IsInfoEnabled)
{
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Resolved assembly '{0}'", assemblyName);
}
});
"MSTest.AssemblyResolver.OnResolve: Resolved assembly '{0}'",
assemblyName);

return assembly;
}
catch (FileLoadException ex)
{
SafeLog(
SafeLogInfo(
assemblyName,
() =>
{
if (EqtTrace.IsInfoEnabled)
{
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Failed to load assembly '{0}'. Reason:{1} ", assemblyName, ex);
}
});
"MSTest.AssemblyResolver.OnResolve: Failed to load assembly '{0}'. Reason:{1} ",
assemblyName,
ex);

// Re-throw FileLoadException, because this exception means that the assembly
// was found, but could not be loaded. This will allow us to report a more
Expand All @@ -747,15 +749,11 @@ private static void SafeLog(string? assemblyName, Action loggerAction)
catch (Exception ex)
{
// For all other exceptions, try the next extension.
SafeLog(
SafeLogInfo(
assemblyName,
() =>
{
if (EqtTrace.IsInfoEnabled)
{
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Failed to load assembly '{0}'. Reason:{1} ", assemblyName, ex);
}
});
"MSTest.AssemblyResolver.OnResolve: Failed to load assembly '{0}'. Reason:{1} ",
assemblyName,
ex);
}

return null;
Expand Down
Loading