Description
Information
- OS: Windows 10
- Version: Spectre.Console 0.49.1, XUnit 2.9.3
- Terminal: Windows Terminal
Describe the bug
CommandAppTester throws System.IO.IOException : The handle is invalid.
when testing an AnsiConsole.Live(table) call.
To Reproduce
Run the below, self-contained, unit test:
using Spectre.Console;
using Spectre.Console.Cli;
using Spectre.Console.Testing;
using Xunit;
public class HelloCommand : Command
{
public class Server
{
public string? Name { get; set; }
public string? Sponsor { get; set; }
public string? Url { get; set; }
}
public override int Execute(CommandContext context)
{
var servers = new Server[]
{
new Server { Name = "Test Server 1", Sponsor = "Test Sponsor 1", Url = "http://test1.com" },
new Server { Name = "Test Server 2", Sponsor = "Test Sponsor 2", Url = "http://test2.com" },
new Server { Name = "Test Server 3", Sponsor = "Test Sponsor 3", Url = "http://test3.com" },
};
var table = new Table()
.Border(TableBorder.Square)
.BorderColor(Color.Red)
.AddColumn(new TableColumn("Country"))
.AddColumn(new TableColumn("Sponsor"))
.AddColumn(new TableColumn("Latency"));
// Add the initial server list (without latency)
foreach (var server in servers)
{
table.AddRow(server.Name ?? string.Empty, server.Sponsor ?? string.Empty);
}
AnsiConsole.Live(table)
.AutoClear(false)
.Start(async ctx =>
{
// Fetch the latency for each server
// and update the table as they come back
for (int i = 0; i < servers.Count(); i++)
{
var server = servers[i];
var latency = 100; //hardcoded latency for test purposes
table.UpdateCell(i, 2, $"{latency}ms");
ctx.Refresh();
}
});
return 0;
}
}
public class Tests
{
[Fact]
public void Should_Display_Speed_Test_Servers_With_Latency()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<HelloCommand>();
app.Configure(config => config.PropagateExceptions());
// When
var result = app.Run();
// Then
Assert.Equal(0, result.ExitCode);
}
}
Expected behavior
The CommandAppTester, which instantiates its own TestConsole (which in turn instantiates a NoopCursor), handles the call gracefully, likely recording the successive calls in a sequential manner, so the full redraw frames can be unit tested.
Actual behavior
A Live call to the console to set the cursor position is causing the throw, because no actual Console is present (ie. executing in non-interactive/batch mode).
Message:
System.IO.IOException : The handle is invalid.
Stack Trace:
ConsolePal.set_CursorVisible(Boolean value)
LegacyConsoleCursor.Show(Boolean show) line 7
CursorExtensions.Hide(IAnsiConsoleCursor cursor) line 33
LiveDisplayRenderer.Started() line 16
<<StartAsync>b__0>d.MoveNext() line 104
--- End of stack trace from previous location ---
DefaultExclusivityMode.RunAsync[T](Func`1 func) line 40
LiveDisplay.StartAsync[T](Func`2 func) line 98
LiveDisplay.Start[T](Func`2 func) line 63
HelloCommand.Execute(CommandContext context) line 39
ICommand.Execute(CommandContext context, CommandSettings settings) line 25
CommandExecutor.Execute(CommandTree leaf, CommandTree tree, CommandContext context, ITypeResolver resolver, IConfiguration configuration) line 166
CommandExecutor.Execute(IConfiguration configuration, IEnumerable`1 args) line 100
CommandApp.RunAsync(IEnumerable`1 args) line 84
CommandApp.Run(IEnumerable`1 args) line 58
CommandAppTester.Run(String[] args, TestConsole console, Action`1 config) line 136
CommandAppTester.Run(String[] args) line 108
Tests.Should_Display_Speed_Test_Servers_With_Latency() line 73
RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Additional context
Closely related to: #1723 and #1393 (comment)
The exception is happening because LiveDisplay calls Console directly on AnsiConsole, see:
/// <summary>
/// A console capable of writing ANSI escape sequences.
/// </summary>
public static partial class AnsiConsole
{
/// <summary>
/// Creates a new <see cref="LiveDisplay"/> instance.
/// </summary>
/// <param name="target">The target renderable to update.</param>
/// <returns>A <see cref="LiveDisplay"/> instance.</returns>
public static LiveDisplay Live(IRenderable target)
{
return Console.Live(target);
}
And Console is being instantiated here:
/// <summary>
/// A console capable of writing ANSI escape sequences.
/// </summary>
public static partial class AnsiConsole
{
//...
private static Lazy<IAnsiConsole> _console = new Lazy<IAnsiConsole>(
() =>
{
var console = Create(new AnsiConsoleSettings
{
Ansi = AnsiSupport.Detect,
ColorSystem = ColorSystemSupport.Detect,
Out = new AnsiConsoleOutput(System.Console.Out),
});
Created = true;
return console;
});
So the console being used by Live(table) is an AnsiConsoleFacade, not the TestConsole the CommandAppTester has explicitly instantiated:
Please upvote 👍 this issue if you are interested in it.
Metadata
Metadata
Assignees
Type
Projects
Status
Done 🚀