diff --git a/src/Dependencies/PooledObjects/ArrayBuilder.cs b/src/Dependencies/PooledObjects/ArrayBuilder.cs index 35ab00e03a50b..159cb02f1d3c0 100644 --- a/src/Dependencies/PooledObjects/ArrayBuilder.cs +++ b/src/Dependencies/PooledObjects/ArrayBuilder.cs @@ -306,7 +306,7 @@ public void Sort() _builder.Sort(); } - public void Sort(IComparer comparer) + public void Sort(IComparer? comparer) { _builder.Sort(comparer); } @@ -684,13 +684,15 @@ public void RemoveDuplicates() set.Free(); } - public void SortAndRemoveDuplicates(IComparer comparer) + public void SortAndRemoveDuplicates(IComparer? comparer = null) { if (Count <= 1) { return; } + comparer ??= Comparer.Default; + Sort(comparer); int j = 0; diff --git a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs index 34add1bee8489..5ca93c60e330a 100644 --- a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs +++ b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.Diagnostics; @@ -375,8 +376,12 @@ public async ValueTask GetUpdatesAsync(ImmutableArray.GetInstance(out var runningProjectPaths); runningProjectPaths.AddAll(runningProjects); - var runningProjectIds = solution.Projects.Where(p => p.FilePath != null && runningProjectPaths.Contains(p.FilePath)).Select(static p => p.Id).ToImmutableHashSet(); - var result = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, runningProjectIds, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); + // TODO: Update once implemented: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2449700 + var runningProjectInfos = solution.Projects.Where(p => p.FilePath != null && runningProjectPaths.Contains(p.FilePath)).ToImmutableDictionary( + keySelector: static p => p.Id, + elementSelector: static p => new RunningProjectInfo { RestartWhenChangesHaveNoEffect = false, AllowPartialUpdate = false }); + + var result = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, runningProjectInfos, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); switch (result.ModuleUpdates.Status) { @@ -399,9 +404,9 @@ public async ValueTask GetUpdatesAsync(ImmutableArray GetProjectPaths(ImmutableArray ids) - => ids.SelectAsArray(static (id, solution) => solution.GetRequiredProject(id).FilePath!, solution); + ImmutableArray GetProjectPaths(IEnumerable ids) + => ids.SelectAsArray(id => solution.GetRequiredProject(id).FilePath!); } } diff --git a/src/EditorFeatures/ExternalAccess/Debugger/GlassTestsHotReloadService.cs b/src/EditorFeatures/ExternalAccess/Debugger/GlassTestsHotReloadService.cs index d021b1b841fc7..f0c4168d22a9d 100644 --- a/src/EditorFeatures/ExternalAccess/Debugger/GlassTestsHotReloadService.cs +++ b/src/EditorFeatures/ExternalAccess/Debugger/GlassTestsHotReloadService.cs @@ -84,7 +84,7 @@ public void EndDebuggingSession() public async ValueTask GetUpdatesAsync(Solution solution, CancellationToken cancellationToken) { - var results = (await _encService.EmitSolutionUpdateAsync(GetSessionId(), solution, runningProjects: [], s_noActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false)).Dehydrate(); + var results = (await _encService.EmitSolutionUpdateAsync(GetSessionId(), solution, runningProjects: ImmutableDictionary.Empty, s_noActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false)).Dehydrate(); return new ManagedHotReloadUpdates(results.ModuleUpdates.Updates.FromContract(), results.GetAllDiagnostics().FromContract(), [], []); } } diff --git a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueLanguageServiceTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueLanguageServiceTests.cs index 0cd09d6134681..65eb90e8d5a6c 100644 --- a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueLanguageServiceTests.cs +++ b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueLanguageServiceTests.cs @@ -5,6 +5,7 @@ #nullable disable using System; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Text; @@ -115,11 +116,9 @@ public async Task Test(bool commitChanges) var localService = localWorkspace.GetService(); - DocumentId documentId; await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution - .AddTestProject("proj", out var projectId).Solution - .AddMetadataReferences(projectId, TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)) - .AddDocument(documentId = DocumentId.CreateNewId(projectId), "test.cs", SourceText.From("class C { }", Encoding.UTF8), filePath: "test.cs")); + .AddTestProject("proj", out var projectId) + .AddTestDocument("test.cs", "class C { }", out var documentId).Project.Solution); var solution = localWorkspace.CurrentSolution; var project = solution.GetRequiredProject(projectId); @@ -159,7 +158,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution var diagnosticDescriptor1 = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile); - mockEncService.EmitSolutionUpdateImpl = (solution, runningProjects, _) => + mockEncService.EmitSolutionUpdateImpl = (solution, _, _) => { var syntaxTree = solution.GetRequiredDocument(documentId).GetSyntaxTreeSynchronously(CancellationToken.None)!; @@ -176,7 +175,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution RudeEdits = [new ProjectDiagnostics(project.Id, [rudeEditDiagnostic])], SyntaxError = syntaxError, ProjectsToRebuild = [project.Id], - ProjectsToRestart = [project.Id] + ProjectsToRestart = ImmutableDictionary>.Empty.Add(project.Id, []) }; }; @@ -186,16 +185,16 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution AssertEx.Equal( [ - $"Error ENC1001: test.cs(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "error 1")}", - $"Error ENC1001: proj.csproj(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}" + $"Error ENC1001: {document.FilePath}(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "error 1")}", + $"Error ENC1001: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}" ], sessionState.ApplyChangesDiagnostics.Select(Inspect)); AssertEx.Equal( [ - $"Error ENC1001: test.cs(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "error 1")}", - $"Error ENC1001: proj.csproj(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}", - $"Error ENC1001: test.cs(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "syntax error 3")}", - $"RestartRequired ENC0033: test.cs(0, 2, 0, 3): {string.Format(FeaturesResources.Deleting_0_requires_restarting_the_application, "x")}" + $"Error ENC1001: {document.FilePath}(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "error 1")}", + $"Error ENC1001: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}", + $"Error ENC1001: {document.FilePath}(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "syntax error 3")}", + $"RestartRequired ENC0033: {document.FilePath}(0, 2, 0, 3): {string.Format(FeaturesResources.Deleting_0_requires_restarting_the_application, "x")}" ], updates.Diagnostics.Select(Inspect)); Assert.True(sessionState.IsSessionActive); diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs index c66e8c279ce41..48d4809726805 100644 --- a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs @@ -512,7 +512,7 @@ CommittedSolution.DocumentState.Indeterminate or public async ValueTask EmitSolutionUpdateAsync( Solution solution, - IImmutableSet runningProjects, + ImmutableDictionary runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) { @@ -553,7 +553,7 @@ public async ValueTask EmitSolutionUpdateAsync( } using var _ = ArrayBuilder.GetInstance(out var rudeEditDiagnostics); - foreach (var (projectId, projectRudeEdits) in solutionUpdate.DocumentsWithRudeEdits.GroupBy(static e => e.DocumentId.ProjectId)) + foreach (var (projectId, projectRudeEdits) in solutionUpdate.DocumentsWithRudeEdits.GroupBy(static e => e.DocumentId.ProjectId).OrderBy(static id => id)) { foreach (var (documentId, rudeEdits) in projectRudeEdits) { diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs index 33cc69f123215..65a6f9c4410dd 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs @@ -4,10 +4,11 @@ #nullable disable +using System; using System.Collections.Generic; using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; +using Microsoft.CodeAnalysis.Diagnostics; namespace Microsoft.CodeAnalysis.EditAndContinue; @@ -45,7 +46,7 @@ void add(int index, int id, string resourceName, LocalizableResourceString title } builder[index] = new DiagnosticDescriptor( - $"ENC{id:D4}", + GetDiagnosticId(id), title, messageFormat: new LocalizableResourceString(resourceName, FeaturesResources.ResourceManager, typeof(FeaturesResources)), DiagnosticCategory.EditAndContinue, @@ -210,4 +211,10 @@ private static int GetDescriptorIndex(RudeEditKind kind) private static int GetDescriptorIndex(EditAndContinueErrorCode errorCode) => s_diagnosticBaseIndex + (int)errorCode; + + private static string GetDiagnosticId(int id) + => $"ENC{id:D4}"; + + public static RudeEditKind GetRudeEditKind(string diagnosticId) + => diagnosticId.StartsWith("ENC", StringComparison.Ordinal) && int.TryParse(diagnosticId[3..], out var id) ? (RudeEditKind)id : RudeEditKind.None; } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs index 4974ec41aba52..227931eec072e 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs @@ -220,7 +220,7 @@ public ValueTask> GetDocumentDiagnosticsAsync(Documen public ValueTask EmitSolutionUpdateAsync( DebuggingSessionId sessionId, Solution solution, - IImmutableSet runningProjects, + ImmutableDictionary runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) { diff --git a/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs b/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs index 1a0e49782101a..5478af5286b7b 100644 --- a/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs +++ b/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs @@ -33,7 +33,7 @@ internal readonly struct Data public required DiagnosticData? SyntaxError { get; init; } [DataMember] - public required ImmutableArray ProjectsToRestart { get; init; } + public required ImmutableDictionary> ProjectsToRestart { get; init; } [DataMember] public required ImmutableArray ProjectsToRebuild { get; init; } @@ -84,7 +84,7 @@ internal ImmutableArray GetAllDiagnostics() Diagnostics = [], RudeEdits = [], SyntaxError = null, - ProjectsToRestart = [], + ProjectsToRestart = ImmutableDictionary>.Empty, ProjectsToRebuild = [], }; @@ -108,7 +108,16 @@ internal ImmutableArray GetAllDiagnostics() public required ImmutableArray RudeEdits { get; init; } public required Diagnostic? SyntaxError { get; init; } - public required ImmutableArray ProjectsToRestart { get; init; } + /// + /// Running projects that have to be restarted and a list of projects with rude edits that caused the restart. + /// + public required ImmutableDictionary> ProjectsToRestart { get; init; } + + /// + /// Projects whose source have been updated and need to be rebuilt. Does not include projects without change that depend on such projects. + /// It is assumed that the host automatically rebuilds all such projects that need rebuilding because it detects the dependent project outputs have been updated. + /// Unordered set. + /// public required ImmutableArray ProjectsToRebuild { get; init; } public Data Dehydrate() @@ -119,7 +128,7 @@ public Data Dehydrate() Diagnostics = [], RudeEdits = [], SyntaxError = null, - ProjectsToRestart = [], + ProjectsToRestart = ImmutableDictionary>.Empty, ProjectsToRebuild = [], } : new() @@ -148,127 +157,203 @@ public Data Dehydrate() /// Returns projects that need to be rebuilt and/or restarted due to blocking rude edits in order to apply changes. /// /// Identifies projects that have been launched. - /// Running projects that have to be restarted. - /// Projects whose source have been updated and need to be rebuilt. + /// + /// Running projects that have to be restarted and a list of projects with rude edits that caused the restart. + /// + /// + /// Projects whose source have been updated and need to be rebuilt. Does not include projects without change that depend on such projects. + /// It is assumed that the host automatically rebuilds all such projects that need rebuilding because it detects the dependent project outputs have been updated. + /// Unordered set. + /// internal static void GetProjectsToRebuildAndRestart( Solution solution, ModuleUpdates moduleUpdates, - IEnumerable rudeEdits, - IImmutableSet runningProjects, - out ImmutableArray projectsToRestart, + ArrayBuilder rudeEdits, + ImmutableDictionary runningProjects, + out ImmutableDictionary> projectsToRestart, out ImmutableArray projectsToRebuild) { + Debug.Assert(!rudeEdits.HasDuplicates(d => d.ProjectId)); + Debug.Assert(rudeEdits.Select(re => re.ProjectId).IsSorted()); + + // Projects with blocking rude edits should not have updates: + Debug.Assert(rudeEdits + .Where(r => r.Diagnostics.HasBlockingRudeEdits()) + .Select(r => r.ProjectId) + .Intersect(moduleUpdates.Updates.Select(u => u.ProjectId)) + .IsEmpty()); + var graph = solution.GetProjectDependencyGraph(); - // First, find all running projects that transitively depend on projects with rude edits. + // First, find all running projects that transitively depend on projects with blocking rude edits + // or edits that have no effect until restart. Note that the latter only trigger restart + // of projects that are configured to restart on no-effect change. + // // These will need to be rebuilt and restarted. In order to rebuilt these projects - // all their transitive references must either be free of source changes or be rebuilt as well. + // all their transitive references must either be free of source changes [*] or be rebuilt as well. // This may add more running projects to the set of projects we need to restart. // We need to repeat this process until we find a fixed point. + // + // [*] If a running project depended on a project with changes and was not restarted, + // the debugger might stop at the changed method body and its current source code + // wouldn't match the IL being executed. - using var _1 = ArrayBuilder.GetInstance(out var traversalStack); - using var _2 = PooledHashSet.GetInstance(out var projectsToRestartBuilder); - using var _3 = ArrayBuilder.GetInstance(out var projectsToRebuildBuilder); + using var _1 = ArrayBuilder.GetInstance(out var traversalStack); - foreach (var projectWithRudeEdit in GetProjectsContainingBlockingRudeEdits(solution)) + // Maps project to restart to all projects with rude edits that caused the restart: + var projectsToRestartBuilder = PooledDictionary>.GetInstance(); + + using var _3 = PooledHashSet.GetInstance(out var projectsToRebuildBuilder); + using var _4 = ArrayBuilder<(ProjectId projectWithRudeEdits, ImmutableArray impactedRunningProjects)>.GetInstance(out var impactedRunningProjectMap); + using var _5 = ArrayBuilder.GetInstance(out var impactedRunningProjects); + + for (var i = 0; i < rudeEdits.Count; i++) { - if (AddImpactedRunningProjects(projectsToRestartBuilder, projectWithRudeEdit)) + var (projectId, projectDiagnostics) = rudeEdits[i]; + + var hasBlocking = projectDiagnostics.HasBlockingRudeEdits(); + var hasNoEffect = projectDiagnostics.HasNoEffectRudeEdits(); + if (!hasBlocking && !hasNoEffect) + { + continue; + } + + AddImpactedRunningProjects(impactedRunningProjects, projectId, hasBlocking); + + foreach (var impactedRunningProject in impactedRunningProjects) { - projectsToRebuildBuilder.Add(projectWithRudeEdit.Id); + projectsToRestartBuilder.MultiAdd(impactedRunningProject, projectId); } + + if (hasBlocking && impactedRunningProjects is []) + { + // Projects with rude edits that do not impact running projects has to be rebuilt, + // so that the change takes effect if it is loaded in future. + projectsToRebuildBuilder.Add(projectId); + } + + impactedRunningProjects.Clear(); } - // At this point the restart set contains all running projects directly affected by rude edits. + // At this point the restart set contains all running projects transitively affected by rude edits. // Next, find projects that were successfully updated and affect running projects. - if (moduleUpdates.Updates.IsEmpty || projectsToRestartBuilder.Count == 0) + // Remove once https://github.com/dotnet/roslyn/issues/78244 is implemented. + if (!runningProjects.Any(static p => p.Value.AllowPartialUpdate)) { - projectsToRestart = [.. projectsToRestartBuilder]; - projectsToRebuild = [.. projectsToRebuildBuilder]; - return; - } - - // The set of updated projects is usually much smaller then the number of all projects in the solution. - // We iterate over this set updating the reset set until no new project is added to the reset set. - // Once a project is determined to affect a running process, all running processes that - // reference this project are added to the reset set. The project is then removed from updated - // project set as it can't contribute any more running projects to the reset set. - // If an updated project does not affect reset set in a given iteration, it stays in the set - // because it may affect reset set later on, after another running project is added to it. + // Partial solution update not supported. + if (projectsToRestartBuilder.Any()) + { + foreach (var update in moduleUpdates.Updates) + { + AddImpactedRunningProjects(impactedRunningProjects, update.ProjectId, isBlocking: true); - using var _4 = PooledHashSet.GetInstance(out var updatedProjects); - using var _5 = ArrayBuilder.GetInstance(out var updatedProjectsToRemove); + foreach (var impactedRunningProject in impactedRunningProjects) + { + projectsToRestartBuilder.TryAdd(impactedRunningProject, []); + } - foreach (var update in moduleUpdates.Updates) - { - updatedProjects.Add(solution.GetRequiredProject(update.ProjectId)); + impactedRunningProjects.Clear(); + } + } } + else if (!moduleUpdates.Updates.IsEmpty && projectsToRestartBuilder.Count > 0) + { + // The set of updated projects is usually much smaller than the number of all projects in the solution. + // We iterate over this set updating the reset set until no new project is added to the reset set. + // Once a project is determined to affect a running process, all running processes that + // reference this project are added to the reset set. The project is then removed from updated + // project set as it can't contribute any more running projects to the reset set. + // If an updated project does not affect reset set in a given iteration, it stays in the set + // because it may affect reset set later on, after another running project is added to it. - using var _6 = ArrayBuilder.GetInstance(out var impactedProjects); + using var _6 = PooledHashSet.GetInstance(out var updatedProjects); + using var _7 = ArrayBuilder.GetInstance(out var updatedProjectsToRemove); + using var _8 = PooledHashSet.GetInstance(out var projectsThatCausedRestart); - while (true) - { - Debug.Assert(updatedProjectsToRemove.Count == 0); + updatedProjects.AddRange(moduleUpdates.Updates.Select(static u => u.ProjectId)); - foreach (var updatedProject in updatedProjects) + while (true) { - if (AddImpactedRunningProjects(impactedProjects, updatedProject) && - impactedProjects.Any(projectsToRestartBuilder.Contains)) + Debug.Assert(updatedProjectsToRemove.IsEmpty); + + foreach (var updatedProjectId in updatedProjects) { - projectsToRestartBuilder.AddRange(impactedProjects); - updatedProjectsToRemove.Add(updatedProject); - projectsToRebuildBuilder.Add(updatedProject.Id); + AddImpactedRunningProjects(impactedRunningProjects, updatedProjectId, isBlocking: true); + + Debug.Assert(projectsThatCausedRestart.Count == 0); + + // collect all projects that caused restart of any of the impacted running projects: + foreach (var impactedRunningProject in impactedRunningProjects) + { + if (projectsToRestartBuilder.TryGetValue(impactedRunningProject, out var causes)) + { + projectsThatCausedRestart.AddRange(causes); + } + } + + if (projectsThatCausedRestart.Any()) + { + // The projects that caused the impacted running project to be restarted + // indirectly cause the running project that depends on the updated project to be restarted. + foreach (var impactedRunningProject in impactedRunningProjects) + { + if (!projectsToRestartBuilder.ContainsKey(impactedRunningProject)) + { + projectsToRestartBuilder.MultiAddRange(impactedRunningProject, projectsThatCausedRestart); + } + } + + updatedProjectsToRemove.Add(updatedProjectId); + } + + impactedRunningProjects.Clear(); + projectsThatCausedRestart.Clear(); } - impactedProjects.Clear(); - } + if (updatedProjectsToRemove is []) + { + // none of the remaining updated projects affect restart set: + break; + } - if (updatedProjectsToRemove is []) - { - // none of the remaining updated projects affect restart set: - break; + updatedProjects.RemoveAll(updatedProjectsToRemove); + updatedProjectsToRemove.Clear(); } + } - updatedProjects.RemoveAll(updatedProjectsToRemove); - updatedProjectsToRemove.Clear(); + foreach (var (_, causes) in projectsToRestartBuilder) + { + causes.SortAndRemoveDuplicates(); } - projectsToRestart = [.. projectsToRestartBuilder]; + projectsToRebuildBuilder.AddRange(projectsToRestartBuilder.Keys); + projectsToRestart = projectsToRestartBuilder.ToImmutableMultiDictionaryAndFree(); projectsToRebuild = [.. projectsToRebuildBuilder]; return; - bool AddImpactedRunningProjects(ICollection impactedProjects, Project initialProject) + void AddImpactedRunningProjects(ArrayBuilder impactedProjects, ProjectId initialProject, bool isBlocking) { + Debug.Assert(impactedProjects.IsEmpty); + Debug.Assert(traversalStack.Count == 0); traversalStack.Push(initialProject); - var added = false; - while (traversalStack.Count > 0) { - var project = traversalStack.Pop(); - if (runningProjects.Contains(project.Id)) + var projectId = traversalStack.Pop(); + if (runningProjects.TryGetValue(projectId, out var runningProject) && + (isBlocking || runningProject.RestartWhenChangesHaveNoEffect)) { - impactedProjects.Add(project.Id); - added = true; + impactedProjects.Add(projectId); } - foreach (var referencingProjectId in graph.GetProjectsThatDirectlyDependOnThisProject(project.Id)) + foreach (var referencingProjectId in graph.GetProjectsThatDirectlyDependOnThisProject(projectId)) { - traversalStack.Push(solution.GetRequiredProject(referencingProjectId)); + traversalStack.Push(referencingProjectId); } } - - return added; } - - IEnumerable GetProjectsContainingBlockingRudeEdits(Solution solution) - => rudeEdits - .Where(static e => e.Diagnostics.HasBlockingRudeEdits()) - .Select(static e => e.ProjectId) - .Distinct() - .OrderBy(static id => id) - .Select(solution.GetRequiredProject); } public ImmutableArray GetAllDiagnostics() @@ -295,4 +380,23 @@ public ImmutableArray GetAllDiagnostics() return diagnostics.ToImmutableAndClear(); } + + public ImmutableArray GetAllCompilationDiagnostics() + { + using var _ = ArrayBuilder.GetInstance(out var diagnostics); + + // add semantic and lowering diagnostics reported during delta emit: + foreach (var (_, projectEmitDiagnostics) in Diagnostics) + { + diagnostics.AddRange(projectEmitDiagnostics); + } + + // add syntax error: + if (SyntaxError != null) + { + diagnostics.Add(SyntaxError); + } + + return diagnostics.ToImmutableAndClear(); + } } diff --git a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueService.cs index abbbf80f06865..77384c0fc39fe 100644 --- a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueService.cs +++ b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueService.cs @@ -19,7 +19,7 @@ internal interface IEditAndContinueWorkspaceService : IWorkspaceService internal interface IEditAndContinueService { ValueTask> GetDocumentDiagnosticsAsync(Document document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); - ValueTask EmitSolutionUpdateAsync(DebuggingSessionId sessionId, Solution solution, IImmutableSet runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); + ValueTask EmitSolutionUpdateAsync(DebuggingSessionId sessionId, Solution solution, ImmutableDictionary runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); void CommitSolutionUpdate(DebuggingSessionId sessionId); void DiscardSolutionUpdate(DebuggingSessionId sessionId); diff --git a/src/Features/Core/Portable/EditAndContinue/Remote/IRemoteEditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/Remote/IRemoteEditAndContinueService.cs index 7b60868698964..806734d41b877 100644 --- a/src/Features/Core/Portable/EditAndContinue/Remote/IRemoteEditAndContinueService.cs +++ b/src/Features/Core/Portable/EditAndContinue/Remote/IRemoteEditAndContinueService.cs @@ -27,7 +27,7 @@ internal interface ICallback } ValueTask> GetDocumentDiagnosticsAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DocumentId documentId, CancellationToken cancellationToken); - ValueTask EmitSolutionUpdateAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DebuggingSessionId sessionId, IImmutableSet runningProjects, CancellationToken cancellationToken); + ValueTask EmitSolutionUpdateAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DebuggingSessionId sessionId, ImmutableDictionary runningProjects, CancellationToken cancellationToken); /// /// Returns ids of documents for which diagnostics need to be refreshed in-proc. diff --git a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs b/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs index 8fa893a7e06f0..6ec4de320e5e9 100644 --- a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs +++ b/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs @@ -56,7 +56,7 @@ await client.TryInvokeAsync( public async ValueTask EmitSolutionUpdateAsync( Solution solution, - IImmutableSet runningProjects, + ImmutableDictionary runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) { @@ -81,7 +81,7 @@ await client.TryInvokeAsync( RudeEdits = [], SyntaxError = null, ProjectsToRebuild = [], - ProjectsToRestart = [], + ProjectsToRestart = ImmutableDictionary>.Empty, }; } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) @@ -93,7 +93,7 @@ await client.TryInvokeAsync( RudeEdits = [], SyntaxError = null, ProjectsToRebuild = [], - ProjectsToRestart = [], + ProjectsToRestart = ImmutableDictionary>.Empty, }; } @@ -101,7 +101,7 @@ ImmutableArray GetInternalErrorDiagnosticData(string message) { var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(RudeEditKind.InternalError); - var firstProject = solution.GetProject(runningProjects.FirstOrDefault()) ?? solution.Projects.First(); + var firstProject = solution.GetProject(runningProjects.FirstOrDefault().Key) ?? solution.Projects.First(); var diagnostic = Diagnostic.Create( descriptor, Location.None, diff --git a/src/Features/Core/Portable/EditAndContinue/RudeEditDiagnostic.cs b/src/Features/Core/Portable/EditAndContinue/RudeEditDiagnostic.cs index 9b3d43d4488f4..6178b9344d51f 100644 --- a/src/Features/Core/Portable/EditAndContinue/RudeEditDiagnostic.cs +++ b/src/Features/Core/Portable/EditAndContinue/RudeEditDiagnostic.cs @@ -56,9 +56,15 @@ internal static bool IsBlocking(this RudeEditKind kind) internal static bool IsBlockingRudeEdit(this Diagnostic diagnostic) => diagnostic.Descriptor.DefaultSeverity == DiagnosticSeverity.Error; + internal static bool IsNoEffectRudeEdit(this Diagnostic diagnostic) + => EditAndContinueDiagnosticDescriptors.GetRudeEditKind(diagnostic.Id) == RudeEditKind.UpdateMightNotHaveAnyEffect; + public static bool HasBlockingRudeEdits(this ImmutableArray diagnostics) => diagnostics.Any(IsBlockingRudeEdit); - public static bool HasBlockingRudeEdits(this IEnumerable diagnostics) + public static bool HasNoEffectRudeEdits(this ImmutableArray diagnostics) + => diagnostics.Any(IsNoEffectRudeEdit); + + public static bool HasBlockingRudeEdits(this ImmutableArray diagnostics) => diagnostics.Any(static e => e.Kind.IsBlocking()); } diff --git a/src/Features/Core/Portable/EditAndContinue/RunningProjectInfo.cs b/src/Features/Core/Portable/EditAndContinue/RunningProjectInfo.cs new file mode 100644 index 0000000000000..deafbf97e3e06 --- /dev/null +++ b/src/Features/Core/Portable/EditAndContinue/RunningProjectInfo.cs @@ -0,0 +1,24 @@ +// 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.Runtime.Serialization; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[DataContract] +internal readonly struct RunningProjectInfo +{ + /// + /// Required restart of the project when an edit that has no effect until the app is restarted is made to any dependent project. + /// + [DataMember] + public required bool RestartWhenChangesHaveNoEffect { get; init; } + + /// + /// TODO: remove when implemented: https://github.com/dotnet/roslyn/issues/78244 + /// Indicates that the info has been passed from debugger. + /// + [DataMember] + public required bool AllowPartialUpdate { get; init; } +} diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/UnitTestingHotReloadService.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/UnitTestingHotReloadService.cs index dc3c06712f871..f8b8e4b97aa2b 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/UnitTestingHotReloadService.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/UnitTestingHotReloadService.cs @@ -95,7 +95,7 @@ public async Task StartSessionAsync(Solution solution, ImmutableArray ca Contract.ThrowIfFalse(sessionId != default, "Session has not started"); var results = await _encService - .EmitSolutionUpdateAsync(sessionId, solution, runningProjects: [], s_solutionActiveStatementSpanProvider, cancellationToken) + .EmitSolutionUpdateAsync(sessionId, solution, runningProjects: ImmutableDictionary.Empty, s_solutionActiveStatementSpanProvider, cancellationToken) .ConfigureAwait(false); if (results.ModuleUpdates.Status == ModuleUpdateStatus.Ready) diff --git a/src/Features/Core/Portable/ExternalAccess/Watch/Api/WatchHotReloadService.cs b/src/Features/Core/Portable/ExternalAccess/Watch/Api/WatchHotReloadService.cs index 2985b5b576ddc..330331bf71a6b 100644 --- a/src/Features/Core/Portable/ExternalAccess/Watch/Api/WatchHotReloadService.cs +++ b/src/Features/Core/Portable/ExternalAccess/Watch/Api/WatchHotReloadService.cs @@ -64,6 +64,12 @@ internal Update( } } + public readonly struct RunningProjectInfo + { + public required bool RestartWhenChangesHaveNoEffect { get; init; } + } + + [Obsolete("Use Updates2")] public readonly struct Updates( ModuleUpdateStatus status, ImmutableArray diagnostics, @@ -110,6 +116,62 @@ public readonly struct Updates( public ImmutableArray ProjectIdsToRebuild { get; } = projectsToRebuild.SelectAsArray(p => p.Id); } + public enum Status + { + /// + /// No significant changes made that need to be applied. + /// + NoChangesToApply, + + /// + /// Changes can be applied either via updates or restart. + /// + ReadyToApply, + + /// + /// Some changes are errors that block rebuild of the module. + /// This means that the code is in a broken state that cannot be resolved by restarting the application. + /// + Blocked, + } + + public readonly struct Updates2 + { + /// + /// Status of the updates. + /// + public readonly Status Status { get; init; } + + /// + /// Syntactic, semantic and emit diagnostics. + /// + /// + /// is if these diagnostics contain any errors. + /// + public required ImmutableArray CompilationDiagnostics { get; init; } + + /// + /// Rude edits per project. + /// + public required ImmutableArray<(ProjectId project, ImmutableArray diagnostics)> RudeEdits { get; init; } + + /// + /// Updates to be applied to modules. Empty if there are blocking rude edits. + /// Only updates to projects that are not included in are listed. + /// + public ImmutableArray ProjectUpdates { get; init; } + + /// + /// Running projects that need to be restarted due to rude edits in order to apply changes. + /// + public ImmutableDictionary> ProjectsToRestart { get; init; } + + /// + /// Projects with changes that need to be rebuilt in order to apply changes. + /// + public ImmutableArray ProjectsToRebuild { get; init; } + } + private static readonly ActiveStatementSpanProvider s_solutionActiveStatementSpanProvider = (_, _, _) => ValueTaskFactory.FromResult(ImmutableArray.Empty); @@ -163,32 +225,24 @@ public void CapabilitiesChanged() _encService.BreakStateOrCapabilitiesChanged(GetDebuggingSession(), inBreakState: null); } - [Obsolete] - public async Task<(ImmutableArray updates, ImmutableArray diagnostics)> EmitSolutionUpdateAsync(Solution solution, CancellationToken cancellationToken) - { - var result = await GetUpdatesAsync(solution, isRunningProject: static _ => false, cancellationToken).ConfigureAwait(false); - return (result.ProjectUpdates, result.Diagnostics); - } - - [Obsolete] - public Task GetUpdatesAsync(Solution solution, Func isRunningProject, CancellationToken cancellationToken) - => GetUpdatesAsync(solution, solution.Projects.Where(isRunningProject).Select(static p => p.Id).ToImmutableHashSet(), cancellationToken); - /// - /// Emits updates for all projects that differ between the given snapshot and the one given to the previous successful call or - /// the one passed to for the first invocation. + /// Returns TFM of a given project. /// - /// Solution snapshot. - /// Identifies projects that launched a process. - /// Cancellation token. - /// - /// Updates (one for each changed project) and Rude Edit diagnostics. Does not include syntax or semantic diagnostics. - /// + public static string? GetTargetFramework(Project project) + => project.State.NameAndFlavor.flavor; + + [Obsolete] public async Task GetUpdatesAsync(Solution solution, IImmutableSet runningProjects, CancellationToken cancellationToken) { var sessionId = GetDebuggingSession(); - var results = await _encService.EmitSolutionUpdateAsync(sessionId, solution, runningProjects, s_solutionActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false); + var runningProjectsImpl = runningProjects.ToImmutableDictionary(keySelector: p => p, elementSelector: _ => new EditAndContinue.RunningProjectInfo() + { + RestartWhenChangesHaveNoEffect = false, + AllowPartialUpdate = false + }); + + var results = await _encService.EmitSolutionUpdateAsync(sessionId, solution, runningProjectsImpl, s_solutionActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false); // If the changes fail to apply dotnet-watch fails. // We don't support discarding the changes and letting the user retry. @@ -202,7 +256,7 @@ public async Task GetUpdatesAsync(Solution solution, IImmutableSet + /// Emits updates for all projects that differ between the given snapshot and the one given to the previous successful call or + /// the one passed to for the first invocation. + /// + /// Solution snapshot. + /// Identifies projects that launched a process. + /// Cancellation token. + /// + /// Updates (one for each changed project) and Rude Edit diagnostics. Does not include syntax or semantic diagnostics. + /// May include both updates and Rude Edits for different projects. + /// + public async Task GetUpdatesAsync(Solution solution, ImmutableDictionary runningProjects, CancellationToken cancellationToken) + { + var sessionId = GetDebuggingSession(); + + var runningProjectsImpl = runningProjects.ToImmutableDictionary( + static e => e.Key, + static e => new EditAndContinue.RunningProjectInfo() + { + RestartWhenChangesHaveNoEffect = e.Value.RestartWhenChangesHaveNoEffect, + AllowPartialUpdate = true + }); + + var results = await _encService.EmitSolutionUpdateAsync(sessionId, solution, runningProjectsImpl, s_solutionActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false); + + // If the changes fail to apply dotnet-watch fails. + // We don't support discarding the changes and letting the user retry. + if (!results.ModuleUpdates.Updates.IsEmpty) + { + _encService.CommitSolutionUpdate(sessionId); + } + + return new Updates2 + { + Status = results.ModuleUpdates.Status switch + { + ModuleUpdateStatus.None => Status.NoChangesToApply, + ModuleUpdateStatus.Ready or ModuleUpdateStatus.RestartRequired => Status.ReadyToApply, + ModuleUpdateStatus.Blocked => Status.Blocked, + _ => throw ExceptionUtilities.UnexpectedValue(results.ModuleUpdates.Status) + }, + CompilationDiagnostics = results.GetAllCompilationDiagnostics(), + RudeEdits = results.RudeEdits.SelectAsArray(static re => (re.ProjectId, re.Diagnostics)), + ProjectUpdates = results.ModuleUpdates.Updates.SelectAsArray(static update => new Update( + update.Module, + update.ProjectId, + update.ILDelta, + update.MetadataDelta, + update.PdbDelta, + update.UpdatedTypes, + update.RequiredCapabilities)), + ProjectsToRestart = results.ProjectsToRestart, + ProjectsToRebuild = results.ProjectsToRebuild + }; + } + public void UpdateBaselines(Solution solution, ImmutableArray projectIds) { var sessionId = GetDebuggingSession(); diff --git a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs index 150df10079f24..bcc11ab01cf40 100644 --- a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs +++ b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs @@ -82,7 +82,7 @@ public async Task StartDebuggingSession_CapturingDocuments(bool captureAllDocume using var _ = CreateWorkspace(out var solution, out var service, [typeof(NoCompilationLanguageService)]); solution = solution - .AddTestProject("P", LanguageNames.CSharp, projectPId).Solution + .AddTestProject("P", LanguageNames.CSharp, id: projectPId).Solution .WithProjectChecksumAlgorithm(projectPId, SourceHashAlgorithm.Sha1); var documentIdA = DocumentId.CreateNewId(projectPId, debugName: "A"); @@ -189,7 +189,7 @@ public async Task ProjectNotBuilt() Assert.Empty(updates.Updates); AssertEx.Equal( [ - $"proj.csproj: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, document1.FilePath)}" + $"{document1.Project.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, document1.FilePath)}" ], InspectDiagnostics(emitDiagnostics)); EndDebuggingSession(debuggingSession); @@ -404,7 +404,6 @@ public async Task DesignTimeOnlyDocument_Wpf([CombinatorialValues(LanguageNames. solution = solution. AddTestProject("test", language, out var projectId). - AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)). AddTestDocument(source, path: sourceFilePath, out var documentId).Project.Solution; var designTimeOnlyDocumentId = DocumentId.CreateNewId(projectId); @@ -525,7 +524,7 @@ public async Task ErrorReadingModuleFile(bool breakMode) var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.RestartRequired, updates.Status); Assert.Empty(updates.Updates); - AssertEx.Equal([$"proj.csproj: (0,0)-(0,0): Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, moduleFile.Path, expectedErrorMessage)}"], InspectDiagnostics(emitDiagnostics)); + AssertEx.Equal([$"{document1.Project.FilePath}: (0,0)-(0,0): Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, moduleFile.Path, expectedErrorMessage)}"], InspectDiagnostics(emitDiagnostics)); // correct the error: EmitLibrary(projectId, source2); @@ -604,7 +603,7 @@ public async Task ErrorReadingPdbFile() var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Blocked, updates.Status); Assert.Empty(updates.Updates); - AssertEx.Equal([$"proj.csproj: (0,0)-(0,0): Error ENC1006: {string.Format(FeaturesResources.UnableToReadSourceFileOrPdb, sourceFile.Path)}"], InspectDiagnostics(emitDiagnostics)); + AssertEx.Equal([$"{project.FilePath}: (0,0)-(0,0): Error ENC1006: {string.Format(FeaturesResources.UnableToReadSourceFileOrPdb, sourceFile.Path)}"], InspectDiagnostics(emitDiagnostics)); EndDebuggingSession(debuggingSession); @@ -626,7 +625,6 @@ public async Task ErrorReadingSourceFile() var document1 = solution. AddTestProject("test"). - AddMetadataReferences(TargetFrameworkUtil.GetReferences(DefaultTargetFramework)). AddDocument("a.cs", SourceText.From(source1, Encoding.UTF8, SourceHashAlgorithm.Sha1), filePath: sourceFile.Path); var project = document1.Project; @@ -651,7 +649,7 @@ public async Task ErrorReadingSourceFile() var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Blocked, updates.Status); Assert.Empty(updates.Updates); - AssertEx.Equal([$"test.csproj: (0,0)-(0,0): Error ENC1006: {string.Format(FeaturesResources.UnableToReadSourceFileOrPdb, sourceFile.Path)}"], InspectDiagnostics(emitDiagnostics)); + AssertEx.Equal([$"{document1.Project.FilePath}: (0,0)-(0,0): Error ENC1006: {string.Format(FeaturesResources.UnableToReadSourceFileOrPdb, sourceFile.Path)}"], InspectDiagnostics(emitDiagnostics)); fileLock.Dispose(); @@ -684,7 +682,6 @@ public async Task FileAdded(bool breakMode) var documentA = solution. AddTestProject("test"). - AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)). AddDocument("test.cs", CreateText(sourceA), filePath: sourceFileA.Path); var project = documentA.Project; @@ -1368,13 +1365,14 @@ public async Task RudeEdits_UpdateBaseline(bool validChangeBeforeRudeEdit) EmitSolutionUpdateResults result; var readers = ImmutableArray.Empty; + var runningProjects = ImmutableDictionary.Empty.Add(projectId, new RunningProjectInfo() { RestartWhenChangesHaveNoEffect = false, AllowPartialUpdate = true }); // change the source (valid edit): if (validChangeBeforeRudeEdit) { solution = solution.WithDocumentText(documentId, CreateText(source2)); - result = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects: [projectId], s_noActiveSpans, CancellationToken.None); + result = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects, s_noActiveSpans, CancellationToken.None); Assert.Equal(ModuleUpdateStatus.Ready, result.ModuleUpdates.Status); Assert.Empty(result.ProjectsToRebuild); Assert.Empty(result.ProjectsToRestart); @@ -1403,10 +1401,10 @@ public async Task RudeEdits_UpdateBaseline(bool validChangeBeforeRudeEdit) diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); // validate solution update status and emit: - result = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects: [projectId], s_noActiveSpans, CancellationToken.None); + result = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects, s_noActiveSpans, CancellationToken.None); Assert.Equal(ModuleUpdateStatus.RestartRequired, result.ModuleUpdates.Status); AssertEx.Equal([projectId], result.ProjectsToRebuild); - AssertEx.Equal([projectId], result.ProjectsToRestart); + AssertEx.Equal([projectId], result.ProjectsToRestart.Keys); // restart and rebuild: _debuggerService.LoadedModules.Remove(moduleId); @@ -1433,7 +1431,7 @@ public async Task RudeEdits_UpdateBaseline(bool validChangeBeforeRudeEdit) Assert.Empty(await service.GetDocumentDiagnosticsAsync(solution.GetRequiredDocument(documentId), s_noActiveSpans, CancellationToken.None)); // apply valid change: - result = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects: [projectId], s_noActiveSpans, CancellationToken.None); + result = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects, s_noActiveSpans, CancellationToken.None); Assert.Equal(ModuleUpdateStatus.Ready, result.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); @@ -1561,12 +1559,12 @@ public async Task HasChanges() var projectCId = ProjectId.CreateNewId("C"); solution = solution. - AddTestProject("A", projectAId). + AddTestProject("A", id: projectAId). AddDocument("A.cs", "class Program { void Main() { System.Console.WriteLine(1); } }", filePath: pathA).Project.Solution. - AddTestProject("B", projectBId). + AddTestProject("B", id: projectBId). AddDocument("Common.cs", "class Common {}", filePath: pathCommon).Project. AddDocument("B.cs", "class B {}", filePath: pathB).Project.Solution. - AddTestProject("C", projectCId). + AddTestProject("C", id: projectCId). AddDocument("Common.cs", "class Common {}", filePath: pathCommon).Project. AddDocument("C.cs", "class C {}", filePath: pathC).Project.Solution; @@ -1918,7 +1916,7 @@ public async Task Project_Add() // add project that matches assembly B and update the document: var documentB2 = solution. - AddTestProject("B", projectBId). + AddTestProject("B", id: projectBId). AddTestDocument(sourceB2, path: sourceFileB.Path); solution = documentB2.Project.Solution; @@ -2138,7 +2136,7 @@ public async Task Capabilities_SynthesizedNewType() // They are reported as emit diagnostics var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - AssertEx.Equal([$"proj.csproj: (0,0)-(0,0): Error ENC1007: {FeaturesResources.ChangesRequiredSynthesizedType}"], InspectDiagnostics(emitDiagnostics)); + AssertEx.Equal([$"{project.FilePath}: (0,0)-(0,0): Error ENC1007: {FeaturesResources.ChangesRequiredSynthesizedType}"], InspectDiagnostics(emitDiagnostics)); // no emitted delta: Assert.Empty(updates.Updates); @@ -2227,7 +2225,6 @@ public async Task ValidSignificantChange_ApplyBeforeFileWatcherEvent(bool saveDo // the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build): var document1 = solution. AddTestProject("test"). - AddMetadataReferences(TargetFrameworkUtil.GetReferences(DefaultTargetFramework)). AddDocument("test.cs", CreateText("class C1 { void M() { System.Console.WriteLine(0); } }"), filePath: sourceFile.Path); var documentId = document1.Id; @@ -2315,7 +2312,6 @@ public async Task ValidSignificantChange_FileUpdateNotObservedBeforeDebuggingSes // the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build): var document2 = solution. AddTestProject("test"). - AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)). AddDocument("test.cs", CreateText(source2), filePath: sourceFile.Path); var documentId = document2.Id; @@ -2361,7 +2357,7 @@ public async Task ValidSignificantChange_FileUpdateNotObservedBeforeDebuggingSes (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); AssertEx.Equal( [ - $"test.csproj: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourceFile.Path)}" + $"{project.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourceFile.Path)}" ], InspectDiagnostics(emitDiagnostics)); // the content actually hasn't changed: @@ -2785,6 +2781,187 @@ partial class C { int Y = 2; } EndDebuggingSession(debuggingSession); } + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/78244")] + public async Task MultiProjectUpdates_ValidSignificantChange_RudeEdit() + { + var sourceA1 = """ + using System; + + class A + { + static void F() + { + Console.WriteLine(1); + } + } + """; + + var sourceB1 = """ + using System; + + interface I + { + } + """; + + var sourceA2 = """ + using System; + class A + { + static void F() + { + Console.WriteLine(2); + } + } + """; + + var sourceB2 = """ + using System; + + interface I + { + void F() {} + } + """; + + using var _ = CreateWorkspace(out var solution, out var service); + + solution = solution + .AddTestProject("A", out var projectAId) + .AddTestDocument(sourceA1, "A.cs", out var documentAId).Project.Solution + .AddTestProject("B", out var projectBId) + .AddTestDocument(sourceB1, "B.cs", out var documentBId).Project.Solution; + + EmitAndLoadLibraryToDebuggee(solution.GetRequiredDocument(documentAId)); + EmitAndLoadLibraryToDebuggee(solution.GetRequiredDocument(documentBId)); + + var debuggingSession = await StartDebuggingSessionAsync(service, solution); + + // change the source (valid edit in A and rude edit in B): + solution = solution + .WithDocumentText(documentAId, CreateText(sourceA2)) + .WithDocumentText(documentBId, CreateText(sourceB2)); + + // Rude Edit reported: + var diagnostics = await service.GetDocumentDiagnosticsAsync(solution.GetRequiredDocument(documentBId), s_noActiveSpans, CancellationToken.None); + AssertEx.Equal( + ["ENC0023: " + string.Format(FeaturesResources.Adding_an_abstract_0_or_overriding_an_inherited_0_requires_restarting_the_application, FeaturesResources.method)], + diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); + + // validate solution update status and emit: + var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(emitDiagnostics); + Assert.Equal(ModuleUpdateStatus.RestartRequired, updates.Status); + + // TODO: https://github.com/dotnet/roslyn/issues/78244 + // Should emit delta for the valid change + + //// check emitted delta: + //var delta = updates.Updates.Single(); + //Assert.Empty(delta.ActiveStatements); + //Assert.NotEmpty(delta.ILDelta); + //Assert.NotEmpty(delta.MetadataDelta); + //Assert.NotEmpty(delta.PdbDelta); + //Assert.Equal(6, delta.UpdatedMethods.Length); // F, C.C(), D.D(), E.E(int), E.E(int, int), lambda + //AssertEx.SetEqual([0x02000002, 0x02000003, 0x02000004, 0x02000005], delta.UpdatedTypes, itemInspector: t => "0x" + t.ToString("X")); + + //debuggingSession.DiscardSolutionUpdate(); + EndDebuggingSession(debuggingSession); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/78244")] + public async Task MultiProjectUpdates_ValidSignificantChange_NoEffectEdit() + { + var sourceA1 = """ + class A + { + static void F() + { + System.Console.WriteLine(1); + } + } + """; + + var sourceB1 = """ + class B + { + static B() + { + System.Console.WriteLine(10); + } + } + """; + + var sourceA2 = """ + class A + { + static void F() + { + System.Console.WriteLine(2); + } + } + """; + + var sourceB2 = """ + class B + { + static B() + { + System.Console.WriteLine(20); + } + } + """; + + using var _ = CreateWorkspace(out var solution, out var service); + + solution = solution + .AddTestProject("A", out var projectAId) + .AddTestDocument(sourceA1, "A.cs", out var documentAId).Project.Solution + .AddTestProject("B", out var projectBId) + .AddTestDocument(sourceB1, "B.cs", out var documentBId).Project.Solution; + + EmitAndLoadLibraryToDebuggee(solution.GetRequiredDocument(documentAId)); + EmitAndLoadLibraryToDebuggee(solution.GetRequiredDocument(documentBId)); + + var debuggingSession = await StartDebuggingSessionAsync(service, solution); + + // change the source (valid edit in A and no-effect edit in B): + solution = solution + .WithDocumentText(documentAId, CreateText(sourceA2)) + .WithDocumentText(documentBId, CreateText(sourceB2)); + + // no-effect warning reported: + var diagnostics = await service.GetDocumentDiagnosticsAsync(solution.GetRequiredDocument(documentBId), s_noActiveSpans, CancellationToken.None); + AssertEx.Equal( + ["ENC0118: Warning: " + string.Format(FeaturesResources.Changing_0_might_not_have_any_effect_until_the_application_is_restarted, FeaturesResources.static_constructor)], + diagnostics.Select(d => $"{d.Id}: {d.Severity}: {d.GetMessage()}")); + + // TODO: Set RestartWhenChangesHaveNoEffect=true and AllowPartialUpdate=true + // https://github.com/dotnet/roslyn/issues/78244 + var runningProjects = ImmutableDictionary.Empty + .Add(projectAId, new RunningProjectInfo() { RestartWhenChangesHaveNoEffect = false, AllowPartialUpdate = false }) + .Add(projectBId, new RunningProjectInfo() { RestartWhenChangesHaveNoEffect = false, AllowPartialUpdate = false }); + + // emit updates: + var result = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects, s_noActiveSpans, CancellationToken.None); + + AssertEx.SetEqual([], result.ProjectsToRestart.Select(p => p.Key.DebugName)); + + var updates = result.ModuleUpdates; + Assert.Empty(result.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + + // check emitted delta: + Assert.Equal(2, updates.Updates.Length); + + // Process will be restarted, so discard all updates: + debuggingSession.DiscardSolutionUpdate(); + + EndDebuggingSession(debuggingSession); + } + [Theory] [CombinatorialData] [WorkItem("https://github.com/dotnet/roslyn/issues/72331")] @@ -2883,7 +3060,7 @@ class C { int Y => 2; } solution = solution.WithDocumentText(document1.Id, CreateText(sourceV2)); // validate solution update status and emit: - var results = (await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects: [], s_noActiveSpans, CancellationToken.None).ConfigureAwait(false)).Dehydrate(); + var results = (await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None).ConfigureAwait(false)).Dehydrate(); var diagnostics = results.GetAllDiagnostics(); var generatedFilePath = Path.Combine( @@ -3212,7 +3389,7 @@ public async Task RudeEdit() // They are reported as emit diagnostics var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - AssertEx.Equal([$"proj.csproj: (0,0)-(0,0): Error ENC1007: {FeaturesResources.ChangesRequiredSynthesizedType}"], InspectDiagnostics(emitDiagnostics)); + AssertEx.Equal([$"{project.FilePath}: (0,0)-(0,0): Error ENC1007: {FeaturesResources.ChangesRequiredSynthesizedType}"], InspectDiagnostics(emitDiagnostics)); // no emitted delta: Assert.Empty(updates.Updates); @@ -3252,7 +3429,6 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule() var projectA = documentA.Project; var projectB = solution.AddTestProject("B").WithAssemblyName("A"). - AddMetadataReferences(projectA.MetadataReferences). AddDocument("DocB", source1, filePath: Path.Combine(TempRoot.Root, "DocB.cs")).Project; solution = projectB.Solution; @@ -3393,12 +3569,10 @@ public async Task MultiTargetedPartiallyBuiltProjects() var sourcePath = dir.CreateFile("Lib.cs").WriteAllText(source1, Encoding.UTF8).Path; - var documentA = solution.AddTestProject("A").WithAssemblyName("A"). - AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.NetStandard20)). + var documentA = solution.AddTestProject("A", targetFramework: TargetFramework.NetStandard20).WithAssemblyName("A"). AddDocument("Lib.cs", source1, filePath: sourcePath); - var documentB = documentA.Project.Solution.AddTestProject("B").WithAssemblyName("A"). - AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Net90)). + var documentB = documentA.Project.Solution.AddTestProject("B", targetFramework: TargetFramework.Net90).WithAssemblyName("A"). AddDocument("Lib.cs", source1, filePath: sourcePath); solution = documentB.Project.Solution; @@ -3492,12 +3666,10 @@ public async Task MultiTargeted_AllTargetsStale() var sourcePath = dir.CreateFile("Lib.cs").WriteAllText(source1, Encoding.UTF8).Path; - var documentA = solution.AddTestProject("A").WithAssemblyName("A"). - AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.NetStandard20)). + var documentA = solution.AddTestProject("A", targetFramework: TargetFramework.NetStandard20).WithAssemblyName("A"). AddDocument("Lib.cs", source1, filePath: sourcePath); - var documentB = documentA.Project.Solution.AddTestProject("B").WithAssemblyName("A"). - AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Net90)). + var documentB = documentA.Project.Solution.AddTestProject("B", targetFramework: TargetFramework.Net90).WithAssemblyName("A"). AddDocument("Lib.cs", source1, filePath: sourcePath); solution = documentB.Project.Solution; @@ -3524,8 +3696,8 @@ public async Task MultiTargeted_AllTargetsStale() Assert.Equal(ModuleUpdateStatus.None, updates.Status); AssertEx.Equal( [ - $"A.csproj: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourcePath)}", - $"B.csproj: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourcePath)}" + $"{documentA.Project.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourcePath)}", + $"{documentB.Project.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourcePath)}" ], InspectDiagnostics(emitDiagnostics)); EndDebuggingSession(debuggingSession); @@ -3552,7 +3724,7 @@ public async Task ValidSignificantChange_BaselineCreationFailed_NoStream() solution = solution.WithDocumentText(document1.Id, CreateText("class C1 { void M() { System.Console.WriteLine(2); } }")); var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - AssertEx.Equal([$"proj.csproj: (0,0)-(0,0): Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "test-pdb", new FileNotFoundException().Message)}"], InspectDiagnostics(emitDiagnostics)); + AssertEx.Equal([$"{document1.Project.FilePath}: (0,0)-(0,0): Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "test-pdb", new FileNotFoundException().Message)}"], InspectDiagnostics(emitDiagnostics)); Assert.Equal(ModuleUpdateStatus.RestartRequired, updates.Status); } @@ -3585,7 +3757,7 @@ public async Task ValidSignificantChange_BaselineCreationFailed_AssemblyReadErro solution = solution.WithDocumentText(document1.Id, CreateText("class C1 { void M() { System.Console.WriteLine(2); } }")); var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - AssertEx.Equal([$"proj.csproj: (0,0)-(0,0): Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "test-assembly", "*message*")}"], InspectDiagnostics(emitDiagnostics)); + AssertEx.Equal([$"{document.Project.FilePath}: (0,0)-(0,0): Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "test-assembly", "*message*")}"], InspectDiagnostics(emitDiagnostics)); Assert.Equal(ModuleUpdateStatus.RestartRequired, updates.Status); EndDebuggingSession(debuggingSession); @@ -4570,9 +4742,7 @@ public async Task MultiSession() using var _ = CreateWorkspace(out var solution, out var encService); - var projectP = solution. - AddTestProject("P"). - WithMetadataReferences(TargetFrameworkUtil.GetReferences(DefaultTargetFramework)); + var projectP = solution.AddTestProject("P"); solution = projectP.Solution; @@ -4598,14 +4768,14 @@ public async Task MultiSession() var solution1 = solution.WithDocumentText(documentIdA, CreateText("class C { void M() { System.Console.WriteLine(" + i + "); } }")); - var result1 = await encService.EmitSolutionUpdateAsync(sessionId, solution1, runningProjects: [], s_noActiveSpans, CancellationToken.None); + var result1 = await encService.EmitSolutionUpdateAsync(sessionId, solution1, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None); Assert.Empty(result1.Diagnostics); Assert.Equal(1, result1.ModuleUpdates.Updates.Length); encService.DiscardSolutionUpdate(sessionId); var solution2 = solution1.WithDocumentText(documentIdA, CreateText(source3)); - var result2 = await encService.EmitSolutionUpdateAsync(sessionId, solution2, runningProjects: [], s_noActiveSpans, CancellationToken.None); + var result2 = await encService.EmitSolutionUpdateAsync(sessionId, solution2, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None); Assert.Equal("CS0103", result2.Diagnostics.Single().Diagnostics.Single().Id); Assert.Empty(result2.ModuleUpdates.Updates); @@ -4628,7 +4798,7 @@ public async Task Disposal() EndDebuggingSession(debuggingSession); // The folling methods shall not be called after the debugging session ended. - await Assert.ThrowsAsync(async () => await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects: [], s_noActiveSpans, CancellationToken.None)); + await Assert.ThrowsAsync(async () => await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None)); Assert.Throws(() => debuggingSession.BreakStateOrCapabilitiesChanged(inBreakState: true)); Assert.Throws(() => debuggingSession.DiscardSolutionUpdate()); Assert.Throws(() => debuggingSession.CommitSolutionUpdate()); diff --git a/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs b/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs index 7140022638f38..db72d32fd5a77 100644 --- a/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs +++ b/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs @@ -11,10 +11,12 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.PooledObjects; 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; @@ -44,17 +46,22 @@ private static ManagedHotReloadUpdate CreateMockUpdate(ProjectId projectId) activeStatements: [], exceptionRegions: []); - private static EmitSolutionUpdateResults CreateMockResults(Solution solution, IEnumerable updates, IEnumerable 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, - ProjectsToRebuild = [], - ProjectsToRestart = [], - }; + private static ModuleUpdates CreateValidUpdates(params IEnumerable projectIds) + => new(ModuleUpdateStatus.Blocked, [.. projectIds.Select(CreateMockUpdate)]); + + private static ArrayBuilder CreateProjectRudeEdits(IEnumerable blocking, IEnumerable noEffect) + => [.. blocking.Select(id => (id, kind: RudeEditKind.InternalError)).Concat(noEffect.Select(id => (id, kind: RudeEditKind.UpdateMightNotHaveAnyEffect))) + .GroupBy(e => e.id) + .OrderBy(g => g.Key) + .Select(g => new ProjectDiagnostics(g.Key, [.. g.Select(e => Diagnostic.Create(EditAndContinueDiagnosticDescriptors.GetDescriptor(e.kind), Location.None))]))]; + + private static ImmutableDictionary CreateRunningProjects(IEnumerable<(ProjectId id, bool noEffectRestarts)> projectIds, bool allowPartialUpdate = true) + => projectIds.ToImmutableDictionary(keySelector: e => e.id, elementSelector: e => new RunningProjectInfo() { RestartWhenChangesHaveNoEffect = e.noEffectRestarts, AllowPartialUpdate = allowPartialUpdate }); + + private static IEnumerable Inspect(ImmutableDictionary> projectsToRestart) + => projectsToRestart + .OrderBy(kvp => kvp.Key.DebugName) + .Select(kvp => $"{kvp.Key.DebugName}: [{string.Join(",", kvp.Value.Select(id => id.DebugName).Order())}]"); [Fact] public async Task GetHotReloadDiagnostics() @@ -137,7 +144,7 @@ public async Task GetHotReloadDiagnostics() SyntaxError = syntaxError, ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Blocked, Updates: []), ProjectsToRebuild = [], - ProjectsToRestart = [], + ProjectsToRestart = ImmutableDictionary>.Empty, }; var actual = data.GetAllDiagnostics(); @@ -163,14 +170,11 @@ public void RunningProjects_Updates() .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 }.ToImmutableHashSet(); - var results = CreateMockResults(solution, updates: [c, d], rudeEdits: []); - EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( solution, - results.ModuleUpdates, - results.RudeEdits, - runningProjects, + CreateValidUpdates(c, d), + CreateProjectRudeEdits(blocking: [], noEffect: []), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: false)]), out var projectsToRestart, out var projectsToRebuild); @@ -179,7 +183,7 @@ public void RunningProjects_Updates() } [Fact] - public void RunningProjects_RudeEdits() + public void RunningProjects_RudeEdits_SingleImpactedRunningProject() { using var _ = CreateWorkspace(out var solution); @@ -189,22 +193,48 @@ public void RunningProjects_RudeEdits() .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 }.ToImmutableHashSet(); - var results = CreateMockResults(solution, updates: [], rudeEdits: [d]); - EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( solution, - results.ModuleUpdates, - results.RudeEdits, - runningProjects, + CreateValidUpdates(), + CreateProjectRudeEdits(blocking: [d], noEffect: []), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: false)]), out var projectsToRestart, out var projectsToRebuild); // D has rude edit ==> B has to restart - AssertEx.SetEqual([b], projectsToRestart); + AssertEx.Equal(["B: [D]"], Inspect(projectsToRestart)); - // D has rude edit: - AssertEx.SetEqual([d], projectsToRebuild); + AssertEx.SetEqual([b], projectsToRebuild); + } + + [Fact] + public void RunningProjects_RudeEdits_MultipleImpactedRunningProjects() + { + 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; + + EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( + solution, + CreateValidUpdates(), + CreateProjectRudeEdits(blocking: [c], noEffect: []), + CreateRunningProjects([(a, noEffectRestarts: true), (b, noEffectRestarts: false)]), + out var projectsToRestart, + out var projectsToRebuild); + + // C has rude edit + // ==> A, B have to restart: + AssertEx.Equal( + [ + "A: [C]", + "B: [C]", + ], Inspect(projectsToRestart)); + + AssertEx.SetEqual([a, b], projectsToRebuild); } [Fact] @@ -218,23 +248,106 @@ public void RunningProjects_RudeEdits_NotImpactingRunningProjects() .AddTestProject("A", out var a).AddProjectReferences([new(c)]).Solution .AddTestProject("B", out var b).AddProjectReferences([new(c), new(d)]).Solution; - var runningProjects = new[] { a }.ToImmutableHashSet(); - var results = CreateMockResults(solution, updates: [], rudeEdits: [d]); + EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( + solution, + CreateValidUpdates(), + CreateProjectRudeEdits(blocking: [d], noEffect: []), + CreateRunningProjects([(a, noEffectRestarts: false)]), + out var projectsToRestart, + out var projectsToRebuild); + + Assert.Empty(projectsToRestart); + + // Rude edit in projects that doesn't affect running project still causes the updated project to be rebuilt, + // so that the change takes effect if it is loaded in future. + AssertEx.SetEqual([d], projectsToRebuild); + } + + [Fact] + public void RunningProjects_NoEffectEdits_NoEffectRestarts() + { + 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; + + EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( + solution, + CreateValidUpdates(c), + CreateProjectRudeEdits(blocking: [], noEffect: [c]), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: true)]), + out var projectsToRestart, + out var projectsToRebuild); + + // C has no-effect edit + // B restarts on no effect changes + // A restarts on blocking changes + // ==> B has to restart + // ==> A has to restart as well since B is restarting and C has an update + AssertEx.Equal( + [ + "A: [C]", + "B: [C]", + ], Inspect(projectsToRestart)); + + AssertEx.SetEqual([a, b], projectsToRebuild); + } + + [Fact] + public void RunningProjects_NoEffectEdits_BlockingRestartsOnly() + { + 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; EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( solution, - results.ModuleUpdates, - results.RudeEdits, - runningProjects, + CreateValidUpdates(c), + CreateProjectRudeEdits(blocking: [], noEffect: [c]), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: false)]), out var projectsToRestart, out var projectsToRebuild); + // C has no-effect edit + // B restarts on blocking changes + // A restarts on blocking changes + // ==> no restarts/rebuild Assert.Empty(projectsToRestart); Assert.Empty(projectsToRebuild); } [Fact] - public void RunningProjects_RudeEditAndUpdate_Dependent() + public void RunningProjects_NoEffectEdits_NoImpactedRunningProject() + { + 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; + + EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( + solution, + CreateValidUpdates(d), + CreateProjectRudeEdits(blocking: [], noEffect: [d]), + CreateRunningProjects([(a, noEffectRestarts: false)]), + out var projectsToRestart, + out var projectsToRebuild); + + Assert.Empty(projectsToRestart); + Assert.Empty(projectsToRebuild); + } + + [Fact] + public void RunningProjects_NoEffectEditAndRudeEdit_SameProject() { using var _ = CreateWorkspace(out var solution); @@ -244,27 +357,110 @@ public void RunningProjects_RudeEditAndUpdate_Dependent() .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 }.ToImmutableHashSet(); - var results = CreateMockResults(solution, updates: [c], rudeEdits: [d]); + EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( + solution, + CreateValidUpdates(), + CreateProjectRudeEdits(blocking: [c], noEffect: [c]), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: false)]), + out var projectsToRestart, + out var projectsToRebuild); + + // C has rude edit + // ==> A, B have to restart + AssertEx.Equal( + [ + "A: [C]", + "B: [C]", + ], Inspect(projectsToRestart)); + + AssertEx.SetEqual([a, b], projectsToRebuild); + } + + [Theory] + [CombinatorialData] + public void RunningProjects_NoEffectEditAndRudeEdit_DifferentProjects(bool allowPartialUpdate) + { + using var _ = CreateWorkspace(out var solution); + + solution = solution + .AddTestProject("Q", out var q).Solution + .AddTestProject("P0", out var p0).AddProjectReferences([new(q)]).Solution + .AddTestProject("P1", out var p1).AddProjectReferences([new(q)]).Solution + .AddTestProject("P2", out var p2).Solution + .AddTestProject("R0", out var r0).AddProjectReferences([new(p0)]).Solution + .AddTestProject("R1", out var r1).AddProjectReferences([new(p1), new(p0)]).Solution + .AddTestProject("R2", out var r2).AddProjectReferences([new(p2), new(p0)]).Solution; + + EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( + solution, + CreateValidUpdates(p0, q), + CreateProjectRudeEdits(blocking: [p1, p2], noEffect: [q]), + CreateRunningProjects([(r0, noEffectRestarts: false), (r1, noEffectRestarts: false), (r2, noEffectRestarts: false)], allowPartialUpdate), + out var projectsToRestart, + out var projectsToRebuild); + + // P1, P2 have rude edits + // ==> R1, R2 have to restart + // P0 has no-effect edit, but R0, R1, R2 do not restart on no-effect edits + // P0 has update, R1 -> P0, R2 -> P0, R1 and R2 are restarting due to rude edits in P1 and P2 + // ==> R0 has to restart due to rude edits in P1 and P2 + // Q has update + // ==> R0 has to restart due to rude edits in P1 and P2 + if (allowPartialUpdate) + { + AssertEx.Equal( + [ + "R0: [P1,P2]", + "R1: [P1]", + "R2: [P2]", + ], Inspect(projectsToRestart)); + } + else + { + AssertEx.Equal( + [ + "R0: []", + "R1: [P1]", + "R2: [P2]", + ], Inspect(projectsToRestart)); + } + + AssertEx.SetEqual([r0, r1, r2], 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; EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( solution, - results.ModuleUpdates, - results.RudeEdits, - runningProjects, + CreateValidUpdates(c), + CreateProjectRudeEdits(blocking: [d], noEffect: []), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: false)]), out var projectsToRestart, out var 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); + AssertEx.Equal( + [ + "A: [D]", + "B: [D]", + ], Inspect(projectsToRestart)); - // D has rude edit, C has update that impacts restart set: - AssertEx.SetEqual([c, d], projectsToRebuild); + AssertEx.SetEqual([a, b], projectsToRebuild); } - [Fact] - public void RunningProjects_RudeEditAndUpdate_Independent() + [Theory] + [CombinatorialData] + public void RunningProjects_RudeEditAndUpdate_Independent(bool allowPartialUpdate) { using var _ = CreateWorkspace(out var solution); @@ -274,22 +470,74 @@ public void RunningProjects_RudeEditAndUpdate_Independent() .AddTestProject("A", out var a).AddProjectReferences([new(c)]).Solution .AddTestProject("B", out var b).AddProjectReferences([new(d)]).Solution; - var runningProjects = new[] { a, b }.ToImmutableHashSet(); - var results = CreateMockResults(solution, updates: [c], rudeEdits: [d]); - EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( solution, - results.ModuleUpdates, - results.RudeEdits, - runningProjects, + CreateValidUpdates(c), + CreateProjectRudeEdits(blocking: [d], noEffect: []), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: false)], allowPartialUpdate), out var projectsToRestart, out var projectsToRebuild); - // D has rude edit => B has to restart - AssertEx.SetEqual([b], projectsToRestart); + if (allowPartialUpdate) + { + // D has rude edit => B has to restart + AssertEx.Equal(["B: [D]"], Inspect(projectsToRestart)); + AssertEx.SetEqual([b], projectsToRebuild); + } + else + { + AssertEx.Equal( + [ + "A: []", + "B: [D]", + ], Inspect(projectsToRestart)); + + AssertEx.SetEqual([a, b], projectsToRebuild); + } + } - // D has rude edit, C has update that does not impacts restart set: - AssertEx.SetEqual([d], projectsToRebuild); + [Theory] + [CombinatorialData] + public void RunningProjects_NoEffectEditAndUpdate(bool allowPartialUpdate) + { + 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; + + EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( + solution, + CreateValidUpdates(c, d), + CreateProjectRudeEdits(blocking: [], noEffect: [d]), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: true)], allowPartialUpdate), + out var projectsToRestart, + out var projectsToRebuild); + + // D has no-effect edit + // ==> B has to restart + // C has update, A -> C, B -> C, B restarting + // ==> A has to restart even though it does not restart on no-effect edits + if (allowPartialUpdate) + { + AssertEx.Equal( + [ + "A: [D]", + "B: [D]", + ], Inspect(projectsToRestart)); + } + else + { + AssertEx.Equal( + [ + "A: []", + "B: [D]", + ], Inspect(projectsToRestart)); + } + + AssertEx.SetEqual([a, b], projectsToRebuild); } [Theory] @@ -308,18 +556,22 @@ public void RunningProjects_RudeEditAndUpdate_Chain(bool reverse) .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 }.ToImmutableHashSet(); - var results = CreateMockResults(solution, updates: reverse ? [p4, p3, p2] : [p2, p3, p4], rudeEdits: [p1]); - EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart( solution, - results.ModuleUpdates, - results.RudeEdits, - runningProjects, + CreateValidUpdates(reverse ? [p4, p3, p2] : [p2, p3, p4]), + CreateProjectRudeEdits(blocking: [p1], noEffect: []), + CreateRunningProjects([(r1, noEffectRestarts: false), (r2, noEffectRestarts: false), (r3, noEffectRestarts: false), (r4, noEffectRestarts: false)]), out var projectsToRestart, out var projectsToRebuild); - AssertEx.SetEqual([r1, r2, r3, r4], projectsToRestart); - AssertEx.SetEqual([p1, p2, p3, p4], projectsToRebuild); + AssertEx.Equal( + [ + "R1: [P1]", + "R2: [P1]", + "R3: [P1]", + "R4: [P1]", + ], Inspect(projectsToRestart)); + + AssertEx.SetEqual([r1, r2, r3, r4], projectsToRebuild); } } diff --git a/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs b/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs index 4117e4fe454de..8c84333be83a5 100644 --- a/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs +++ b/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; @@ -172,12 +173,16 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution var diagnosticDescriptor1 = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile); + var runningProjects1 = new Dictionary + { + { project.Id, new RunningProjectInfo() { RestartWhenChangesHaveNoEffect = true, AllowPartialUpdate = true} } + }.ToImmutableDictionary(); + mockEncService.EmitSolutionUpdateImpl = (solution, runningProjects, activeStatementSpanProvider) => { var project = solution.GetRequiredProject(projectId); Assert.Equal("proj", project.Name); - AssertEx.Equal(activeSpans1, activeStatementSpanProvider(documentId, "test.cs", CancellationToken.None).AsTask().Result); - AssertEx.Equal([project.Id], runningProjects); + AssertEx.SetEqual(runningProjects1, runningProjects); var deltas = ImmutableArray.Create(new ManagedHotReloadUpdate( module: moduleId1, @@ -211,11 +216,11 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution RudeEdits = [], SyntaxError = syntaxError, ProjectsToRebuild = [project.Id], - ProjectsToRestart = [project.Id], + ProjectsToRestart = ImmutableDictionary>.Empty.Add(project.Id, []), }; }; - var results = await sessionProxy.EmitSolutionUpdateAsync(localWorkspace.CurrentSolution, runningProjects: [project.Id], activeStatementSpanProvider, CancellationToken.None); + var results = await sessionProxy.EmitSolutionUpdateAsync(localWorkspace.CurrentSolution, runningProjects1, activeStatementSpanProvider, CancellationToken.None); AssertEx.Equal($"[{projectId}] Error ENC1001: test.cs(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "syntax error")}", Inspect(results.SyntaxError!)); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); diff --git a/src/Features/Test/EditAndContinue/WatchHotReloadServiceTests.cs b/src/Features/Test/EditAndContinue/WatchHotReloadServiceTests.cs index e3feb6a928f5d..322572403d401 100644 --- a/src/Features/Test/EditAndContinue/WatchHotReloadServiceTests.cs +++ b/src/Features/Test/EditAndContinue/WatchHotReloadServiceTests.cs @@ -46,14 +46,13 @@ public async Task Test() using var workspace = CreateWorkspace(out var solution, out var encService); var projectP = solution. - AddTestProject("P"). - WithMetadataReferences(TargetFrameworkUtil.GetReferences(DefaultTargetFramework)); + AddTestProject("P", out var projectId); solution = projectP.Solution; var moduleId = EmitLibrary(projectP.Id, source1, sourceFileA.Path, assemblyName: "Proj"); - var documentIdA = DocumentId.CreateNewId(projectP.Id, debugName: "A"); + var documentIdA = DocumentId.CreateNewId(projectId, debugName: "A"); solution = solution.AddDocument(DocumentInfo.Create( id: documentIdA, name: "A", @@ -75,19 +74,19 @@ public async Task Test() // Valid update: solution = solution.WithDocumentText(documentIdA, CreateText(source2)); - var result = await hotReload.GetUpdatesAsync(solution, runningProjects: [], CancellationToken.None); - Assert.Empty(result.Diagnostics); + var result = await hotReload.GetUpdatesAsync(solution, runningProjects: ImmutableDictionary.Empty, CancellationToken.None); + Assert.Empty(result.CompilationDiagnostics); Assert.Equal(1, result.ProjectUpdates.Length); AssertEx.Equal([0x02000002], result.ProjectUpdates[0].UpdatedTypes); // Insignificant change: solution = solution.WithDocumentText(documentIdA, CreateText(source3)); - result = await hotReload.GetUpdatesAsync(solution, runningProjects: [], CancellationToken.None); - Assert.Empty(result.Diagnostics); - Assert.Empty(result.Diagnostics); + result = await hotReload.GetUpdatesAsync(solution, runningProjects: ImmutableDictionary.Empty, CancellationToken.None); + Assert.Empty(result.CompilationDiagnostics); + Assert.Empty(result.CompilationDiagnostics); Assert.Empty(result.ProjectUpdates); - Assert.Equal(ModuleUpdateStatus.None, result.Status); + Assert.Equal(WatchHotReloadService.Status.NoChangesToApply, result.Status); var updatedText = await ((EditAndContinueService)hotReload.GetTestAccessor().EncService) .GetTestAccessor() @@ -103,35 +102,38 @@ public async Task Test() // Rude edit: solution = solution.WithDocumentText(documentIdA, CreateText(source4)); - result = await hotReload.GetUpdatesAsync(solution, runningProjects: solution.ProjectIds.ToImmutableHashSet(), CancellationToken.None); + var runningProjects = ImmutableDictionary.Empty + .Add(projectId, new WatchHotReloadService.RunningProjectInfo() { RestartWhenChangesHaveNoEffect = false }); + + result = await hotReload.GetUpdatesAsync(solution, runningProjects, CancellationToken.None); AssertEx.Equal( - ["ENC0110: " + string.Format(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime, FeaturesResources.method)], - result.Diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); + ["P: ENC0110: " + string.Format(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime, FeaturesResources.method)], + result.RudeEdits.SelectMany(re => re.diagnostics.Select(d => $"{re.project.DebugName}: {d.Id}: {d.GetMessage()}"))); Assert.Empty(result.ProjectUpdates); - AssertEx.SetEqual(["P"], result.ProjectIdsToRestart.Select(p => solution.GetRequiredProject(p).Name)); - AssertEx.SetEqual(["P"], result.ProjectIdsToRebuild.Select(p => solution.GetRequiredProject(p).Name)); + AssertEx.SetEqual(["P"], result.ProjectsToRestart.Select(p => solution.GetRequiredProject(p.Key).Name)); + AssertEx.SetEqual(["P"], result.ProjectsToRebuild.Select(p => solution.GetRequiredProject(p).Name)); // Syntax error: solution = solution.WithDocumentText(documentIdA, CreateText(source5)); - result = await hotReload.GetUpdatesAsync(solution, runningProjects: solution.ProjectIds.ToImmutableHashSet(), CancellationToken.None); + result = await hotReload.GetUpdatesAsync(solution, runningProjects, CancellationToken.None); AssertEx.Equal( ["CS1002: " + CSharpResources.ERR_SemicolonExpected], - result.Diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); + result.CompilationDiagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); Assert.Empty(result.ProjectUpdates); - Assert.Empty(result.ProjectIdsToRestart); - Assert.Empty(result.ProjectIdsToRebuild); + Assert.Empty(result.ProjectsToRestart); + Assert.Empty(result.ProjectsToRebuild); // Semantic error: solution = solution.WithDocumentText(documentIdA, CreateText(source6)); - result = await hotReload.GetUpdatesAsync(solution, runningProjects: solution.ProjectIds.ToImmutableHashSet(), CancellationToken.None); + result = await hotReload.GetUpdatesAsync(solution, runningProjects, CancellationToken.None); AssertEx.Equal( ["CS0103: " + string.Format(CSharpResources.ERR_NameNotInContext, "Unknown")], - result.Diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); + result.CompilationDiagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); Assert.Empty(result.ProjectUpdates); - Assert.Empty(result.ProjectIdsToRestart); - Assert.Empty(result.ProjectIdsToRebuild); + Assert.Empty(result.ProjectsToRestart); + Assert.Empty(result.ProjectsToRebuild); hotReload.EndSession(); } @@ -183,8 +185,11 @@ public async Task SourceGeneratorFailure() solution = solution.WithAdditionalDocumentText(aId, CreateText("updated text")); - var result = await hotReload.GetUpdatesAsync(solution, runningProjects: solution.ProjectIds.ToImmutableHashSet(), CancellationToken.None); - var diagnostic = result.Diagnostics.Single(); + var runningProjects = ImmutableDictionary.Empty + .Add(projectId, new WatchHotReloadService.RunningProjectInfo() { RestartWhenChangesHaveNoEffect = false }); + + var result = await hotReload.GetUpdatesAsync(solution, runningProjects, CancellationToken.None); + var diagnostic = result.CompilationDiagnostics.Single(); Assert.Equal("CS8785", diagnostic.Id); Assert.Contains("Source generator failed", diagnostic.GetMessage()); hotReload.EndSession(); diff --git a/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs b/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs index 2bddd42177e9a..d80c80b20628c 100644 --- a/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs +++ b/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs @@ -40,7 +40,7 @@ public abstract class EditAndContinueWorkspaceTestBase : TestBase, IDisposable private protected static readonly ActiveStatementSpanProvider s_noActiveSpans = (_, _, _) => new([]); - private protected const TargetFramework DefaultTargetFramework = TargetFramework.NetStandard20; + private protected const TargetFramework DefaultTargetFramework = TargetFramework.NetLatest; private protected readonly Dictionary _mockCompilationOutputs = []; private protected readonly List _telemetryLog = []; @@ -224,7 +224,7 @@ internal static void EndDebuggingSession(DebuggingSession session) Solution solution, ActiveStatementSpanProvider? activeStatementSpanProvider = null) { - var result = await session.EmitSolutionUpdateAsync(solution, runningProjects: [], activeStatementSpanProvider ?? s_noActiveSpans, CancellationToken.None); + var result = await session.EmitSolutionUpdateAsync(solution, runningProjects: ImmutableDictionary.Empty, activeStatementSpanProvider ?? s_noActiveSpans, CancellationToken.None); return (result.ModuleUpdates, result.Diagnostics.OrderBy(d => d.ProjectId.DebugName).ToImmutableArray().ToDiagnosticData(solution)); } diff --git a/src/Features/TestUtilities/EditAndContinue/Extensions.cs b/src/Features/TestUtilities/EditAndContinue/Extensions.cs index d4606b98a0c90..52d6b6b3882b1 100644 --- a/src/Features/TestUtilities/EditAndContinue/Extensions.cs +++ b/src/Features/TestUtilities/EditAndContinue/Extensions.cs @@ -17,6 +17,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.UnitTests; using Microsoft.CodeAnalysis.VisualBasic; +using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; @@ -52,22 +53,29 @@ public static IEnumerable ToLines(this string str) } } #nullable enable - public static Project AddTestProject(this Solution solution, string projectName, string language = LanguageNames.CSharp) - => AddTestProject(solution, projectName, language, out _); - - public static Project AddTestProject(this Solution solution, string projectName, ProjectId id) - => AddTestProject(solution, projectName, LanguageNames.CSharp, id); public static Project AddTestProject(this Solution solution, string projectName, out ProjectId id) => AddTestProject(solution, projectName, LanguageNames.CSharp, out id); public static Project AddTestProject(this Solution solution, string projectName, string language, out ProjectId id) - => AddTestProject(solution, projectName, language, id = ProjectId.CreateNewId(debugName: projectName)); + => AddTestProject(solution, projectName, language, TargetFramework.NetLatest, id = ProjectId.CreateNewId(debugName: projectName)); - public static Project AddTestProject(this Solution solution, string projectName, string language, ProjectId id) + public static Project AddTestProject(this Solution solution, string projectName, string language, TargetFramework targetFramework, out ProjectId id) { + var project = AddTestProject(solution, projectName, language, targetFramework, id: null); + id = project.Id; + return project; + } + + public static Project AddTestProject(this Solution solution, string projectName, string language = LanguageNames.CSharp, TargetFramework targetFramework = TargetFramework.NetLatest, ProjectId? id = null) + { + id ??= ProjectId.CreateNewId(debugName: projectName); + var info = CreateProjectInfo(projectName, id, language); - return solution.AddProject(info).GetRequiredProject(id); + return solution + .AddProject(info) + .WithProjectMetadataReferences(id, TargetFrameworkUtil.GetReferences(targetFramework)) + .GetRequiredProject(id); } public static Document AddTestDocument(this Project project, string source, string path) @@ -81,7 +89,9 @@ public static Document AddTestDocument(this Solution solution, ProjectId project id = DocumentId.CreateNewId(projectId), name: PathUtilities.GetFileName(path), SourceText.From(source, Encoding.UTF8, SourceHashAlgorithms.Default), - filePath: path).GetRequiredDocument(id); + filePath: PathUtilities.IsAbsolute(path) + ? path : Path.Combine(Path.GetDirectoryName(solution.GetRequiredProject(projectId).FilePath!)!, path)) + .GetRequiredDocument(id); public static Guid CreateProjectTelemetryId(string projectName) { @@ -104,13 +114,13 @@ public static ProjectInfo CreateProjectInfo(string projectName, ProjectId id, st _ => throw ExceptionUtilities.UnexpectedValue(language) }, compilationOptions: TestOptions.DebugDll, - filePath: projectName + language switch + filePath: Path.Combine(TempRoot.Root, projectName, projectName + language switch { LanguageNames.CSharp => ".csproj", LanguageNames.VisualBasic => ".vbproj", NoCompilationConstants.LanguageName => ".noproj", _ => throw ExceptionUtilities.UnexpectedValue(language) - }) + })) .WithCompilationOutputInfo(new CompilationOutputInfo( assemblyPath: Path.Combine(TempRoot.Root, projectName + ".dll"), generatedFilesOutputDirectory: null)) diff --git a/src/Features/TestUtilities/EditAndContinue/MockEditAndContinueService.cs b/src/Features/TestUtilities/EditAndContinue/MockEditAndContinueService.cs index e0e5d2e4b28cd..4a90b5ecf172f 100644 --- a/src/Features/TestUtilities/EditAndContinue/MockEditAndContinueService.cs +++ b/src/Features/TestUtilities/EditAndContinue/MockEditAndContinueService.cs @@ -23,7 +23,7 @@ internal sealed class MockEditAndContinueService() : IEditAndContinueService public Func, bool, bool, DebuggingSessionId>? StartDebuggingSessionImpl; public Action? EndDebuggingSessionImpl; - public Func, ActiveStatementSpanProvider, EmitSolutionUpdateResults>? EmitSolutionUpdateImpl; + public Func, ActiveStatementSpanProvider, EmitSolutionUpdateResults>? EmitSolutionUpdateImpl; public Action? OnSourceFileUpdatedImpl; public Action? CommitSolutionUpdateImpl; public Action>? UpdateBaselinesImpl; @@ -43,7 +43,7 @@ public void DiscardSolutionUpdate(DebuggingSessionId sessionId) public void UpdateBaselines(DebuggingSessionId sessionId, Solution solution, ImmutableArray rebuiltProjects) => UpdateBaselinesImpl?.Invoke(solution, rebuiltProjects); - public ValueTask EmitSolutionUpdateAsync(DebuggingSessionId sessionId, Solution solution, IImmutableSet runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + public ValueTask EmitSolutionUpdateAsync(DebuggingSessionId sessionId, Solution solution, ImmutableDictionary runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) => new((EmitSolutionUpdateImpl ?? throw new NotImplementedException()).Invoke(solution, runningProjects, activeStatementSpanProvider)); public void EndDebuggingSession(DebuggingSessionId sessionId) diff --git a/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs b/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs index 8a86caf87ff33..57b6da1ce129a 100644 --- a/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs +++ b/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs @@ -287,13 +287,17 @@ public async ValueTask GetUpdatesAsync(ImmutableArray.GetInstance(out var runningProjectPaths); runningProjectPaths.AddAll(runningProjects); - var runningProjectIds = solution.Projects.Where(p => p.FilePath != null && runningProjectPaths.Contains(p.FilePath)).Select(static p => p.Id).ToImmutableHashSet(); + + // TODO: Update once implemented: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2449700 + var runningProjectInfos = solution.Projects.Where(p => p.FilePath != null && runningProjectPaths.Contains(p.FilePath)).ToImmutableDictionary( + keySelector: static p => p.Id, + elementSelector: static p => new RunningProjectInfo { RestartWhenChangesHaveNoEffect = false, AllowPartialUpdate = false }); EmitSolutionUpdateResults.Data results; try { - results = (await encService.EmitSolutionUpdateAsync(_debuggingSession.Value, solution, runningProjectIds, s_emptyActiveStatementProvider, cancellationToken).ConfigureAwait(false)).Dehydrate(); + results = (await encService.EmitSolutionUpdateAsync(_debuggingSession.Value, solution, runningProjectInfos, s_emptyActiveStatementProvider, cancellationToken).ConfigureAwait(false)).Dehydrate(); } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { @@ -304,7 +308,7 @@ public async ValueTask GetUpdatesAsync(ImmutableArray GetUpdatesAsync(ImmutableArray>.Empty, }; } diff --git a/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/RemoteEditAndContinueService.cs b/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/RemoteEditAndContinueService.cs index 0c4dc3e39a026..e6172ce24965e 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/RemoteEditAndContinueService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/RemoteEditAndContinueService.cs @@ -142,13 +142,12 @@ public ValueTask> GetDocumentDiagnosticsAsync(Che /// Remote API. /// public ValueTask EmitSolutionUpdateAsync( - Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DebuggingSessionId sessionId, IImmutableSet runningProjects, CancellationToken cancellationToken) + Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DebuggingSessionId sessionId, ImmutableDictionary runningProjects, CancellationToken cancellationToken) { return RunServiceAsync(solutionChecksum, async solution => { var service = GetService(); - var firstProject = solution.GetProject(runningProjects.FirstOrDefault()) ?? solution.Projects.First(); try { return (await service.EmitSolutionUpdateAsync(sessionId, solution, runningProjects, CreateActiveStatementSpanProvider(callbackId), cancellationToken).ConfigureAwait(false)).Dehydrate(); @@ -158,11 +157,11 @@ public ValueTask> GetDocumentDiagnosticsAsync(Che return new EmitSolutionUpdateResults.Data() { ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Blocked, []), - Diagnostics = GetUnexpectedUpdateError(firstProject, e.Message), + Diagnostics = GetUnexpectedUpdateError(solution.GetProject(runningProjects.FirstOrDefault().Key) ?? solution.Projects.First(), e.Message), RudeEdits = [], SyntaxError = null, ProjectsToRebuild = [], - ProjectsToRestart = [], + ProjectsToRestart = ImmutableDictionary>.Empty, }; } }, cancellationToken);