File: EditAndContinue\EmitSolutionUpdateResultsTests.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.EditAndContinue.UnitTests
{
    [UseExportProvider]
    public class EmitSolutionUpdateResultsTests
    {
        private static TestWorkspace CreateWorkspace(out Solution solution)
        {
            var workspace = new TestWorkspace(composition: FeaturesTestCompositions.Features);
            solution = workspace.CurrentSolution;
            return workspace;
        }
 
        private static ManagedHotReloadUpdate CreateMockUpdate(ProjectId projectId)
            => new(
                projectId.Id,
                projectId.ToString(),
                projectId,
                ilDelta: [],
                metadataDelta: [],
                pdbDelta: [],
                updatedTypes: [],
                requiredCapabilities: [],
                updatedMethods: [],
                sequencePoints: [],
                activeStatements: [],
                exceptionRegions: []);
 
        private static EmitSolutionUpdateResults CreateMockResults(Solution solution, IEnumerable<ProjectId> updates, IEnumerable<ProjectId> rudeEdits)
        => new()
        {
            Solution = solution,
            ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Blocked, [.. updates.Select(CreateMockUpdate)]),
            RudeEdits = [.. rudeEdits.Select(id => new ProjectDiagnostics(id, [Diagnostic.Create(EditAndContinueDiagnosticDescriptors.GetDescriptor(RudeEditKind.InternalError), location: null)]))],
            Diagnostics = [],
            SyntaxError = null,
        };
 
        [Fact]
        public async Task GetHotReloadDiagnostics()
        {
            using var workspace = new TestWorkspace(composition: FeaturesTestCompositions.Features);
 
            var sourcePath = Path.Combine(TempRoot.Root, "x", "a.cs");
            var razorPath1 = Path.Combine(TempRoot.Root, "x", "a.razor");
            var razorPath2 = Path.Combine(TempRoot.Root, "a.razor");
 
            var document = workspace.CurrentSolution.
                AddProject("proj", "proj", LanguageNames.CSharp).
                WithMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Standard)).
                AddDocument(sourcePath, SourceText.From("class C {}", Encoding.UTF8), filePath: Path.Combine(TempRoot.Root, sourcePath));
 
            var solution = document.Project.Solution;
            var tree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
 
            var diagnostics = ImmutableArray.Create(
                new DiagnosticData(
                    id: "CS0001",
                    category: "Test",
                    message: "warning",
                    severity: DiagnosticSeverity.Warning,
                    defaultSeverity: DiagnosticSeverity.Warning,
                    isEnabledByDefault: true,
                    warningLevel: 0,
                    customTags: ImmutableArray.Create("Test2"),
                    properties: ImmutableDictionary<string, string?>.Empty,
                    document.Project.Id,
                    DiagnosticDataLocation.TestAccessor.Create(new(sourcePath, new(0, 0), new(0, 5)), document.Id, new("a.razor", new(10, 10), new(10, 15)), forceMappedPath: true),
                    language: "C#",
                    title: "title",
                    description: "description",
                    helpLink: "http://link"),
                new DiagnosticData(
                    id: "CS0012",
                    category: "Test",
                    message: "error",
                    severity: DiagnosticSeverity.Error,
                    defaultSeverity: DiagnosticSeverity.Warning,
                    isEnabledByDefault: true,
                    warningLevel: 0,
                    customTags: ImmutableArray.Create("Test2"),
                    properties: ImmutableDictionary<string, string?>.Empty,
                    document.Project.Id,
                    DiagnosticDataLocation.TestAccessor.Create(new(sourcePath, new(0, 0), new(0, 5)), document.Id, new(@"..\a.razor", new(10, 10), new(10, 15)), forceMappedPath: true),
                    language: "C#",
                    title: "title",
                    description: "description",
                    helpLink: "http://link"));
 
            var syntaxError = new DiagnosticData(
                id: "CS0002",
                category: "Test",
                message: "syntax error",
                severity: DiagnosticSeverity.Error,
                defaultSeverity: DiagnosticSeverity.Error,
                isEnabledByDefault: true,
                warningLevel: 0,
                customTags: ImmutableArray.Create("Test3"),
                properties: ImmutableDictionary<string, string?>.Empty,
                document.Project.Id,
                new DiagnosticDataLocation(new(sourcePath, new(0, 1), new(0, 5)), document.Id),
                language: "C#",
                title: "title",
                description: "description",
                helpLink: "http://link");
 
            var rudeEdits = ImmutableArray.Create(new ProjectDiagnostics(document.Project.Id,
            [
                new RudeEditDiagnostic(RudeEditKind.Insert, TextSpan.FromBounds(1, 10), 123, ["a"]).ToDiagnostic(tree),
                new RudeEditDiagnostic(RudeEditKind.Delete, TextSpan.FromBounds(1, 10), 123, ["b"]).ToDiagnostic(tree)
            ])).ToDiagnosticData(solution);
 
            var data = new EmitSolutionUpdateResults.Data()
            {
                Diagnostics = diagnostics,
                RudeEdits = rudeEdits,
                SyntaxError = syntaxError,
                ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Blocked, Updates: [])
            };
 
            var actual = data.GetAllDiagnostics();
 
            AssertEx.Equal(
            [
                $@"Warning CS0001: {razorPath1} (10,10)-(10,15): warning",
                $@"Error CS0012: {razorPath2} (10,10)-(10,15): error",
                $@"Error CS0002: {sourcePath} (0,1)-(0,5): syntax error",
                $@"RestartRequired ENC0021: {sourcePath} (0,1)-(0,10): {string.Format(FeaturesResources.Adding_0_requires_restarting_the_application, "a")}",
                $@"RestartRequired ENC0033: {sourcePath} (0,1)-(0,10): {string.Format(FeaturesResources.Deleting_0_requires_restarting_the_application, "b")}",
            ], actual.Select(d => $"{d.Severity} {d.Id}: {d.FilePath} {d.Span.GetDebuggerDisplay()}: {d.Message}"));
        }
 
        [Fact]
        public void RunningProjects_Updates()
        {
            using var _ = CreateWorkspace(out var solution);
 
            solution = solution
                .AddTestProject("C", out var c).Solution
                .AddTestProject("D", out var d).Solution
                .AddTestProject("A", out var a).AddProjectReferences([new(c)]).Solution
                .AddTestProject("B", out var b).AddProjectReferences([new(c), new(d)]).Solution;
 
            var runningProjects = new[] { a, b };
            var results = CreateMockResults(solution, updates: [c, d], rudeEdits: []);
 
            var projectsToRestart = new HashSet<Project>();
            var projectsToRebuild = new HashSet<Project>();
 
            results.GetProjectsToRebuildAndRestart(solution, p => runningProjects.Contains(p.Id), projectsToRestart, projectsToRebuild);
 
            Assert.Empty(projectsToRestart);
            Assert.Empty(projectsToRebuild);
        }
 
        [Fact]
        public void RunningProjects_RudeEdits()
        {
            using var _ = CreateWorkspace(out var solution);
 
            solution = solution
                .AddTestProject("C", out var c).Solution
                .AddTestProject("D", out var d).Solution
                .AddTestProject("A", out var a).AddProjectReferences([new(c)]).Solution
                .AddTestProject("B", out var b).AddProjectReferences([new(c), new(d)]).Solution;
 
            var runningProjects = new[] { a, b };
            var results = CreateMockResults(solution, updates: [], rudeEdits: [d]);
 
            var projectsToRestart = new HashSet<Project>();
            var projectsToRebuild = new HashSet<Project>();
 
            results.GetProjectsToRebuildAndRestart(solution, p => runningProjects.Contains(p.Id), projectsToRestart, projectsToRebuild);
 
            // D has rude edit ==> B has to restart
            AssertEx.SetEqual([b], projectsToRestart.Select(p => p.Id));
 
            // D has rude edit:
            AssertEx.SetEqual([d], projectsToRebuild.Select(p => p.Id));
        }
 
        [Fact]
        public void RunningProjects_RudeEdits_NotImpactingRunningProjects()
        {
            using var _ = CreateWorkspace(out var solution);
 
            solution = solution
                .AddTestProject("C", out var c).Solution
                .AddTestProject("D", out var d).Solution
                .AddTestProject("A", out var a).AddProjectReferences([new(c)]).Solution
                .AddTestProject("B", out var b).AddProjectReferences([new(c), new(d)]).Solution;
 
            var runningProjects = new[] { a };
            var results = CreateMockResults(solution, updates: [], rudeEdits: [d]);
 
            var projectsToRestart = new HashSet<Project>();
            var projectsToRebuild = new HashSet<Project>();
 
            results.GetProjectsToRebuildAndRestart(solution, p => runningProjects.Contains(p.Id), projectsToRestart, projectsToRebuild);
 
            Assert.Empty(projectsToRestart);
            Assert.Empty(projectsToRebuild);
        }
 
        [Fact]
        public void RunningProjects_RudeEditAndUpdate_Dependent()
        {
            using var _ = CreateWorkspace(out var solution);
 
            solution = solution
                .AddTestProject("C", out var c).Solution
                .AddTestProject("D", out var d).Solution
                .AddTestProject("A", out var a).AddProjectReferences([new(c)]).Solution
                .AddTestProject("B", out var b).AddProjectReferences([new(c), new(d)]).Solution;
 
            var runningProjects = new[] { a, b };
            var results = CreateMockResults(solution, updates: [c], rudeEdits: [d]);
 
            var projectsToRestart = new HashSet<Project>();
            var projectsToRebuild = new HashSet<Project>();
 
            results.GetProjectsToRebuildAndRestart(solution, p => runningProjects.Contains(p.Id), projectsToRestart, projectsToRebuild);
 
            // D has rude edit => B has to restart
            // C has update, B -> C and A -> C ==> A has to restart
            AssertEx.SetEqual([a, b], projectsToRestart.Select(p => p.Id));
 
            // D has rude edit, C has update that impacts restart set:
            AssertEx.SetEqual([c, d], projectsToRebuild.Select(p => p.Id));
        }
 
        [Fact]
        public void RunningProjects_RudeEditAndUpdate_Independent()
        {
            using var _ = CreateWorkspace(out var solution);
 
            solution = solution
                .AddTestProject("C", out var c).Solution
                .AddTestProject("D", out var d).Solution
                .AddTestProject("A", out var a).AddProjectReferences([new(c)]).Solution
                .AddTestProject("B", out var b).AddProjectReferences([new(d)]).Solution;
 
            var runningProjects = new[] { a, b };
            var results = CreateMockResults(solution, updates: [c], rudeEdits: [d]);
 
            var projectsToRestart = new HashSet<Project>();
            var projectsToRebuild = new HashSet<Project>();
 
            results.GetProjectsToRebuildAndRestart(solution, p => runningProjects.Contains(p.Id), projectsToRestart, projectsToRebuild);
 
            // D has rude edit => B has to restart
            AssertEx.SetEqual([b], projectsToRestart.Select(p => p.Id));
 
            // D has rude edit, C has update that does not impacts restart set:
            AssertEx.SetEqual([d], projectsToRebuild.Select(p => p.Id));
        }
 
        [Theory]
        [CombinatorialData]
        public void RunningProjects_RudeEditAndUpdate_Chain(bool reverse)
        {
            using var _ = CreateWorkspace(out var solution);
 
            solution = solution
                .AddTestProject("P1", out var p1).Solution
                .AddTestProject("P2", out var p2).Solution
                .AddTestProject("P3", out var p3).Solution
                .AddTestProject("P4", out var p4).Solution
                .AddTestProject("R1", out var r1).AddProjectReferences([new(p1), new(p2)]).Solution
                .AddTestProject("R2", out var r2).AddProjectReferences([new(p2), new(p3)]).Solution
                .AddTestProject("R3", out var r3).AddProjectReferences([new(p3), new(p4)]).Solution
                .AddTestProject("R4", out var r4).AddProjectReferences([new(p4)]).Solution;
 
            var runningProjects = new[] { r1, r2, r3, r4 };
            var results = CreateMockResults(solution, updates: reverse ? [p4, p3, p2] : [p2, p3, p4], rudeEdits: [p1]);
 
            var projectsToRestart = new HashSet<Project>();
            var projectsToRebuild = new HashSet<Project>();
 
            results.GetProjectsToRebuildAndRestart(solution, p => runningProjects.Contains(p.Id), projectsToRestart, projectsToRebuild);
 
            AssertEx.SetEqual([r1, r2, r3, r4], projectsToRestart.Select(p => p.Id));
            AssertEx.SetEqual([p1, p2, p3, p4], projectsToRebuild.Select(p => p.Id));
        }
    }
}