diff --git a/src/Aspire.Cli/Commands/BaseCommand.cs b/src/Aspire.Cli/Commands/BaseCommand.cs index e90f57cef46..0e23e7b0e93 100644 --- a/src/Aspire.Cli/Commands/BaseCommand.cs +++ b/src/Aspire.Cli/Commands/BaseCommand.cs @@ -61,6 +61,8 @@ protected BaseCommand(string name, string description, IFeatures features, ICliU result = CommandResult.Failure((int)CliExitCodes.MissingRequiredArgument); } + var isErrorExitCode = result.ExitCode != CliExitCodes.Success; + if (result.ErrorMessage is not null) { interactionService.DisplayError(result.ErrorMessage); @@ -74,18 +76,19 @@ protected BaseCommand(string name, string description, IFeatures features, ICliU if (result.ShouldDisplayCancellationMessage) { - interactionService.DisplayCancellationMessage(); + interactionService.DisplayCancellationMessage(isErrorExitCode ? ConsoleOutput.Error : null); } // Display the CLI log file path on non-zero exit codes so the user knows // where to find diagnostic details. Suppress for user-input errors where // the log wouldn't contain useful context (e.g., missing required arguments). - if (result.ExitCode != CliExitCodes.Success && result.ExitCode != CliExitCodes.MissingRequiredArgument) + if (isErrorExitCode && result.ExitCode != CliExitCodes.MissingRequiredArgument) { interactionService.DisplayMessage( KnownEmojis.PageFacingUp, string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, MarkupHelpers.SafeFileLink(interactionService, executionContext.LogFilePath)), - allowMarkup: true); + allowMarkup: true, + consoleOverride: ConsoleOutput.Error); // If we connected to a running app host, also display the log file path of // the CLI process that launched it so users can diagnose issues in both processes. @@ -94,7 +97,8 @@ protected BaseCommand(string name, string description, IFeatures features, ICliU interactionService.DisplayMessage( KnownEmojis.MagnifyingGlassTiltedLeft, string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeAppHostLogsAt, MarkupHelpers.SafeFileLink(interactionService, executionContext.AppHostCliLogFilePath)), - allowMarkup: true); + allowMarkup: true, + consoleOverride: ConsoleOutput.Error); } } diff --git a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs index 36cfa874c21..98df4e7e650 100644 --- a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs @@ -36,10 +36,22 @@ internal class ConsoleInteractionService : IInteractionService /// /// Console used for human-readable messages; routes to stderr when is set to . /// - private IAnsiConsole MessageConsole => Console == ConsoleOutput.Error ? _errorConsole : _outConsole; + private IAnsiConsole MessageConsole => GetConsoleOutput(null); // Limit logging to prompts and messages. Don't log raw text output since it may contain sensitive information. - private ILogger MessageLogger => Console == ConsoleOutput.Error ? _stderrLogger : _stdoutLogger; + private ILogger MessageLogger => GetLogger(null); + + private IAnsiConsole GetConsoleOutput(ConsoleOutput? consoleOverride) => (consoleOverride ?? Console) switch + { + ConsoleOutput.Error => _errorConsole, + _ => _outConsole + }; + + private ILogger GetLogger(ConsoleOutput? consoleOverride) => (consoleOverride ?? Console) switch + { + ConsoleOutput.Error => _stderrLogger, + _ => _stdoutLogger + }; public ConsoleOutput Console { get; set; } @@ -373,8 +385,10 @@ public void DisplayError(string errorMessage, bool allowMarkup = false) WriteEmojiMessage(_errorConsole, _stderrLogger, KnownEmojis.CrossMark, $"[red bold]{formatted}[/]", allowMarkup: true); } - public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) - => WriteEmojiMessage(MessageConsole, MessageLogger, emoji, message, allowMarkup); + public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false, ConsoleOutput? consoleOverride = null) + { + WriteEmojiMessage(GetConsoleOutput(consoleOverride), GetLogger(consoleOverride), emoji, message, allowMarkup); + } private static void WriteEmojiMessage(IAnsiConsole target, ILogger logger, KnownEmoji emoji, string message, bool allowMarkup) { @@ -533,10 +547,10 @@ await MessageConsole.Live(initialRenderable) }); } - public void DisplayCancellationMessage() + public void DisplayCancellationMessage(ConsoleOutput? consoleOverride = null) { - MessageConsole.WriteLine(); - DisplayMessage(KnownEmojis.StopSign, $"[teal bold]{InteractionServiceStrings.StoppingAspire}[/]", allowMarkup: true); + GetConsoleOutput(consoleOverride).WriteLine(); + DisplayMessage(KnownEmojis.StopSign, $"[teal bold]{InteractionServiceStrings.StoppingAspire}[/]", allowMarkup: true, consoleOverride: consoleOverride); } public async Task PromptConfirmAsync(string promptText, PromptBinding? binding = null, CancellationToken cancellationToken = default) diff --git a/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs b/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs index e908e26cc26..19c252333d1 100644 --- a/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs @@ -352,12 +352,12 @@ public void DisplayError(string errorMessage, bool allowMarkup = false) Debug.Assert(result); } - public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) + public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false, ConsoleOutput? consoleOverride = null) { var result = _extensionTaskChannel.Writer.TryWrite(async () => { await Backchannel.DisplayMessageAsync(emoji.Name, message.RemoveSpectreFormatting(), _cancellationToken); - _consoleInteractionService.DisplayMessage(emoji, message, allowMarkup); + _consoleInteractionService.DisplayMessage(emoji, message, allowMarkup, consoleOverride); }); Debug.Assert(result); } @@ -405,11 +405,11 @@ public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lin // here would surface every line twice. } - public void DisplayCancellationMessage() + public void DisplayCancellationMessage(ConsoleOutput? consoleOverride = null) { var result = _extensionTaskChannel.Writer.TryWrite(() => Backchannel.DisplayCancellationMessageAsync(_cancellationToken)); Debug.Assert(result); - _consoleInteractionService.DisplayCancellationMessage(); + _consoleInteractionService.DisplayCancellationMessage(consoleOverride); } public void DisplayEmptyLine() diff --git a/src/Aspire.Cli/Interaction/IInteractionService.cs b/src/Aspire.Cli/Interaction/IInteractionService.cs index 77cac8fb208..869894c7172 100644 --- a/src/Aspire.Cli/Interaction/IInteractionService.cs +++ b/src/Aspire.Cli/Interaction/IInteractionService.cs @@ -19,7 +19,7 @@ internal interface IInteractionService Task> PromptForSelectionsAsync(string promptText, IEnumerable choices, Func choiceFormatter, IEnumerable? preSelected = null, bool optional = false, PromptBinding? binding = null, bool echoSelected = true, CancellationToken cancellationToken = default) where T : notnull; int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingVersion); void DisplayError(string errorMessage, bool allowMarkup = false); - void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false); + void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false, ConsoleOutput? consoleOverride = null); void DisplayPlainText(string text); void DisplayRawText(string text, ConsoleOutput? consoleOverride = null); void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null, int? maxWidth = null); @@ -29,7 +29,7 @@ internal interface IInteractionService void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines); void DisplayRenderable(IRenderable renderable); Task DisplayLiveAsync(IRenderable initialRenderable, Func, Task> callback); - void DisplayCancellationMessage(); + void DisplayCancellationMessage(ConsoleOutput? consoleOverride = null); void DisplayEmptyLine(); /// diff --git a/tests/Aspire.Cli.Tests/Commands/BaseCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/BaseCommandTests.cs index d704c13e397..6124e5afd9d 100644 --- a/tests/Aspire.Cli.Tests/Commands/BaseCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/BaseCommandTests.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using Aspire.Cli.Commands; using Aspire.Cli.Interaction; +using Aspire.Cli.Projects; +using Aspire.Cli.Resources; using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Tests.Utils; using Microsoft.AspNetCore.InternalTesting; @@ -108,4 +111,163 @@ public async Task BaseCommand_WithUpdateNotification_DoesNotDisplayTrailingBlank Assert.Equal(0, testInteractionService.DisplayEmptyLineCount); } + + [Fact] + public async Task BaseCommand_OnFailure_DisplaysLogFilePathOnStderr() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var testInteractionService = new TestInteractionService(); + var projectLocator = new TestProjectLocator + { + UseOrFindAppHostProjectFileWithBehaviorAsyncCallback = (_, _, _, _) => + Task.FromResult(new AppHostProjectSearchResult(null, [])) + }; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => testInteractionService; + options.ProjectLocatorFactory = _ => projectLocator; + }); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("run"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.NotEqual(CliExitCodes.Success, exitCode); + + var executionContext = provider.GetRequiredService(); + var expectedLogMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, executionContext.LogFilePath); + var logMessage = Assert.Single(testInteractionService.DisplayedMessages, m => m.Message == expectedLogMessage); + Assert.Equal(ConsoleOutput.Error, logMessage.ConsoleOverride); + } + + [Fact] + public async Task BaseCommand_OnFailure_DisplaysAppHostLogFilePathOnStderr() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var testInteractionService = new TestInteractionService(); + var projectLocator = new TestProjectLocator + { + UseOrFindAppHostProjectFileWithBehaviorAsyncCallback = (_, _, _, _) => + Task.FromResult(new AppHostProjectSearchResult(null, [])) + }; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => testInteractionService; + options.ProjectLocatorFactory = _ => projectLocator; + options.CliExecutionContextFactory = _ => + { + var ctx = workspace.CreateExecutionContext(); + ctx.AppHostCliLogFilePath = "/tmp/aspire-logs/apphost.log"; + return ctx; + }; + }); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("run"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.NotEqual(CliExitCodes.Success, exitCode); + + var executionContext = provider.GetRequiredService(); + var expectedCliLogMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, executionContext.LogFilePath); + var expectedAppHostLogMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeAppHostLogsAt, "/tmp/aspire-logs/apphost.log"); + + var cliLogMessage = Assert.Single(testInteractionService.DisplayedMessages, m => m.Message == expectedCliLogMessage); + Assert.Equal(ConsoleOutput.Error, cliLogMessage.ConsoleOverride); + + var appHostLogMessage = Assert.Single(testInteractionService.DisplayedMessages, m => m.Message == expectedAppHostLogMessage); + Assert.Equal(ConsoleOutput.Error, appHostLogMessage.ConsoleOverride); + } + + [Fact] + public async Task BaseCommand_OnCancellationWithErrorExitCode_DisplaysCancellationMessageOnStderr() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var testInteractionService = new TestInteractionService(); + var projectLocator = new TestProjectLocator + { + UseOrFindAppHostProjectFileWithBehaviorAsyncCallback = (_, _, _, _) => + throw new OperationCanceledException() + }; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => testInteractionService; + options.ProjectLocatorFactory = _ => projectLocator; + }); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("add SomePackage"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + // add catches OperationCanceledException and returns CommandResult.Cancelled() (exit code 130) + Assert.Equal(CliExitCodes.Cancelled, exitCode); + + var cancellationOverride = Assert.Single(testInteractionService.DisplayedCancellations); + Assert.Equal(ConsoleOutput.Error, cancellationOverride); + } + + [Fact] + public async Task BaseCommand_OnCancellationWithSuccessExitCode_DisplaysCancellationMessageOnStdout() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var testInteractionService = new TestInteractionService(); + var projectLocator = new TestProjectLocator + { + // Throw with the cancellation token so RunCommand's + // `catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken)` + // matches and returns CommandResult.Cancelled(CliExitCodes.Success). + UseOrFindAppHostProjectFileWithBehaviorAsyncCallback = (_, _, _, ct) => + throw new OperationCanceledException(ct) + }; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => testInteractionService; + options.ProjectLocatorFactory = _ => projectLocator; + }); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + // run catches OperationCanceledException and returns CommandResult.Cancelled(CliExitCodes.Success) + var result = command.Parse("run"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(CliExitCodes.Success, exitCode); + + var cancellationOverride = Assert.Single(testInteractionService.DisplayedCancellations); + Assert.Null(cancellationOverride); + } + + [Fact] + public async Task BaseCommand_OnSuccess_DoesNotDisplayLogFilePath() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var testInteractionService = new TestInteractionService(); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => testInteractionService; + }); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("ps"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(CliExitCodes.Success, exitCode); + + // On success, no log file messages should be displayed + Assert.DoesNotContain(testInteractionService.DisplayedMessages, + m => m.ConsoleOverride == ConsoleOutput.Error); + } } diff --git a/tests/Aspire.Cli.Tests/Commands/DashboardRunCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/DashboardRunCommandTests.cs index e70b350718f..51202ac5425 100644 --- a/tests/Aspire.Cli.Tests/Commands/DashboardRunCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/DashboardRunCommandTests.cs @@ -364,7 +364,7 @@ public async Task DashboardRunCommand_WhenCancelled_DisplaysCancellationMessageA var exitCode = await pendingRun.DefaultTimeout(); Assert.Equal(CliExitCodes.Success, exitCode); - Assert.Equal(1, testInteractionService.DisplayCancellationMessageCount); + Assert.Single(testInteractionService.DisplayedCancellations); } [Theory] diff --git a/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs b/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs index 6b034692269..fbac53444a6 100644 --- a/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs @@ -950,11 +950,11 @@ public Task PromptConfirmAsync(string promptText, PromptBinding? bin public void ShowStatus(string statusText, Action action, KnownEmoji? emoji = null, bool allowMarkup = false) => action(); public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingVersion) => 0; public void DisplayError(string errorMessage, bool allowMarkup = false) => DisplayedErrors.Add(errorMessage); - public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) { } + public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false, ConsoleOutput? consoleOverride = null) { } public void DisplaySuccess(string message, bool allowMarkup = false) { } public void DisplaySubtleMessage(string message, bool allowMarkup = false) { } public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { } - public void DisplayCancellationMessage() { } + public void DisplayCancellationMessage(ConsoleOutput? consoleOverride = null) { } public void DisplayEmptyLine() { } public void DisplayPlainText(string text) { } public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) { } diff --git a/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs index 110d780e762..d217b684558 100644 --- a/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs @@ -754,7 +754,7 @@ public async Task RunCommand_WhenDashboardFailsToStart_ContinuesWithWarning() // The command should handle cancellation gracefully Assert.Equal(CliExitCodes.Success, exitCode); - Assert.Equal(1, testInteractionService.DisplayCancellationMessageCount); + Assert.Single(testInteractionService.DisplayedCancellations); // Verify a warning was displayed (not an error) var m = Assert.Single(testInteractionService.DisplayedMessages); diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs index 457bcd480ca..c4096028ed2 100644 --- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs @@ -2364,7 +2364,7 @@ public Task> PromptForSelectionsAsync(string promptText, IEn public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingVersion) => _innerService.DisplayIncompatibleVersionError(ex, appHostHostingVersion); public void DisplayError(string errorMessage, bool allowMarkup = false) => _innerService.DisplayError(errorMessage, allowMarkup); - public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) => _innerService.DisplayMessage(emoji, message, allowMarkup); + public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false, ConsoleOutput? consoleOverride = null) => _innerService.DisplayMessage(emoji, message, allowMarkup, consoleOverride); public void DisplayPlainText(string text) => _innerService.DisplayPlainText(text); public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) => _innerService.DisplayRawText(text, consoleOverride); public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null, int? maxWidth = null) => _innerService.DisplayMarkdown(markdown, consoleOverride, maxWidth); @@ -2372,10 +2372,10 @@ public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, stri public void DisplaySuccess(string message, bool allowMarkup = false) => _innerService.DisplaySuccess(message, allowMarkup); public void DisplaySubtleMessage(string message, bool allowMarkup = false) => _innerService.DisplaySubtleMessage(message, allowMarkup); public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) => _innerService.DisplayLines(lines); - public void DisplayCancellationMessage() + public void DisplayCancellationMessage(ConsoleOutput? consoleOverride = null) { OnCancellationMessageDisplayed?.Invoke(); - _innerService.DisplayCancellationMessage(); + _innerService.DisplayCancellationMessage(consoleOverride); } public void DisplayEmptyLine() => _innerService.DisplayEmptyLine(); public void DisplayVersionUpdateNotification(string newerVersion, string? updateCommand = null) diff --git a/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs b/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs index 4436e857f92..399971642ae 100644 --- a/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs @@ -165,10 +165,10 @@ public Task LaunchAppHostAsync(string projectFile, List arguments, List< public Task> PromptForSelectionsAsync(string promptText, IEnumerable choices, Func choiceFormatter, IEnumerable? preSelected = null, bool optional = false, PromptBinding? binding = null, bool echoSelected = true, CancellationToken cancellationToken = default) where T : notnull => throw new NotImplementedException(); public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingVersion) => throw new NotImplementedException(); public void DisplayError(string errorMessage, bool allowMarkup = false) => throw new NotImplementedException(); - public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) => throw new NotImplementedException(); + public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false, ConsoleOutput? consoleOverride = null) => throw new NotImplementedException(); public void DisplaySuccess(string message, bool allowMarkup = false) => throw new NotImplementedException(); public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) => throw new NotImplementedException(); - public void DisplayCancellationMessage() => throw new NotImplementedException(); + public void DisplayCancellationMessage(ConsoleOutput? consoleOverride = null) => throw new NotImplementedException(); public Task PromptConfirmAsync(string promptText, PromptBinding? binding = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public void DisplaySubtleMessage(string message, bool allowMarkup = false) => throw new NotImplementedException(); public void DisplayEmptyLine() => throw new NotImplementedException(); diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs index 0948cf06eee..0a9cdf29808 100644 --- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs +++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs @@ -397,9 +397,9 @@ public void ShowStatus(string message, Action work, KnownEmoji? emoji = null, bo public void DisplaySuccess(string message, bool allowMarkup = false) { } public void DisplayError(string message, bool allowMarkup = false) { } - public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) { } + public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false, ConsoleOutput? consoleOverride = null) { } public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { } - public void DisplayCancellationMessage() { } + public void DisplayCancellationMessage(ConsoleOutput? consoleOverride = null) { } public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingVersion) => 0; public void DisplayPlainText(string text) { } public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) { } diff --git a/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs b/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs index dde56a140b3..34be3af7485 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs @@ -81,7 +81,7 @@ public void DisplayError(string errorMessage, bool allowMarkup = false) DisplayErrorCallback?.Invoke(errorMessage); } - public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) + public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false, ConsoleOutput? consoleOverride = null) { } @@ -118,7 +118,7 @@ public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lin { } - public void DisplayCancellationMessage() + public void DisplayCancellationMessage(ConsoleOutput? consoleOverride = null) { } diff --git a/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs b/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs index e23f69e33a7..464d2ccb555 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs @@ -44,14 +44,14 @@ internal sealed class TestInteractionService : IInteractionService public List StringPromptCalls { get; } = []; public List BooleanPromptCalls { get; } = []; public List DisplayedErrors { get; } = []; - public List<(KnownEmoji Emoji, string Message)> DisplayedMessages { get; } = []; + public List<(KnownEmoji Emoji, string Message, ConsoleOutput? ConsoleOverride)> DisplayedMessages { get; } = []; public List<(OutputLineStream Stream, string Line)> DisplayedLines { get; } = []; public List DisplayedPlainText { get; } = []; public List<(string Text, ConsoleOutput? ConsoleOverride)> DisplayedRawText { get; } = []; public List DisplayedSuccess { get; } = []; public List ShownStatuses { get; } = []; public int DisplayEmptyLineCount { get; private set; } - public int DisplayCancellationMessageCount { get; private set; } + public List DisplayedCancellations { get; } = []; // Response queue setup methods public void SetupStringPromptResponse(string response) => _responses.Enqueue((response, ResponseType.String)); @@ -199,11 +199,11 @@ public void DisplayError(string errorMessage, bool allowMarkup = false) } } - public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) + public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false, ConsoleOutput? consoleOverride = null) { lock (_displayLock) { - DisplayedMessages.Add((emoji, message)); + DisplayedMessages.Add((emoji, message, consoleOverride)); } } @@ -223,9 +223,12 @@ public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lin } } - public void DisplayCancellationMessage() + public void DisplayCancellationMessage(ConsoleOutput? consoleOverride = null) { - DisplayCancellationMessageCount++; + lock (_displayLock) + { + DisplayedCancellations.Add(consoleOverride); + } } public Task PromptConfirmAsync(string promptText, PromptBinding? binding = null, CancellationToken cancellationToken = default)