diff --git a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs index 260b35c23042..6a796e464d79 100644 --- a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs +++ b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs @@ -11,8 +11,7 @@ public static async Task RunAsync(string workingDirectory, DotNetWatchOpti { var globalOptions = new GlobalOptions() { - Quiet = options.IsQuiet, - Verbose = options.IsVerbose, + LogLevel = options.LogLevel, NoHotReload = false, NonInteractive = true, }; @@ -42,10 +41,10 @@ public static async Task RunAsync(string workingDirectory, DotNetWatchOpti var muxerPath = Path.GetFullPath(Path.Combine(options.SdkDirectory, "..", "..", "dotnet" + PathUtilities.ExecutableExtension)); var console = new PhysicalConsole(TestFlags.None); - var reporter = new ConsoleReporter(console, globalOptions.Verbose, globalOptions.Quiet, suppressEmojis: false); + var reporter = new ConsoleReporter(console, suppressEmojis: false); var environmentOptions = EnvironmentOptions.FromEnvironment(muxerPath); var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout(isHotReloadEnabled: true)); - var loggerFactory = new LoggerFactory(reporter); + var loggerFactory = new LoggerFactory(reporter, globalOptions.LogLevel); var logger = loggerFactory.CreateLogger(DotNetWatchContext.DefaultLogComponentName); using var context = new DotNetWatchContext() diff --git a/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs b/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs index 2ccf84b885b5..1d8f526c1fff 100644 --- a/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs +++ b/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.CommandLine; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; @@ -17,8 +18,7 @@ internal sealed class DotNetWatchOptions public required string ProjectPath { get; init; } public required ImmutableArray ApplicationArguments { get; init; } - public bool IsVerbose { get; init; } - public bool IsQuiet { get; init; } + public LogLevel LogLevel { get; init; } public bool NoLaunchProfile { get; init; } public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOptions? options) @@ -71,8 +71,7 @@ public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOp { SdkDirectory = parseResult.GetRequiredValue(sdkOption), ProjectPath = parseResult.GetRequiredValue(projectOption), - IsQuiet = parseResult.GetValue(quietOption), - IsVerbose = parseResult.GetValue(verboseOption), + LogLevel = parseResult.GetValue(quietOption) ? LogLevel.Warning : parseResult.GetValue(verboseOption) ? LogLevel.Debug : LogLevel.Information, ApplicationArguments = [.. parseResult.GetValue(applicationArguments) ?? []], NoLaunchProfile = parseResult.GetValue(noLaunchProfileOption), }; diff --git a/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs b/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs index cefcd43cf320..0f1fbb74d5d8 100644 --- a/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs +++ b/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs @@ -71,13 +71,13 @@ public bool IsServerSupported(ProjectGraphNode projectNode, ILogger logger) { if (context.EnvironmentOptions.SuppressBrowserRefresh) { - logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable.WithSeverityWhen(MessageSeverity.Error, RequiresBrowserRefresh), EnvironmentVariables.Names.SuppressBrowserRefresh); + logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable.WithLevelWhen(LogLevel.Error, RequiresBrowserRefresh), EnvironmentVariables.Names.SuppressBrowserRefresh); return false; } if (!projectNode.IsNetCoreApp(minVersion: s_minimumSupportedVersion)) { - logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported.WithSeverityWhen(MessageSeverity.Error, RequiresBrowserRefresh)); + logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported.WithLevelWhen(LogLevel.Error, RequiresBrowserRefresh)); return false; } diff --git a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs index 70c121ccf7c1..8eb705b17926 100644 --- a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs +++ b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch { @@ -35,6 +36,7 @@ internal sealed record EnvironmentOptions( bool SuppressBrowserRefresh = false, bool SuppressEmojis = false, bool RestartOnRudeEdit = false, + LogLevel? CliLogLevel = null, string? AutoReloadWebSocketHostName = null, int? AutoReloadWebSocketPort = null, string? BrowserPath = null, @@ -53,6 +55,7 @@ internal sealed record EnvironmentOptions( SuppressBrowserRefresh: EnvironmentVariables.SuppressBrowserRefresh, SuppressEmojis: EnvironmentVariables.SuppressEmojis, RestartOnRudeEdit: EnvironmentVariables.RestartOnRudeEdit, + CliLogLevel: EnvironmentVariables.CliLogLevel, AutoReloadWebSocketHostName: EnvironmentVariables.AutoReloadWSHostName, AutoReloadWebSocketPort: EnvironmentVariables.AutoReloadWSPort, BrowserPath: EnvironmentVariables.BrowserPath, diff --git a/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs b/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs index 28763d7f3223..41ac7d88edea 100644 --- a/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs +++ b/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch; internal static class EnvironmentVariables @@ -20,7 +22,19 @@ public static class Names public const string SuppressBrowserRefresh = "DOTNET_WATCH_SUPPRESS_BROWSER_REFRESH"; } - public static bool VerboseCliOutput => ReadBool("DOTNET_CLI_CONTEXT_VERBOSE"); + public static LogLevel? CliLogLevel + { + get + { + var value = Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"); + return string.Equals(value, "trace", StringComparison.OrdinalIgnoreCase) + ? LogLevel.Trace + : ParseBool(value) + ? LogLevel.Debug + : null; + } + } + public static bool IsPollingEnabled => ReadBool("DOTNET_USE_POLLING_FILE_WATCHER"); public static bool SuppressEmojis => ReadBool("DOTNET_WATCH_SUPPRESS_EMOJIS"); public static bool RestartOnRudeEdit => ReadBool("DOTNET_WATCH_RESTART_ON_RUDE_EDIT"); @@ -46,11 +60,14 @@ public static class Names public static string? BrowserPath => Environment.GetEnvironmentVariable("DOTNET_WATCH_BROWSER_PATH"); private static bool ReadBool(string variableName) - => Environment.GetEnvironmentVariable(variableName) is var value && (value == "1" || bool.TryParse(value, out var boolValue) && boolValue); + => ParseBool(Environment.GetEnvironmentVariable(variableName)); private static TimeSpan? ReadTimeSpan(string variableName) => Environment.GetEnvironmentVariable(variableName) is var value && long.TryParse(value, out var intValue) && intValue >= 0 ? TimeSpan.FromMilliseconds(intValue) : null; private static int? ReadInt(string variableName) => Environment.GetEnvironmentVariable(variableName) is var value && int.TryParse(value, out var intValue) ? intValue : null; + + private static bool ParseBool(string? value) + => value == "1" || bool.TryParse(value, out var boolValue) && boolValue; } diff --git a/src/BuiltInTools/Watch/Context/GlobalOptions.cs b/src/BuiltInTools/Watch/Context/GlobalOptions.cs index 0f24692c8522..2dc002f6070e 100644 --- a/src/BuiltInTools/Watch/Context/GlobalOptions.cs +++ b/src/BuiltInTools/Watch/Context/GlobalOptions.cs @@ -1,12 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch; internal sealed class GlobalOptions { - public bool Quiet { get; init; } - public bool Verbose { get; init; } + public LogLevel LogLevel { get; init; } public bool NoHotReload { get; init; } public bool NonInteractive { get; init; } diff --git a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs index 2929d4f1e71e..e95ccc1be34f 100644 --- a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs +++ b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs @@ -521,7 +521,7 @@ void ReportDiagnostic(Diagnostic diagnostic, MessageDescriptor descriptor, strin { errorsToDisplayInApp.Add(MessageDescriptor.RestartingApplicationToApplyChanges.GetMessage()); } - else if (descriptor.Severity != MessageSeverity.None) + else if (descriptor.Level != LogLevel.None) { errorsToDisplayInApp.Add(descriptor.GetMessage(args)); } diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs index 5466d27b32c2..52df0fa62829 100644 --- a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs @@ -31,7 +31,7 @@ public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRun _runtimeProcessLauncherFactory = runtimeProcessLauncherFactory; if (!context.Options.NonInteractive) { - var consoleInput = new ConsoleInputReader(_console, context.Options.Quiet, context.EnvironmentOptions.SuppressEmojis); + var consoleInput = new ConsoleInputReader(_console, context.Options.LogLevel, context.EnvironmentOptions.SuppressEmojis); var noPrompt = context.EnvironmentOptions.RestartOnRudeEdit; if (noPrompt) diff --git a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs index 06b79772ddf6..2fc7e54a9b34 100644 --- a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs +++ b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs @@ -230,16 +230,13 @@ private Task UpdateSolutionAsync(Solution newSolution, string operationDisplayNa private async Task ReportSolutionFilesAsync(Solution solution, int updateId, string operationDisplayName, CancellationToken cancellationToken) { -#if DEBUG - _logger.LogDebug("Solution: {Path}", solution.FilePath); + _logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId); - if (!_logger.IsEnabled(LogLevel.Debug)) + if (!_logger.IsEnabled(LogLevel.Trace)) { return; } - _logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId); - foreach (var project in solution.Projects) { _logger.LogDebug(" Project: {Path}", project.FilePath); @@ -265,8 +262,5 @@ async ValueTask InspectDocumentAsync(TextDocument document, string kind) var text = await document.GetTextAsync(cancellationToken); _logger.LogDebug(" {Kind}: {FilePath} [{Checksum}]", kind, document.FilePath, Convert.ToBase64String(text.GetChecksum().ToArray())); } -#else - await Task.CompletedTask; -#endif } } diff --git a/src/BuiltInTools/Watch/UI/ConsoleInputReader.cs b/src/BuiltInTools/Watch/UI/ConsoleInputReader.cs index 4c9e930b245a..233d320765ba 100644 --- a/src/BuiltInTools/Watch/UI/ConsoleInputReader.cs +++ b/src/BuiltInTools/Watch/UI/ConsoleInputReader.cs @@ -1,15 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch { - internal sealed class ConsoleInputReader(IConsole console, bool quiet, bool suppressEmojis) + internal sealed class ConsoleInputReader(IConsole console, LogLevel logLevel, bool suppressEmojis) { private readonly object _writeLock = new(); public async Task GetKeyAsync(string prompt, Func validateInput, CancellationToken cancellationToken) { - if (quiet) + if (logLevel > LogLevel.Information) { return ConsoleKey.Escape; } diff --git a/src/BuiltInTools/Watch/UI/ConsoleReporter.cs b/src/BuiltInTools/Watch/UI/ConsoleReporter.cs index 198e518590bb..60b156142b08 100644 --- a/src/BuiltInTools/Watch/UI/ConsoleReporter.cs +++ b/src/BuiltInTools/Watch/UI/ConsoleReporter.cs @@ -9,10 +9,8 @@ namespace Microsoft.DotNet.Watch /// This API supports infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - internal sealed class ConsoleReporter(IConsole console, bool verbose, bool quiet, bool suppressEmojis) : IReporter, IProcessOutputReporter + internal sealed class ConsoleReporter(IConsole console, bool suppressEmojis) : IReporter, IProcessOutputReporter { - public bool IsVerbose { get; } = verbose; - public bool IsQuiet { get; } = quiet; public bool SuppressEmojis { get; } = suppressEmojis; private readonly Lock _writeLock = new(); @@ -50,33 +48,18 @@ private void WriteLine(TextWriter writer, string message, ConsoleColor? color, E } } - public void Report(EventId id, Emoji emoji, MessageSeverity severity, string message) + public void Report(EventId id, Emoji emoji, LogLevel level, string message) { - switch (severity) + var color = level switch { - case MessageSeverity.Error: - // Use stdout for error messages to preserve ordering with respect to other output. - WriteLine(console.Error, message, ConsoleColor.Red, emoji); - break; + LogLevel.Critical or LogLevel.Error => ConsoleColor.Red, + LogLevel.Warning => ConsoleColor.Yellow, + LogLevel.Information => (ConsoleColor?)null, + _ => ConsoleColor.DarkGray, + }; - case MessageSeverity.Warning: - WriteLine(console.Error, message, ConsoleColor.Yellow, emoji); - break; - - case MessageSeverity.Output: - if (!IsQuiet) - { - WriteLine(console.Error, message, color: null, emoji); - } - break; - - case MessageSeverity.Verbose: - if (IsVerbose) - { - WriteLine(console.Error, message, ConsoleColor.DarkGray, emoji); - } - break; - } + // Use stdout for error messages to preserve ordering with respect to other output. + WriteLine(console.Error, message, color, emoji); } } } diff --git a/src/BuiltInTools/Watch/UI/IReporter.cs b/src/BuiltInTools/Watch/UI/IReporter.cs index 2a118db336ee..82b60996e1ab 100644 --- a/src/BuiltInTools/Watch/UI/IReporter.cs +++ b/src/BuiltInTools/Watch/UI/IReporter.cs @@ -2,21 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; -using System.Diagnostics; using Microsoft.DotNet.HotReload; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch { - internal enum MessageSeverity - { - None, - Verbose, - Output, - Warning, - Error, - } - internal enum Emoji { Default = 0, @@ -66,61 +56,43 @@ public static string GetLogMessagePrefix(this Emoji emoji) public static void Log(this ILogger logger, MessageDescriptor descriptor, params object?[] args) { logger.Log( - descriptor.Severity.ToLogLevel(), + descriptor.Level, descriptor.Id, state: (descriptor, args), exception: null, formatter: static (state, _) => state.descriptor.GetMessage(state.args)); } - - public static LogLevel ToLogLevel(this MessageSeverity severity) - => severity switch - { - MessageSeverity.None => LogLevel.None, - MessageSeverity.Verbose => LogLevel.Debug, - MessageSeverity.Output => LogLevel.Information, - MessageSeverity.Warning => LogLevel.Warning, - MessageSeverity.Error => LogLevel.Error, - _ => throw new InvalidOperationException() - }; - - public static MessageSeverity ToSeverity(this LogLevel level) - => level switch - { - LogLevel.Debug => MessageSeverity.Verbose, - LogLevel.Information => MessageSeverity.Output, - LogLevel.Warning => MessageSeverity.Warning, - LogLevel.Error => MessageSeverity.Error, - LogLevel.None => MessageSeverity.None, - _ => throw new InvalidOperationException() - }; } - internal sealed class LoggerFactory(IReporter reporter) : ILoggerFactory + internal sealed class LoggerFactory(IReporter reporter, LogLevel level) : ILoggerFactory { - private sealed class Logger(IReporter reporter, string categoryName) : ILogger + private sealed class Logger(IReporter reporter, LogLevel level, string categoryName) : ILogger { public bool IsEnabled(LogLevel logLevel) - => reporter.IsVerbose || logLevel > LogLevel.Debug; + => logLevel >= level; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { + if (!IsEnabled(logLevel)) + { + return; + } + var (name, display) = LoggingUtilities.ParseCategoryName(categoryName); var prefix = display != null ? $"[{display}] " : ""; - var severity = logLevel.ToSeverity(); var descriptor = eventId.Id != 0 ? MessageDescriptor.GetDescriptor(eventId) : default; - var emoji = severity switch + var emoji = logLevel switch { _ when descriptor.Emoji != Emoji.Default => descriptor.Emoji, - MessageSeverity.Error => Emoji.Error, - MessageSeverity.Warning => Emoji.Warning, + LogLevel.Error => Emoji.Error, + LogLevel.Warning => Emoji.Warning, _ when MessageDescriptor.ComponentEmojis.TryGetValue(name, out var componentEmoji) => componentEmoji, _ => Emoji.Watch }; - reporter.Report(eventId, emoji, severity, prefix + formatter(state, exception)); + reporter.Report(eventId, emoji, logLevel, prefix + formatter(state, exception)); } public IDisposable? BeginScope(TState state) where TState : notnull @@ -132,27 +104,27 @@ public void Dispose() } public ILogger CreateLogger(string categoryName) - => new Logger(reporter, categoryName); + => new Logger(reporter, level, categoryName); public void AddProvider(ILoggerProvider provider) => throw new NotImplementedException(); } - internal readonly record struct MessageDescriptor(string Format, Emoji Emoji, MessageSeverity Severity, EventId Id) + internal readonly record struct MessageDescriptor(string Format, Emoji Emoji, LogLevel Level, EventId Id) { private static int s_id; private static ImmutableDictionary s_descriptors = []; - private static MessageDescriptor Create(string format, Emoji emoji, MessageSeverity severity) + private static MessageDescriptor Create(string format, Emoji emoji, LogLevel level) // reserve event id 0 for ad-hoc messages - => Create(new EventId(++s_id), format, emoji, severity); + => Create(new EventId(++s_id), format, emoji, level); private static MessageDescriptor Create(LogEvent logEvent, Emoji emoji) - => Create(logEvent.Id, logEvent.Message, emoji, logEvent.Level.ToSeverity()); + => Create(logEvent.Id, logEvent.Message, emoji, logEvent.Level); - private static MessageDescriptor Create(EventId id, string format, Emoji emoji, MessageSeverity severity) + private static MessageDescriptor Create(EventId id, string format, Emoji emoji, LogLevel level) { - var descriptor = new MessageDescriptor(format, emoji, severity, id.Id); + var descriptor = new MessageDescriptor(format, emoji, level, id.Id); s_descriptors = s_descriptors.Add(id, descriptor); return descriptor; } @@ -163,9 +135,18 @@ public static MessageDescriptor GetDescriptor(EventId id) public string GetMessage(params object?[] args) => Id.Id == 0 ? Format : string.Format(Format, args); - public MessageDescriptor WithSeverityWhen(MessageSeverity severity, bool condition) - => condition && Severity != severity - ? this with { Severity = severity, Emoji = severity switch { MessageSeverity.Error => Emoji.Error, MessageSeverity.Warning => Emoji.Warning, _ => Emoji } } + public MessageDescriptor WithLevelWhen(LogLevel level, bool condition) + => condition && Level != level + ? this with + { + Level = level, + Emoji = level switch + { + LogLevel.Error or LogLevel.Critical => Emoji.Error, + LogLevel.Warning => Emoji.Warning, + _ => Emoji + } + } : this; public static readonly ImmutableDictionary ComponentEmojis = ImmutableDictionary.Empty @@ -179,83 +160,82 @@ public MessageDescriptor WithSeverityWhen(MessageSeverity severity, bool conditi .Add(AspireServiceFactory.AspireLogComponentName, Emoji.Aspire); // predefined messages used for testing: - public static readonly MessageDescriptor HotReloadSessionStarting = Create("Hot reload session starting.", Emoji.HotReload, MessageSeverity.None); - public static readonly MessageDescriptor HotReloadSessionStarted = Create("Hot reload session started.", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor ProjectsRebuilt = Create("Projects rebuilt ({0})", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor ProjectsRestarted = Create("Projects restarted ({0})", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor ProjectDependenciesDeployed = Create("Project dependencies deployed ({0})", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor FixBuildError = Create("Fix the error to continue or press Ctrl+C to exit.", Emoji.Watch, MessageSeverity.Warning); - public static readonly MessageDescriptor WaitingForChanges = Create("Waiting for changes", Emoji.Watch, MessageSeverity.Output); - public static readonly MessageDescriptor LaunchedProcess = Create("Launched '{0}' with arguments '{1}': process id {2}", Emoji.Launch, MessageSeverity.Verbose); - public static readonly MessageDescriptor HotReloadChangeHandled = Create("Hot reload change handled in {0}ms.", Emoji.HotReload, MessageSeverity.Verbose); + public static readonly MessageDescriptor HotReloadSessionStarting = Create("Hot reload session starting.", Emoji.HotReload, LogLevel.None); + public static readonly MessageDescriptor HotReloadSessionStarted = Create("Hot reload session started.", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor ProjectsRebuilt = Create("Projects rebuilt ({0})", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor ProjectsRestarted = Create("Projects restarted ({0})", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor ProjectDependenciesDeployed = Create("Project dependencies deployed ({0})", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor FixBuildError = Create("Fix the error to continue or press Ctrl+C to exit.", Emoji.Watch, LogLevel.Warning); + public static readonly MessageDescriptor WaitingForChanges = Create("Waiting for changes", Emoji.Watch, LogLevel.Information); + public static readonly MessageDescriptor LaunchedProcess = Create("Launched '{0}' with arguments '{1}': process id {2}", Emoji.Launch, LogLevel.Debug); + public static readonly MessageDescriptor HotReloadChangeHandled = Create("Hot reload change handled in {0}ms.", Emoji.HotReload, LogLevel.Debug); public static readonly MessageDescriptor HotReloadSucceeded = Create(LogEvents.HotReloadSucceeded, Emoji.HotReload); public static readonly MessageDescriptor UpdatesApplied = Create(LogEvents.UpdatesApplied, Emoji.HotReload); public static readonly MessageDescriptor Capabilities = Create(LogEvents.Capabilities, Emoji.HotReload); - public static readonly MessageDescriptor WaitingForFileChangeBeforeRestarting = Create("Waiting for a file to change before restarting ...", Emoji.Wait, MessageSeverity.Warning); - public static readonly MessageDescriptor WatchingWithHotReload = Create("Watching with Hot Reload.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor RestartInProgress = Create("Restart in progress.", Emoji.Restart, MessageSeverity.Output); - public static readonly MessageDescriptor RestartRequested = Create("Restart requested.", Emoji.Restart, MessageSeverity.Output); - public static readonly MessageDescriptor ShutdownRequested = Create("Shutdown requested. Press Ctrl+C again to force exit.", Emoji.Stop, MessageSeverity.Output); - public static readonly MessageDescriptor ApplyUpdate_Error = Create("{0}{1}", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor ApplyUpdate_Warning = Create("{0}{1}", Emoji.Warning, MessageSeverity.Warning); - public static readonly MessageDescriptor ApplyUpdate_Verbose = Create("{0}{1}", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplyUpdate_ChangingEntryPoint = Create("{0} Press \"Ctrl + R\" to restart.", Emoji.Warning, MessageSeverity.Warning); - public static readonly MessageDescriptor ApplyUpdate_FileContentDoesNotMatchBuiltSource = Create("{0} Expected if a source file is updated that is linked to project whose build is not up-to-date.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor ConfiguredToLaunchBrowser = Create("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor ConfiguredToUseBrowserRefresh = Create("Using browser-refresh middleware", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable = Create("Skipping configuring browser-refresh middleware since its refresh server suppressed via environment variable {0}.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported = Create("Skipping configuring browser-refresh middleware since the target framework version is not supported. For more information see 'https://aka.ms/dotnet/watch/unsupported-tfm'.", Emoji.Watch, MessageSeverity.Warning); + public static readonly MessageDescriptor WaitingForFileChangeBeforeRestarting = Create("Waiting for a file to change before restarting ...", Emoji.Wait, LogLevel.Warning); + public static readonly MessageDescriptor WatchingWithHotReload = Create("Watching with Hot Reload.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor RestartInProgress = Create("Restart in progress.", Emoji.Restart, LogLevel.Information); + public static readonly MessageDescriptor RestartRequested = Create("Restart requested.", Emoji.Restart, LogLevel.Information); + public static readonly MessageDescriptor ShutdownRequested = Create("Shutdown requested. Press Ctrl+C again to force exit.", Emoji.Stop, LogLevel.Information); + public static readonly MessageDescriptor ApplyUpdate_Error = Create("{0}{1}", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor ApplyUpdate_Warning = Create("{0}{1}", Emoji.Warning, LogLevel.Warning); + public static readonly MessageDescriptor ApplyUpdate_Verbose = Create("{0}{1}", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor ApplyUpdate_ChangingEntryPoint = Create("{0} Press \"Ctrl + R\" to restart.", Emoji.Warning, LogLevel.Warning); + public static readonly MessageDescriptor ApplyUpdate_FileContentDoesNotMatchBuiltSource = Create("{0} Expected if a source file is updated that is linked to project whose build is not up-to-date.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor ConfiguredToLaunchBrowser = Create("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor ConfiguredToUseBrowserRefresh = Create("Using browser-refresh middleware", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable = Create("Skipping configuring browser-refresh middleware since its refresh server suppressed via environment variable {0}.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported = Create("Skipping configuring browser-refresh middleware since the target framework version is not supported. For more information see 'https://aka.ms/dotnet/watch/unsupported-tfm'.", Emoji.Watch, LogLevel.Warning); public static readonly MessageDescriptor UpdatingDiagnostics = Create(LogEvents.UpdatingDiagnostics, Emoji.Default); public static readonly MessageDescriptor FailedToReceiveResponseFromConnectedBrowser = Create(LogEvents.FailedToReceiveResponseFromConnectedBrowser, Emoji.Default); public static readonly MessageDescriptor NoBrowserConnected = Create(LogEvents.NoBrowserConnected, Emoji.Default); - public static readonly MessageDescriptor LaunchingBrowser = Create("Launching browser: {0} {1}", Emoji.Default, MessageSeverity.Verbose); + public static readonly MessageDescriptor LaunchingBrowser = Create("Launching browser: {0} {1}", Emoji.Default, LogLevel.Debug); public static readonly MessageDescriptor RefreshingBrowser = Create(LogEvents.RefreshingBrowser, Emoji.Default); public static readonly MessageDescriptor ReloadingBrowser = Create(LogEvents.ReloadingBrowser, Emoji.Default); public static readonly MessageDescriptor RefreshServerRunningAt = Create(LogEvents.RefreshServerRunningAt, Emoji.Default); public static readonly MessageDescriptor ConnectedToRefreshServer = Create(LogEvents.ConnectedToRefreshServer, Emoji.Default); - public static readonly MessageDescriptor RestartingApplicationToApplyChanges = Create("Restarting application to apply changes ...", Emoji.Default, MessageSeverity.Output); - public static readonly MessageDescriptor RestartingApplication = Create("Restarting application ...", Emoji.Default, MessageSeverity.Output); - public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = Create("Ignoring change in hidden directory '{0}': {1} '{2}'", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = Create("Ignoring change in output directory: {0} '{1}'", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor IgnoringChangeInExcludedFile = Create("Ignoring change in excluded file '{0}': {1}. Path matches {2} glob '{3}' set in '{4}'.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = Create("File addition triggered re-evaluation.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor ReEvaluationCompleted = Create("Re-evaluation completed.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor ProjectChangeTriggeredReEvaluation = Create("Project change triggered re-evaluation.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor NoCSharpChangesToApply = Create("No C# changes to apply.", Emoji.Watch, MessageSeverity.Output); - public static readonly MessageDescriptor Exited = Create("Exited", Emoji.Watch, MessageSeverity.Output); - public static readonly MessageDescriptor ExitedWithUnknownErrorCode = Create("Exited with unknown error code", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor ExitedWithErrorCode = Create("Exited with error code {0}", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor FailedToLaunchProcess = Create("Failed to launch '{0}' with arguments '{1}': {2}", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor ApplicationFailed = Create("Application failed: {0}", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor ProcessRunAndExited = Create("Process id {0} ran for {1}ms and exited with exit code {2}.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor WaitingForProcessToExitWithin = Create("Waiting for process {0} to exit within {1}s.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor WaitingForProcessToExit = Create("Waiting for process {0} to exit ({1}).", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor FailedToKillProcess = Create("Failed to kill process {0}: {1}.", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor TerminatingProcess = Create("Terminating process {0} ({1}).", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor FailedToSendSignalToProcess = Create("Failed to send {0} signal to process {1}: {2}", Emoji.Warning, MessageSeverity.Warning); - public static readonly MessageDescriptor ErrorReadingProcessOutput = Create("Error reading {0} of process {1}: {2}", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor HotReloadOfScopedCssSucceeded = Create("Hot reload of scoped css succeeded.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor HotReloadOfScopedCssPartiallySucceeded = Create("Hot reload of scoped css partially succeeded: {0} project(s) out of {1} were updated.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor HotReloadOfScopedCssFailed = Create("Hot reload of scoped css failed.", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor HotReloadOfStaticAssetsSucceeded = Create("Hot reload of static assets succeeded.", Emoji.HotReload, MessageSeverity.Output); + public static readonly MessageDescriptor RestartingApplicationToApplyChanges = Create("Restarting application to apply changes ...", Emoji.Default, LogLevel.Information); + public static readonly MessageDescriptor RestartingApplication = Create("Restarting application ...", Emoji.Default, LogLevel.Information); + public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = Create("Ignoring change in hidden directory '{0}': {1} '{2}'", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = Create("Ignoring change in output directory: {0} '{1}'", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor IgnoringChangeInExcludedFile = Create("Ignoring change in excluded file '{0}': {1}. Path matches {2} glob '{3}' set in '{4}'.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = Create("File addition triggered re-evaluation.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor ReEvaluationCompleted = Create("Re-evaluation completed.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor ProjectChangeTriggeredReEvaluation = Create("Project change triggered re-evaluation.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor NoCSharpChangesToApply = Create("No C# changes to apply.", Emoji.Watch, LogLevel.Information); + public static readonly MessageDescriptor Exited = Create("Exited", Emoji.Watch, LogLevel.Information); + public static readonly MessageDescriptor ExitedWithUnknownErrorCode = Create("Exited with unknown error code", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor ExitedWithErrorCode = Create("Exited with error code {0}", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor FailedToLaunchProcess = Create("Failed to launch '{0}' with arguments '{1}': {2}", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor ApplicationFailed = Create("Application failed: {0}", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor ProcessRunAndExited = Create("Process id {0} ran for {1}ms and exited with exit code {2}.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor WaitingForProcessToExitWithin = Create("Waiting for process {0} to exit within {1}s.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor WaitingForProcessToExit = Create("Waiting for process {0} to exit ({1}).", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor FailedToKillProcess = Create("Failed to kill process {0}: {1}.", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor TerminatingProcess = Create("Terminating process {0} ({1}).", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor FailedToSendSignalToProcess = Create("Failed to send {0} signal to process {1}: {2}", Emoji.Warning, LogLevel.Warning); + public static readonly MessageDescriptor ErrorReadingProcessOutput = Create("Error reading {0} of process {1}: {2}", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor HotReloadOfScopedCssSucceeded = Create("Hot reload of scoped css succeeded.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor HotReloadOfScopedCssPartiallySucceeded = Create("Hot reload of scoped css partially succeeded: {0} project(s) out of {1} were updated.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor HotReloadOfScopedCssFailed = Create("Hot reload of scoped css failed.", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor HotReloadOfStaticAssetsSucceeded = Create("Hot reload of static assets succeeded.", Emoji.HotReload, LogLevel.Information); public static readonly MessageDescriptor SendingStaticAssetUpdateRequest = Create(LogEvents.SendingStaticAssetUpdateRequest, Emoji.Default); - public static readonly MessageDescriptor HotReloadCapabilities = Create("Hot reload capabilities: {0}.", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor HotReloadSuspended = Create("Hot reload suspended. To continue hot reload, press \"Ctrl + R\".", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor UnableToApplyChanges = Create("Unable to apply changes due to compilation errors.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor RestartNeededToApplyChanges = Create("Restart is needed to apply the changes.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor HotReloadEnabled = Create("Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor PressCtrlRToRestart = Create("Press Ctrl+R to restart.", Emoji.LightBulb, MessageSeverity.Output); - public static readonly MessageDescriptor HotReloadCanceledProcessExited = Create("Hot reload canceled because the process exited.", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplicationKind_BlazorHosted = Create("Application kind: BlazorHosted. '{0}' references BlazorWebAssembly project '{1}'.", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplicationKind_BlazorWebAssembly = Create("Application kind: BlazorWebAssembly.", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplicationKind_WebApplication = Create("Application kind: WebApplication.", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplicationKind_Default = Create("Application kind: Default.", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor WatchingFilesForChanges = Create("Watching {0} file(s) for changes", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor WatchingFilesForChanges_FilePath = Create("> {0}", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor Building = Create("Building {0} ...", Emoji.Default, MessageSeverity.Output); - public static readonly MessageDescriptor BuildSucceeded = Create("Build succeeded: {0}", Emoji.Default, MessageSeverity.Output); - public static readonly MessageDescriptor BuildFailed = Create("Build failed: {0}", Emoji.Default, MessageSeverity.Output); - + public static readonly MessageDescriptor HotReloadCapabilities = Create("Hot reload capabilities: {0}.", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor HotReloadSuspended = Create("Hot reload suspended. To continue hot reload, press \"Ctrl + R\".", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor UnableToApplyChanges = Create("Unable to apply changes due to compilation errors.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor RestartNeededToApplyChanges = Create("Restart is needed to apply the changes.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor HotReloadEnabled = Create("Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor PressCtrlRToRestart = Create("Press Ctrl+R to restart.", Emoji.LightBulb, LogLevel.Information); + public static readonly MessageDescriptor HotReloadCanceledProcessExited = Create("Hot reload canceled because the process exited.", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor ApplicationKind_BlazorHosted = Create("Application kind: BlazorHosted. '{0}' references BlazorWebAssembly project '{1}'.", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor ApplicationKind_BlazorWebAssembly = Create("Application kind: BlazorWebAssembly.", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor ApplicationKind_WebApplication = Create("Application kind: WebApplication.", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor ApplicationKind_Default = Create("Application kind: Default.", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor WatchingFilesForChanges = Create("Watching {0} file(s) for changes", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor WatchingFilesForChanges_FilePath = Create("> {0}", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor Building = Create("Building {0} ...", Emoji.Default, LogLevel.Information); + public static readonly MessageDescriptor BuildSucceeded = Create("Build succeeded: {0}", Emoji.Default, LogLevel.Information); + public static readonly MessageDescriptor BuildFailed = Create("Build failed: {0}", Emoji.Default, LogLevel.Information); } internal interface IProcessOutputReporter @@ -277,9 +257,6 @@ internal interface IProcessOutputReporter internal interface IReporter { - void Report(EventId id, Emoji emoji, MessageSeverity severity, string message); - - public bool IsVerbose - => false; + void Report(EventId id, Emoji emoji, LogLevel level, string message); } } diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs index ba8e3317c62b..8091258ef9b3 100644 --- a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs +++ b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs @@ -171,15 +171,20 @@ internal sealed class CommandLineOptions var targetFrameworkOption = (Option?)buildOptions.SingleOrDefault(option => option.Name == "--framework"); + var logLevel = parseResult.GetValue(verboseOption) + ? LogLevel.Debug + : parseResult.GetValue(quietOption) + ? LogLevel.Warning + : LogLevel.Information; + return new() { List = parseResult.GetValue(listOption), GlobalOptions = new() { - Quiet = parseResult.GetValue(quietOption), + LogLevel = logLevel, NoHotReload = parseResult.GetValue(noHotReloadOption), NonInteractive = parseResult.GetValue(NonInteractiveOption), - Verbose = parseResult.GetValue(verboseOption), BinaryLogPath = ParseBinaryLogFilePath(binLogPath), }, diff --git a/src/BuiltInTools/dotnet-watch/Program.cs b/src/BuiltInTools/dotnet-watch/Program.cs index f4e531d02414..384df13ae5cc 100644 --- a/src/BuiltInTools/dotnet-watch/Program.cs +++ b/src/BuiltInTools/dotnet-watch/Program.cs @@ -50,7 +50,6 @@ public static async Task Main(string[] args) args, new PhysicalConsole(environmentOptions.TestFlags), environmentOptions, - EnvironmentVariables.VerboseCliOutput, out var exitCode); if (program == null) @@ -68,9 +67,9 @@ public static async Task Main(string[] args) } } - private static Program? TryCreate(IReadOnlyList args, IConsole console, EnvironmentOptions environmentOptions, bool verbose, out int errorCode) + private static Program? TryCreate(IReadOnlyList args, IConsole console, EnvironmentOptions environmentOptions, out int errorCode) { - var parsingLoggerFactory = new LoggerFactory(new ConsoleReporter(console, verbose, quiet: false, environmentOptions.SuppressEmojis)); + var parsingLoggerFactory = new LoggerFactory(new ConsoleReporter(console, environmentOptions.SuppressEmojis), environmentOptions.CliLogLevel ?? LogLevel.Information); var options = CommandLineOptions.Parse(args, parsingLoggerFactory.CreateLogger(DotNetWatchContext.DefaultLogComponentName), console.Out, out errorCode); if (options == null) { @@ -78,8 +77,9 @@ public static async Task Main(string[] args) return null; } - var reporter = new ConsoleReporter(console, verbose || options.GlobalOptions.Verbose, options.GlobalOptions.Quiet, environmentOptions.SuppressEmojis); - var loggerFactory = new LoggerFactory(reporter); + var logLevel = environmentOptions.CliLogLevel ?? options.GlobalOptions.LogLevel; + var reporter = new ConsoleReporter(console, environmentOptions.SuppressEmojis); + var loggerFactory = new LoggerFactory(reporter, logLevel); return TryCreate(options, console, environmentOptions, loggerFactory, reporter, out errorCode); } diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/DotNetWatchOptionsTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/DotNetWatchOptionsTests.cs index 6f7d317fdc54..2420ce3e7568 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/DotNetWatchOptionsTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/DotNetWatchOptionsTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch.UnitTests; public class DotNetWatchOptionsTests @@ -47,12 +49,12 @@ public void TryParse_VerboseOption() // With verbose flag var argsVerbose = new[] { "--sdk", "sdk", "--project", "proj", "--verbose" }; Assert.True(DotNetWatchOptions.TryParse(argsVerbose, out var optionsVerbose)); - Assert.True(optionsVerbose.IsVerbose); + Assert.Equal(LogLevel.Debug, optionsVerbose.LogLevel); // Without verbose flag var argsNotVerbose = new[] { "--sdk", "sdk", "--project", "proj" }; Assert.True(DotNetWatchOptions.TryParse(argsNotVerbose, out var optionsNotVerbose)); - Assert.False(optionsNotVerbose.IsVerbose); + Assert.Equal(LogLevel.Information, optionsNotVerbose.LogLevel); } [Fact] @@ -61,12 +63,12 @@ public void TryParse_QuietOption() // With quiet flag var argsQuiet = new[] { "--sdk", "sdk", "--project", "proj", "--quiet" }; Assert.True(DotNetWatchOptions.TryParse(argsQuiet, out var optionsQuiet)); - Assert.True(optionsQuiet.IsQuiet); + Assert.Equal(LogLevel.Warning, optionsQuiet.LogLevel); // Without quiet flag var argsNotQuiet = new[] { "--sdk", "sdk", "--project", "proj" }; Assert.True(DotNetWatchOptions.TryParse(argsNotQuiet, out var optionsNotQuiet)); - Assert.False(optionsNotQuiet.IsQuiet); + Assert.Equal(LogLevel.Information, optionsNotQuiet.LogLevel); } [Fact] @@ -109,8 +111,7 @@ public void TryParse_AllOptionsSet() Assert.True(DotNetWatchOptions.TryParse(args, out var options)); Assert.Equal("myapp.csproj", options.ProjectPath); - Assert.True(options.IsVerbose); - Assert.False(options.IsQuiet); + Assert.Equal(LogLevel.Debug, options.LogLevel); Assert.True(options.NoLaunchProfile); AssertEx.SequenceEqual(["arg1", "arg2", "arg3"], options.ApplicationArguments); } diff --git a/test/dotnet-watch.Tests/CommandLine/CommandLineOptionsTests.cs b/test/dotnet-watch.Tests/CommandLine/CommandLineOptionsTests.cs index 431736a83fe8..2519f041901b 100644 --- a/test/dotnet-watch.Tests/CommandLine/CommandLineOptionsTests.cs +++ b/test/dotnet-watch.Tests/CommandLine/CommandLineOptionsTests.cs @@ -3,6 +3,8 @@ #nullable disable +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch.UnitTests { public class CommandLineOptionsTests @@ -164,7 +166,7 @@ public void RemainingOptions() { var options = VerifyOptions(["-watchArg", "--verbose", "run", "-runArg"]); - Assert.True(options.GlobalOptions.Verbose); + Assert.Equal(LogLevel.Debug, options.GlobalOptions.LogLevel); Assert.Equal("run", options.Command); AssertEx.SequenceEqual(["-watchArg", "-runArg"], options.CommandArguments); } @@ -184,7 +186,7 @@ public void RemainingOptionsDashDash() { var options = VerifyOptions(["-watchArg", "--", "--verbose", "run", "-runArg"]); - Assert.False(options.GlobalOptions.Verbose); + Assert.Equal(LogLevel.Information, options.GlobalOptions.LogLevel); Assert.Equal("run", options.Command); AssertEx.SequenceEqual(["-watchArg", "--", "--verbose", "run", "-runArg",], options.CommandArguments); } @@ -194,7 +196,7 @@ public void RemainingOptionsDashDashRun() { var options = VerifyOptions(["--", "run"]); - Assert.False(options.GlobalOptions.Verbose); + Assert.Equal(LogLevel.Information, options.GlobalOptions.LogLevel); Assert.Equal("run", options.Command); AssertEx.SequenceEqual(["--", "run"], options.CommandArguments); } @@ -334,9 +336,9 @@ public void OptionDuplicates_Allowed_Bool( args = position switch { - ArgPosition.Before => args.Prepend("run").ToArray(), - ArgPosition.Both => args.Concat(new[] { "run" }).Concat(args).ToArray(), - ArgPosition.After => args.Append("run").ToArray(), + ArgPosition.Before => ["run", .. args], + ArgPosition.Both => [.. args, "run", .. args], + ArgPosition.After => [.. args, "run"], _ => args, }; @@ -344,8 +346,8 @@ public void OptionDuplicates_Allowed_Bool( Assert.True(arg switch { - "--verbose" => options.GlobalOptions.Verbose, - "--quiet" => options.GlobalOptions.Quiet, + "--verbose" => options.GlobalOptions.LogLevel == LogLevel.Debug, + "--quiet" => options.GlobalOptions.LogLevel == LogLevel.Warning, "--list" => options.List, "--no-hot-reload" => options.GlobalOptions.NoHotReload, "--non-interactive" => options.GlobalOptions.NonInteractive, diff --git a/test/dotnet-watch.Tests/CommandLine/ProgramTests.cs b/test/dotnet-watch.Tests/CommandLine/ProgramTests.cs index caee7fb19636..c8c57563a0ca 100644 --- a/test/dotnet-watch.Tests/CommandLine/ProgramTests.cs +++ b/test/dotnet-watch.Tests/CommandLine/ProgramTests.cs @@ -3,6 +3,8 @@ #nullable disable +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch.UnitTests { public class ProgramTests(ITestOutputHelper logger) : DotNetWatchTestBase(logger) @@ -15,7 +17,7 @@ public async Task ConsoleCancelKey() var console = new TestConsole(Logger); var reporter = new TestReporter(Logger); - var loggerFactory = new LoggerFactory(reporter); + var loggerFactory = new LoggerFactory(reporter, LogLevel.Debug); var watching = reporter.RegisterSemaphore(MessageDescriptor.WatchingWithHotReload); var shutdownRequested = reporter.RegisterSemaphore(MessageDescriptor.ShutdownRequested); diff --git a/test/dotnet-watch.Tests/ConsoleReporterTests.cs b/test/dotnet-watch.Tests/ConsoleReporterTests.cs index 8df1601dc0dc..748badd9f4d2 100644 --- a/test/dotnet-watch.Tests/ConsoleReporterTests.cs +++ b/test/dotnet-watch.Tests/ConsoleReporterTests.cs @@ -3,6 +3,8 @@ #nullable disable +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch.UnitTests { public class ConsoleReporterTests @@ -15,23 +17,31 @@ public class ConsoleReporterTests public void WritesToStandardStreams(bool suppressEmojis) { var testConsole = new TestConsole(); - var reporter = new ConsoleReporter(testConsole, verbose: true, quiet: false, suppressEmojis: suppressEmojis); + var reporter = new ConsoleReporter(testConsole, suppressEmojis: suppressEmojis); + + reporter.Report(id: default, Emoji.Watch, LogLevel.Trace, "trace {0}"); + Assert.Equal($"dotnet watch {(suppressEmojis ? ":" : "⌚")} trace {{0}}" + EOL, testConsole.GetError()); + testConsole.Clear(); - reporter.Report(id: default, Emoji.Watch, MessageSeverity.Verbose, "verbose {0}"); - Assert.Equal($"dotnet watch {(suppressEmojis ? ":" : "⌚")} verbose {{0}}" + EOL, testConsole.GetError()); + reporter.Report(id: default, Emoji.Watch, LogLevel.Debug, "verbose"); + Assert.Equal($"dotnet watch {(suppressEmojis ? ":" : "⌚")} verbose" + EOL, testConsole.GetError()); testConsole.Clear(); - reporter.Report(id: default, Emoji.Watch, MessageSeverity.Output, "out"); + reporter.Report(id: default, Emoji.Watch, LogLevel.Information, "out"); Assert.Equal($"dotnet watch {(suppressEmojis ? ":" : "⌚")} out" + EOL, testConsole.GetError()); testConsole.Clear(); - reporter.Report(id: default, Emoji.Warning, MessageSeverity.Warning, "warn"); + reporter.Report(id: default, Emoji.Warning, LogLevel.Warning, "warn"); Assert.Equal($"dotnet watch {(suppressEmojis ? ":" : "⚠")} warn" + EOL, testConsole.GetError()); testConsole.Clear(); - reporter.Report(id: default, Emoji.Error, MessageSeverity.Error, "error"); + reporter.Report(id: default, Emoji.Error, LogLevel.Error, "error"); Assert.Equal($"dotnet watch {(suppressEmojis ? ":" : "❌")} error" + EOL, testConsole.GetError()); testConsole.Clear(); + + reporter.Report(id: default, Emoji.Error, LogLevel.Critical, "critical"); + Assert.Equal($"dotnet watch {(suppressEmojis ? ":" : "❌")} critical" + EOL, testConsole.GetError()); + testConsole.Clear(); } private class TestConsole : IConsole diff --git a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs index 4081a1e2102c..d32c0d5edb71 100644 --- a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs +++ b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch.UnitTests; public class CompilationHandlerTests(ITestOutputHelper output) : DotNetWatchTestBase(output) @@ -22,7 +24,7 @@ public async Task ReferenceOutputAssembly_False() var processRunner = new ProcessRunner(processCleanupTimeout: TimeSpan.Zero); var reporter = new TestReporter(Logger); - var loggerFactory = new LoggerFactory(reporter); + var loggerFactory = new LoggerFactory(reporter, LogLevel.Debug); var logger = loggerFactory.CreateLogger("Test"); var factory = new ProjectGraphFactory(globalOptions: []); var projectGraph = factory.TryLoadProjectGraph(options.ProjectPath, logger, projectGraphRequired: false, CancellationToken.None); diff --git a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs index ff2c8491194f..81130fbc2092 100644 --- a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs +++ b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch.UnitTests; @@ -96,7 +97,7 @@ private RunningWatcher StartWatcher(TestAsset testAsset, string[] args, string? { var console = new TestConsole(Logger); var reporter = new TestReporter(Logger); - var loggerFactory = new LoggerFactory(reporter); + var loggerFactory = new LoggerFactory(reporter, LogLevel.Trace); var environmentOptions = TestOptions.GetEnvironmentOptions(workingDirectory ?? testAsset.Path, TestContext.Current.ToolsetUnderTest.DotNetHostPath, testAsset); var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout(isHotReloadEnabled: true)); diff --git a/test/dotnet-watch.Tests/TestUtilities/TestReporter.cs b/test/dotnet-watch.Tests/TestUtilities/TestReporter.cs index cd3da3e4476f..849ca9f6baf8 100644 --- a/test/dotnet-watch.Tests/TestUtilities/TestReporter.cs +++ b/test/dotnet-watch.Tests/TestUtilities/TestReporter.cs @@ -9,7 +9,7 @@ internal class TestReporter(ITestOutputHelper output) : IReporter, IProcessOutpu { private readonly Dictionary _actions = []; public readonly List ProcessOutput = []; - public readonly List<(MessageSeverity severity, string text)> Messages = []; + public readonly List<(LogLevel level, string text)> Messages = []; public bool IsVerbose => true; @@ -51,12 +51,12 @@ public void RegisterAction(EventId eventId, Action action) _actions[eventId] = existing; } - public void Report(EventId id, Emoji emoji, MessageSeverity severity, string message) + public void Report(EventId id, Emoji emoji, LogLevel level, string message) { - if (severity != MessageSeverity.None) + if (level != LogLevel.None) { - Messages.Add((severity, message)); - WriteTestOutput($"{ToString(severity)} {emoji.ToDisplay()} {message}"); + Messages.Add((level, message)); + WriteTestOutput($"{ToString(level)} {emoji.ToDisplay()} {message}"); } if (_actions.TryGetValue(id, out var action)) @@ -77,13 +77,13 @@ private void WriteTestOutput(string line) } } - private static string ToString(MessageSeverity severity) - => severity switch + private static string ToString(LogLevel level) + => level switch { - MessageSeverity.Verbose => "verbose", - MessageSeverity.Output => "output", - MessageSeverity.Warning => "warning", - MessageSeverity.Error => "error", + LogLevel.Trace or LogLevel.Debug => "verbose", + LogLevel.Information => "output", + LogLevel.Warning => "warning", + LogLevel.Critical or LogLevel.Error => "error", _ => throw new InvalidOperationException() }; } diff --git a/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs b/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs index fcdaad958607..272475451b49 100644 --- a/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs +++ b/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs @@ -173,6 +173,7 @@ public void Start(TestAsset asset, IEnumerable arguments, string relativ commandSpec.WithEnvironmentVariable("__DOTNET_WATCH_TEST_FLAGS", testFlags.ToString()); commandSpec.WithEnvironmentVariable("__DOTNET_WATCH_TEST_OUTPUT_DIR", testOutputPath); commandSpec.WithEnvironmentVariable("Microsoft_CodeAnalysis_EditAndContinue_LogDir", testOutputPath); + commandSpec.WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "trace"); // suppress all timeouts: commandSpec.WithEnvironmentVariable("DCP_IDE_REQUEST_TIMEOUT_SECONDS", "100000");