Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 132 additions & 100 deletions src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
}

var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, Context.Reporter);
compilationHandler = new CompilationHandler(Context.Reporter, Context.EnvironmentOptions, shutdownCancellationToken);
compilationHandler = new CompilationHandler(Context.Reporter, Context.EnvironmentOptions, Context.Options, shutdownCancellationToken);
var scopedCssFileHandler = new ScopedCssFileHandler(Context.Reporter, projectMap, browserConnector);
var projectLauncher = new ProjectLauncher(Context, projectMap, browserConnector, compilationHandler, iteration);
var outputDirectories = GetProjectOutputDirectories(evaluationResult.ProjectGraph);
Expand Down
5 changes: 4 additions & 1 deletion src/BuiltInTools/dotnet-watch/Internal/IReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ public bool TryGetMessage(string? prefix, object?[] args, [NotNullWhen(true)] ou
public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = new("Ignoring change in hidden directory '{0}': {1} '{2}'", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = new("Ignoring change in output directory: {0} '{1}'", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = new("File addition triggered re-evaluation.", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor NoCSharpChangesToApply = new ("No C# changes to apply.", "⌚", MessageSeverity.Output, s_id++);
public static readonly MessageDescriptor NoCSharpChangesToApply = new("No C# changes to apply.", "⌚", MessageSeverity.Output, s_id++);
public static readonly MessageDescriptor Exited = new("Exited", "⌚", MessageSeverity.Output, s_id++);
public static readonly MessageDescriptor ExitedWithUnknownErrorCode = new("Exited with unknown error code", "❌", MessageSeverity.Error, s_id++);
public static readonly MessageDescriptor ExitedWithErrorCode = new("Exited with error code {0}", "❌", MessageSeverity.Error, s_id++);
}

internal interface IReporter
Expand Down
6 changes: 3 additions & 3 deletions src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,15 @@ public static async Task<int> RunAsync(ProcessSpec processSpec, IReporter report
{
if (exitCode == 0)
{
reporter.Output("Exited");
reporter.Report(MessageDescriptor.Exited);
}
else if (exitCode == null)
{
reporter.Error("Exited with unknown error code");
reporter.Report(MessageDescriptor.ExitedWithUnknownErrorCode);
}
else
{
reporter.Error($"Exited with error code {exitCode}");
reporter.Report(MessageDescriptor.ExitedWithErrorCode, exitCode);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public static string GetAssemblyName(this ProjectGraphNode projectNode)
public static IEnumerable<string> GetCapabilities(this ProjectGraphNode projectNode)
=> projectNode.ProjectInstance.GetItems("ProjectCapability").Select(item => item.EvaluatedInclude);

public static bool IsAutoRestartEnabled(this ProjectGraphNode projectNode)
=> bool.TryParse(projectNode.ProjectInstance.GetPropertyValue("HotReloadAutoRestart"), out var result) && result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this constant be defined somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of that definition. I can make it a const field, but it's ever going to be used here.


public static IEnumerable<ProjectGraphNode> GetTransitivelyReferencingProjects(this IEnumerable<ProjectGraphNode> projects)
{
var visited = new HashSet<ProjectGraphNode>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@
await Task.Delay(1000);
}

class C { }
class C { /* member placeholder */ }
79 changes: 78 additions & 1 deletion test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,83 @@ public static void Print()
await App.AssertOutputLineStartsWith("Changed!");
}

[Theory]
[CombinatorialData]
public async Task AutoRestartOnRudeEdit(bool nonInteractive)
{
var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp")
.WithSource();

if (!nonInteractive)
{
testAsset = testAsset
.WithProjectChanges(project =>
{
project.Root.Descendants()
.First(e => e.Name.LocalName == "PropertyGroup")
.Add(XElement.Parse("""
<HotReloadAutoRestart>true</HotReloadAutoRestart>
"""));
});
}

var programPath = Path.Combine(testAsset.Path, "Program.cs");

App.Start(testAsset, nonInteractive ? ["--non-interactive"] : []);

await App.AssertWaitingForChanges();
App.Process.ClearOutput();

// rude edit: adding virtual method
UpdateSourceFile(programPath, src => src.Replace("/* member placeholder */", "public virtual void F() {}"));

await App.AssertOutputLineStartsWith(MessageDescriptor.WaitingForChanges, failure: _ => false);

App.AssertOutputContains("⌚ Restart is needed to apply the changes");
App.AssertOutputContains($"⌚ [auto-restart] {programPath}(33,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application.");
App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited");
App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched");
}

[Theory]
[CombinatorialData]
public async Task AutoRestartOnNoEffectEdit(bool nonInteractive)
{
var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp")
.WithSource();

if (!nonInteractive)
{
testAsset = testAsset
.WithProjectChanges(project =>
{
project.Root.Descendants()
.First(e => e.Name.LocalName == "PropertyGroup")
.Add(XElement.Parse("""
<HotReloadAutoRestart>true</HotReloadAutoRestart>
"""));
});
}

var programPath = Path.Combine(testAsset.Path, "Program.cs");

App.Start(testAsset, nonInteractive ? ["--non-interactive"] : []);

await App.AssertWaitingForChanges();
App.Process.ClearOutput();

// rude edit: adding virtual method
UpdateSourceFile(programPath, src => src.Replace("Started", "<Updated>"));

await App.AssertOutputLineStartsWith(MessageDescriptor.WaitingForChanges, failure: _ => false);

App.AssertOutputContains("⌚ Restart is needed to apply the changes");
App.AssertOutputContains($"⌚ [auto-restart] {programPath}(16,19): warning ENC0118: Changing 'top-level code' might not have any effect until the application is restarted.");
App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited");
App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched");
App.AssertOutputContains("<Updated>");
}

/// <summary>
/// Unchanged project doesn't build. Wait for source change and rebuild.
/// </summary>
Expand Down Expand Up @@ -682,7 +759,7 @@ public async Task Aspire()

await App.AssertOutputLineStartsWith(" ❔ Do you want to restart these projects? Yes (y) / No (n) / Always (a) / Never (v)");

App.AssertOutputContains("dotnet watch ⌚ Unable to apply hot reload, restart is needed to apply the changes.");
App.AssertOutputContains("dotnet watch ⌚ Restart is needed to apply the changes.");
App.AssertOutputContains("error ENC0020: Renaming record 'WeatherForecast' requires restarting the application.");
App.AssertOutputContains("dotnet watch ⌚ Affected projects:");
App.AssertOutputContains("dotnet watch ⌚ WatchAspire.ApiService");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public async Task ReferenceOutputAssembly_False()
reporter);

var projectGraph = factory.TryLoadProjectGraph(projectGraphRequired: false);
var handler = new CompilationHandler(reporter, environmentOptions, CancellationToken.None);
var handler = new CompilationHandler(reporter, environmentOptions, new GlobalOptions(), CancellationToken.None);

await handler.Workspace.UpdateProjectConeAsync(hostProject, CancellationToken.None);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ public async Task RudeEditInProjectWithoutRunningProcess()

var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadChangeHandled);
var sessionStarted = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadSessionStarted);
var applyUpdateVerbose = w.Reporter.RegisterSemaphore(MessageDescriptor.ApplyUpdate_Verbose);

// let the host process start:
Log("Waiting for changes...");
Expand All @@ -516,6 +517,7 @@ public async Task RudeEditInProjectWithoutRunningProcess()
await sessionStarted.WaitAsync(w.ShutdownSource.Token);

// Terminate the process:
Log($"Terminating process {runningProject.ProjectNode.GetDisplayName()} ...");
await w.Service.ProjectLauncher.TerminateProcessAsync(runningProject, CancellationToken.None);

// rude edit in A (changing assembly level attribute):
Expand All @@ -526,8 +528,8 @@ public async Task RudeEditInProjectWithoutRunningProcess()
Log("Waiting for change handled ...");
await changeHandled.WaitAsync(w.ShutdownSource.Token);

w.Reporter.ProcessOutput.Contains("verbose ⌚ Rude edits detected but do not affect any running process");
w.Reporter.ProcessOutput.Contains($"verbose ❌ {serviceSourceA2}(1,12): error ENC0003: Updating 'attribute' requires restarting the application.");
Log("Waiting for verbose rude edit reported ...");
await applyUpdateVerbose.WaitAsync(w.ShutdownSource.Token);
}

public enum DirectoryKind
Expand Down