-
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.
Copy (and simplify) VisualStudioSourceInformationProvider from xunit.…
…runner.utility
- Loading branch information
1 parent
3974475
commit 85864c8
Showing
8 changed files
with
343 additions
and
36 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 |
52 changes: 52 additions & 0 deletions
52
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,52 @@ | ||
using System; | ||
using Microsoft.VisualStudio.TestPlatform.ObjectModel; | ||
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Navigation; | ||
|
||
namespace Xunit.Runner.VisualStudio.Utility; | ||
|
||
// 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; | ||
|
||
public DiaSessionWrapper(string assemblyFileName) | ||
{ | ||
session = new DiaSession(assemblyFileName); | ||
|
||
#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 | ||
} | ||
|
||
public INavigationData? GetNavigationData( | ||
string typeName, | ||
string methodName) | ||
{ | ||
if (helper is null) | ||
return null; | ||
|
||
helper.Normalize(ref typeName, ref methodName); | ||
return session.GetNavigationDataForMethod(typeName, methodName); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
session.Dispose(); | ||
#if NETFRAMEWORK | ||
appDomainManager?.Dispose(); | ||
#endif | ||
} | ||
} |
114 changes: 114 additions & 0 deletions
114
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,114 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using Xunit.Sdk; | ||
|
||
namespace Xunit.Runner.VisualStudio.Utility; | ||
|
||
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 ??= new(); | ||
} | ||
|
||
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 { } | ||
} | ||
} |
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
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
45 changes: 45 additions & 0 deletions
45
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,45 @@ | ||
using Xunit.Abstractions; | ||
using Xunit.Runner.VisualStudio.Utility; | ||
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> | ||
public class VisualStudioSourceInformationProvider : LongLivedMarshalByRefObject, ISourceInformationProvider | ||
{ | ||
static readonly SourceInformation EmptySourceInformation = new(); | ||
|
||
readonly DiaSessionWrapper session; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="VisualStudioSourceInformationProvider" /> class. | ||
/// </summary> | ||
/// <param name="assemblyFileName">The assembly file name.</param> | ||
public VisualStudioSourceInformationProvider(string assemblyFileName) | ||
{ | ||
session = new DiaSessionWrapper(assemblyFileName); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public ISourceInformation GetSourceInformation(ITestCase testCase) | ||
{ | ||
var navData = session.GetNavigationData(testCase.TestMethod.TestClass.Class.Name, testCase.TestMethod.Method.Name); | ||
if (navData is null || navData.FileName is null) | ||
return EmptySourceInformation; | ||
|
||
return new SourceInformation | ||
{ | ||
FileName = navData.FileName, | ||
LineNumber = navData.MinLineNumber | ||
}; | ||
} | ||
|
||
/// <inheritdoc/> | ||
public void Dispose() | ||
{ | ||
session.Dispose(); | ||
} | ||
} |
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
Oops, something went wrong.