File: CompilationCacheBehaviorTests.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.
 
#if NET
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis.CommandLine;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.CodeAnalysis.CompilerServer.UnitTests;
 
public sealed class CompilationCacheBehaviorTests(ITestOutputHelper testOutputHelper) : TestBase
{
    private readonly ICompilerServerLogger _logger = new XunitCompilerServerLogger(testOutputHelper);
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CacheHit_OmitsWarningDiagnosticsFromOutput(bool visualBasic)
    {
        var workingDirectory = Temp.CreateDirectory();
        var cacheDirectory = Temp.CreateDirectory();
        var supportAssembly = CreateWarningAnalyzerAssembly(workingDirectory);
        var sourceFileName = visualBasic ? "test.vb" : "test.cs";
        var outputFileName = "test.dll";
        var source = visualBasic
            ? """
            Public Class TestType
            End Class
            """
            : """
            public class TestType
            {
            }
            """;
 
        using var serverData = await ServerUtil.CreateServer(_logger);
        var arguments = BuildCompilationArguments(visualBasic, serverData.PipeName, sourceFileName, outputFileName, additionalArguments: $"/a:{supportAssembly}", cachePath: cacheDirectory.Path);
 
        var (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), arguments, workingDirectory, [new(sourceFileName, source)]);
        Assert.Equal(0, exitCode);
        Assert.Contains("warning CACHWARN001", output, StringComparison.Ordinal);
        Assert.DoesNotContain("Compilation result restored from cache.", output, StringComparison.Ordinal);
 
        File.Delete(Path.Combine(workingDirectory.Path, outputFileName));
 
        (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), arguments, workingDirectory);
        Assert.Equal(0, exitCode);
        Assert.Contains("Compilation result restored from cache.", output, StringComparison.Ordinal);
        Assert.DoesNotContain("warning CACHWARN001", output, StringComparison.Ordinal);
        Assert.True(File.Exists(Path.Combine(workingDirectory.Path, outputFileName)));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CacheHit_DoesNotRecreateTouchedFiles(bool visualBasic)
    {
        var workingDirectory = Temp.CreateDirectory();
        var cacheDirectory = Temp.CreateDirectory();
        var sourceFileName = visualBasic ? "test.vb" : "test.cs";
        var outputFileName = "test.dll";
        var touchedFilesBase = Path.Combine(workingDirectory.Path, "touched");
        var source = visualBasic
            ? """
            Public Class TestType
            End Class
            """
            : """
            public class TestType
            {
            }
            """;
 
        using var serverData = await ServerUtil.CreateServer(_logger);
        var arguments = BuildCompilationArguments(visualBasic, serverData.PipeName, sourceFileName, outputFileName, additionalArguments: $"/touchedfiles:{touchedFilesBase}", cachePath: cacheDirectory.Path);
 
        var (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), arguments, workingDirectory, [new(sourceFileName, source)]);
        Assert.Equal(0, exitCode);
        Assert.True(File.Exists(touchedFilesBase + ".read"));
        Assert.True(File.Exists(touchedFilesBase + ".write"));
 
        File.Delete(Path.Combine(workingDirectory.Path, outputFileName));
        File.Delete(touchedFilesBase + ".read");
        File.Delete(touchedFilesBase + ".write");
 
        (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), arguments, workingDirectory);
        Assert.Equal(0, exitCode);
        Assert.Contains("Compilation result restored from cache.", output, StringComparison.Ordinal);
        Assert.False(File.Exists(touchedFilesBase + ".read"));
        Assert.False(File.Exists(touchedFilesBase + ".write"));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CacheHit_OmitsReportAnalyzerOutput(bool visualBasic)
    {
        var workingDirectory = Temp.CreateDirectory();
        var cacheDirectory = Temp.CreateDirectory();
        var supportAssembly = CreateReportAnalyzerAssembly(workingDirectory);
        var sourceFileName = visualBasic ? "test.vb" : "test.cs";
        var outputFileName = "test.dll";
        var source = visualBasic
            ? """
            Public Class TestType
            End Class
            """
            : """
            public class TestType
            {
            }
            """;
 
        using var serverData = await ServerUtil.CreateServer(_logger);
        var arguments = BuildCompilationArguments(visualBasic, serverData.PipeName, sourceFileName, outputFileName, additionalArguments: $"/reportanalyzer /a:{supportAssembly}", cachePath: cacheDirectory.Path);
 
        var (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), arguments, workingDirectory, [new(sourceFileName, source)]);
        Assert.Equal(0, exitCode);
        Assert.Contains("Total analyzer execution time:", output, StringComparison.Ordinal);
        Assert.Contains("Total generator execution time:", output, StringComparison.Ordinal);
        Assert.Contains("CacheWarningAnalyzer", output, StringComparison.Ordinal);
        Assert.Contains("CacheReportGenerator", output, StringComparison.Ordinal);
 
        File.Delete(Path.Combine(workingDirectory.Path, outputFileName));
 
        (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), arguments, workingDirectory);
        Assert.Equal(0, exitCode);
        Assert.Contains("Compilation result restored from cache.", output, StringComparison.Ordinal);
        Assert.DoesNotContain("Total analyzer execution time:", output, StringComparison.Ordinal);
        Assert.DoesNotContain("Total generator execution time:", output, StringComparison.Ordinal);
        Assert.DoesNotContain("CacheWarningAnalyzer", output, StringComparison.Ordinal);
        Assert.DoesNotContain("CacheReportGenerator", output, StringComparison.Ordinal);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CacheHit_OmitsReportIvtsOutput(bool visualBasic)
    {
        var workingDirectory = Temp.CreateDirectory();
        var cacheDirectory = Temp.CreateDirectory();
        var referenceSourceFileName = visualBasic ? "FriendLib.vb" : "FriendLib.cs";
        var consumerSourceFileName = visualBasic ? "Consumer.vb" : "Consumer.cs";
        var referenceOutputFileName = "FriendLib.dll";
        var consumerOutputFileName = "Consumer.dll";
        var referenceSource = visualBasic
            ? """
            Imports System.Runtime.CompilerServices
 
            <Assembly: InternalsVisibleTo("Consumer")>
            Friend Class BaseType
            End Class
            """
            : """
            using System.Runtime.CompilerServices;
 
            [assembly: InternalsVisibleTo("Consumer")]
            internal class BaseType
            {
            }
            """;
        var consumerSource = visualBasic
            ? """
            Friend Class Consumer
                Inherits BaseType
            End Class
            """
            : """
            internal class Consumer : BaseType
            {
            }
            """;
 
        using var serverData = await ServerUtil.CreateServer(_logger);
 
        var referenceArguments = BuildCompilationArguments(visualBasic, serverData.PipeName, referenceSourceFileName, referenceOutputFileName, cachePath: cacheDirectory.Path);
        var (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), referenceArguments, workingDirectory, [new(referenceSourceFileName, referenceSource)]);
        Assert.Equal(0, exitCode);
 
        var consumerArguments = BuildCompilationArguments(
            visualBasic,
            serverData.PipeName,
            consumerSourceFileName,
            consumerOutputFileName,
            additionalArguments: $"/reportivts /r:{referenceOutputFileName}",
            cachePath: cacheDirectory.Path);
 
        (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), consumerArguments, workingDirectory, [new(consumerSourceFileName, consumerSource)]);
        Assert.Equal(0, exitCode);
        Assert.Contains("Printing 'InternalsVisibleToAttribute' information", output, StringComparison.Ordinal);
        Assert.Contains("Grants IVT to current assembly: True", output, StringComparison.Ordinal);
 
        File.Delete(Path.Combine(workingDirectory.Path, consumerOutputFileName));
 
        (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), consumerArguments, workingDirectory);
        Assert.Equal(0, exitCode);
        Assert.Contains("Compilation result restored from cache.", output, StringComparison.Ordinal);
        Assert.DoesNotContain("Printing 'InternalsVisibleToAttribute' information", output, StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task CacheHit_DoesNotRecreateGeneratedFiles()
    {
        var workingDirectory = Temp.CreateDirectory();
        var cacheDirectory = Temp.CreateDirectory();
        var sourceFileName = "test.cs";
        var outputFileName = "test.dll";
        var generatedFilesDirectory = Path.Combine(workingDirectory.Path, "generated");
        var generatorAssembly = CreateGeneratedFilesGeneratorAssembly(workingDirectory);
        var source = """
            public class TestType
            {
            }
            """;
 
        Directory.CreateDirectory(generatedFilesDirectory);
 
        using var serverData = await ServerUtil.CreateServer(_logger);
        var arguments = BuildCompilationArguments(
            visualBasic: false,
            serverData.PipeName,
            sourceFileName,
            outputFileName,
            additionalArguments: $"/langversion:preview /generatedfilesout:{generatedFilesDirectory} /a:{generatorAssembly}",
            cachePath: cacheDirectory.Path);
 
        var (exitCode, output) = RunCommandLineCompiler(RequestLanguage.CSharpCompile, arguments, workingDirectory, [new(sourceFileName, source)]);
        Assert.Equal(0, exitCode);
        Assert.Single(Directory.GetFiles(generatedFilesDirectory, "generatedSource.cs", SearchOption.AllDirectories));
 
        File.Delete(Path.Combine(workingDirectory.Path, outputFileName));
        Directory.Delete(generatedFilesDirectory, recursive: true);
 
        (exitCode, output) = RunCommandLineCompiler(RequestLanguage.CSharpCompile, arguments, workingDirectory);
        Assert.Equal(0, exitCode);
        Assert.Contains("Compilation result restored from cache.", output, StringComparison.Ordinal);
        Assert.False(Directory.Exists(generatedFilesDirectory));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CacheHit_DoesNotRecreateDebugDeterminismKey(bool visualBasic)
    {
        var workingDirectory = Temp.CreateDirectory();
        var cacheDirectory = Temp.CreateDirectory();
        var sourceFileName = visualBasic ? "test.vb" : "test.cs";
        var outputFileName = "test.dll";
        var pdbFileName = "test.pdb";
        var keyFileName = "test.dll.key";
        var source = visualBasic
            ? """
            Public Class TestType
            End Class
            """
            : """
            public class TestType
            {
            }
            """;
 
        using var serverData = await ServerUtil.CreateServer(_logger);
        var arguments = BuildCompilationArguments(
            visualBasic,
            serverData.PipeName,
            sourceFileName,
            outputFileName,
            additionalArguments: "/debug:portable /features:debug-determinism",
            cachePath: cacheDirectory.Path);
 
        var (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), arguments, workingDirectory, [new(sourceFileName, source)]);
        Assert.Equal(0, exitCode);
        Assert.True(File.Exists(Path.Combine(workingDirectory.Path, keyFileName)));
 
        File.Delete(Path.Combine(workingDirectory.Path, outputFileName));
        File.Delete(Path.Combine(workingDirectory.Path, pdbFileName));
        File.Delete(Path.Combine(workingDirectory.Path, keyFileName));
 
        (exitCode, output) = RunCommandLineCompiler(GetLanguage(visualBasic), arguments, workingDirectory);
        Assert.Equal(0, exitCode);
        Assert.Contains("Compilation result restored from cache.", output, StringComparison.Ordinal);
        Assert.True(File.Exists(Path.Combine(workingDirectory.Path, outputFileName)));
        Assert.True(File.Exists(Path.Combine(workingDirectory.Path, pdbFileName)));
        Assert.False(File.Exists(Path.Combine(workingDirectory.Path, keyFileName)));
    }
 
    private static RequestLanguage GetLanguage(bool visualBasic)
        => visualBasic ? RequestLanguage.VisualBasicCompile : RequestLanguage.CSharpCompile;
 
    private static string BuildCompilationArguments(bool visualBasic, string pipeName, string sourceFileName, string outputFileName, string additionalArguments = "", string cachePath = null)
    {
        var languageArguments = visualBasic ? "/vbruntime*" : "";
        var segments = new[]
        {
            sourceFileName,
            $"/shared:{pipeName}",
            "/deterministic+",
            "/nologo",
            "/t:library",
            $"/out:{outputFileName}",
            cachePath is null ? "" : $"/features:{CompilerOptionParseUtilities.UseGlobalCacheFeatureFlag}={cachePath}",
            languageArguments,
            additionalArguments,
        };
 
        return string.Join(" ", segments.Where(static segment => !string.IsNullOrWhiteSpace(segment)));
    }
 
    private static string CreateWarningAnalyzerAssembly(TempDirectory directory)
        => EmitSupportAssembly(
            directory,
            "CacheWarningAnalyzer",
            """
            using System.Collections.Immutable;
            using Microsoft.CodeAnalysis;
            using Microsoft.CodeAnalysis.Diagnostics;
 
            [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
            public sealed class CacheWarningAnalyzer : DiagnosticAnalyzer
            {
                private static readonly DiagnosticDescriptor s_rule = new(
                    "CACHWARN001",
                    "Cache warning",
                    "Cache warning",
                    "Testing",
                    DiagnosticSeverity.Warning,
                    isEnabledByDefault: true);
 
                public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_rule);
 
                public override void Initialize(AnalysisContext context)
                    => context.RegisterSyntaxTreeAction(static context => context.ReportDiagnostic(Diagnostic.Create(s_rule, context.Tree.GetRoot().GetLocation())));
            }
            """);
 
    private static string CreateReportAnalyzerAssembly(TempDirectory directory)
        => EmitSupportAssembly(
            directory,
            "CacheReportAnalyzer",
            """
            using System.Collections.Immutable;
            using Microsoft.CodeAnalysis;
            using Microsoft.CodeAnalysis.Diagnostics;
 
            [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
            public sealed class CacheWarningAnalyzer : DiagnosticAnalyzer
            {
                private static readonly DiagnosticDescriptor s_rule = new(
                    "CACHWARN001",
                    "Cache warning",
                    "Cache warning",
                    "Testing",
                    DiagnosticSeverity.Warning,
                    isEnabledByDefault: true);
 
                public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_rule);
 
                public override void Initialize(AnalysisContext context)
                    => context.RegisterSyntaxTreeAction(static context => context.ReportDiagnostic(Diagnostic.Create(s_rule, context.Tree.GetRoot().GetLocation())));
            }
 
            [Generator(LanguageNames.CSharp, LanguageNames.VisualBasic)]
            public sealed class CacheReportGenerator : ISourceGenerator
            {
                public void Initialize(GeneratorInitializationContext context)
                {
                }
 
                public void Execute(GeneratorExecutionContext context)
                {
                }
            }
            """);
 
    private static string CreateGeneratedFilesGeneratorAssembly(TempDirectory directory)
        => EmitSupportAssembly(
            directory,
            "CacheGeneratedFilesGenerator",
            """
            using Microsoft.CodeAnalysis;
 
            [Generator(LanguageNames.CSharp)]
            public sealed class CacheGeneratedFileGenerator : ISourceGenerator
            {
                public void Initialize(GeneratorInitializationContext context)
                {
                }
 
                public void Execute(GeneratorExecutionContext context)
                {
                    context.AddSource("generatedSource.cs", "public class GeneratedFromCache { }");
                }
            }
            """);
 
    private static string EmitSupportAssembly(TempDirectory directory, string assemblyName, string source)
    {
        var path = Path.Combine(directory.Path, assemblyName + ".dll");
        var compilation = CSharpCompilation.Create(
            assemblyName,
            syntaxTrees: [CSharpSyntaxTree.ParseText(source)],
            references: GetSupportAssemblyReferences(),
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
 
        var emitResult = compilation.Emit(path);
        Assert.True(emitResult.Success, string.Join(Environment.NewLine, emitResult.Diagnostics));
        return path;
    }
 
    private static IEnumerable<MetadataReference> GetSupportAssemblyReferences()
    {
        var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))
            .Split(Path.PathSeparator)
            .Select(static path => MetadataReference.CreateFromFile(path))
            .ToList();
 
        var codeAnalysisAssemblyPath = typeof(ISourceGenerator).Assembly.Location;
        if (!references.Any(reference => string.Equals(reference.Display, codeAnalysisAssemblyPath, StringComparison.OrdinalIgnoreCase)))
        {
            references.Add(MetadataReference.CreateFromFile(codeAnalysisAssemblyPath));
        }
 
        return references;
    }
 
    private static void ReferenceNetstandardDllIfCoreClr(TempDirectory currentDirectory, List<string> arguments)
    {
        var filePath = Path.Combine(currentDirectory.Path, "netstandard.dll");
        File.WriteAllBytes(filePath, NetStandard20.Resources.netstandard);
        arguments.Add("/nostdlib");
        arguments.Add("/r:netstandard.dll");
    }
 
    private static void CreateFiles(TempDirectory currentDirectory, IEnumerable<KeyValuePair<string, string>> files)
    {
        if (files == null)
        {
            return;
        }
 
        foreach (var pair in files)
        {
            currentDirectory.CreateFile(pair.Key).WriteAllText(pair.Value);
        }
    }
 
    private static (T Result, string Output) UseTextWriter<T>(Encoding encoding, Func<TextWriter, T> func)
    {
        MemoryStream memoryStream;
        TextWriter writer;
        if (encoding == null)
        {
            memoryStream = null;
            writer = new StringWriter();
        }
        else
        {
            memoryStream = new MemoryStream();
            writer = new StreamWriter(memoryStream, encoding);
        }
 
        var result = func(writer);
        writer.Flush();
 
        if (memoryStream != null)
        {
            return (result, encoding.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length));
        }
 
        return (result, ((StringWriter)writer).ToString());
    }
 
    private (int ExitCode, string Output) RunCommandLineCompiler(
        RequestLanguage language,
        string argumentsSingle,
        TempDirectory currentDirectory,
        IEnumerable<KeyValuePair<string, string>> filesInDirectory = null,
        IEnumerable<KeyValuePair<string, string>> additionalEnvironmentVars = null,
        Encoding redirectEncoding = null)
    {
        var arguments = new List<string>(argumentsSingle.Split(' '))
        {
            "/preferreduilang:en"
        };
 
        ReferenceNetstandardDllIfCoreClr(currentDirectory, arguments);
        CreateFiles(currentDirectory, filesInDirectory);
 
        var client = ServerUtil.CreateBuildClient(language, _logger);
        var buildPaths = new BuildPaths(
            clientDir: Path.GetDirectoryName(typeof(CommonCompiler).Assembly.Location),
            workingDir: currentDirectory.Path,
            sdkDir: ServerUtil.DefaultSdkDirectory,
            tempDir: Path.GetTempPath());
 
        var (result, output) = UseTextWriter(
            redirectEncoding,
            writer => ApplyEnvironmentVariables(additionalEnvironmentVars, () => client.RunCompilation(arguments, buildPaths, writer)));
 
        Assert.True(result.RanOnServer);
        return (result.ExitCode, output);
    }
}
#endif