Skip to content

Commit 4de6250

Browse files
committed
3
1 parent 45c379c commit 4de6250

File tree

12 files changed

+129
-70
lines changed

12 files changed

+129
-70
lines changed

src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs

-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ public void Dispose()
6868
public async ValueTask WaitForProcessRunningAsync(CancellationToken cancellationToken)
6969
{
7070
await DeltaApplier.WaitForProcessRunningAsync(cancellationToken);
71-
Reporter.Report(MessageDescriptor.BuildCompleted);
7271
}
7372
}
7473
}

src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs

+2-22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Text.RegularExpressions;
77
using Microsoft.CodeAnalysis;
8+
using Microsoft.DotNet.Watch;
89
using Microsoft.DotNet.Watcher.Internal;
910
using Microsoft.DotNet.Watcher.Tools;
1011
using Microsoft.Extensions.Tools.Internal;
@@ -14,10 +15,6 @@ namespace Microsoft.DotNet.Watcher
1415
internal sealed partial class HotReloadDotNetWatcher : Watcher
1516
{
1617
private static readonly DateTime s_fileNotExistFileTime = DateTime.FromFileTime(0);
17-
private static readonly Regex s_buildErrorRegex = GetBuildErrorRegex();
18-
19-
[GeneratedRegex(@"[^:]+: error [A-Za-z]+[0-9]+: .+")]
20-
private static partial Regex GetBuildErrorRegex();
2118

2219
private readonly IConsole _console;
2320
private readonly IRuntimeProcessLauncherFactory? _runtimeProcessLauncherFactory;
@@ -664,8 +661,6 @@ private async ValueTask<EvaluationResult> EvaluateRootProjectAsync(CancellationT
664661

665662
private async Task<bool> BuildProjectAsync(string projectPath, IReadOnlyList<string> buildArguments, CancellationToken cancellationToken)
666663
{
667-
const string BuildEmoji = "🔨";
668-
669664
var buildOutput = new List<OutputLine>();
670665

671666
var processSpec = new ProcessSpec
@@ -686,22 +681,7 @@ private async Task<bool> BuildProjectAsync(string projectPath, IReadOnlyList<str
686681
Context.Reporter.Output($"Building '{projectPath}' ...");
687682

688683
var exitCode = await ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: false, launchResult: null, cancellationToken);
689-
690-
foreach (var (line, isError) in buildOutput)
691-
{
692-
if (isError || s_buildErrorRegex.IsMatch(line))
693-
{
694-
Context.Reporter.Error(line);
695-
}
696-
else if (exitCode == 0)
697-
{
698-
Context.Reporter.Verbose(line, BuildEmoji);
699-
}
700-
else
701-
{
702-
Context.Reporter.Output(line, BuildEmoji);
703-
}
704-
}
684+
BuildUtilities.ReportBuildOutput(Context.Reporter, buildOutput, verboseOutput: exitCode == 0);
705685

706686
if (exitCode == 0)
707687
{

src/BuiltInTools/dotnet-watch/Internal/IReporter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,8 @@ public bool TryGetMessage(string? prefix, object?[] args, [NotNullWhen(true)] ou
6262
public static readonly MessageDescriptor KillingProcess = new("Killing process {0}", "⌚", MessageSeverity.Verbose, s_id++);
6363
public static readonly MessageDescriptor HotReloadChangeHandled = new("Hot reload change handled in {0}ms.", "🔥", MessageSeverity.Verbose, s_id++);
6464
public static readonly MessageDescriptor HotReloadSucceeded = new("Hot reload succeeded.", "🔥", MessageSeverity.Output, s_id++);
65-
public static readonly MessageDescriptor BuildCompleted = new("Build completed.", "⌚", MessageSeverity.Verbose, s_id++);
6665
public static readonly MessageDescriptor UpdatesApplied = new("Updates applied: {0} out of {1}.", "🔥", MessageSeverity.Verbose, s_id++);
67-
public static readonly MessageDescriptor WaitingForFileChangeBeforeRestarting = new("Waiting for a file to change before restarting dotnet...", "⏳", MessageSeverity.Warning, s_id++);
66+
public static readonly MessageDescriptor WaitingForFileChangeBeforeRestarting = new("Waiting for a file to change before restarting ...", "⏳", MessageSeverity.Warning, s_id++);
6867
public static readonly MessageDescriptor WatchingWithHotReload = new("Watching with Hot Reload.", "⌚", MessageSeverity.Verbose, s_id++);
6968
public static readonly MessageDescriptor RestartInProgress = new("Restart in progress.", "🔄", MessageSeverity.Output, s_id++);
7069
public static readonly MessageDescriptor RestartRequested = new("Restart requested.", "🔄", MessageSeverity.Output, s_id++);

src/BuiltInTools/dotnet-watch/Internal/MsBuildFileSetFactory.cs

+9-19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics;
55
using System.Text.Json;
66
using Microsoft.Build.Graph;
7+
using Microsoft.DotNet.Watch;
78
using Microsoft.DotNet.Watcher.Internal;
89
using Microsoft.Extensions.Tools.Internal;
910

@@ -57,28 +58,17 @@ internal class MSBuildFileSetFactory(
5758

5859
var exitCode = await ProcessRunner.RunAsync(processSpec, reporter, isUserApplication: false, launchResult: null, cancellationToken);
5960

60-
if (exitCode != 0 || !File.Exists(watchList))
61-
{
62-
reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(rootProjectFile)}'");
61+
var success = exitCode == 0 && File.Exists(watchList);
6362

63+
if (!success)
64+
{
65+
reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(rootProjectFile)}'.");
6466
reporter.Output($"MSBuild output from target '{TargetName}':");
65-
reporter.Output(string.Empty);
66-
67-
foreach (var (line, isError) in capturedOutput)
68-
{
69-
var message = " " + line;
70-
if (isError)
71-
{
72-
reporter.Error(message);
73-
}
74-
else
75-
{
76-
reporter.Output(message);
77-
}
78-
}
79-
80-
reporter.Output(string.Empty);
67+
}
8168

69+
BuildUtilities.ReportBuildOutput(reporter, capturedOutput, verboseOutput: success);
70+
if (!success)
71+
{
8272
return null;
8373
}
8474

src/BuiltInTools/dotnet-watch/Internal/PhysicalConsole.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ private async Task ListenToStandardInputAsync()
6262
{
6363
CtrlC => new ConsoleKeyInfo('C', ConsoleKey.C, shift: false, alt: false, control: true),
6464
CtrlR => new ConsoleKeyInfo('R', ConsoleKey.R, shift: false, alt: false, control: true),
65-
>= 'A' and <= 'Z' => new ConsoleKeyInfo(c, ConsoleKey.A + (c - 'A'), shift: false, alt: false, control: false),
65+
>= 'a' and <= 'z' => new ConsoleKeyInfo(c, ConsoleKey.A + (c - 'a'), shift: false, alt: false, control: false),
66+
>= 'A' and <= 'Z' => new ConsoleKeyInfo(c, ConsoleKey.A + (c - 'A'), shift: true, alt: false, control: false),
6667
_ => default
6768
};
6869

src/BuiltInTools/dotnet-watch/Properties/launchSettings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"dotnet-watch": {
44
"commandName": "Project",
55
"commandLineArgs": "--verbose /bl:DotnetRun.binlog",
6-
"workingDirectory": "C:\\sdk\\artifacts\\tmp\\Debug\\BlazorWasm_Ap---8DA5F107",
6+
"workingDirectory": "C:\\sdk1\\artifacts\\tmp\\Debug\\Aspire_ApplyD---C6DC4E42\\WatchAspire.AppHost",
77
"environmentVariables": {
88
"DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)",
99
"DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.RegularExpressions;
5+
using Microsoft.DotNet.Watcher.Internal;
6+
using Microsoft.Extensions.Tools.Internal;
7+
8+
namespace Microsoft.DotNet.Watch;
9+
10+
internal static partial class BuildUtilities
11+
{
12+
private static readonly Regex s_buildDiagnosticRegex = GetBuildDiagnosticRegex();
13+
14+
[GeneratedRegex(@"[^:]+: (error|warning) [A-Za-z]+[0-9]+: .+")]
15+
private static partial Regex GetBuildDiagnosticRegex();
16+
17+
public static void ReportBuildOutput(IReporter reporter, IEnumerable<OutputLine> buildOutput, bool verboseOutput)
18+
{
19+
const string BuildEmoji = "🔨";
20+
21+
foreach (var (line, isError) in buildOutput)
22+
{
23+
if (isError)
24+
{
25+
reporter.Error(line);
26+
}
27+
else if (s_buildDiagnosticRegex.Match(line) is { Success: true } match)
28+
{
29+
if (match.Groups[1].Value == "error")
30+
{
31+
reporter.Error(line);
32+
}
33+
else
34+
{
35+
reporter.Warn(line);
36+
}
37+
}
38+
else if (verboseOutput)
39+
{
40+
reporter.Verbose(line, BuildEmoji);
41+
}
42+
else
43+
{
44+
reporter.Output(line, BuildEmoji);
45+
}
46+
}
47+
}
48+
}

test/Microsoft.NET.TestFramework/TestAsset.cs

+17-15
Original file line numberDiff line numberDiff line change
@@ -89,22 +89,24 @@ public TestAsset WithSource()
8989
File.Copy(srcFile, destFile, true);
9090
}
9191

92-
string[][] Properties = {
93-
new string[] { "TargetFramework", "$(CurrentTargetFramework)", ToolsetInfo.CurrentTargetFramework },
94-
new string[] { "CurrentTargetFramework", "$(CurrentTargetFramework)", ToolsetInfo.CurrentTargetFramework },
95-
new string[] { "RuntimeIdentifier", "$(LatestWinRuntimeIdentifier)", ToolsetInfo.LatestWinRuntimeIdentifier },
96-
new string[] { "RuntimeIdentifier", "$(LatestLinuxRuntimeIdentifier)", ToolsetInfo.LatestLinuxRuntimeIdentifier },
97-
new string[] { "RuntimeIdentifier", "$(LatestMacRuntimeIdentifier)", ToolsetInfo.LatestMacRuntimeIdentifier },
98-
new string[] { "RuntimeIdentifier", "$(LatestRuntimeIdentifiers)", ToolsetInfo.LatestRuntimeIdentifiers } };
99-
100-
foreach (string[] property in Properties)
92+
var substitutions = new[]
10193
{
102-
UpdateProjProperty(property[0], property[1], property[2]);
94+
(propertyName: "TargetFramework", variableName: "CurrentTargetFramework", value: ToolsetInfo.CurrentTargetFramework),
95+
(propertyName: "CurrentTargetFramework", variableName: "CurrentTargetFramework", value: ToolsetInfo.CurrentTargetFramework),
96+
(propertyName: "RuntimeIdentifier", variableName: "LatestWinRuntimeIdentifier", value: ToolsetInfo.LatestWinRuntimeIdentifier),
97+
(propertyName: "RuntimeIdentifier", variableName: "LatestLinuxRuntimeIdentifier", value: ToolsetInfo.LatestLinuxRuntimeIdentifier),
98+
(propertyName: "RuntimeIdentifier", variableName: "LatestMacRuntimeIdentifier", value: ToolsetInfo.LatestMacRuntimeIdentifier),
99+
(propertyName: "RuntimeIdentifier", variableName: "LatestRuntimeIdentifiers", value: ToolsetInfo.LatestRuntimeIdentifiers)
100+
};
101+
102+
foreach (var (propertyName, variableName, value) in substitutions)
103+
{
104+
UpdateProjProperty(propertyName, variableName, value);
103105
}
104106

105107
foreach (var (propertyName, version) in ToolsetInfo.GetPackageVersionProperties())
106108
{
107-
this.ReplacePackageVersionVariable(propertyName, version);
109+
ReplacePackageVersionVariable(propertyName, version);
108110
}
109111

110112
return this;
@@ -118,21 +120,21 @@ public TestAsset UpdateProjProperty(string propertyName, string variableName, st
118120
var ns = p.Root.Name.Namespace;
119121
var getNode = p.Root.Elements(ns + "PropertyGroup").Elements(ns + propertyName).FirstOrDefault();
120122
getNode ??= p.Root.Elements(ns + "PropertyGroup").Elements(ns + $"{propertyName}s").FirstOrDefault();
121-
getNode?.SetValue(getNode?.Value.Replace(variableName, targetValue));
123+
getNode?.SetValue(getNode?.Value.Replace($"$({variableName})", targetValue));
122124
});
123125
}
124126

125127
public TestAsset ReplacePackageVersionVariable(string targetName, string targetValue)
126128
{
127-
string[] PropertyNames = new[] { "PackageReference", "Package" };
129+
var elementsWithVersionAttribute = new[] { "PackageReference", "Package", "Sdk" };
128130

129131
return WithProjectChanges(project =>
130132
{
131133
var ns = project.Root.Name.Namespace;
132-
foreach (var PropertyName in PropertyNames)
134+
foreach (var elementName in elementsWithVersionAttribute)
133135
{
134136
var packageReferencesToUpdate =
135-
project.Root.Descendants(ns + PropertyName)
137+
project.Root.Descendants(ns + elementName)
136138
.Where(p => p.Attribute("Version") != null && p.Attribute("Version").Value.Equals($"$({targetName})", StringComparison.OrdinalIgnoreCase));
137139
foreach (var packageReference in packageReferencesToUpdate)
138140
{

test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/WatchAspire.AppHost.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2+
<Sdk Name="Aspire.AppHost.Sdk" Version="$(AspirePackageVersion)" />
23

34
<PropertyGroup>
45
<OutputType>Exe</OutputType>

test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs

+39-3
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ public async Task Aspire()
384384
.WithSource();
385385

386386
var serviceSourcePath = Path.Combine(testAsset.Path, "WatchAspire.ApiService", "Program.cs");
387+
var serviceProjectPath = Path.Combine(testAsset.Path, "WatchAspire.ApiService", "WatchAspire.ApiService.csproj");
388+
var originalSource = File.ReadAllText(serviceSourcePath, Encoding.UTF8);
389+
387390
App.Start(testAsset, ["-lp", "http"], relativeProjectDirectory: "WatchAspire.AppHost", testFlags: TestFlags.ReadKeyFromStdin);
388391

389392
await App.AssertWaitingForChanges();
@@ -394,9 +397,10 @@ public async Task Aspire()
394397
// wait until after DCP session started:
395398
await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #1");
396399

397-
var newSource = File.ReadAllText(serviceSourcePath, Encoding.UTF8);
398-
newSource = newSource.Replace("Enumerable.Range(1, 5)", "Enumerable.Range(1, 10)");
399-
UpdateSourceFile(serviceSourcePath, newSource);
400+
// valid code change:
401+
UpdateSourceFile(
402+
serviceSourcePath,
403+
originalSource.Replace("Enumerable.Range(1, 5)", "Enumerable.Range(1, 10)"));
400404

401405
await App.AssertOutputLineStartsWith("dotnet watch 🔥 Hot reload change handled");
402406

@@ -407,6 +411,38 @@ public async Task Aspire()
407411
// Only one browser should be launched (dashboard). The child process shouldn't launch a browser.
408412
Assert.Equal(1, App.Process.Output.Count(line => line.StartsWith("dotnet watch ⌚ Launching browser: ")));
409413

414+
// rude edit with build error:
415+
UpdateSourceFile(
416+
serviceSourcePath,
417+
originalSource.Replace("record WeatherForecast", "record WeatherForecast2"));
418+
419+
await App.AssertOutputLineStartsWith(" ❔ Do you want to restart these projects? Yes (y) / No (n) / Always (a) / Never (v)");
420+
421+
App.AssertOutputContains("dotnet watch ⌚ Unable to apply hot reload, restart is needed to apply the changes.");
422+
App.AssertOutputContains("error ENC0020: Renaming record 'WeatherForecast' requires restarting the application.");
423+
App.AssertOutputContains("dotnet watch ⌚ Affected projects:");
424+
App.AssertOutputContains("dotnet watch ⌚ WatchAspire.ApiService");
425+
App.Process.ClearOutput();
426+
427+
App.SendKey('y');
428+
429+
await App.AssertOutputLineStartsWith(MessageDescriptor.FixBuildError, failure: _ => false);
430+
431+
App.AssertOutputContains("dotnet watch ❌ [WatchAspire.ApiService (net9.0)] Exited with error code -1");
432+
App.AssertOutputContains($"dotnet watch ⌚ Building '{serviceProjectPath}' ...");
433+
App.AssertOutputContains("error CS0246: The type or namespace name 'WeatherForecast' could not be found");
434+
App.Process.ClearOutput();
435+
436+
// fix build error:
437+
UpdateSourceFile(
438+
serviceSourcePath,
439+
originalSource.Replace("WeatherForecast", "WeatherForecast2"));
440+
441+
await App.AssertOutputLineStartsWith("dotnet watch ⌚ [WatchAspire.ApiService (net9.0)] Capabilities");
442+
443+
App.AssertOutputContains("dotnet watch ⌚ Build succeeded.");
444+
App.AssertOutputContains($"dotnet watch ⭐ Starting project: {serviceProjectPath}");
445+
410446
App.SendControlC();
411447

412448
await App.AssertOutputLineStartsWith("dotnet watch 🛑 Shutdown requested. Press Ctrl+C again to force exit.");

test/dotnet-watch.Tests/MSBuildEvaluationFilterTest.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public async Task ProcessAsync_EvaluatesFileSetIfProjFileChanges()
2828

2929
evaluator.RequiresRevaluation = false;
3030

31-
await evaluator.EvaluateAsync(changedFile: new(new() { FilePath = "Test.csproj" }, ChangeKind.Update), CancellationToken.None);
31+
await evaluator.EvaluateAsync(changedFile: new(new() { FilePath = "Test.csproj", ContainingProjectPaths = [] }, ChangeKind.Update), CancellationToken.None);
3232

3333
Assert.True(evaluator.RequiresRevaluation);
3434
}
@@ -52,7 +52,7 @@ public async Task ProcessAsync_DoesNotEvaluateFileSetIfNonProjFileChanges()
5252

5353
evaluator.RequiresRevaluation = false;
5454

55-
await evaluator.EvaluateAsync(changedFile: new(new() { FilePath = "Controller.cs" }, ChangeKind.Update), CancellationToken.None);
55+
await evaluator.EvaluateAsync(changedFile: new(new() { FilePath = "Controller.cs", ContainingProjectPaths = [] }, ChangeKind.Update), CancellationToken.None);
5656

5757
Assert.False(evaluator.RequiresRevaluation);
5858
Assert.Equal(1, counter);
@@ -78,7 +78,7 @@ public async Task ProcessAsync_EvaluateFileSetOnEveryChangeIfOptimizationIsSuppr
7878

7979
evaluator.RequiresRevaluation = false;
8080

81-
await evaluator.EvaluateAsync(changedFile: new(new() { FilePath = "Controller.cs" }, ChangeKind.Update), CancellationToken.None);
81+
await evaluator.EvaluateAsync(changedFile: new(new() { FilePath = "Controller.cs", ContainingProjectPaths = [] }, ChangeKind.Update), CancellationToken.None);
8282

8383
Assert.True(evaluator.RequiresRevaluation);
8484
Assert.Equal(2, counter);
@@ -93,8 +93,8 @@ public async Task ProcessAsync_SetsEvaluationRequired_IfMSBuildFileChanges_ButIs
9393
var result = new EvaluationResult(
9494
new Dictionary<string, FileItem>()
9595
{
96-
{ "Controlller.cs", new FileItem { FilePath = "Controlller.cs" } },
97-
{ "Proj.csproj", new FileItem { FilePath = "Proj.csproj" } },
96+
{ "Controlller.cs", new FileItem { FilePath = "Controlller.cs", ContainingProjectPaths = []} },
97+
{ "Proj.csproj", new FileItem { FilePath = "Proj.csproj", ContainingProjectPaths = [] } },
9898
},
9999
projectGraph: null);
100100

@@ -121,7 +121,7 @@ public async Task ProcessAsync_SetsEvaluationRequired_IfMSBuildFileChanges_ButIs
121121
evaluator.RequiresRevaluation = false;
122122
evaluator.Timestamps["Proj.csproj"] = new DateTime(1007);
123123

124-
await evaluator.EvaluateAsync(new(new() { FilePath = "Controller.cs" }, ChangeKind.Update), CancellationToken.None);
124+
await evaluator.EvaluateAsync(new(new() { FilePath = "Controller.cs", ContainingProjectPaths = [] }, ChangeKind.Update), CancellationToken.None);
125125

126126
Assert.True(evaluator.RequiresRevaluation);
127127
}

test/dotnet-watch.Tests/Utilities/AwaitableProcess.cs

+3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ public void Start()
5454
WriteTestOutput($"{DateTime.Now}: process started: '{_process.StartInfo.FileName} {_process.StartInfo.Arguments}'");
5555
}
5656

57+
public void ClearOutput()
58+
=> _lines.Clear();
59+
5760
public async Task<string> GetOutputLineAsync(Predicate<string> success, Predicate<string> failure)
5861
{
5962
using var cancellationOnFailure = new CancellationTokenSource();

0 commit comments

Comments
 (0)