diff --git a/src/Aspire.Cli/Commands/AddCommand.cs b/src/Aspire.Cli/Commands/AddCommand.cs index 291da3ad965..26a4e17af1c 100644 --- a/src/Aspire.Cli/Commands/AddCommand.cs +++ b/src/Aspire.Cli/Commands/AddCommand.cs @@ -12,10 +12,10 @@ namespace Aspire.Cli.Commands; internal sealed class AddCommand : BaseCommand { private readonly ActivitySource _activitySource = new ActivitySource(nameof(AddCommand)); - private readonly DotNetCliRunner _runner; + private readonly IDotNetCliRunner _runner; private readonly INuGetPackageCache _nuGetPackageCache; - public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) + public AddCommand(IDotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) : base("add", "Add an integration to the Aspire project.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); @@ -105,9 +105,9 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell _ => throw new InvalidOperationException("Unexpected number of packages found.") }; - var addPackageResult = await AnsiConsole.Status().StartAsync( + var addPackageResult = await InteractionUtils.ShowStatusAsync( "Adding Aspire integration...", - async context => { + async () => { var addPackageResult = await _runner.AddPackageAsync( effectiveAppHostProjectFile, selectedNuGetPackage.Package.Id, diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 44a0de26cec..74545763daf 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -11,10 +11,10 @@ namespace Aspire.Cli.Commands; internal sealed class NewCommand : BaseCommand { private readonly ActivitySource _activitySource = new ActivitySource(nameof(NewCommand)); - private readonly DotNetCliRunner _runner; + private readonly IDotNetCliRunner _runner; private readonly INuGetPackageCache _nuGetPackageCache; - public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) + public NewCommand(IDotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) : base("new", "Create a new Aspire sample project.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); @@ -140,14 +140,9 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var source = parseResult.GetValue("--source"); var version = await GetProjectTemplatesVersionAsync(parseResult, prerelease, source, cancellationToken); - var templateInstallResult = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync( - ":ice: Getting latest templates...", - async context => { - return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", version, source, true, cancellationToken); - }); + var templateInstallResult = await InteractionUtils.ShowStatusAsync( + ":ice: Getting latest templates...", + () => _runner.InstallTemplateAsync("Aspire.ProjectTemplates", version, source, true, cancellationToken)); if (templateInstallResult.ExitCode != 0) { @@ -157,18 +152,13 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell AnsiConsole.MarkupLine($":package: Using project templates version: {templateInstallResult.TemplateVersion}"); - int newProjectExitCode = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync( - ":rocket: Creating new Aspire project...", - async context => { - return await _runner.NewProjectAsync( + var newProjectExitCode = await InteractionUtils.ShowStatusAsync( + ":rocket: Creating new Aspire project...", + () => _runner.NewProjectAsync( template.TemplateName, name, outputPath, - cancellationToken); - }); + cancellationToken)); if (newProjectExitCode != 0) { diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index b09ab140acf..6831ad614bb 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -13,9 +13,9 @@ namespace Aspire.Cli.Commands; internal sealed class PublishCommand : BaseCommand { private readonly ActivitySource _activitySource = new ActivitySource(nameof(PublishCommand)); - private readonly DotNetCliRunner _runner; + private readonly IDotNetCliRunner _runner; - public PublishCommand(DotNetCliRunner runner) + public PublishCommand(IDotNetCliRunner runner) : base("publish", "Generates deployment artifacts for an Aspire app host project.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); @@ -38,7 +38,7 @@ public PublishCommand(DotNetCliRunner runner) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatabilityCheck = null; + (bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatibilityCheck = null; try { @@ -59,9 +59,9 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell env[KnownConfigNames.WaitForDebugger] = "true"; } - appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + appHostCompatibilityCheck = await AppHostHelper.CheckAppHostCompatibilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null")) + if (!appHostCompatibilityCheck?.IsCompatibleAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null")) { return ExitCodeConstants.FailedToDotnetRunAppHost; } @@ -78,36 +78,32 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var outputPath = parseResult.GetValue("--output-path"); var fullyQualifiedOutputPath = Path.GetFullPath(outputPath ?? "."); - var publishersResult = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync<(int ExitCode, string[]? Publishers)>( - publisher is { } ? ":package: Getting publisher..." : ":package: Getting publishers...", - async context => { - - using var getPublishersActivity = _activitySource.StartActivity( - $"{nameof(ExecuteAsync)}-Action-GetPublishers", - ActivityKind.Client); - - var backchannelCompletionSource = new TaskCompletionSource(); - var pendingInspectRun = _runner.RunAsync( - effectiveAppHostProjectFile, - false, - true, - ["--operation", "inspect"], - null, - backchannelCompletionSource, - cancellationToken).ConfigureAwait(false); - - var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); - var publishers = await backchannel.GetPublishersAsync(cancellationToken).ConfigureAwait(false); - - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - var exitCode = await pendingInspectRun; + var publishersResult = await InteractionUtils.ShowStatusAsync<(int ExitCode, string[] Publishers)>( + publisher is { } ? ":package: Getting publisher..." : ":package: Getting publishers...", + async () => { + using var getPublishersActivity = _activitySource.StartActivity( + $"{nameof(ExecuteAsync)}-Action-GetPublishers", + ActivityKind.Client); + + var backchannelCompletionSource = new TaskCompletionSource(); + var pendingInspectRun = _runner.RunAsync( + effectiveAppHostProjectFile, + false, + true, + ["--operation", "inspect"], + null, + backchannelCompletionSource, + cancellationToken).ConfigureAwait(false); - return (exitCode, publishers); + var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); + var publishers = await backchannel.GetPublishersAsync(cancellationToken).ConfigureAwait(false); + + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingInspectRun; - }).ConfigureAwait(false); + return (exitCode, publishers); + } + ); if (publishersResult.ExitCode != 0) { @@ -255,7 +251,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell { return InteractionUtils.DisplayIncompatibleVersionError( ex, - appHostCompatabilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") + appHostCompatibilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") ); } } diff --git a/src/Aspire.Cli/Commands/RootCommand.cs b/src/Aspire.Cli/Commands/RootCommand.cs index 22d4ad6bbdf..390940ae994 100644 --- a/src/Aspire.Cli/Commands/RootCommand.cs +++ b/src/Aspire.Cli/Commands/RootCommand.cs @@ -5,7 +5,7 @@ #if DEBUG using System.Diagnostics; -using Spectre.Console; +using Aspire.Cli.Utils; #endif using BaseRootCommand = System.CommandLine.RootCommand; @@ -34,15 +34,14 @@ public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addC if (waitForDebugger) { - AnsiConsole.Status().Start( + InteractionUtils.ShowStatus( $":bug: Waiting for debugger to attach to process ID: {Environment.ProcessId}", - context => { + () => { while (!Debugger.IsAttached) { Thread.Sleep(1000); } - } - ); + }); } }); #endif diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 6cce7664566..4b526f658d2 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -15,9 +15,9 @@ namespace Aspire.Cli.Commands; internal sealed class RunCommand : BaseCommand { private readonly ActivitySource _activitySource = new ActivitySource(nameof(RunCommand)); - private readonly DotNetCliRunner _runner; + private readonly IDotNetCliRunner _runner; - public RunCommand(DotNetCliRunner runner) + public RunCommand(IDotNetCliRunner runner) : base("run", "Run an Aspire app host in development mode.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); @@ -36,7 +36,7 @@ public RunCommand(DotNetCliRunner runner) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatabilityCheck = null; + (bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatibilityCheck = null; try { using var activity = _activitySource.StartActivity(); @@ -87,9 +87,9 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell } } - appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + appHostCompatibilityCheck = await AppHostHelper.CheckAppHostCompatibilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null")) + if (!appHostCompatibilityCheck?.IsCompatibleAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null")) { return ExitCodeConstants.FailedToDotnetRunAppHost; } @@ -109,20 +109,14 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell { // We wait for the back channel to be created to signal that // the AppHost is ready to accept requests. - var backchannel = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync(":linked_paperclips: Starting Aspire app host...", async context => { - return await backchannelCompletitionSource.Task; - }); + var backchannel = await InteractionUtils.ShowStatusAsync( + ":linked_paperclips: Starting Aspire app host...", + () => backchannelCompletitionSource.Task); // We wait for the first update of the console model via RPC from the AppHost. - var dashboardUrls = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync(":chart_increasing: Starting Aspire dashboard...", async context => { - return await backchannel.GetDashboardUrlsAsync(cancellationToken); - }); + var dashboardUrls = await InteractionUtils.ShowStatusAsync( + ":chart_increasing: Starting Aspire dashboard...", + () => backchannel.GetDashboardUrlsAsync(cancellationToken)); AnsiConsole.WriteLine(); AnsiConsole.MarkupLine($"[green bold]Dashboard[/]:"); @@ -217,7 +211,7 @@ await AnsiConsole.Live(table).StartAsync(async context => { { return InteractionUtils.DisplayIncompatibleVersionError( ex, - appHostCompatabilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") + appHostCompatibilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") ); } } diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs index 97d318fb952..59189471f70 100644 --- a/src/Aspire.Cli/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNetCliRunner.cs @@ -13,7 +13,21 @@ namespace Aspire.Cli; -internal sealed class DotNetCliRunner(ILogger logger, IServiceProvider serviceProvider) +internal interface IDotNetCliRunner +{ + Task<(int ExitCode, bool IsAspireHost, string? AspireHostingSdkVersion)> GetAppHostInformationAsync(FileInfo projectFile, CancellationToken cancellationToken); + Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, CancellationToken cancellationToken); + Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, CancellationToken cancellationToken); + Task CheckHttpCertificateAsync(CancellationToken cancellationToken); + Task TrustHttpCertificateAsync(CancellationToken cancellationToken); + Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, string? nugetSource, bool force, CancellationToken cancellationToken); + Task NewProjectAsync(string templateName, string name, string outputPath, CancellationToken cancellationToken); + Task BuildAsync(FileInfo projectFilePath, CancellationToken cancellationToken); + Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, CancellationToken cancellationToken); + Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, string? nugetSource, CancellationToken cancellationToken); +} + +internal sealed class DotNetCliRunner(ILogger logger, IServiceProvider serviceProvider) : IDotNetCliRunner { private readonly ActivitySource _activitySource = new ActivitySource(nameof(DotNetCliRunner)); @@ -478,7 +492,7 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas ex.RequiredCapability ); - // If the app host is incompatable then there is no point + // If the app host is incompatible then there is no point // trying to reconnect, we should propogate the exception // up to the code that needs to back channel so it can display // and error message to the user. diff --git a/src/Aspire.Cli/NuGetPackageCache.cs b/src/Aspire.Cli/NuGetPackageCache.cs index ec3a7de7c15..2af089c00c8 100644 --- a/src/Aspire.Cli/NuGetPackageCache.cs +++ b/src/Aspire.Cli/NuGetPackageCache.cs @@ -12,7 +12,7 @@ internal interface INuGetPackageCache Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken); } -internal sealed class NuGetPackageCache(ILogger logger, DotNetCliRunner cliRunner) : INuGetPackageCache +internal sealed class NuGetPackageCache(ILogger logger, IDotNetCliRunner cliRunner) : INuGetPackageCache { private readonly ActivitySource _activitySource = new(nameof(NuGetPackageCache)); diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs index 3a7cb7e944f..692486b4e33 100644 --- a/src/Aspire.Cli/Program.cs +++ b/src/Aspire.Cli/Program.cs @@ -72,7 +72,7 @@ private static IHost BuildApplication(string[] args) } // Shared services. - builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddTransient(); diff --git a/src/Aspire.Cli/Utils/AppHostHelper.cs b/src/Aspire.Cli/Utils/AppHostHelper.cs index 29908c6fe38..4078d249428 100644 --- a/src/Aspire.Cli/Utils/AppHostHelper.cs +++ b/src/Aspire.Cli/Utils/AppHostHelper.cs @@ -11,7 +11,7 @@ internal static class AppHostHelper { private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(AppHostHelper)); - internal static async Task<(bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatabilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) + internal static async Task<(bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatibilityAsync(IDotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) { var appHostInformation = await GetAppHostInformationAsync(runner, projectFile, cancellationToken); @@ -47,29 +47,22 @@ internal static class AppHostHelper } } - internal static async Task<(int ExitCode, bool IsAspireHost, string? AspireHostingSdkVersion)> GetAppHostInformationAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) + internal static async Task<(int ExitCode, bool IsAspireHost, string? AspireHostingSdkVersion)> GetAppHostInformationAsync(IDotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) { using var activity = s_activitySource.StartActivity(nameof(GetAppHostInformationAsync), ActivityKind.Client); - var appHostInformationResult = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync( - ":microscope: Checking project type...", - async (context) => { - return await runner.GetAppHostInformationAsync(projectFile, cancellationToken); - }); + var appHostInformationResult = await InteractionUtils.ShowStatusAsync( + ":microscope: Checking project type...", + () => runner.GetAppHostInformationAsync(projectFile, cancellationToken) + ); return appHostInformationResult; } - internal static async Task BuildAppHostAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) + internal static async Task BuildAppHostAsync(IDotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) { - return await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync(":hammer_and_wrench: Building app host...", async context => { - return await runner.BuildAsync(projectFile, cancellationToken).ConfigureAwait(false); - }); + return await InteractionUtils.ShowStatusAsync( + ":hammer_and_wrench: Building app host...", + () => runner.BuildAsync(projectFile, cancellationToken)); } } \ No newline at end of file diff --git a/src/Aspire.Cli/Utils/CertificatesHelper.cs b/src/Aspire.Cli/Utils/CertificatesHelper.cs index 8c6c904812c..9547df0f348 100644 --- a/src/Aspire.Cli/Utils/CertificatesHelper.cs +++ b/src/Aspire.Cli/Utils/CertificatesHelper.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Spectre.Console; using System.Diagnostics; namespace Aspire.Cli.Utils; @@ -10,29 +9,20 @@ internal static class CertificatesHelper { private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(CertificatesHelper)); - internal static async Task EnsureCertificatesTrustedAsync(DotNetCliRunner runner, CancellationToken cancellationToken) + internal static async Task EnsureCertificatesTrustedAsync(IDotNetCliRunner runner, CancellationToken cancellationToken) { using var activity = s_activitySource.StartActivity(nameof(EnsureCertificatesTrustedAsync), ActivityKind.Client); - var checkExitCode = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync( - ":locked_with_key: Checking certificates...", - async (context) => { - return await runner.CheckHttpCertificateAsync(cancellationToken); - }); + var checkExitCode = await InteractionUtils.ShowStatusAsync( + ":locked_with_key: Checking certificates...", + () => runner.CheckHttpCertificateAsync(cancellationToken)); if (checkExitCode != 0) { - var trustExitCode = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync( - ":locked_with_key: Trusting certificates...", - async (context) => { - return await runner.TrustHttpCertificateAsync(cancellationToken); - }); + var trustExitCode = await InteractionUtils.ShowStatusAsync( + ":locked_with_key: Trusting certificates...", + () => runner.TrustHttpCertificateAsync(cancellationToken) + ); if (trustExitCode != 0) { diff --git a/src/Aspire.Cli/Utils/InteractionUtils.cs b/src/Aspire.Cli/Utils/InteractionUtils.cs index 900fc5faa36..46edbc96d9e 100644 --- a/src/Aspire.Cli/Utils/InteractionUtils.cs +++ b/src/Aspire.Cli/Utils/InteractionUtils.cs @@ -16,6 +16,14 @@ public static async Task ShowStatusAsync(string statusText, Func> .StartAsync(statusText, (context) => action()); } + public static void ShowStatus(string statusText, Action action) + { + AnsiConsole.Status() + .Spinner(Spinner.Known.Dots3) + .SpinnerStyle(Style.Parse("purple")) + .Start(statusText, (context) => action()); + } + public static async Task PromptForTemplatesVersionAsync(IEnumerable candidatePackages, CancellationToken cancellationToken) { return await PromptForSelectionAsync( diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 81fcc1a5c6c..97725ed11c3 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -484,7 +484,7 @@ private static void ApplyEndpoints(this IResourceBuilder builder, IResourc /// /// The method allows /// developers to mutate any aspect of an endpoint annotation. Note that changing one value does not automatically change - /// other values to compatable/consistent values. For example setting the property + /// other values to compatible/consistent values. For example setting the property /// of the endpoint annotation in the callback will not automatically change the . /// All values should be set in the callback if the defaults are not acceptable. /// diff --git a/tests/Aspire.Cli.Tests/CliTestConstants.cs b/tests/Aspire.Cli.Tests/CliTestConstants.cs new file mode 100644 index 00000000000..e3eeb3a109a --- /dev/null +++ b/tests/Aspire.Cli.Tests/CliTestConstants.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Cli.Tests; + +public static class CliTestConstants +{ + public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); + public static readonly TimeSpan LongTimeout = TimeSpan.FromSeconds(10); +} \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs new file mode 100644 index 00000000000..3bb7979050c --- /dev/null +++ b/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Commands; +using Aspire.Cli.Tests.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Cli.Tests.Commands; + +public class AddCommandTests +{ + [Fact] + public async Task AddCommandWithHelpArgumentReturnsZero() + { + var services = CliTestHelper.CreateServiceCollection(); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("add --help"); + + var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout); + Assert.Equal(0, exitCode); + } +} \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs new file mode 100644 index 00000000000..40340568847 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Commands; +using Aspire.Cli.Tests.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Cli.Tests.Commands; + +public class NewCommandTests +{ + [Fact] + public async Task NewCommandWithHelpArgumentReturnsZero() + { + var services = CliTestHelper.CreateServiceCollection(); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("new --help"); + + var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout); + Assert.Equal(0, exitCode); + } +} \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Commands/PublishCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/PublishCommandTests.cs new file mode 100644 index 00000000000..a1fc1f1d905 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Commands/PublishCommandTests.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Commands; +using Aspire.Cli.Tests.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Cli.Tests.Commands; + +public class PublishCommandTests +{ + [Fact] + public async Task PublishCommandWithHelpArgumentReturnsZero() + { + var services = CliTestHelper.CreateServiceCollection(); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("publish --help"); + + var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout); + Assert.Equal(0, exitCode); + } +} \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Commands/RootCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/RootCommandTests.cs new file mode 100644 index 00000000000..909891769a7 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Commands/RootCommandTests.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Commands; +using Aspire.Cli.Tests.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Cli.Tests.Commands; + +public class RootCommandTests +{ + [Fact] + public async Task RootCommandWithHelpArgumentReturnsZero() + { + var services = CliTestHelper.CreateServiceCollection(); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("--help"); + + var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout); + Assert.Equal(0, exitCode); + } +} \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs new file mode 100644 index 00000000000..b91e3caf4ff --- /dev/null +++ b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Commands; +using Aspire.Cli.Tests.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Cli.Tests.Commands; + +public class RunCommandTests +{ + [Fact] + public async Task RunCommandWithHelpArgumentReturnsZero() + { + var services = CliTestHelper.CreateServiceCollection(); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("run --help"); + + var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout); + Assert.Equal(0, exitCode); + } +} \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs new file mode 100644 index 00000000000..42aafaa7f60 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Commands; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Aspire.Cli.Tests.Utils; + +internal static class CliTestHelper +{ + public static IServiceCollection CreateServiceCollection(Action? configure = null) + { + var options = new CliServiceCollectionTestOptions(); + configure?.Invoke(options); + + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddTransient(options.DotNetCliRunnerFactory); + services.AddTransient(options.NuGetPackageCacheFactory); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + return services; + } +} + +internal sealed class CliServiceCollectionTestOptions +{ + public Func DotNetCliRunnerFactory { get; set; } = (IServiceProvider serviceProvider) => { + var logger = serviceProvider.GetRequiredService>(); + return new DotNetCliRunner(logger, serviceProvider); + }; + + public Func NuGetPackageCacheFactory { get; set; } = (IServiceProvider serviceProvider) => { + var logger = serviceProvider.GetRequiredService>(); + var runner = serviceProvider.GetRequiredService(); + return new NuGetPackageCache(logger, runner); + }; +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Tests/OperationModesTests.cs b/tests/Aspire.Hosting.Tests/OperationModesTests.cs index 5485846f872..4bab3a3234e 100644 --- a/tests/Aspire.Hosting.Tests/OperationModesTests.cs +++ b/tests/Aspire.Hosting.Tests/OperationModesTests.cs @@ -14,7 +14,7 @@ namespace Aspire.Hosting.Tests; public class OperationModesTests(ITestOutputHelper outputHelper) { [Fact] - public async Task VerifyBackwardsCompatableRunModeInvocation() + public async Task VerifyBackwardsCompatibleRunModeInvocation() { // The purpose of this test is to verify that the apphost executable will continue // to enter run mode if executed without any arguments. @@ -100,7 +100,7 @@ public async Task VerifyExplicitRunModeWithPublisherInvocation() } [Fact] - public async Task VerifyBackwardsCompatablePublishModeInvocation() + public async Task VerifyBackwardsCompatiblePublishModeInvocation() { // The purpose of this test is to verify that the apphost executable will continue // to enter publish mode if the --publisher argument is specified.