Skip to content

Commit 8cbe1c3

Browse files
committed
Restore VisualStudioSourceInformationProvider for #433
1 parent d71328f commit 8cbe1c3

6 files changed

+320
-2
lines changed
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#if NETFRAMEWORK
2+
3+
using System;
4+
using System.IO;
5+
using System.Reflection;
6+
using System.Runtime.ExceptionServices;
7+
using System.Security;
8+
using System.Security.Permissions;
9+
using Xunit.Internal;
10+
11+
namespace Xunit.Runner.VisualStudio;
12+
13+
class AppDomainManager
14+
{
15+
readonly AppDomain appDomain;
16+
17+
public AppDomainManager(string assemblyFileName)
18+
{
19+
Guard.ArgumentNotNullOrEmpty(assemblyFileName);
20+
21+
assemblyFileName = Path.GetFullPath(assemblyFileName);
22+
Guard.FileExists(assemblyFileName);
23+
24+
var applicationBase = Path.GetDirectoryName(assemblyFileName);
25+
var applicationName = Guid.NewGuid().ToString();
26+
var setup = new AppDomainSetup
27+
{
28+
ApplicationBase = applicationBase,
29+
ApplicationName = applicationName,
30+
ShadowCopyFiles = "true",
31+
ShadowCopyDirectories = applicationBase,
32+
CachePath = Path.Combine(Path.GetTempPath(), applicationName)
33+
};
34+
35+
appDomain = AppDomain.CreateDomain(Path.GetFileNameWithoutExtension(assemblyFileName), AppDomain.CurrentDomain.Evidence, setup, new PermissionSet(PermissionState.Unrestricted));
36+
}
37+
38+
public TObject? CreateObject<TObject>(
39+
AssemblyName assemblyName,
40+
string typeName,
41+
params object[] args)
42+
where TObject : class
43+
{
44+
try
45+
{
46+
return appDomain.CreateInstanceAndUnwrap(assemblyName.FullName, typeName, false, BindingFlags.Default, null, args, null, null) as TObject;
47+
}
48+
catch (TargetInvocationException ex)
49+
{
50+
ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
51+
return default; // Will never reach here, but the compiler doesn't know that
52+
}
53+
}
54+
55+
public virtual void Dispose()
56+
{
57+
if (appDomain is not null)
58+
{
59+
var cachePath = appDomain.SetupInformation.CachePath;
60+
61+
try
62+
{
63+
AppDomain.Unload(appDomain);
64+
65+
if (cachePath is not null)
66+
Directory.Delete(cachePath, true);
67+
}
68+
catch { }
69+
}
70+
}
71+
}
72+
73+
#endif

Diff for: src/xunit.runner.visualstudio/Utility/AssemblyExtensions.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
using System.Reflection;
2+
3+
#if NETFRAMEWORK
14
using System;
25
using System.IO;
3-
using System.Reflection;
6+
#endif
47

58
internal static class AssemblyExtensions
69
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
3+
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Navigation;
4+
using Xunit.Internal;
5+
using Xunit.Runner.Common;
6+
7+
namespace Xunit.Runner.VisualStudio;
8+
9+
// This class wraps DiaSession, and uses DiaSessionWrapperHelper to discover when a test is an async test
10+
// (since that requires special handling by DIA). The wrapper helper needs to exist in a separate AppDomain
11+
// so that we can do discovery without locking the assembly under test (for .NET Framework).
12+
class DiaSessionWrapper : IDisposable
13+
{
14+
#if NETFRAMEWORK
15+
readonly AppDomainManager? appDomainManager;
16+
#endif
17+
readonly DiaSessionWrapperHelper? helper;
18+
readonly DiaSession? session;
19+
readonly DiagnosticMessageSink diagnosticMessageSink;
20+
21+
public DiaSessionWrapper(
22+
string assemblyFileName,
23+
DiagnosticMessageSink diagnosticMessageSink)
24+
{
25+
this.diagnosticMessageSink = Guard.ArgumentNotNull(diagnosticMessageSink);
26+
27+
try
28+
{
29+
session = new DiaSession(assemblyFileName);
30+
}
31+
catch (Exception ex)
32+
{
33+
diagnosticMessageSink.OnMessage(new InternalDiagnosticMessage($"Exception creating DiaSession: {ex}"));
34+
}
35+
36+
try
37+
{
38+
#if NETFRAMEWORK
39+
var adapterFileName = typeof(DiaSessionWrapperHelper).Assembly.GetLocalCodeBase();
40+
if (adapterFileName is not null)
41+
{
42+
appDomainManager = new AppDomainManager(assemblyFileName);
43+
helper = appDomainManager.CreateObject<DiaSessionWrapperHelper>(typeof(DiaSessionWrapperHelper).Assembly.GetName(), typeof(DiaSessionWrapperHelper).FullName!, adapterFileName);
44+
}
45+
#else
46+
helper = new DiaSessionWrapperHelper(assemblyFileName);
47+
#endif
48+
}
49+
catch (Exception ex)
50+
{
51+
diagnosticMessageSink.OnMessage(new DiagnosticMessage($"Exception creating DiaSessionWrapperHelper: {ex}"));
52+
}
53+
}
54+
55+
public INavigationData? GetNavigationData(
56+
string typeName,
57+
string methodName)
58+
{
59+
if (session is null || helper is null)
60+
return null;
61+
62+
try
63+
{
64+
helper.Normalize(ref typeName, ref methodName);
65+
return session.GetNavigationDataForMethod(typeName, methodName);
66+
}
67+
catch (Exception ex)
68+
{
69+
diagnosticMessageSink.OnMessage(new DiagnosticMessage($"Exception getting source mapping for {typeName}.{methodName}: {ex}"));
70+
return null;
71+
}
72+
}
73+
74+
public void Dispose()
75+
{
76+
session?.Dispose();
77+
#if NETFRAMEWORK
78+
appDomainManager?.Dispose();
79+
#endif
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Reflection;
6+
using System.Runtime.CompilerServices;
7+
using Xunit.Internal;
8+
using Xunit.Sdk;
9+
10+
namespace Xunit.Runner.VisualStudio;
11+
12+
class DiaSessionWrapperHelper : LongLivedMarshalByRefObject
13+
{
14+
readonly Assembly? assembly;
15+
readonly Dictionary<string, Type> typeNameMap;
16+
17+
public DiaSessionWrapperHelper(string assemblyFileName)
18+
{
19+
try
20+
{
21+
#if NETFRAMEWORK
22+
assembly = Assembly.ReflectionOnlyLoadFrom(assemblyFileName);
23+
var assemblyDirectory = Path.GetDirectoryName(assemblyFileName);
24+
25+
if (assemblyDirectory is not null)
26+
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += (sender, args) =>
27+
{
28+
try
29+
{
30+
// Try to load it normally
31+
var name = AppDomain.CurrentDomain.ApplyPolicy(args.Name);
32+
return Assembly.ReflectionOnlyLoad(name);
33+
}
34+
catch
35+
{
36+
try
37+
{
38+
// If a normal implicit load fails, try to load it from the directory that
39+
// the test assembly lives in
40+
return Assembly.ReflectionOnlyLoadFrom(
41+
Path.Combine(
42+
assemblyDirectory,
43+
new AssemblyName(args.Name).Name + ".dll"
44+
)
45+
);
46+
}
47+
catch
48+
{
49+
// If all else fails, say we couldn't find it
50+
return null;
51+
}
52+
}
53+
};
54+
#else
55+
assembly = Assembly.Load(new AssemblyName { Name = Path.GetFileNameWithoutExtension(assemblyFileName) });
56+
#endif
57+
}
58+
catch { }
59+
60+
if (assembly is not null)
61+
{
62+
Type?[]? types = null;
63+
64+
try
65+
{
66+
types = assembly.GetTypes();
67+
}
68+
catch (ReflectionTypeLoadException ex)
69+
{
70+
types = ex.Types;
71+
}
72+
catch { } // Ignore anything other than ReflectionTypeLoadException
73+
74+
if (types is not null)
75+
typeNameMap =
76+
types
77+
.WhereNotNull()
78+
.Where(t => !string.IsNullOrEmpty(t.FullName))
79+
.ToDictionaryIgnoringDuplicateKeys(k => k.FullName!);
80+
}
81+
82+
typeNameMap ??= [];
83+
}
84+
85+
public void Normalize(
86+
ref string typeName,
87+
ref string methodName)
88+
{
89+
try
90+
{
91+
if (assembly is null)
92+
return;
93+
94+
if (typeNameMap.TryGetValue(typeName, out var type) && type is not null)
95+
{
96+
var method = type.GetMethod(methodName);
97+
if (method is not null && method.DeclaringType is not null && method.DeclaringType.FullName is not null)
98+
{
99+
// DiaSession only ever wants you to ask for the declaring type
100+
typeName = method.DeclaringType.FullName;
101+
102+
// See if this is an async method by looking for [AsyncStateMachine] on the method,
103+
// which means we need to pass the state machine's "MoveNext" method.
104+
var stateMachineType = method.GetCustomAttribute<AsyncStateMachineAttribute>()?.StateMachineType;
105+
if (stateMachineType is not null && stateMachineType.FullName is not null)
106+
{
107+
typeName = stateMachineType.FullName;
108+
methodName = "MoveNext";
109+
}
110+
}
111+
}
112+
}
113+
catch { }
114+
}
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System.Threading.Tasks;
2+
using Xunit.Runner.Common;
3+
using Xunit.Sdk;
4+
5+
namespace Xunit.Runner.VisualStudio;
6+
7+
/// <summary>
8+
/// An implementation of <see cref="ISourceInformationProvider"/> that will provide source information
9+
/// when running inside of Visual Studio (via the DiaSession class).
10+
/// </summary>
11+
/// <param name="assemblyFileName">The assembly file name.</param>
12+
/// <param name="diagnosticMessageSink">The message sink to send internal diagnostic messages to.</param>
13+
internal class VisualStudioSourceInformationProvider(
14+
string assemblyFileName,
15+
DiagnosticMessageSink diagnosticMessageSink) :
16+
LongLivedMarshalByRefObject, ISourceInformationProvider
17+
{
18+
static readonly SourceInformation EmptySourceInformation = new();
19+
20+
readonly DiaSessionWrapper session = new DiaSessionWrapper(assemblyFileName, diagnosticMessageSink);
21+
22+
/// <inheritdoc/>
23+
public SourceInformation GetSourceInformation(
24+
string? testClassName,
25+
string? testMethodName)
26+
{
27+
if (testClassName is null || testMethodName is null)
28+
return EmptySourceInformation;
29+
30+
var navData = session.GetNavigationData(testClassName, testMethodName);
31+
if (navData is null || navData.FileName is null)
32+
return EmptySourceInformation;
33+
34+
return new SourceInformation(navData.FileName, navData.MinLineNumber);
35+
}
36+
37+
/// <inheritdoc/>
38+
public ValueTask DisposeAsync()
39+
{
40+
session.Dispose();
41+
42+
return default;
43+
}
44+
}

Diff for: src/xunit.runner.visualstudio/VsTestRunner.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,8 @@ async Task DiscoverTests<TVisitor>(
217217
var assemblyDisplayName = Path.GetFileNameWithoutExtension(assembly.AssemblyFileName);
218218
var diagnosticMessageSink = new DiagnosticMessageSink(logger, assemblyDisplayName, assembly.Configuration.DiagnosticMessagesOrDefault, assembly.Configuration.InternalDiagnosticMessagesOrDefault);
219219

220-
await using var controller = XunitFrontController.Create(assembly, null, diagnosticMessageSink);
220+
await using var sourceInformationProvider = new VisualStudioSourceInformationProvider(assemblyFileName, diagnosticMessageSink);
221+
await using var controller = XunitFrontController.Create(assembly, sourceInformationProvider, diagnosticMessageSink);
221222
if (controller is null)
222223
return;
223224

@@ -508,6 +509,7 @@ async Task RunTestsInAssembly(
508509
if (runContext.IsBeingDebugged && frameworkHandle2 is not null)
509510
testProcessLauncher = new DebuggerProcessLauncher(frameworkHandle2);
510511

512+
await using var sourceInformationProvider = new VisualStudioSourceInformationProvider(assemblyFileName, diagnosticSink);
511513
await using var controller = XunitFrontController.Create(runInfo.Assembly, null, diagnosticSink, testProcessLauncher);
512514
if (controller is null)
513515
return;

0 commit comments

Comments
 (0)