Skip to content

Commit c7ddaf1

Browse files
authored
Minimal changes to improve CLI testability. (#8657)
* WIP * Spelling * Replace all Spectre console Status() calls with interaction utils (decoupling).
1 parent e52cdd0 commit c7ddaf1

20 files changed

+285
-122
lines changed

src/Aspire.Cli/Commands/AddCommand.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ namespace Aspire.Cli.Commands;
1212
internal sealed class AddCommand : BaseCommand
1313
{
1414
private readonly ActivitySource _activitySource = new ActivitySource(nameof(AddCommand));
15-
private readonly DotNetCliRunner _runner;
15+
private readonly IDotNetCliRunner _runner;
1616
private readonly INuGetPackageCache _nuGetPackageCache;
1717

18-
public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache)
18+
public AddCommand(IDotNetCliRunner runner, INuGetPackageCache nuGetPackageCache)
1919
: base("add", "Add an integration to the Aspire project.")
2020
{
2121
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
@@ -105,9 +105,9 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
105105
_ => throw new InvalidOperationException("Unexpected number of packages found.")
106106
};
107107

108-
var addPackageResult = await AnsiConsole.Status().StartAsync(
108+
var addPackageResult = await InteractionUtils.ShowStatusAsync(
109109
"Adding Aspire integration...",
110-
async context => {
110+
async () => {
111111
var addPackageResult = await _runner.AddPackageAsync(
112112
effectiveAppHostProjectFile,
113113
selectedNuGetPackage.Package.Id,

src/Aspire.Cli/Commands/NewCommand.cs

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ namespace Aspire.Cli.Commands;
1111
internal sealed class NewCommand : BaseCommand
1212
{
1313
private readonly ActivitySource _activitySource = new ActivitySource(nameof(NewCommand));
14-
private readonly DotNetCliRunner _runner;
14+
private readonly IDotNetCliRunner _runner;
1515
private readonly INuGetPackageCache _nuGetPackageCache;
1616

17-
public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache)
17+
public NewCommand(IDotNetCliRunner runner, INuGetPackageCache nuGetPackageCache)
1818
: base("new", "Create a new Aspire sample project.")
1919
{
2020
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
@@ -140,14 +140,9 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
140140
var source = parseResult.GetValue<string?>("--source");
141141
var version = await GetProjectTemplatesVersionAsync(parseResult, prerelease, source, cancellationToken);
142142

143-
var templateInstallResult = await AnsiConsole.Status()
144-
.Spinner(Spinner.Known.Dots3)
145-
.SpinnerStyle(Style.Parse("purple"))
146-
.StartAsync(
147-
":ice: Getting latest templates...",
148-
async context => {
149-
return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", version, source, true, cancellationToken);
150-
});
143+
var templateInstallResult = await InteractionUtils.ShowStatusAsync(
144+
":ice: Getting latest templates...",
145+
() => _runner.InstallTemplateAsync("Aspire.ProjectTemplates", version, source, true, cancellationToken));
151146

152147
if (templateInstallResult.ExitCode != 0)
153148
{
@@ -157,18 +152,13 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
157152

158153
AnsiConsole.MarkupLine($":package: Using project templates version: {templateInstallResult.TemplateVersion}");
159154

160-
int newProjectExitCode = await AnsiConsole.Status()
161-
.Spinner(Spinner.Known.Dots3)
162-
.SpinnerStyle(Style.Parse("purple"))
163-
.StartAsync(
164-
":rocket: Creating new Aspire project...",
165-
async context => {
166-
return await _runner.NewProjectAsync(
155+
var newProjectExitCode = await InteractionUtils.ShowStatusAsync(
156+
":rocket: Creating new Aspire project...",
157+
() => _runner.NewProjectAsync(
167158
template.TemplateName,
168159
name,
169160
outputPath,
170-
cancellationToken);
171-
});
161+
cancellationToken));
172162

173163
if (newProjectExitCode != 0)
174164
{

src/Aspire.Cli/Commands/PublishCommand.cs

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ namespace Aspire.Cli.Commands;
1313
internal sealed class PublishCommand : BaseCommand
1414
{
1515
private readonly ActivitySource _activitySource = new ActivitySource(nameof(PublishCommand));
16-
private readonly DotNetCliRunner _runner;
16+
private readonly IDotNetCliRunner _runner;
1717

18-
public PublishCommand(DotNetCliRunner runner)
18+
public PublishCommand(IDotNetCliRunner runner)
1919
: base("publish", "Generates deployment artifacts for an Aspire app host project.")
2020
{
2121
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
@@ -38,7 +38,7 @@ public PublishCommand(DotNetCliRunner runner)
3838

3939
protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
4040
{
41-
(bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatabilityCheck = null;
41+
(bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatibilityCheck = null;
4242

4343
try
4444
{
@@ -59,9 +59,9 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
5959
env[KnownConfigNames.WaitForDebugger] = "true";
6060
}
6161

62-
appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken);
62+
appHostCompatibilityCheck = await AppHostHelper.CheckAppHostCompatibilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken);
6363

64-
if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null"))
64+
if (!appHostCompatibilityCheck?.IsCompatibleAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null"))
6565
{
6666
return ExitCodeConstants.FailedToDotnetRunAppHost;
6767
}
@@ -78,36 +78,32 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
7878
var outputPath = parseResult.GetValue<string>("--output-path");
7979
var fullyQualifiedOutputPath = Path.GetFullPath(outputPath ?? ".");
8080

81-
var publishersResult = await AnsiConsole.Status()
82-
.Spinner(Spinner.Known.Dots3)
83-
.SpinnerStyle(Style.Parse("purple"))
84-
.StartAsync<(int ExitCode, string[]? Publishers)>(
85-
publisher is { } ? ":package: Getting publisher..." : ":package: Getting publishers...",
86-
async context => {
87-
88-
using var getPublishersActivity = _activitySource.StartActivity(
89-
$"{nameof(ExecuteAsync)}-Action-GetPublishers",
90-
ActivityKind.Client);
91-
92-
var backchannelCompletionSource = new TaskCompletionSource<AppHostBackchannel>();
93-
var pendingInspectRun = _runner.RunAsync(
94-
effectiveAppHostProjectFile,
95-
false,
96-
true,
97-
["--operation", "inspect"],
98-
null,
99-
backchannelCompletionSource,
100-
cancellationToken).ConfigureAwait(false);
101-
102-
var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false);
103-
var publishers = await backchannel.GetPublishersAsync(cancellationToken).ConfigureAwait(false);
104-
105-
await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false);
106-
var exitCode = await pendingInspectRun;
81+
var publishersResult = await InteractionUtils.ShowStatusAsync<(int ExitCode, string[] Publishers)>(
82+
publisher is { } ? ":package: Getting publisher..." : ":package: Getting publishers...",
83+
async () => {
84+
using var getPublishersActivity = _activitySource.StartActivity(
85+
$"{nameof(ExecuteAsync)}-Action-GetPublishers",
86+
ActivityKind.Client);
87+
88+
var backchannelCompletionSource = new TaskCompletionSource<AppHostBackchannel>();
89+
var pendingInspectRun = _runner.RunAsync(
90+
effectiveAppHostProjectFile,
91+
false,
92+
true,
93+
["--operation", "inspect"],
94+
null,
95+
backchannelCompletionSource,
96+
cancellationToken).ConfigureAwait(false);
10797

108-
return (exitCode, publishers);
98+
var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false);
99+
var publishers = await backchannel.GetPublishersAsync(cancellationToken).ConfigureAwait(false);
100+
101+
await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false);
102+
var exitCode = await pendingInspectRun;
109103

110-
}).ConfigureAwait(false);
104+
return (exitCode, publishers);
105+
}
106+
);
111107

112108
if (publishersResult.ExitCode != 0)
113109
{
@@ -255,7 +251,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
255251
{
256252
return InteractionUtils.DisplayIncompatibleVersionError(
257253
ex,
258-
appHostCompatabilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null")
254+
appHostCompatibilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null")
259255
);
260256
}
261257
}

src/Aspire.Cli/Commands/RootCommand.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
#if DEBUG
77
using System.Diagnostics;
8-
using Spectre.Console;
8+
using Aspire.Cli.Utils;
99
#endif
1010

1111
using BaseRootCommand = System.CommandLine.RootCommand;
@@ -34,15 +34,14 @@ public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addC
3434

3535
if (waitForDebugger)
3636
{
37-
AnsiConsole.Status().Start(
37+
InteractionUtils.ShowStatus(
3838
$":bug: Waiting for debugger to attach to process ID: {Environment.ProcessId}",
39-
context => {
39+
() => {
4040
while (!Debugger.IsAttached)
4141
{
4242
Thread.Sleep(1000);
4343
}
44-
}
45-
);
44+
});
4645
}
4746
});
4847
#endif

src/Aspire.Cli/Commands/RunCommand.cs

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ namespace Aspire.Cli.Commands;
1515
internal sealed class RunCommand : BaseCommand
1616
{
1717
private readonly ActivitySource _activitySource = new ActivitySource(nameof(RunCommand));
18-
private readonly DotNetCliRunner _runner;
18+
private readonly IDotNetCliRunner _runner;
1919

20-
public RunCommand(DotNetCliRunner runner)
20+
public RunCommand(IDotNetCliRunner runner)
2121
: base("run", "Run an Aspire app host in development mode.")
2222
{
2323
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
@@ -36,7 +36,7 @@ public RunCommand(DotNetCliRunner runner)
3636

3737
protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
3838
{
39-
(bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatabilityCheck = null;
39+
(bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatibilityCheck = null;
4040
try
4141
{
4242
using var activity = _activitySource.StartActivity();
@@ -87,9 +87,9 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
8787
}
8888
}
8989

90-
appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken);
90+
appHostCompatibilityCheck = await AppHostHelper.CheckAppHostCompatibilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken);
9191

92-
if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null"))
92+
if (!appHostCompatibilityCheck?.IsCompatibleAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null"))
9393
{
9494
return ExitCodeConstants.FailedToDotnetRunAppHost;
9595
}
@@ -109,20 +109,14 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
109109
{
110110
// We wait for the back channel to be created to signal that
111111
// the AppHost is ready to accept requests.
112-
var backchannel = await AnsiConsole.Status()
113-
.Spinner(Spinner.Known.Dots3)
114-
.SpinnerStyle(Style.Parse("purple"))
115-
.StartAsync(":linked_paperclips: Starting Aspire app host...", async context => {
116-
return await backchannelCompletitionSource.Task;
117-
});
112+
var backchannel = await InteractionUtils.ShowStatusAsync(
113+
":linked_paperclips: Starting Aspire app host...",
114+
() => backchannelCompletitionSource.Task);
118115

119116
// We wait for the first update of the console model via RPC from the AppHost.
120-
var dashboardUrls = await AnsiConsole.Status()
121-
.Spinner(Spinner.Known.Dots3)
122-
.SpinnerStyle(Style.Parse("purple"))
123-
.StartAsync(":chart_increasing: Starting Aspire dashboard...", async context => {
124-
return await backchannel.GetDashboardUrlsAsync(cancellationToken);
125-
});
117+
var dashboardUrls = await InteractionUtils.ShowStatusAsync(
118+
":chart_increasing: Starting Aspire dashboard...",
119+
() => backchannel.GetDashboardUrlsAsync(cancellationToken));
126120

127121
AnsiConsole.WriteLine();
128122
AnsiConsole.MarkupLine($"[green bold]Dashboard[/]:");
@@ -217,7 +211,7 @@ await AnsiConsole.Live(table).StartAsync(async context => {
217211
{
218212
return InteractionUtils.DisplayIncompatibleVersionError(
219213
ex,
220-
appHostCompatabilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null")
214+
appHostCompatibilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null")
221215
);
222216
}
223217
}

src/Aspire.Cli/DotNetCliRunner.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,21 @@
1313

1414
namespace Aspire.Cli;
1515

16-
internal sealed class DotNetCliRunner(ILogger<DotNetCliRunner> logger, IServiceProvider serviceProvider)
16+
internal interface IDotNetCliRunner
17+
{
18+
Task<(int ExitCode, bool IsAspireHost, string? AspireHostingSdkVersion)> GetAppHostInformationAsync(FileInfo projectFile, CancellationToken cancellationToken);
19+
Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, CancellationToken cancellationToken);
20+
Task<int> RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] args, IDictionary<string, string>? env, TaskCompletionSource<AppHostBackchannel>? backchannelCompletionSource, CancellationToken cancellationToken);
21+
Task<int> CheckHttpCertificateAsync(CancellationToken cancellationToken);
22+
Task<int> TrustHttpCertificateAsync(CancellationToken cancellationToken);
23+
Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, string? nugetSource, bool force, CancellationToken cancellationToken);
24+
Task<int> NewProjectAsync(string templateName, string name, string outputPath, CancellationToken cancellationToken);
25+
Task<int> BuildAsync(FileInfo projectFilePath, CancellationToken cancellationToken);
26+
Task<int> AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, CancellationToken cancellationToken);
27+
Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, string? nugetSource, CancellationToken cancellationToken);
28+
}
29+
30+
internal sealed class DotNetCliRunner(ILogger<DotNetCliRunner> logger, IServiceProvider serviceProvider) : IDotNetCliRunner
1731
{
1832
private readonly ActivitySource _activitySource = new ActivitySource(nameof(DotNetCliRunner));
1933

@@ -478,7 +492,7 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas
478492
ex.RequiredCapability
479493
);
480494

481-
// If the app host is incompatable then there is no point
495+
// If the app host is incompatible then there is no point
482496
// trying to reconnect, we should propogate the exception
483497
// up to the code that needs to back channel so it can display
484498
// and error message to the user.

src/Aspire.Cli/NuGetPackageCache.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal interface INuGetPackageCache
1212
Task<IEnumerable<NuGetPackage>> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken);
1313
}
1414

15-
internal sealed class NuGetPackageCache(ILogger<NuGetPackageCache> logger, DotNetCliRunner cliRunner) : INuGetPackageCache
15+
internal sealed class NuGetPackageCache(ILogger<NuGetPackageCache> logger, IDotNetCliRunner cliRunner) : INuGetPackageCache
1616
{
1717
private readonly ActivitySource _activitySource = new(nameof(NuGetPackageCache));
1818

src/Aspire.Cli/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private static IHost BuildApplication(string[] args)
7272
}
7373

7474
// Shared services.
75-
builder.Services.AddTransient<DotNetCliRunner>();
75+
builder.Services.AddTransient<IDotNetCliRunner, DotNetCliRunner>();
7676
builder.Services.AddTransient<AppHostBackchannel>();
7777
builder.Services.AddSingleton<CliRpcTarget>();
7878
builder.Services.AddTransient<INuGetPackageCache, NuGetPackageCache>();

0 commit comments

Comments
 (0)