Skip to content

CommandAppTester throws System.IO.IOException : The handle is invalid. when testing an AnsiConsole.Live(table) call. #1733

Closed
@FrankRay78

Description

@FrankRay78

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:

image


Please upvote 👍 this issue if you are interested in it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-Testing-FrameworkSpectre.Console testing framework for developers.bugSomething isn't working

    Type

    No type

    Projects

    • Status

      Done 🚀

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions