Expose raw streams in IConsole to allow writing/reading binary data

This commit is contained in:
Alexey Golub
2020-03-11 23:23:01 +02:00
parent f4f6d04857
commit 79e1a2e3d7
8 changed files with 128 additions and 140 deletions

View File

@@ -1,6 +1,5 @@
using NUnit.Framework;
using System;
using System.IO;
using CliFx.Tests.TestCommands;
namespace CliFx.Tests
@@ -27,7 +26,7 @@ namespace CliFx.Tests
.UseExecutableName("test")
.UseVersionText("test")
.UseDescription("test")
.UseConsole(new VirtualConsole(TextWriter.Null))
.UseConsole(new VirtualConsole())
.UseTypeActivator(Activator.CreateInstance)
.Build();
}

View File

@@ -320,8 +320,7 @@ namespace CliFx.Tests
string? expectedStdOut = null)
{
// Arrange
await using var stdOutStream = new StringWriter();
var console = new VirtualConsole(stdOutStream);
using var console = new VirtualConsole();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -333,7 +332,7 @@ namespace CliFx.Tests
// Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim();
var stdOut = console.ReadOutputString().Trim();
// Assert
exitCode.Should().Be(0);
@@ -354,8 +353,7 @@ namespace CliFx.Tests
int? expectedExitCode = null)
{
// Arrange
await using var stdErrStream = new StringWriter();
var console = new VirtualConsole(TextWriter.Null, stdErrStream);
using var console = new VirtualConsole();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -367,19 +365,19 @@ namespace CliFx.Tests
// Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stderr = stdErrStream.ToString().Trim();
var stdErr = console.ReadErrorString().Trim();
// Assert
exitCode.Should().NotBe(0);
stderr.Should().NotBeNullOrWhiteSpace();
stdErr.Should().NotBeNullOrWhiteSpace();
if (expectedExitCode != null)
exitCode.Should().Be(expectedExitCode);
if (expectedStdErr != null)
stderr.Should().Be(expectedStdErr);
stdErr.Should().Be(expectedStdErr);
Console.WriteLine(stderr);
Console.WriteLine(stdErr);
}
[TestCaseSource(nameof(GetTestCases_RunAsync_Help))]
@@ -389,8 +387,7 @@ namespace CliFx.Tests
IReadOnlyList<string>? expectedSubstrings = null)
{
// Arrange
await using var stdOutStream = new StringWriter();
var console = new VirtualConsole(stdOutStream);
using var console = new VirtualConsole();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -404,7 +401,7 @@ namespace CliFx.Tests
// Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim();
var stdOut = console.ReadOutputString().Trim();
// Assert
exitCode.Should().Be(0);
@@ -420,11 +417,7 @@ namespace CliFx.Tests
public async Task RunAsync_Cancellation_Test()
{
// Arrange
using var cancellationTokenSource = new CancellationTokenSource();
await using var stdOutStream = new StringWriter();
await using var stdErrStream = new StringWriter();
var console = new VirtualConsole(stdOutStream, stdErrStream, cancellationTokenSource.Token);
using var console = new VirtualConsole();
var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand))
@@ -435,10 +428,11 @@ namespace CliFx.Tests
var environmentVariables = new Dictionary<string, string>();
// Act
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(0.2));
console.CancelAfter(TimeSpan.FromSeconds(0.2));
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim();
var stdErr = stdErrStream.ToString().Trim();
var stdOut = console.ReadOutputString().Trim();
var stdErr = console.ReadErrorString().Trim();
// Assert
exitCode.Should().NotBe(0);

View File

@@ -1,39 +0,0 @@
using System;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class SystemConsoleTests
{
[TearDown]
public void TearDown()
{
// Reset console color so it doesn't carry on into the next tests
Console.ResetColor();
}
[Test(Description = "Must be in sync with system console")]
public void Smoke_Test()
{
// Arrange
var console = new SystemConsole();
// Act
console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
// Assert
console.Input.Should().BeSameAs(Console.In);
console.IsInputRedirected.Should().Be(Console.IsInputRedirected);
console.Output.Should().BeSameAs(Console.Out);
console.IsOutputRedirected.Should().Be(Console.IsOutputRedirected);
console.Error.Should().BeSameAs(Console.Error);
console.IsErrorRedirected.Should().Be(Console.IsErrorRedirected);
console.ForegroundColor.Should().Be(Console.ForegroundColor);
console.BackgroundColor.Should().Be(Console.BackgroundColor);
}
}
}

View File

@@ -1,6 +1,4 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq;
using CliFx.Utilities;
using FluentAssertions;
using NUnit.Framework;
@@ -14,31 +12,25 @@ namespace CliFx.Tests.Utilities
public void Report_Test()
{
// Arrange
var formatProvider = CultureInfo.InvariantCulture;
using var stdout = new StringWriter(formatProvider);
var console = new VirtualConsole(TextReader.Null, false, stdout, false, TextWriter.Null, false);
using var console = new VirtualConsole(false);
var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
var progressStringValues = progressValues.Select(p => p.ToString("P2", formatProvider)).ToArray();
var progressStringValues = progressValues.Select(p => p.ToString("P2")).ToArray();
// Act
foreach (var progress in progressValues)
ticker.Report(progress);
// Assert
stdout.ToString().Should().ContainAll(progressStringValues);
console.ReadOutputString().Should().ContainAll(progressStringValues);
}
[Test]
public void Report_Redirected_Test()
{
// Arrange
using var stdout = new StringWriter();
var console = new VirtualConsole(stdout);
using var console = new VirtualConsole();
var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
@@ -48,7 +40,7 @@ namespace CliFx.Tests.Utilities
ticker.Report(progress);
// Assert
stdout.ToString().Should().BeEmpty();
console.ReadOutputString().Should().BeEmpty();
}
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.IO;
using FluentAssertions;
using NUnit.Framework;
@@ -12,11 +11,8 @@ namespace CliFx.Tests
public void Smoke_Test()
{
// Arrange
using var stdin = new StringReader("hello world");
using var stdout = new StringWriter();
using var stderr = new StringWriter();
var console = new VirtualConsole(stdin, stdout, stderr);
using var console = new VirtualConsole();
console.WriteInputString("hello world");
// Act
console.ResetColor();
@@ -24,13 +20,10 @@ namespace CliFx.Tests
console.BackgroundColor = ConsoleColor.DarkMagenta;
// Assert
console.Input.Should().BeSameAs(stdin);
console.Input.Should().NotBeSameAs(Console.In);
console.IsInputRedirected.Should().BeTrue();
console.Output.Should().BeSameAs(stdout);
console.Output.Should().NotBeSameAs(Console.Out);
console.IsOutputRedirected.Should().BeTrue();
console.Error.Should().BeSameAs(stderr);
console.Error.Should().NotBeSameAs(Console.Error);
console.IsErrorRedirected.Should().BeTrue();
console.ForegroundColor.Should().NotBe(Console.ForegroundColor);

View File

@@ -12,7 +12,7 @@ namespace CliFx
/// <summary>
/// Input stream (stdin).
/// </summary>
TextReader Input { get; }
StreamReader Input { get; }
/// <summary>
/// Whether the input stream is redirected.
@@ -22,7 +22,7 @@ namespace CliFx
/// <summary>
/// Output stream (stdout).
/// </summary>
TextWriter Output { get; }
StreamWriter Output { get; }
/// <summary>
/// Whether the output stream is redirected.
@@ -32,7 +32,7 @@ namespace CliFx
/// <summary>
/// Error stream (stderr).
/// </summary>
TextWriter Error { get; }
StreamWriter Error { get; }
/// <summary>
/// Whether the error stream is redirected.

View File

@@ -12,19 +12,19 @@ namespace CliFx
private CancellationTokenSource? _cancellationTokenSource;
/// <inheritdoc />
public TextReader Input => Console.In;
public StreamReader Input { get; }
/// <inheritdoc />
public bool IsInputRedirected => Console.IsInputRedirected;
/// <inheritdoc />
public TextWriter Output => Console.Out;
public StreamWriter Output { get; }
/// <inheritdoc />
public bool IsOutputRedirected => Console.IsOutputRedirected;
/// <inheritdoc />
public TextWriter Error => Console.Error;
public StreamWriter Error { get; }
/// <inheritdoc />
public bool IsErrorRedirected => Console.IsErrorRedirected;
@@ -43,6 +43,16 @@ namespace CliFx
set => Console.BackgroundColor = value;
}
/// <summary>
/// Initializes an instance of <see cref="SystemConsole"/>.
/// </summary>
public SystemConsole()
{
Input = new StreamReader(Console.OpenStandardInput(), Console.InputEncoding, false);
Output = new StreamWriter(Console.OpenStandardOutput(), Console.OutputEncoding) {AutoFlush = true};
Error = new StreamWriter(Console.OpenStandardError(), Console.OutputEncoding) {AutoFlush = true};
}
/// <inheritdoc />
public void ResetColor() => Console.ResetColor();

View File

@@ -9,24 +9,92 @@ namespace CliFx
/// Does not leak to system console in any way.
/// Use this class as a substitute for system console when running tests.
/// </summary>
public class VirtualConsole : IConsole
public partial class VirtualConsole
{
private readonly CancellationToken _cancellationToken;
private readonly MemoryStream _inputStream = new MemoryStream();
private readonly MemoryStream _outputStream = new MemoryStream();
private readonly MemoryStream _errorStream = new MemoryStream();
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>.
/// </summary>
public VirtualConsole(bool isRedirected)
{
Input = new StreamReader(_inputStream, Console.InputEncoding, false);
Output = new StreamWriter(_outputStream, Console.OutputEncoding) {AutoFlush = true};
Error = new StreamWriter(_errorStream, Console.OutputEncoding) {AutoFlush = true};
IsInputRedirected = isRedirected;
IsOutputRedirected = isRedirected;
IsErrorRedirected = isRedirected;
}
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>.
/// </summary>
public VirtualConsole()
: this(true)
{
}
/// <summary>
/// Writes raw data to input stream.
/// </summary>
public void WriteInputData(byte[] data) => _inputStream.Write(data, 0, data.Length);
/// <summary>
/// Writes text to input stream.
/// </summary>
public void WriteInputString(string str) => WriteInputData(Input.CurrentEncoding.GetBytes(str));
/// <summary>
/// Reads all data written to output stream thus far.
/// </summary>
public byte[] ReadOutputData() => _outputStream.ToArray();
/// <summary>
/// Reads all text written to output stream thus far.
/// </summary>
public string ReadOutputString() => Output.Encoding.GetString(ReadOutputData());
/// <summary>
/// Reads all data written to error stream thus far.
/// </summary>
public byte[] ReadErrorData() => _errorStream.ToArray();
/// <summary>
/// Reads all text written to error stream thus far.
/// </summary>
public string ReadErrorString() => Error.Encoding.GetString(ReadErrorData());
/// <summary>
/// Sends an interrupt signal.
/// </summary>
public void Cancel() => _cts.Cancel();
/// <summary>
/// Sends an interrupt signal after a delay.
/// </summary>
public void CancelAfter(TimeSpan delay) => _cts.CancelAfter(delay);
}
public partial class VirtualConsole : IConsole
{
/// <inheritdoc />
public TextReader Input { get; }
public StreamReader Input { get; }
/// <inheritdoc />
public bool IsInputRedirected { get; }
/// <inheritdoc />
public TextWriter Output { get; }
public StreamWriter Output { get; }
/// <inheritdoc />
public bool IsOutputRedirected { get; }
/// <inheritdoc />
public TextWriter Error { get; }
public StreamWriter Error { get; }
/// <inheritdoc />
public bool IsErrorRedirected { get; }
@@ -37,50 +105,6 @@ namespace CliFx
/// <inheritdoc />
public ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black;
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>.
/// </summary>
public VirtualConsole(TextReader input, bool isInputRedirected,
TextWriter output, bool isOutputRedirected,
TextWriter error, bool isErrorRedirected,
CancellationToken cancellationToken = default)
{
Input = input;
IsInputRedirected = isInputRedirected;
Output = output;
IsOutputRedirected = isOutputRedirected;
Error = error;
IsErrorRedirected = isErrorRedirected;
_cancellationToken = cancellationToken;
}
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>.
/// </summary>
public VirtualConsole(TextReader input, TextWriter output, TextWriter error,
CancellationToken cancellationToken = default)
: this(input, true, output, true, error, true, cancellationToken)
{
}
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/> using output stream (stdout) and error stream (stderr).
/// Input stream (stdin) is replaced with a no-op stub.
/// </summary>
public VirtualConsole(TextWriter output, TextWriter error, CancellationToken cancellationToken = default)
: this(TextReader.Null, output, error, cancellationToken)
{
}
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/> using output stream (stdout).
/// Input stream (stdin) and error stream (stderr) are replaced with no-op stubs.
/// </summary>
public VirtualConsole(TextWriter output, CancellationToken cancellationToken = default)
: this(output, TextWriter.Null, cancellationToken)
{
}
/// <inheritdoc />
public void ResetColor()
{
@@ -89,6 +113,21 @@ namespace CliFx
}
/// <inheritdoc />
public CancellationToken GetCancellationToken() => _cancellationToken;
public CancellationToken GetCancellationToken() => _cts.Token;
}
public partial class VirtualConsole : IDisposable
{
/// <inheritdoc />
public void Dispose()
{
_inputStream.Dispose();
_outputStream.Dispose();
_errorStream.Dispose();
_cts.Dispose();
Input.Dispose();
Output.Dispose();
Error.Dispose();
}
}
}