-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Restore VisualStudioSourceInformationProvider for #433
- Loading branch information
1 parent
d71328f
commit 8cbe1c3
Showing
6 changed files
with
320 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
#if NETFRAMEWORK | ||
|
||
using System; | ||
using System.IO; | ||
using System.Reflection; | ||
using System.Runtime.ExceptionServices; | ||
using System.Security; | ||
using System.Security.Permissions; | ||
using Xunit.Internal; | ||
|
||
namespace Xunit.Runner.VisualStudio; | ||
|
||
class AppDomainManager | ||
{ | ||
readonly AppDomain appDomain; | ||
|
||
public AppDomainManager(string assemblyFileName) | ||
{ | ||
Guard.ArgumentNotNullOrEmpty(assemblyFileName); | ||
|
||
assemblyFileName = Path.GetFullPath(assemblyFileName); | ||
Guard.FileExists(assemblyFileName); | ||
|
||
var applicationBase = Path.GetDirectoryName(assemblyFileName); | ||
var applicationName = Guid.NewGuid().ToString(); | ||
var setup = new AppDomainSetup | ||
{ | ||
ApplicationBase = applicationBase, | ||
ApplicationName = applicationName, | ||
ShadowCopyFiles = "true", | ||
ShadowCopyDirectories = applicationBase, | ||
CachePath = Path.Combine(Path.GetTempPath(), applicationName) | ||
}; | ||
|
||
appDomain = AppDomain.CreateDomain(Path.GetFileNameWithoutExtension(assemblyFileName), AppDomain.CurrentDomain.Evidence, setup, new PermissionSet(PermissionState.Unrestricted)); | ||
} | ||
|
||
public TObject? CreateObject<TObject>( | ||
AssemblyName assemblyName, | ||
string typeName, | ||
params object[] args) | ||
where TObject : class | ||
{ | ||
try | ||
{ | ||
return appDomain.CreateInstanceAndUnwrap(assemblyName.FullName, typeName, false, BindingFlags.Default, null, args, null, null) as TObject; | ||
} | ||
catch (TargetInvocationException ex) | ||
{ | ||
ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); | ||
return default; // Will never reach here, but the compiler doesn't know that | ||
} | ||
} | ||
|
||
public virtual void Dispose() | ||
{ | ||
if (appDomain is not null) | ||
{ | ||
var cachePath = appDomain.SetupInformation.CachePath; | ||
|
||
try | ||
{ | ||
AppDomain.Unload(appDomain); | ||
|
||
if (cachePath is not null) | ||
Directory.Delete(cachePath, true); | ||
} | ||
catch { } | ||
} | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
src/xunit.runner.visualstudio/Utility/DiaSessionWrapper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
using System; | ||
using Microsoft.VisualStudio.TestPlatform.ObjectModel; | ||
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Navigation; | ||
using Xunit.Internal; | ||
using Xunit.Runner.Common; | ||
|
||
namespace Xunit.Runner.VisualStudio; | ||
|
||
// This class wraps DiaSession, and uses DiaSessionWrapperHelper to discover when a test is an async test | ||
// (since that requires special handling by DIA). The wrapper helper needs to exist in a separate AppDomain | ||
// so that we can do discovery without locking the assembly under test (for .NET Framework). | ||
class DiaSessionWrapper : IDisposable | ||
{ | ||
#if NETFRAMEWORK | ||
readonly AppDomainManager? appDomainManager; | ||
#endif | ||
readonly DiaSessionWrapperHelper? helper; | ||
readonly DiaSession? session; | ||
readonly DiagnosticMessageSink diagnosticMessageSink; | ||
|
||
public DiaSessionWrapper( | ||
string assemblyFileName, | ||
DiagnosticMessageSink diagnosticMessageSink) | ||
{ | ||
this.diagnosticMessageSink = Guard.ArgumentNotNull(diagnosticMessageSink); | ||
|
||
try | ||
{ | ||
session = new DiaSession(assemblyFileName); | ||
} | ||
catch (Exception ex) | ||
{ | ||
diagnosticMessageSink.OnMessage(new InternalDiagnosticMessage($"Exception creating DiaSession: {ex}")); | ||
} | ||
|
||
try | ||
{ | ||
#if NETFRAMEWORK | ||
var adapterFileName = typeof(DiaSessionWrapperHelper).Assembly.GetLocalCodeBase(); | ||
if (adapterFileName is not null) | ||
{ | ||
appDomainManager = new AppDomainManager(assemblyFileName); | ||
helper = appDomainManager.CreateObject<DiaSessionWrapperHelper>(typeof(DiaSessionWrapperHelper).Assembly.GetName(), typeof(DiaSessionWrapperHelper).FullName!, adapterFileName); | ||
} | ||
#else | ||
helper = new DiaSessionWrapperHelper(assemblyFileName); | ||
#endif | ||
} | ||
catch (Exception ex) | ||
{ | ||
diagnosticMessageSink.OnMessage(new DiagnosticMessage($"Exception creating DiaSessionWrapperHelper: {ex}")); | ||
} | ||
} | ||
|
||
public INavigationData? GetNavigationData( | ||
string typeName, | ||
string methodName) | ||
{ | ||
if (session is null || helper is null) | ||
return null; | ||
|
||
try | ||
{ | ||
helper.Normalize(ref typeName, ref methodName); | ||
return session.GetNavigationDataForMethod(typeName, methodName); | ||
} | ||
catch (Exception ex) | ||
{ | ||
diagnosticMessageSink.OnMessage(new DiagnosticMessage($"Exception getting source mapping for {typeName}.{methodName}: {ex}")); | ||
return null; | ||
} | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
session?.Dispose(); | ||
#if NETFRAMEWORK | ||
appDomainManager?.Dispose(); | ||
#endif | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
src/xunit.runner.visualstudio/Utility/DiaSessionWrapperHelper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using Xunit.Internal; | ||
using Xunit.Sdk; | ||
|
||
namespace Xunit.Runner.VisualStudio; | ||
|
||
class DiaSessionWrapperHelper : LongLivedMarshalByRefObject | ||
{ | ||
readonly Assembly? assembly; | ||
readonly Dictionary<string, Type> typeNameMap; | ||
|
||
public DiaSessionWrapperHelper(string assemblyFileName) | ||
{ | ||
try | ||
{ | ||
#if NETFRAMEWORK | ||
assembly = Assembly.ReflectionOnlyLoadFrom(assemblyFileName); | ||
var assemblyDirectory = Path.GetDirectoryName(assemblyFileName); | ||
|
||
if (assemblyDirectory is not null) | ||
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += (sender, args) => | ||
{ | ||
try | ||
{ | ||
// Try to load it normally | ||
var name = AppDomain.CurrentDomain.ApplyPolicy(args.Name); | ||
return Assembly.ReflectionOnlyLoad(name); | ||
} | ||
catch | ||
{ | ||
try | ||
{ | ||
// If a normal implicit load fails, try to load it from the directory that | ||
// the test assembly lives in | ||
return Assembly.ReflectionOnlyLoadFrom( | ||
Path.Combine( | ||
assemblyDirectory, | ||
new AssemblyName(args.Name).Name + ".dll" | ||
) | ||
); | ||
} | ||
catch | ||
{ | ||
// If all else fails, say we couldn't find it | ||
return null; | ||
} | ||
} | ||
}; | ||
#else | ||
assembly = Assembly.Load(new AssemblyName { Name = Path.GetFileNameWithoutExtension(assemblyFileName) }); | ||
#endif | ||
} | ||
catch { } | ||
|
||
if (assembly is not null) | ||
{ | ||
Type?[]? types = null; | ||
|
||
try | ||
{ | ||
types = assembly.GetTypes(); | ||
} | ||
catch (ReflectionTypeLoadException ex) | ||
{ | ||
types = ex.Types; | ||
} | ||
catch { } // Ignore anything other than ReflectionTypeLoadException | ||
|
||
if (types is not null) | ||
typeNameMap = | ||
types | ||
.WhereNotNull() | ||
.Where(t => !string.IsNullOrEmpty(t.FullName)) | ||
.ToDictionaryIgnoringDuplicateKeys(k => k.FullName!); | ||
} | ||
|
||
typeNameMap ??= []; | ||
} | ||
|
||
public void Normalize( | ||
ref string typeName, | ||
ref string methodName) | ||
{ | ||
try | ||
{ | ||
if (assembly is null) | ||
return; | ||
|
||
if (typeNameMap.TryGetValue(typeName, out var type) && type is not null) | ||
{ | ||
var method = type.GetMethod(methodName); | ||
if (method is not null && method.DeclaringType is not null && method.DeclaringType.FullName is not null) | ||
{ | ||
// DiaSession only ever wants you to ask for the declaring type | ||
typeName = method.DeclaringType.FullName; | ||
|
||
// See if this is an async method by looking for [AsyncStateMachine] on the method, | ||
// which means we need to pass the state machine's "MoveNext" method. | ||
var stateMachineType = method.GetCustomAttribute<AsyncStateMachineAttribute>()?.StateMachineType; | ||
if (stateMachineType is not null && stateMachineType.FullName is not null) | ||
{ | ||
typeName = stateMachineType.FullName; | ||
methodName = "MoveNext"; | ||
} | ||
} | ||
} | ||
} | ||
catch { } | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
src/xunit.runner.visualstudio/Utility/VisualStudioSourceInformationProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
using System.Threading.Tasks; | ||
using Xunit.Runner.Common; | ||
using Xunit.Sdk; | ||
|
||
namespace Xunit.Runner.VisualStudio; | ||
|
||
/// <summary> | ||
/// An implementation of <see cref="ISourceInformationProvider"/> that will provide source information | ||
/// when running inside of Visual Studio (via the DiaSession class). | ||
/// </summary> | ||
/// <param name="assemblyFileName">The assembly file name.</param> | ||
/// <param name="diagnosticMessageSink">The message sink to send internal diagnostic messages to.</param> | ||
internal class VisualStudioSourceInformationProvider( | ||
string assemblyFileName, | ||
DiagnosticMessageSink diagnosticMessageSink) : | ||
LongLivedMarshalByRefObject, ISourceInformationProvider | ||
{ | ||
static readonly SourceInformation EmptySourceInformation = new(); | ||
|
||
readonly DiaSessionWrapper session = new DiaSessionWrapper(assemblyFileName, diagnosticMessageSink); | ||
|
||
/// <inheritdoc/> | ||
public SourceInformation GetSourceInformation( | ||
string? testClassName, | ||
string? testMethodName) | ||
{ | ||
if (testClassName is null || testMethodName is null) | ||
return EmptySourceInformation; | ||
|
||
var navData = session.GetNavigationData(testClassName, testMethodName); | ||
if (navData is null || navData.FileName is null) | ||
return EmptySourceInformation; | ||
|
||
return new SourceInformation(navData.FileName, navData.MinLineNumber); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public ValueTask DisposeAsync() | ||
{ | ||
session.Dispose(); | ||
|
||
return default; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters