Skip to content

Commit bdb724f

Browse files
authored
Testing improvements for the aspire add command. (#8767)
1 parent c2d2ae8 commit bdb724f

File tree

10 files changed

+573
-160
lines changed

10 files changed

+573
-160
lines changed

src/Aspire.Cli/Commands/AddCommand.cs

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,22 @@ internal sealed class AddCommand : BaseCommand
1818
private readonly INuGetPackageCache _nuGetPackageCache;
1919
private readonly IInteractionService _interactionService;
2020
private readonly IProjectLocator _projectLocator;
21+
private readonly IAddCommandPrompter _prompter;
2122

22-
public AddCommand(IDotNetCliRunner runner, INuGetPackageCache nuGetPackageCache, IInteractionService interactionService, IProjectLocator projectLocator)
23+
public AddCommand(IDotNetCliRunner runner, INuGetPackageCache nuGetPackageCache, IInteractionService interactionService, IProjectLocator projectLocator, IAddCommandPrompter prompter)
2324
: base("add", "Add an integration to the Aspire project.")
2425
{
2526
ArgumentNullException.ThrowIfNull(runner);
2627
ArgumentNullException.ThrowIfNull(nuGetPackageCache);
2728
ArgumentNullException.ThrowIfNull(interactionService);
2829
ArgumentNullException.ThrowIfNull(projectLocator);
29-
30+
ArgumentNullException.ThrowIfNull(prompter);
31+
3032
_runner = runner;
3133
_nuGetPackageCache = nuGetPackageCache;
3234
_interactionService = interactionService;
3335
_projectLocator = projectLocator;
36+
_prompter = prompter;
3437

3538
var integrationArgument = new Argument<string>("integration");
3639
integrationArgument.Description = "The name of the integration to add (e.g. redis, postgres).";
@@ -170,23 +173,11 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
170173
{
171174
var distinctPackages = possiblePackages.DistinctBy(p => p.Package.Id);
172175

173-
var packagePrompt = new SelectionPrompt<(string FriendlyName, NuGetPackage Package)>()
174-
.Title("Select an integration to add:")
175-
.UseConverter(PackageNameWithFriendlyNameIfAvailable)
176-
.PageSize(10)
177-
.EnableSearch()
178-
.HighlightStyle(Style.Parse("darkmagenta"))
179-
.AddChoices(distinctPackages);
180-
181176
// If there is only one package, we can skip the prompt and just use it.
182177
var selectedPackage = distinctPackages.Count() switch
183178
{
184179
1 => distinctPackages.First(),
185-
> 1 => await _interactionService.PromptForSelectionAsync(
186-
"Select an integration to add:",
187-
distinctPackages,
188-
PackageNameWithFriendlyNameIfAvailable,
189-
cancellationToken),
180+
> 1 => await _prompter.PromptForIntegrationAsync(distinctPackages, cancellationToken),
190181
_ => throw new InvalidOperationException("Unexpected number of packages found.")
191182
};
192183

@@ -201,25 +192,9 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
201192
}
202193

203194
// ... otherwise we had better prompt.
204-
var version = await _interactionService.PromptForSelectionAsync(
205-
$"Select a version of the {selectedPackage.Package.Id}:",
206-
packageVersions,
207-
p => p.Package.Version,
208-
cancellationToken);
195+
var version = await _prompter.PromptForIntegrationVersionAsync(packageVersions, cancellationToken);
209196

210197
return version;
211-
212-
static string PackageNameWithFriendlyNameIfAvailable((string FriendlyName, NuGetPackage Package) packageWithFriendlyName)
213-
{
214-
if (packageWithFriendlyName.FriendlyName is { } friendlyName)
215-
{
216-
return $"[bold]{friendlyName}[/] ({packageWithFriendlyName.Package.Id})";
217-
}
218-
else
219-
{
220-
return packageWithFriendlyName.Package.Id;
221-
}
222-
}
223198
}
224199

225200
private static (string FriendlyName, NuGetPackage Package) GenerateFriendlyName(NuGetPackage package)
@@ -244,3 +219,45 @@ private static (string FriendlyName, NuGetPackage Package) GenerateFriendlyName(
244219
return (shortNameBuilder.ToString(), package);
245220
}
246221
}
222+
223+
internal interface IAddCommandPrompter
224+
{
225+
Task<(string FriendlyName, NuGetPackage Package)> PromptForIntegrationAsync(IEnumerable<(string FriendlyName, NuGetPackage Package)> packages, CancellationToken cancellationToken);
226+
Task<(string FriendlyName, NuGetPackage Package)> PromptForIntegrationVersionAsync(IEnumerable<(string FriendlyName, NuGetPackage Package)> packages, CancellationToken cancellationToken);
227+
}
228+
229+
internal class AddCommandPrompter(IInteractionService interactionService) : IAddCommandPrompter
230+
{
231+
public virtual async Task<(string FriendlyName, NuGetPackage Package)> PromptForIntegrationVersionAsync(IEnumerable<(string FriendlyName, NuGetPackage Package)> packages, CancellationToken cancellationToken)
232+
{
233+
var selectedPackage = packages.First();
234+
var version = await interactionService.PromptForSelectionAsync(
235+
$"Select a version of the {selectedPackage.Package.Id}:",
236+
packages,
237+
p => p.Package.Version,
238+
cancellationToken);
239+
return version;
240+
}
241+
242+
public virtual async Task<(string FriendlyName, NuGetPackage Package)> PromptForIntegrationAsync(IEnumerable<(string FriendlyName, NuGetPackage Package)> packages, CancellationToken cancellationToken)
243+
{
244+
var selectedIntegration = await interactionService.PromptForSelectionAsync(
245+
"Select an integration to add:",
246+
packages,
247+
PackageNameWithFriendlyNameIfAvailable,
248+
cancellationToken);
249+
return selectedIntegration;
250+
}
251+
252+
private static string PackageNameWithFriendlyNameIfAvailable((string FriendlyName, NuGetPackage Package) packageWithFriendlyName)
253+
{
254+
if (packageWithFriendlyName.FriendlyName is { } friendlyName)
255+
{
256+
return $"[bold]{friendlyName}[/] ({packageWithFriendlyName.Package.Id})";
257+
}
258+
else
259+
{
260+
return packageWithFriendlyName.Package.Id;
261+
}
262+
}
263+
}

src/Aspire.Cli/Interaction/InteractionService.cs

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,29 @@ namespace Aspire.Cli.Interaction;
99

1010
internal class InteractionService : IInteractionService
1111
{
12+
private readonly IAnsiConsole _ansiConsole;
13+
14+
public InteractionService() : this(AnsiConsole.Console)
15+
{
16+
}
17+
18+
public InteractionService(IAnsiConsole ansiConsole)
19+
{
20+
ArgumentNullException.ThrowIfNull(ansiConsole);
21+
_ansiConsole = ansiConsole;
22+
}
23+
1224
public async Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action)
1325
{
14-
return await AnsiConsole.Status()
26+
return await _ansiConsole.Status()
1527
.Spinner(Spinner.Known.Dots3)
1628
.SpinnerStyle(Style.Parse("purple"))
1729
.StartAsync(statusText, (context) => action());
1830
}
1931

2032
public void ShowStatus(string statusText, Action action)
2133
{
22-
AnsiConsole.Status()
34+
_ansiConsole.Status()
2335
.Spinner(Spinner.Known.Dots3)
2436
.SpinnerStyle(Style.Parse("purple"))
2537
.Start(statusText, (context) => action());
@@ -40,8 +52,8 @@ public async Task<string> PromptForStringAsync(string promptText, string? defaul
4052
{
4153
prompt.Validate(validator);
4254
}
43-
44-
return await AnsiConsole.PromptAsync(prompt, cancellationToken);
55+
56+
return await _ansiConsole.PromptAsync(prompt, cancellationToken);
4557
}
4658

4759
public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T> choices, Func<T, string> choiceFormatter, CancellationToken cancellationToken = default) where T : notnull
@@ -58,7 +70,7 @@ public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T
5870
.EnableSearch()
5971
.HighlightStyle(Style.Parse("darkmagenta"));
6072

61-
return await AnsiConsole.PromptAsync(prompt, cancellationToken);
73+
return await _ansiConsole.PromptAsync(prompt, cancellationToken);
6274
}
6375

6476
public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingSdkVersion)
@@ -67,9 +79,9 @@ public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, stri
6779

6880
DisplayError("The app host is not compatible. Consider upgrading the app host or Aspire CLI.");
6981
Console.WriteLine();
70-
AnsiConsole.MarkupLine($"\t[bold]Aspire Hosting SDK Version[/]: {appHostHostingSdkVersion}");
71-
AnsiConsole.MarkupLine($"\t[bold]Aspire CLI Version[/]: {cliInformationalVersion}");
72-
AnsiConsole.MarkupLine($"\t[bold]Required Capability[/]: {ex.RequiredCapability}");
82+
_ansiConsole.MarkupLine($"\t[bold]Aspire Hosting SDK Version[/]: {appHostHostingSdkVersion}");
83+
_ansiConsole.MarkupLine($"\t[bold]Aspire CLI Version[/]: {cliInformationalVersion}");
84+
_ansiConsole.MarkupLine($"\t[bold]Required Capability[/]: {ex.RequiredCapability}");
7385
Console.WriteLine();
7486
return ExitCodeConstants.AppHostIncompatible;
7587
}
@@ -81,7 +93,7 @@ public void DisplayError(string errorMessage)
8193

8294
public void DisplayMessage(string emoji, string message)
8395
{
84-
AnsiConsole.MarkupLine($":{emoji}: {message}");
96+
_ansiConsole.MarkupLine($":{emoji}: {message}");
8597
}
8698

8799
public void DisplaySuccess(string message)
@@ -91,17 +103,17 @@ public void DisplaySuccess(string message)
91103

92104
public void DisplayDashboardUrls((string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken) dashboardUrls)
93105
{
94-
AnsiConsole.WriteLine();
95-
AnsiConsole.MarkupLine($"[green bold]Dashboard[/]:");
106+
_ansiConsole.WriteLine();
107+
_ansiConsole.MarkupLine($"[green bold]Dashboard[/]:");
96108
if (dashboardUrls.CodespacesUrlWithLoginToken is not null)
97109
{
98-
AnsiConsole.MarkupLine($":chart_increasing: Direct: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
99-
AnsiConsole.MarkupLine($":chart_increasing: Codespaces: [link={dashboardUrls.CodespacesUrlWithLoginToken}]{dashboardUrls.CodespacesUrlWithLoginToken}[/]");
110+
_ansiConsole.MarkupLine($":chart_increasing: Direct: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
111+
_ansiConsole.MarkupLine($":chart_increasing: Codespaces: [link={dashboardUrls.CodespacesUrlWithLoginToken}]{dashboardUrls.CodespacesUrlWithLoginToken}[/]");
100112
}
101113
else
102114
{
103-
AnsiConsole.MarkupLine($":chart_increasing: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
115+
_ansiConsole.MarkupLine($":chart_increasing: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
104116
}
105-
AnsiConsole.WriteLine();
117+
_ansiConsole.WriteLine();
106118
}
107119
}

src/Aspire.Cli/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ private static IHost BuildApplication(string[] args)
7777
// Shared services.
7878
builder.Services.AddSingleton(BuildProjectLocator);
7979
builder.Services.AddSingleton<INewCommandPrompter, NewCommandPrompter>();
80+
builder.Services.AddSingleton<IAddCommandPrompter, AddCommandPrompter>();
8081
builder.Services.AddSingleton<IInteractionService, InteractionService>();
8182
builder.Services.AddSingleton<ICertificateService, CertificateService>();
8283
builder.Services.AddTransient<IDotNetCliRunner, DotNetCliRunner>();

0 commit comments

Comments
 (0)