File: AnalyzerConsistencyCheckerTests.cs
Web Access
Project: src\src\Compilers\Server\VBCSCompilerTests\VBCSCompiler.UnitTests.csproj (VBCSCompiler.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CommandLine;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Basic.Reference.Assemblies;
 
namespace Microsoft.CodeAnalysis.CompilerServer.UnitTests
{
    [CollectionDefinition(Name)]
    public class AssemblyLoadTestFixtureCollection : ICollectionFixture<AssemblyLoadTestFixture>
    {
        public const string Name = nameof(AssemblyLoadTestFixtureCollection);
        private AssemblyLoadTestFixtureCollection() { }
    }
 
    [Collection(AssemblyLoadTestFixtureCollection.Name)]
    public class AnalyzerConsistencyCheckerTests : TestBase
    {
        private ICompilerServerLogger Logger { get; }
        private AssemblyLoadTestFixture TestFixture { get; }
 
        public AnalyzerConsistencyCheckerTests(ITestOutputHelper testOutputHelper, AssemblyLoadTestFixture testFixture)
        {
            Logger = new XunitCompilerServerLogger(testOutputHelper);
            TestFixture = testFixture;
        }
 
        private TempFile CreateNetStandardDll(TempDirectory directory, string assemblyName, string version, ImmutableArray<byte> publicKey, string? extraSource = null)
        {
            var source = $$"""
                using System;
                using System.Reflection;
 
                [assembly: AssemblyVersion("{{version}}")]
                [assembly: AssemblyFileVersion("{{version}}")]
                """;
 
            var sources = extraSource is null
                ? new[] { CSharpTestSource.Parse(source) }
                : new[] { CSharpTestSource.Parse(source), CSharpTestSource.Parse(extraSource) };
 
            var options = new CSharpCompilationOptions(
                OutputKind.DynamicallyLinkedLibrary,
                warningLevel: Diagnostic.MaxWarningLevel,
                cryptoPublicKey: publicKey,
                deterministic: true,
                publicSign: true);
 
            var comp = CSharpCompilation.Create(
                assemblyName,
                sources,
                references: NetStandard20.References.All,
                options: options);
 
            var file = directory.CreateFile($"{assemblyName}.dll");
            var emitResult = comp.Emit(file.Path);
            Assert.Empty(emitResult.Diagnostics.Where(x => x.Severity == DiagnosticSeverity.Error));
            return file;
        }
 
        /// <summary>
        /// Must support loading a DLL without all of it's references being present. It is common for analyzer
        /// assemblies to have missing references because customers often pair code fixers and analyzers into
        /// the same assembly.Here Alpha depends on Gamma which is not included 
        /// </summary>
        [Fact]
        public void LoadLibraryWithMissingReference()
        {
            var directory = Temp.CreateDirectory();
            _ = directory.CopyFile(TestFixture.Alpha);
            var assemblyLoader = DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(directory.CreateDirectory("shadow").Path);
 
            var analyzerReferences = ImmutableArray.Create(new CommandLineAnalyzerReference("Alpha.dll"));
            var result = AnalyzerConsistencyChecker.Check(directory.Path, analyzerReferences, assemblyLoader, Logger);
            Assert.True(result);
        }
 
        [Fact]
        public void LoadLibraryAll()
        {
            var directory = Temp.CreateDirectory();
            var assemblyLoader = DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(directory.CreateDirectory("shadow").Path);
            var analyzerReferences = ImmutableArray.Create(
                new CommandLineAnalyzerReference("Alpha.dll"),
                new CommandLineAnalyzerReference("Beta.dll"),
                new CommandLineAnalyzerReference("Gamma.dll"),
                new CommandLineAnalyzerReference("Delta.dll"));
 
            var result = AnalyzerConsistencyChecker.Check(Path.GetDirectoryName(TestFixture.Alpha)!, analyzerReferences, assemblyLoader, Logger);
            Assert.True(result);
        }
 
        [Fact]
        public void DifferingMvidsDifferentDirectory()
        {
            var directory = Temp.CreateDirectory();
            var assemblyLoader = DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(directory.CreateDirectory("shadow").Path);
 
            var key = NetStandard20.References.netstandard.GetAssemblyIdentity().PublicKey;
            var mvidAlpha1 = CreateNetStandardDll(directory.CreateDirectory("mvid1"), "MvidAlpha", "1.0.0.0", key, "class C { }");
            var mvidAlpha2 = CreateNetStandardDll(directory.CreateDirectory("mvid2"), "MvidAlpha", "1.0.0.0", key, "class D { }");
 
            var analyzerReferences = ImmutableArray.Create(
                new CommandLineAnalyzerReference(mvidAlpha1.Path),
                new CommandLineAnalyzerReference(mvidAlpha2.Path));
 
            var result = AnalyzerConsistencyChecker.Check(directory.Path, analyzerReferences, assemblyLoader, Logger);
 
#if NETFRAMEWORK
            Assert.False(result);
#else
            // In .NET Core assembly loading is partitioned per directory so it's possible to load the same 
            // simple name with different MVID
            Assert.True(result);
#endif
        }
 
        [Fact]
        public void DifferingMvidsSameDirectory()
        {
            var directory = Temp.CreateDirectory();
            var assemblyLoader = DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(directory.CreateDirectory("shadow").Path);
 
            var key = NetStandard20.References.netstandard.GetAssemblyIdentity().PublicKey;
            var mvidAlpha1 = CreateNetStandardDll(directory, "MvidAlpha", "1.0.0.0", key, "class C { }");
 
            var result = AnalyzerConsistencyChecker.Check(
                directory.Path,
                ImmutableArray.Create(new CommandLineAnalyzerReference(mvidAlpha1.Path)),
                assemblyLoader,
                Logger);
            Assert.True(result);
 
            File.Delete(mvidAlpha1.Path);
            var mvidAlpha2 = CreateNetStandardDll(directory, "MvidAlpha", "1.0.0.0", key, "class D { }");
            Assert.Equal(mvidAlpha1.Path, mvidAlpha2.Path);
            result = AnalyzerConsistencyChecker.Check(
                directory.Path,
                ImmutableArray.Create(new CommandLineAnalyzerReference(mvidAlpha2.Path)),
                assemblyLoader,
                Logger,
                out List<string>? errorMessages);
            Assert.False(result);
            Assert.NotNull(errorMessages);
 
            // Both the original and failed paths need to appear in the message, not the shadow copy 
            // paths
            var errorMessage = errorMessages!.Single();
            Assert.Contains(mvidAlpha1.Path, errorMessage);
            Assert.Contains(mvidAlpha2.Path, errorMessage);
        }
 
        /// <summary>
        /// A differing MVID is okay when it's loading a DLL from the compiler directory. That is 
        /// considered an exchange type. For example if an analyzer has a reference to System.Memory
        /// it will always load the copy the compiler used and that is not a consistency issue.
        /// </summary>
        [Fact]
        [WorkItem(64826, "https://github.com/dotnet/roslyn/issues/64826")]
        public void LoadingLibraryFromCompiler()
        {
            var directory = Temp.CreateDirectory();
            _ = CreateNetStandardDll(directory, "System.Memory", "2.0.0.0", NetStandard20.References.netstandard.GetAssemblyIdentity().PublicKey);
 
            // This test must use the DefaultAnalyzerAssemblyLoader as we want assembly binding redirects
            // to take affect here.
            var assemblyLoader = new DefaultAnalyzerAssemblyLoader();
            var analyzerReferences = ImmutableArray.Create(
                new CommandLineAnalyzerReference("System.Memory.dll"));
 
            var result = AnalyzerConsistencyChecker.Check(directory.Path, analyzerReferences, assemblyLoader, Logger);
 
            Assert.True(result);
        }
 
        /// <summary>
        /// A differing MVID is okay when it's loading a DLL from the GAC. There is no reason that 
        /// falling back to csc would change the load result.
        /// </summary>
        [Fact]
        [WorkItem(64826, "https://github.com/dotnet/roslyn/issues/64826")]
        public void LoadingLibraryFromRuntime()
        {
            var directory = Temp.CreateDirectory();
            var dllFile = directory.CreateFile("System.Core.dll");
            dllFile.WriteAllBytes(NetStandard20.Resources.SystemCore);
 
            // This test must use the DefaultAnalyzerAssemblyLoader as we want assembly binding redirects
            // to take affect here.
            var assemblyLoader = new DefaultAnalyzerAssemblyLoader();
            var analyzerReferences = ImmutableArray.Create(
                new CommandLineAnalyzerReference("System.Core.dll"));
 
            var result = AnalyzerConsistencyChecker.Check(directory.Path, analyzerReferences, assemblyLoader, Logger);
 
#if NETFRAMEWORK
            Assert.True(assemblyLoader.LoadFromPath(dllFile.Path).GlobalAssemblyCache);
#endif
 
            Assert.True(result);
        }
 
        [Fact]
        public void AssemblyLoadException()
        {
            var directory = Temp.CreateDirectory();
            directory.CopyFile(TestFixture.Delta1);
 
            var analyzerReferences = ImmutableArray.Create(
                new CommandLineAnalyzerReference("Delta.dll"));
 
            var result = AnalyzerConsistencyChecker.Check(directory.Path, analyzerReferences, new ThrowingLoader(), Logger);
 
            Assert.False(result);
        }
 
        [Fact]
        public void LoadingSimpleLibrary()
        {
            var directory = Temp.CreateDirectory();
            var key = NetStandard20.References.netstandard.GetAssemblyIdentity().PublicKey;
            var compFile = CreateNetStandardDll(directory, "netstandardRef", "1.0.0.0", key);
 
            var analyzerReferences = ImmutableArray.Create(new CommandLineAnalyzerReference(compFile.Path));
 
            var result = AnalyzerConsistencyChecker.Check(directory.Path, analyzerReferences, new DefaultAnalyzerAssemblyLoader(), Logger);
 
            Assert.True(result);
        }
    }
 
    file sealed class ThrowingLoader : IAnalyzerAssemblyLoaderInternal
    {
        public void AddDependencyLocation(string fullPath) { }
        public bool IsHostAssembly(Assembly assembly) => false;
        public Assembly LoadFromPath(string fullPath) => throw new Exception();
        public string? GetOriginalDependencyLocation(AssemblyName assembly) => throw new Exception();
        public void Dispose() { }
    }
}