Skip to content

Commit

Permalink
Test case for analyzer dependency loading (#75487)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredpar authored Oct 14, 2024
1 parent 1772666 commit 14e053a
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 41 deletions.
232 changes: 220 additions & 12 deletions src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
using Xunit;
using Xunit.Abstractions;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.CodeAnalysis.Text;
using Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.Emit;

#if NET
using Roslyn.Test.Utilities.CoreClr;
Expand Down Expand Up @@ -95,27 +99,60 @@ public AnalyzerAssemblyLoaderTests(ITestOutputHelper testOutputHelper, AssemblyL

#if NET

private void Run(AnalyzerTestKind kind, Action<AnalyzerAssemblyLoader, AssemblyLoadTestFixture> testAction, IAnalyzerAssemblyResolver[]? externalResolvers = null, [CallerMemberName] string? memberName = null) =>
private void Run(
AnalyzerTestKind kind,
Action<AnalyzerAssemblyLoader, AssemblyLoadTestFixture> testAction,
IAnalyzerAssemblyResolver[]? externalResolvers = null,
[CallerMemberName] string? memberName = null) =>
Run(
kind,
static (_, _) => { },
testAction,
externalResolvers,
memberName);

private void Run(
AnalyzerTestKind kind,
object state,
Action<AnalyzerAssemblyLoader, AssemblyLoadTestFixture, object> testAction,
IAnalyzerAssemblyResolver[]? externalResolvers = null,
[CallerMemberName] string? memberName = null) =>
Run(
kind,
state,
static (_, _) => { },
testAction.Method,
externalResolvers,
memberName);

private void Run(
AnalyzerTestKind kind,
Action<AssemblyLoadContext, AssemblyLoadTestFixture> prepLoadContextAction,
Action<AnalyzerAssemblyLoader, AssemblyLoadTestFixture> testAction,
IAnalyzerAssemblyResolver[]? externalResolvers = null,
[CallerMemberName] string? memberName = null)
[CallerMemberName] string? memberName = null) =>
Run(
kind,
state: null,
prepLoadContextAction,
testAction.Method,
externalResolvers,
memberName);

private void Run(
AnalyzerTestKind kind,
object? state,
Action<AssemblyLoadContext, AssemblyLoadTestFixture> prepLoadContextAction,
MethodInfo method,
IAnalyzerAssemblyResolver[]? externalResolvers,
string? memberName)
{
var alc = new AssemblyLoadContext($"Test {memberName}", isCollectible: true);
try
{
prepLoadContextAction(alc, TestFixture);
var util = new InvokeUtil();
util.Exec(TestOutputHelper, alc, TestFixture, kind, testAction.Method.DeclaringType!.FullName!, testAction.Method.Name, externalResolvers ?? []);
util.Exec(TestOutputHelper, alc, TestFixture, kind, method.DeclaringType!.FullName!, method.Name, externalResolvers ?? [], state);
}
finally
{
Expand All @@ -129,7 +166,32 @@ private void Run(
AnalyzerTestKind kind,
Action<AnalyzerAssemblyLoader, AssemblyLoadTestFixture> testAction,
IAnalyzerAssemblyResolver[]? externalResolvers = null,
[CallerMemberName] string? memberName = null)
[CallerMemberName] string? memberName = null) =>
Run(
kind,
state: null,
testAction.Method,
externalResolvers,
memberName);

private void Run(
AnalyzerTestKind kind,
object state,
Action<AnalyzerAssemblyLoader, AssemblyLoadTestFixture, object> testAction,
IAnalyzerAssemblyResolver[]? externalResolvers = null,
[CallerMemberName] string? memberName = null) =>
Run(kind,
state,
testAction.Method,
externalResolvers,
memberName);

private void Run(
AnalyzerTestKind kind,
object state,
MethodInfo method,
IAnalyzerAssemblyResolver[]? externalResolvers,
string? memberName)
{
AppDomain? appDomain = null;
try
Expand All @@ -138,7 +200,7 @@ private void Run(
var testOutputHelper = new AppDomainTestOutputHelper(TestOutputHelper);
var type = typeof(InvokeUtil);
var util = (InvokeUtil)appDomain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName);
util.Exec(testOutputHelper, TestFixture, kind, testAction.Method.DeclaringType.FullName, testAction.Method.Name, externalResolvers ?? []);
util.Exec(testOutputHelper, TestFixture, kind, method.DeclaringType.FullName, method.Name, externalResolvers ?? [], state);
}
finally
{
Expand All @@ -153,7 +215,7 @@ private void Run(
/// us back to the actual test code to execute. The intent is to invoke the lambda / static
/// local func where the code exists.
/// </summary>
internal static void InvokeTestCode(AnalyzerAssemblyLoader loader, AssemblyLoadTestFixture fixture, string typeName, string methodName)
internal static void InvokeTestCode(AnalyzerAssemblyLoader loader, AssemblyLoadTestFixture fixture, string typeName, string methodName, object? state)
{
var type = typeof(AnalyzerAssemblyLoaderTests).Assembly.GetType(typeName, throwOnError: false)!;
var member = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)!;
Expand All @@ -164,7 +226,10 @@ internal static void InvokeTestCode(AnalyzerAssemblyLoader loader, AssemblyLoadT
? null
: type.Assembly.CreateInstance(typeName);

member.Invoke(obj, new object[] { loader, fixture });
object[] args = state is null
? [loader, fixture]
: [loader, fixture, state];
member.Invoke(obj, args);
}

[Theory]
Expand Down Expand Up @@ -346,6 +411,7 @@ private static void VerifyAssemblies(AnalyzerAssemblyLoader loader, IEnumerable<
Assert.Equal(
expected
.Select(x => (x.simpleName, x.version, getExpectedLoadPath(x.path)))
.OrderBy(static x => x)
.ToArray(),
assemblies.Select(assembly => (assembly.GetName().Name!, assembly.GetName().Version!.ToString(), assembly.Location))
.OrderBy(static x => x)
Expand Down Expand Up @@ -419,11 +485,8 @@ private static void VerifyDependencyAssemblies(AnalyzerAssemblyLoader loader, in
IEnumerable<Assembly> loadedAssemblies;

#if NET
// This verify only works where there is a single load context.
var alcs = loader.GetDirectoryLoadContextsSnapshot();
Assert.Equal(1, alcs.Length);

loadedAssemblies = alcs[0].Assemblies;
loadedAssemblies = alcs.SelectMany(x => x.Assemblies);
#else

// The assemblies in the LoadFrom context are the assemblies loaded from
Expand Down Expand Up @@ -590,7 +653,7 @@ public void AssemblyLoading_RazorCompiler2(AnalyzerTestKind kind)
VerifyDependencyAssemblies(
loader,
copyCount: copyCount,
deltaFile);
assemblyPaths: [deltaFile]);
});
}

Expand Down Expand Up @@ -1124,6 +1187,151 @@ public void AssemblyLoading_MultipleVersions_MissingVersion(AnalyzerTestKind kin
});
}

/// <summary>
/// Test the case where a utility is loaded by multiple analyzers at different versions. Ensure that no matter
/// what order we load the analyzers we correctly resolve the utility version.
/// </summary>
[Theory]
[CombinatorialData]
public void AssemblyLoading_MultipleVersions_AnalyzerDependency(AnalyzerTestKind kind, bool normalOrder)
{
Run(kind, state: normalOrder, static (AnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture, object state) =>
{
using var temp = new TempRoot();
var analyzerFilePaths = new List<string>();
var compilerReference = MetadataReference.CreateFromFile(typeof(SyntaxNode).Assembly.Location);
var immutableReference = MetadataReference.CreateFromFile(typeof(ImmutableArray).Assembly.Location);
var testCode = """
using System;
Console.WriteLine("Hello World");
""";
var compilation = CSharpCompilation.Create(
"test",
[CSharpSyntaxTree.ParseText(SourceText.From(testCode, encoding: null, checksumAlgorithm: SourceHashAlgorithms.Default))],
NetStandard20.References.All);
// Test loading the analyzers in different orders. That makes sure we verify the loading handles
// the higher version of delta being loaded first or second.
ImmutableArray<DiagnosticAnalyzer> analyzers = state is true
? [loadAnalyzer1(), loadAnalyzer2()]
: [loadAnalyzer2(), loadAnalyzer1()];
var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);
compilation.VerifyEmitDiagnostics();
Assert.Empty(compilationWithAnalyzers.GetAllDiagnosticsAsync().Result);
foreach (var analyzer in analyzers)
{
assertRan(analyzer);
}
VerifyDependencyAssemblies(loader, analyzerFilePaths.ToArray());
void assertRan(DiagnosticAnalyzer a)
{
var prop = a.GetType().GetProperty("Ran", BindingFlags.Public | BindingFlags.Instance);
Assert.NotNull(prop);
var value = prop.GetValue(a, null);
Assert.True(value is true);
}
DiagnosticAnalyzer loadAnalyzer1()
{
var code = """
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class Analyzer1: DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor Warning = new DiagnosticDescriptor(
"Warning2",
"",
"",
"",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray<DiagnosticDescriptor>.Empty.Add(Warning);
public bool Ran { get; set; }
public override void Initialize(AnalysisContext context)
{
var d = new Delta.D();
d.M1();
Ran = true;
}
}
""";
var assemblyFilePath = buildWithCode("analyzer1", code, testFixture.DeltaPublicSigned1);
var assembly = loader.LoadFromPath(assemblyFilePath);
return (DiagnosticAnalyzer)assembly.CreateInstance("Analyzer1")!;
}
DiagnosticAnalyzer loadAnalyzer2()
{
var code = """
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class Analyzer2: DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor Warning = new DiagnosticDescriptor(
"Warning1",
"",
"",
"",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray<DiagnosticDescriptor>.Empty.Add(Warning);
public bool Ran { get; set; }
public override void Initialize(AnalysisContext context)
{
var d = new Delta.D();
d.M2();
Ran = true;
}
}
""";
var assemblyFilePath = buildWithCode("analyzer2", code, testFixture.DeltaPublicSigned2);
var assembly = loader.LoadFromPath(assemblyFilePath);
return (DiagnosticAnalyzer)assembly.CreateInstance("Analyzer2")!;
}
string buildWithCode(string assemblyName, string analyzerCode, string deltaFilePath)
{
var dir = temp.CreateDirectory();
var deltaNewFilePath = dir.CopyFile(deltaFilePath).Path;
var compilation = CSharpCompilation.Create(
assemblyName,
[CSharpSyntaxTree.ParseText(SourceText.From(analyzerCode, encoding: null, checksumAlgorithm: SourceHashAlgorithms.Default))],
[
.. NetStandard20.References.All,
compilerReference,
immutableReference,
MetadataReference.CreateFromFile(deltaFilePath)
],
TestOptions.DebugDll.WithPublicSign(true).WithCryptoPublicKey(SigningTestHelpers.PublicKey));
var array = compilation.EmitToArray(EmitOptions.Default);
var assemblyFilePath = dir.CreateFile(assemblyName + ".dll").WriteAllBytes(array).Path;
loader.AddDependencyLocation(deltaNewFilePath);
analyzerFilePaths.Add(deltaNewFilePath);
loader.AddDependencyLocation(assemblyFilePath);
analyzerFilePaths.Add(assemblyFilePath);
return assemblyFilePath;
}
});
}

[Theory]
[CombinatorialData]
public void AssemblyLoading_UnifyToHighest(AnalyzerTestKind kind)
Expand Down
8 changes: 4 additions & 4 deletions src/Compilers/Core/CodeAnalysisTest/InvokeUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ namespace Microsoft.CodeAnalysis.UnitTests

public sealed class InvokeUtil
{
internal void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compilerContext, AssemblyLoadTestFixture fixture, AnalyzerTestKind kind, string typeName, string methodName, IAnalyzerAssemblyResolver[] externalResolvers)
internal void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compilerContext, AssemblyLoadTestFixture fixture, AnalyzerTestKind kind, string typeName, string methodName, IAnalyzerAssemblyResolver[] externalResolvers, object? state = null)
{
// Ensure that the test did not load any of the test fixture assemblies into
// the default load context. That should never happen. Assemblies should either
Expand All @@ -56,7 +56,7 @@ internal void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compi

try
{
AnalyzerAssemblyLoaderTests.InvokeTestCode(loader, fixture, typeName, methodName);
AnalyzerAssemblyLoaderTests.InvokeTestCode(loader, fixture, typeName, methodName, state);
}
finally
{
Expand Down Expand Up @@ -92,7 +92,7 @@ internal void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compi

public sealed class InvokeUtil : MarshalByRefObject
{
internal void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadTestFixture fixture, AnalyzerTestKind kind, string typeName, string methodName, IAnalyzerAssemblyResolver[] externalResolvers)
internal void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadTestFixture fixture, AnalyzerTestKind kind, string typeName, string methodName, IAnalyzerAssemblyResolver[] externalResolvers, object? state)
{
using var tempRoot = new TempRoot();
AnalyzerAssemblyLoader loader = kind switch
Expand All @@ -104,7 +104,7 @@ internal void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadTestFixture f

try
{
AnalyzerAssemblyLoaderTests.InvokeTestCode(loader, fixture, typeName, methodName);
AnalyzerAssemblyLoaderTests.InvokeTestCode(loader, fixture, typeName, methodName, state);
}
catch (TargetInvocationException ex) when (ex.InnerException is XunitException)
{
Expand Down
Loading

0 comments on commit 14e053a

Please sign in to comment.