File: EditAndContinue\CompileTimeSolutionProviderTests.cs
Web Access
Project: src\src\Features\Test\Microsoft.CodeAnalysis.Features.UnitTests.csproj (Microsoft.CodeAnalysis.Features.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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Test.Utilities.TestGenerators;
using Xunit;
 
namespace Microsoft.CodeAnalysis.EditAndContinue.UnitTests
{
    [UseExportProvider]
    public class CompileTimeSolutionProviderTests
    {
        [Theory]
        [InlineData("razor")]
        [InlineData("cshtml")]
        public async Task TryGetCompileTimeDocumentAsync(string kind)
        {
            var workspace = new TestWorkspace(composition: FeaturesTestCompositions.Features);
            var projectId = ProjectId.CreateNewId();
 
            var projectFilePath = Path.Combine(TempRoot.Root, "a.csproj");
            var additionalFilePath = Path.Combine(TempRoot.Root, "a", $"X.{kind}");
            var designTimeFilePath = Path.Combine(TempRoot.Root, "a", $"X.{kind}.g.cs");
 
            var generator = new TestSourceGenerator() { ExecuteImpl = context => context.AddSource($"a_X_{kind}.g.cs", "") };
            var sourceGeneratedPathPrefix = Path.Combine(TempRoot.Root, typeof(TestSourceGenerator).Assembly.GetName().Name!, typeof(TestSourceGenerator).FullName);
            var analyzerConfigId = DocumentId.CreateNewId(projectId);
            var documentId = DocumentId.CreateNewId(projectId);
            var additionalDocumentId = DocumentId.CreateNewId(projectId);
            var designTimeDocumentId = DocumentId.CreateNewId(projectId);
 
            var designTimeSolution = workspace.CurrentSolution.
                AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, "proj", "proj", LanguageNames.CSharp, filePath: projectFilePath)).
                WithProjectCompilationOutputInfo(projectId, new CompilationOutputInfo(
                    assemblyPath: Path.Combine(TempRoot.Root, "proj"),
                    generatedFilesOutputDirectory: null)).
                WithProjectMetadataReferences(projectId, TargetFrameworkUtil.GetReferences(TargetFramework.NetStandard20)).
                AddAnalyzerReference(projectId, new TestGeneratorReference(generator)).
                AddAdditionalDocument(additionalDocumentId, "additional", SourceText.From(""), filePath: additionalFilePath).
                AddAnalyzerConfigDocument(analyzerConfigId, "config", SourceText.From(""), filePath: "RazorSourceGenerator.razorencconfig").
                AddDocument(documentId, "a.cs", "").
                AddDocument(DocumentInfo.Create(
                    designTimeDocumentId,
                    name: "a",
                    loader: null,
                    filePath: designTimeFilePath,
                    isGenerated: true).WithDesignTimeOnly(true));
 
            var designTimeDocument = designTimeSolution.GetRequiredDocument(designTimeDocumentId);
 
            var provider = workspace.Services.GetRequiredService<ICompileTimeSolutionProvider>();
            var compileTimeSolution = provider.GetCompileTimeSolution(designTimeSolution);
 
            Assert.False(compileTimeSolution.ContainsAnalyzerConfigDocument(analyzerConfigId));
            Assert.False(compileTimeSolution.ContainsDocument(designTimeDocumentId));
            Assert.True(compileTimeSolution.ContainsDocument(documentId));
 
            var sourceGeneratedDoc = (await compileTimeSolution.Projects.Single().GetSourceGeneratedDocumentsAsync()).Single();
 
            var compileTimeDocument = await CompileTimeSolutionProvider.TryGetCompileTimeDocumentAsync(designTimeDocument, compileTimeSolution, CancellationToken.None, sourceGeneratedPathPrefix);
            Assert.Same(sourceGeneratedDoc, compileTimeDocument);
        }
 
        [Fact]
        public async Task GeneratorOutputCachedBetweenAcrossCompileTimeSolutions()
        {
            var workspace = new TestWorkspace(composition: FeaturesTestCompositions.Features);
            var projectId = ProjectId.CreateNewId();
 
            var generatorInvocations = 0;
 
            var generator = new PipelineCallbackGenerator(context =>
            {
                // We'll replicate a simple example of how the razor generator handles disabling here so the test
                // functions similar to the real world
                var isDisabled = context.AnalyzerConfigOptionsProvider.Select(
                    (o, ct) => o.GlobalOptions.TryGetValue("build_property.SuppressRazorSourceGenerator", out var value) && bool.Parse(value));
 
                var sources = context.AdditionalTextsProvider.Combine(isDisabled).Select((pair, ct) =>
                {
                    var (additionalText, isDisabledFlag) = pair;
 
                    if (isDisabledFlag)
                        return null;
 
                    Interlocked.Increment(ref generatorInvocations);
                    return "// " + additionalText.GetText(ct)!.ToString();
                });
 
                context.RegisterSourceOutput(sources, (context, s) =>
                {
                    if (s != null)
                        context.AddSource("hint", SourceText.From(s));
                });
            });
 
            var analyzerConfigId = DocumentId.CreateNewId(projectId);
            var additionalDocumentId = DocumentId.CreateNewId(projectId);
 
            var analyzerConfigText = "is_global = true\r\nbuild_property.SuppressRazorSourceGenerator = true";
 
            workspace.SetCurrentSolution(s => s
                .AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, "proj", "proj", LanguageNames.CSharp))
                .WithProjectCompilationOutputInfo(projectId, new CompilationOutputInfo(
                    assemblyPath: Path.Combine(TempRoot.Root, "proj"),
                    generatedFilesOutputDirectory: null))
                .AddAnalyzerReference(projectId, new TestGeneratorReference(generator))
                .AddAdditionalDocument(additionalDocumentId, "additional", SourceText.From(""), filePath: "additional.razor")
                .AddAnalyzerConfigDocument(analyzerConfigId, "config", SourceText.From(analyzerConfigText), filePath: "Z:\\RazorSourceGenerator.razorencconfig"),
                WorkspaceChangeKind.SolutionAdded);
 
            // Fetch a compilation first for the base solution; we're doing this because currently if we try to move the
            // cached generator state to a snapshot that has no CompilationTracker at all, we won't update the state.
            _ = await workspace.CurrentSolution.GetRequiredProject(projectId).GetCompilationAsync();
 
            var provider = workspace.Services.GetRequiredService<ICompileTimeSolutionProvider>();
            var compileTimeSolution1 = provider.GetCompileTimeSolution(workspace.CurrentSolution);
 
            _ = await compileTimeSolution1.GetRequiredProject(projectId).GetCompilationAsync();
 
            Assert.Equal(1, generatorInvocations);
 
            // Now do something that shouldn't force the generator to rerun; we must change this through the workspace since the
            // service itself uses versions that won't change otherwise
            var documentId = DocumentId.CreateNewId(projectId);
            workspace.SetCurrentSolution(
                s => s.AddDocument(documentId, "Test.cs", "// source file"),
                WorkspaceChangeKind.DocumentAdded,
                projectId,
                documentId);
 
            var compileTimeSolution2 = provider.GetCompileTimeSolution(workspace.CurrentSolution);
            Assert.NotSame(compileTimeSolution1, compileTimeSolution2);
 
            _ = await compileTimeSolution2.GetRequiredProject(projectId).GetCompilationAsync();
 
            Assert.Equal(1, generatorInvocations);
        }
    }
}