Skip to content

Attempt to write to closed TextWriter on exception in test execution #65

@busy-work-only

Description

@busy-work-only

Information

  • OS: Linux
  • Version: 0.50.0
  • Terminal: VS Code

Describe the bug
When uncaught exception is thrown during command execution while executing test, CommandAppTester throws System.ObjectDisposedException : Cannot write to a closed TextWriter..

To Reproduce

using System.Reflection;

using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;
using Spectre.Console.Cli;
using Spectre.Console.Testing;
using Xunit;

namespace GitHub.Issues;

/// <summary>
/// A Spectre.Console Command
/// </summary>
public class HelloWorldCommand(IAnsiConsole console) : Command
{
    private readonly IAnsiConsole _console = console;

    public override int Execute(CommandContext context)
    {
        throw new NotImplementedException(); // intentionally throw to reproduce easily
    }
}

/// <summary>
/// A Spectre.Console AsyncCommand
/// </summary>
public class AsyncHelloWorldCommand(IAnsiConsole console) : AsyncCommand
{
    private readonly IAnsiConsole _console = console;

    public override Task<int> ExecuteAsync(CommandContext context)
    {
        throw new NotImplementedException(); // intentionally throw to reproduce easily
    }
}

public class TestConsoleBugTests
{
    [Fact]
    public void Should_Not_Throw_ObjectDisposed_Exception_When_TypeRegistrar_Is_Used()
    {
        // Given
        var app = new CommandAppTester(WithServices());
        app.SetDefaultCommand<HelloWorldCommand>();

        // When
        var result = app.RunAndCatch<Exception>();

        // Then
        result.Exception.Should().NotBeOfType<ObjectDisposedException>();
    }

    [Fact]
    public void Should_Not_Throw_ObjectDisposed_Exception_When_Custom_TypeRegistrar_Is_Used()
    {
        // Given
        var app = new CommandAppTester(WithServices());
        app.SetDefaultCommand<HelloWorldCommand>();

        // When
        var result = app.Run();

        // Then
        result.ExitCode.Should().NotBe(0);
        result.Output.Should().Be("Error: The method or operation is not implemented.");
    }

    [Fact]
    public void Should_Not_Throw_ObjectDisposed_Exception_When_TypeRegistrar_Is_Not_Used()
    {
        // Given
        var app = new CommandAppTester();
        app.SetDefaultCommand<HelloWorldCommand>();

        // When
        var result = app.Run();

        // Then
        result.ExitCode.Should().NotBe(0);
        result.Output.Should().Be("Error: The method or operation is not implemented.");
    }

    [Fact]
    public async Task Should_Not_Throw_ObjectDisposed_Exception_When_TypeRegistrar_Is_Used_Async()
    {
        // Given
        var app = new CommandAppTester(WithServices());
        app.SetDefaultCommand<AsyncHelloWorldCommand>();

        // When
        var result = await app.RunAsync();

        // Then
        result.ExitCode.Should().NotBe(0);
        result.Output.Should().Be("Error: The method or operation is not implemented.");
    }

    private ITypeRegistrar WithServices()
    {
        var services = new ServiceCollection();

        // Create a type registrar and register any dependencies.
        // A type registrar is an adapter for a DI framework.
        return new MyTypeRegistrar(services);
    }
}

public sealed class MyTypeResolver : ITypeResolver, IDisposable
{
    private readonly IServiceProvider _provider;

    public MyTypeResolver(IServiceProvider provider)
    {
        _provider = provider ?? throw new ArgumentNullException(nameof(provider));
    }

    public object Resolve(Type type)
    {
        if (type == null)
        {
            return null;
        }

        return _provider.GetService(type);
    }

    public void Dispose()
    {
        if (_provider is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

public sealed class MyTypeRegistrar : ITypeRegistrar
{
    private readonly IServiceCollection _builder;

    public MyTypeRegistrar(IServiceCollection builder)
    {
        _builder = builder;
    }

    public ITypeResolver Build()
    {
        return new MyTypeResolver(_builder.BuildServiceProvider());
    }

    public void Register(Type service, Type implementation)
    {
        _builder.AddSingleton(service, implementation);
    }

    public void RegisterInstance(Type service, object implementation)
    {
        _builder.AddSingleton(service, implementation);
    }

    public void RegisterLazy(Type service, Func<object> func)
    {
        if (func is null)
        {
            throw new ArgumentNullException(nameof(func));
        }

        _builder.AddSingleton(service, (provider) => func());
    }
}

Expected behavior
I expect new CommandAppTester().Run() and new CommandAppTester(myTypeRegistrar).Run() to behave the same way. A non-zero exit code should be returned and message printed to console.

Screenshots
Behavior can be observed by running the above tests.

Additional context
None.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions