Skip to content

Commit 09bcfce

Browse files
authored
aspire run test coverage (#8835)
1 parent 29d110e commit 09bcfce

13 files changed

+424
-54
lines changed

src/Aspire.Cli/Backchannel/AppHostBackchannel.cs

+14-5
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,22 @@
99

1010
namespace Aspire.Cli.Backchannel;
1111

12-
internal sealed class AppHostBackchannel(ILogger<AppHostBackchannel> logger, CliRpcTarget target)
12+
internal interface IAppHostBackchannel
13+
{
14+
Task<long> PingAsync(long timestamp, CancellationToken cancellationToken);
15+
Task RequestStopAsync(CancellationToken cancellationToken);
16+
Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync(CancellationToken cancellationToken);
17+
IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync(CancellationToken cancellationToken);
18+
Task ConnectAsync(string socketPath, CancellationToken cancellationToken);
19+
Task<string[]> GetPublishersAsync(CancellationToken cancellationToken);
20+
IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync(CancellationToken cancellationToken);
21+
Task<string[]> GetCapabilitiesAsync(CancellationToken cancellationToken);
22+
}
23+
24+
internal sealed class AppHostBackchannel(ILogger<AppHostBackchannel> logger, CliRpcTarget target) : IAppHostBackchannel
1325
{
1426
private readonly ActivitySource _activitySource = new(nameof(AppHostBackchannel));
1527
private readonly TaskCompletionSource<JsonRpc> _rpcTaskCompletionSource = new();
16-
private Process? _process;
1728

1829
public async Task<long> PingAsync(long timestamp, CancellationToken cancellationToken)
1930
{
@@ -86,14 +97,12 @@ await rpc.InvokeWithCancellationAsync(
8697
}
8798
}
8899

89-
public async Task ConnectAsync(Process process, string socketPath, CancellationToken cancellationToken)
100+
public async Task ConnectAsync(string socketPath, CancellationToken cancellationToken)
90101
{
91102
try
92103
{
93104
using var activity = _activitySource.StartActivity();
94105

95-
_process = process;
96-
97106
if (_rpcTaskCompletionSource.Task.IsCompleted)
98107
{
99108
throw new InvalidOperationException("Already connected to AppHost backchannel.");

src/Aspire.Cli/Commands/PublishCommand.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
9494
$"{nameof(ExecuteAsync)}-Action-GetPublishers",
9595
ActivityKind.Client);
9696

97-
var backchannelCompletionSource = new TaskCompletionSource<AppHostBackchannel>();
97+
var backchannelCompletionSource = new TaskCompletionSource<IAppHostBackchannel>();
9898
var pendingInspectRun = _runner.RunAsync(
9999
effectiveAppHostProjectFile,
100100
false,
@@ -156,7 +156,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
156156
$"{nameof(ExecuteAsync)}-Action-GenerateArtifacts",
157157
ActivityKind.Internal);
158158

159-
var backchannelCompletionSource = new TaskCompletionSource<AppHostBackchannel>();
159+
var backchannelCompletionSource = new TaskCompletionSource<IAppHostBackchannel>();
160160

161161
var launchingAppHostTask = context.AddTask(":play_button: Launching apphost");
162162
launchingAppHostTask.IsIndeterminate();

src/Aspire.Cli/Commands/RunCommand.cs

+15-6
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,22 @@ internal sealed class RunCommand : BaseCommand
2222
private readonly IInteractionService _interactionService;
2323
private readonly ICertificateService _certificateService;
2424
private readonly IProjectLocator _projectLocator;
25+
private readonly IAnsiConsole _ansiConsole;
2526

26-
public RunCommand(IDotNetCliRunner runner, IInteractionService interactionService, ICertificateService certificateService, IProjectLocator projectLocator)
27+
public RunCommand(IDotNetCliRunner runner, IInteractionService interactionService, ICertificateService certificateService, IProjectLocator projectLocator, IAnsiConsole ansiConsole)
2728
: base("run", "Run an Aspire app host in development mode.")
2829
{
2930
ArgumentNullException.ThrowIfNull(runner);
3031
ArgumentNullException.ThrowIfNull(interactionService);
3132
ArgumentNullException.ThrowIfNull(certificateService);
3233
ArgumentNullException.ThrowIfNull(projectLocator);
34+
ArgumentNullException.ThrowIfNull(ansiConsole);
3335

3436
_runner = runner;
3537
_interactionService = interactionService;
3638
_certificateService = certificateService;
3739
_projectLocator = projectLocator;
40+
_ansiConsole = ansiConsole;
3841

3942
var projectOption = new Option<FileInfo?>("--project");
4043
projectOption.Description = "The path to the Aspire app host project file.";
@@ -106,7 +109,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
106109
return ExitCodeConstants.FailedToDotnetRunAppHost;
107110
}
108111

109-
var backchannelCompletitionSource = new TaskCompletionSource<AppHostBackchannel>();
112+
var backchannelCompletitionSource = new TaskCompletionSource<IAppHostBackchannel>();
110113

111114
var pendingRun = _runner.RunAsync(
112115
effectiveAppHostProjectFile,
@@ -134,8 +137,8 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
134137

135138
var table = new Table().Border(TableBorder.Rounded);
136139

137-
await AnsiConsole.Live(table).StartAsync(async context => {
138-
140+
await _ansiConsole.Live(table).StartAsync(async context =>
141+
{
139142
var knownResources = new SortedDictionary<string, (string Resource, string Type, string State, string[] Endpoints)>();
140143

141144
table.AddColumn("Resource");
@@ -147,7 +150,7 @@ await AnsiConsole.Live(table).StartAsync(async context => {
147150

148151
try
149152
{
150-
await foreach(var resourceState in resourceStates)
153+
await foreach (var resourceState in resourceStates)
151154
{
152155
knownResources[resourceState.Resource] = resourceState;
153156

@@ -159,7 +162,8 @@ await AnsiConsole.Live(table).StartAsync(async context => {
159162

160163
var typeRenderable = new Text(knownResource.Value.Type, new Style().Foreground(Color.White));
161164

162-
var stateRenderable = knownResource.Value.State switch {
165+
var stateRenderable = knownResource.Value.State switch
166+
{
163167
"Running" => new Text(knownResource.Value.State, new Style().Foreground(Color.Green)),
164168
"Starting" => new Text(knownResource.Value.State, new Style().Foreground(Color.LightGreen)),
165169
"FailedToStart" => new Text(knownResource.Value.State, new Style().Foreground(Color.Red)),
@@ -208,6 +212,11 @@ await AnsiConsole.Live(table).StartAsync(async context => {
208212
return await pendingRun;
209213
}
210214
}
215+
catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken)
216+
{
217+
_interactionService.DisplayMessage("stop_sign", "The run command was cancelled by user.");
218+
return ExitCodeConstants.Success;
219+
}
211220
catch (ProjectLocatorException ex) when (ex.Message == "Project file does not exist.")
212221
{
213222
_interactionService.DisplayError("The --project option specified a project that does not exist.");

src/Aspire.Cli/DotNetCliRunner.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal interface IDotNetCliRunner
1717
{
1818
Task<(int ExitCode, bool IsAspireHost, string? AspireHostingSdkVersion)> GetAppHostInformationAsync(FileInfo projectFile, CancellationToken cancellationToken);
1919
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);
20+
Task<int> RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] args, IDictionary<string, string>? env, TaskCompletionSource<IAppHostBackchannel>? backchannelCompletionSource, CancellationToken cancellationToken);
2121
Task<int> CheckHttpCertificateAsync(CancellationToken cancellationToken);
2222
Task<int> TrustHttpCertificateAsync(CancellationToken cancellationToken);
2323
Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, string? nugetSource, bool force, CancellationToken cancellationToken);
@@ -134,7 +134,7 @@ internal sealed class DotNetCliRunner(ILogger<DotNetCliRunner> logger, IServiceP
134134
}
135135
}
136136

137-
public async Task<int> RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] args, IDictionary<string, string>? env, TaskCompletionSource<AppHostBackchannel>? backchannelCompletionSource, CancellationToken cancellationToken)
137+
public async Task<int> RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] args, IDictionary<string, string>? env, TaskCompletionSource<IAppHostBackchannel>? backchannelCompletionSource, CancellationToken cancellationToken)
138138
{
139139
using var activity = _activitySource.StartActivity();
140140

@@ -316,7 +316,7 @@ internal static string GetBackchannelSocketPath()
316316
return socketPath;
317317
}
318318

319-
public async Task<int> ExecuteAsync(string[] args, IDictionary<string, string>? env, DirectoryInfo workingDirectory, TaskCompletionSource<AppHostBackchannel>? backchannelCompletionSource, Action<StreamWriter, StreamReader, StreamReader>? streamsCallback, CancellationToken cancellationToken)
319+
public async Task<int> ExecuteAsync(string[] args, IDictionary<string, string>? env, DirectoryInfo workingDirectory, TaskCompletionSource<IAppHostBackchannel>? backchannelCompletionSource, Action<StreamWriter, StreamReader, StreamReader>? streamsCallback, CancellationToken cancellationToken)
320320
{
321321
using var activity = _activitySource.StartActivity();
322322

@@ -436,13 +436,13 @@ async Task ForwardStreamToLoggerAsync(StreamReader reader, string identifier, Pr
436436
}
437437
}
438438

439-
private async Task StartBackchannelAsync(Process process, string socketPath, TaskCompletionSource<AppHostBackchannel> backchannelCompletionSource, CancellationToken cancellationToken)
439+
private async Task StartBackchannelAsync(Process process, string socketPath, TaskCompletionSource<IAppHostBackchannel> backchannelCompletionSource, CancellationToken cancellationToken)
440440
{
441441
using var activity = _activitySource.StartActivity();
442442

443443
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50));
444444

445-
var backchannel = serviceProvider.GetRequiredService<AppHostBackchannel>();
445+
var backchannel = serviceProvider.GetRequiredService<IAppHostBackchannel>();
446446
var connectionAttempts = 0;
447447

448448
logger.LogDebug("Starting backchannel connection to AppHost at {SocketPath}", socketPath);
@@ -454,7 +454,7 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas
454454
try
455455
{
456456
logger.LogTrace("Attempting to connect to AppHost backchannel at {SocketPath} (attempt {Attempt})", socketPath, connectionAttempts++);
457-
await backchannel.ConnectAsync(process, socketPath, cancellationToken).ConfigureAwait(false);
457+
await backchannel.ConnectAsync(socketPath, cancellationToken).ConfigureAwait(false);
458458
backchannelCompletionSource.SetResult(backchannel);
459459
logger.LogDebug("Connected to AppHost backchannel at {SocketPath}", socketPath);
460460
return;

src/Aspire.Cli/Interaction/InteractionService.cs

-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ internal class InteractionService : IInteractionService
1111
{
1212
private readonly IAnsiConsole _ansiConsole;
1313

14-
public InteractionService() : this(AnsiConsole.Console)
15-
{
16-
}
17-
1814
public InteractionService(IAnsiConsole ansiConsole)
1915
{
2016
ArgumentNullException.ThrowIfNull(ansiConsole);

src/Aspire.Cli/Program.cs

+15-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Microsoft.Extensions.DependencyInjection;
1313
using Microsoft.Extensions.Hosting;
1414
using Microsoft.Extensions.Logging;
15+
using Spectre.Console;
1516

1617
#if DEBUG
1718
using OpenTelemetry;
@@ -75,13 +76,14 @@ private static IHost BuildApplication(string[] args)
7576
}
7677

7778
// Shared services.
79+
builder.Services.AddSingleton(BuildAnsiConsole);
7880
builder.Services.AddSingleton(BuildProjectLocator);
7981
builder.Services.AddSingleton<INewCommandPrompter, NewCommandPrompter>();
8082
builder.Services.AddSingleton<IAddCommandPrompter, AddCommandPrompter>();
8183
builder.Services.AddSingleton<IInteractionService, InteractionService>();
8284
builder.Services.AddSingleton<ICertificateService, CertificateService>();
8385
builder.Services.AddTransient<IDotNetCliRunner, DotNetCliRunner>();
84-
builder.Services.AddTransient<AppHostBackchannel>();
86+
builder.Services.AddTransient<IAppHostBackchannel, AppHostBackchannel>();
8587
builder.Services.AddSingleton<CliRpcTarget>();
8688
builder.Services.AddTransient<INuGetPackageCache, NuGetPackageCache>();
8789

@@ -96,6 +98,18 @@ private static IHost BuildApplication(string[] args)
9698
return app;
9799
}
98100

101+
private static IAnsiConsole BuildAnsiConsole(IServiceProvider serviceProvider)
102+
{
103+
AnsiConsoleSettings settings = new AnsiConsoleSettings()
104+
{
105+
Ansi = AnsiSupport.Detect,
106+
Interactive = InteractionSupport.Detect,
107+
ColorSystem = ColorSystemSupport.Detect
108+
};
109+
var ansiConsole = AnsiConsole.Create(settings);
110+
return ansiConsole;
111+
}
112+
99113
private static IProjectLocator BuildProjectLocator(IServiceProvider serviceProvider)
100114
{
101115
var logger = serviceProvider.GetRequiredService<ILogger<ProjectLocator>>();

src/Aspire.Cli/Utils/InteractionService.cs

Whitespace-only changes.

tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs

+4-19
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using Aspire.Cli.Commands;
55
using Aspire.Cli.Interaction;
6-
using Aspire.Cli.Projects;
76
using Aspire.Cli.Tests.TestServices;
87
using Aspire.Cli.Tests.Utils;
98
using Microsoft.Extensions.DependencyInjection;
@@ -37,7 +36,7 @@ public async Task AddCommandInteractiveFlowSmokeTest()
3736
return new TestAddCommandPrompter(interactionService);
3837
};
3938

40-
options.ProjectLocatorFactory = _ => new FakeProjectLocator();
39+
options.ProjectLocatorFactory = _ => new TestProjectLocator();
4140

4241
options.DotNetCliRunnerFactory = (sp) =>
4342
{
@@ -110,7 +109,7 @@ public async Task AddCommandDoesNotPromptForIntegrationArgumentIfSpecifiedOnComm
110109
return prompter;
111110
};
112111

113-
options.ProjectLocatorFactory = _ => new FakeProjectLocator();
112+
options.ProjectLocatorFactory = _ => new TestProjectLocator();
114113

115114
options.DotNetCliRunnerFactory = (sp) =>
116115
{
@@ -191,7 +190,7 @@ public async Task AddCommandDoesNotPromptForVersionIfSpecifiedOnCommandLine()
191190
return prompter;
192191
};
193192

194-
options.ProjectLocatorFactory = _ => new FakeProjectLocator();
193+
options.ProjectLocatorFactory = _ => new TestProjectLocator();
195194

196195
options.DotNetCliRunnerFactory = (sp) =>
197196
{
@@ -268,7 +267,7 @@ public async Task AddCommandPromptsForDisambiguation()
268267
return prompter;
269268
};
270269

271-
options.ProjectLocatorFactory = _ => new FakeProjectLocator();
270+
options.ProjectLocatorFactory = _ => new TestProjectLocator();
272271

273272
options.DotNetCliRunnerFactory = (sp) =>
274273
{
@@ -330,20 +329,6 @@ public async Task AddCommandPromptsForDisambiguation()
330329

331330
}
332331

333-
internal sealed class FakeProjectLocator : IProjectLocator
334-
{
335-
public FileInfo? UseOrFindAppHostProjectFile(FileInfo? projectFile)
336-
{
337-
if (projectFile != null)
338-
{
339-
return projectFile;
340-
}
341-
342-
var fakeProjectFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "AppHost.csproj");
343-
return new FileInfo(fakeProjectFilePath);
344-
}
345-
}
346-
347332
internal sealed class TestAddCommandPrompter(IInteractionService interactionService) : AddCommandPrompter(interactionService)
348333
{
349334
public Func<IEnumerable<(string FriendlyName, NuGetPackage Package)>, (string FriendlyName, NuGetPackage Package)>? PromptForIntegrationCallback { get; set; }

0 commit comments

Comments
 (0)