Skip to content

Commit 7b5a5df

Browse files
Copilotmitchdenny
andcommitted
Fix for malformed table output in aspire run command
Co-authored-by: mitchdenny <[email protected]>
1 parent 5c448f3 commit 7b5a5df

File tree

3 files changed

+104
-3
lines changed

3 files changed

+104
-3
lines changed

src/Aspire.Cli/Commands/RunCommand.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,19 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
166166

167167
var table = new Table().Border(TableBorder.Rounded);
168168
var message = new Markup("Press [bold]Ctrl+C[/] to stop the app host and exit.");
169+
var noResourcesMessage = new Markup("No resources are present.");
169170

170-
var rows = new Rows(new List<IRenderable> {
171-
table,
171+
// Start with noResourcesMessage as the default
172+
var renderables = new List<IRenderable> {
173+
noResourcesMessage,
172174
message
173-
});
175+
};
176+
var rows = new Rows(renderables);
174177

175178
await _ansiConsole.Live(rows).StartAsync(async context =>
176179
{
177180
var knownResources = new SortedDictionary<string, RpcResourceState>();
181+
var hasResources = false;
178182

179183
table.AddColumn("Resource");
180184
table.AddColumn("Type");
@@ -188,6 +192,14 @@ await _ansiConsole.Live(rows).StartAsync(async context =>
188192
{
189193
await foreach (var resourceState in resourceStates)
190194
{
195+
if (!hasResources)
196+
{
197+
// First resource found, switch from message to table
198+
hasResources = true;
199+
renderables[0] = table;
200+
context.Refresh();
201+
}
202+
191203
knownResources[resourceState.Resource] = resourceState;
192204

193205
table.Rows.Clear();

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,60 @@ public async Task RunCommand_CompletesSuccessfully()
203203
var exitCode = await pendingRun.WaitAsync(CliTestConstants.DefaultTimeout);
204204
Assert.Equal(ExitCodeConstants.Success, exitCode);
205205
}
206+
207+
[Fact]
208+
public async Task RunCommand_WithNoResources_CompletesSuccessfully()
209+
{
210+
var getResourceStatesAsyncCalled = new TaskCompletionSource();
211+
var backchannelFactory = (IServiceProvider sp) => {
212+
var backchannel = new TestAppHostBackchannel();
213+
backchannel.GetResourceStatesAsyncCalled = getResourceStatesAsyncCalled;
214+
215+
// Return empty resources using an empty enumerable
216+
backchannel.GetResourceStatesAsyncCallback = _ => EmptyAsyncEnumerable<RpcResourceState>.Instance;
217+
218+
return backchannel;
219+
};
220+
221+
var runnerFactory = (IServiceProvider sp) => {
222+
var runner = new TestDotNetCliRunner();
223+
runner.CheckHttpCertificateAsyncCallback = (options, ct) => 0;
224+
runner.BuildAsyncCallback = (projectFile, options, ct) => 0;
225+
runner.GetAppHostInformationAsyncCallback = (projectFile, options, ct) => (0, true, VersionHelper.GetDefaultTemplateVersion());
226+
227+
runner.RunAsyncCallback = async (projectFile, watch, noBuild, args, env, backchannelCompletionSource, options, ct) =>
228+
{
229+
var backchannel = sp.GetRequiredService<IAppHostBackchannel>();
230+
backchannelCompletionSource!.SetResult(backchannel);
231+
await Task.Delay(Timeout.InfiniteTimeSpan, ct);
232+
return 0;
233+
};
234+
235+
return runner;
236+
};
237+
238+
var projectLocatorFactory = (IServiceProvider sp) => new TestProjectLocator();
239+
240+
var services = CliTestHelper.CreateServiceCollection(outputHelper, options =>
241+
{
242+
options.ProjectLocatorFactory = projectLocatorFactory;
243+
options.AppHostBackchannelFactory = backchannelFactory;
244+
options.DotNetCliRunnerFactory = runnerFactory;
245+
});
246+
247+
var provider = services.BuildServiceProvider();
248+
var command = provider.GetRequiredService<RootCommand>();
249+
var result = command.Parse("run");
250+
251+
using var cts = new CancellationTokenSource();
252+
var pendingRun = result.InvokeAsync(cts.Token);
253+
254+
await getResourceStatesAsyncCalled.Task.WaitAsync(CliTestConstants.DefaultTimeout);
255+
256+
// Simulate CTRL-C.
257+
cts.Cancel();
258+
259+
var exitCode = await pendingRun.WaitAsync(CliTestConstants.DefaultTimeout);
260+
Assert.Equal(ExitCodeConstants.Success, exitCode);
261+
}
206262
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Aspire.Cli.Tests.Utils;
5+
6+
/// <summary>
7+
/// A utility class that provides an empty IAsyncEnumerable&lt;T&gt;.
8+
/// </summary>
9+
/// <typeparam name="T">The type of the elements in the empty enumerable.</typeparam>
10+
internal sealed class EmptyAsyncEnumerable<T>
11+
{
12+
/// <summary>
13+
/// Gets a singleton instance of an empty <see cref="IAsyncEnumerable{T}"/>.
14+
/// </summary>
15+
public static readonly IAsyncEnumerable<T> Instance = new EmptyAsyncEnumerableImpl();
16+
17+
private sealed class EmptyAsyncEnumerableImpl : IAsyncEnumerable<T>
18+
{
19+
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
20+
{
21+
return new EmptyAsyncEnumerator();
22+
}
23+
24+
private sealed class EmptyAsyncEnumerator : IAsyncEnumerator<T>
25+
{
26+
public T Current => default!;
27+
28+
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
29+
30+
public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(Task.FromResult(false));
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)