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 NUnit.Framework;
using System; using System;
using System.IO;
using CliFx.Tests.TestCommands; using CliFx.Tests.TestCommands;
namespace CliFx.Tests namespace CliFx.Tests
@@ -27,7 +26,7 @@ namespace CliFx.Tests
.UseExecutableName("test") .UseExecutableName("test")
.UseVersionText("test") .UseVersionText("test")
.UseDescription("test") .UseDescription("test")
.UseConsole(new VirtualConsole(TextWriter.Null)) .UseConsole(new VirtualConsole())
.UseTypeActivator(Activator.CreateInstance) .UseTypeActivator(Activator.CreateInstance)
.Build(); .Build();
} }

View File

@@ -320,8 +320,7 @@ namespace CliFx.Tests
string? expectedStdOut = null) string? expectedStdOut = null)
{ {
// Arrange // Arrange
await using var stdOutStream = new StringWriter(); using var console = new VirtualConsole();
var console = new VirtualConsole(stdOutStream);
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
@@ -333,7 +332,7 @@ namespace CliFx.Tests
// Act // Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim(); var stdOut = console.ReadOutputString().Trim();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
@@ -354,8 +353,7 @@ namespace CliFx.Tests
int? expectedExitCode = null) int? expectedExitCode = null)
{ {
// Arrange // Arrange
await using var stdErrStream = new StringWriter(); using var console = new VirtualConsole();
var console = new VirtualConsole(TextWriter.Null, stdErrStream);
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
@@ -367,19 +365,19 @@ namespace CliFx.Tests
// Act // Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stderr = stdErrStream.ToString().Trim(); var stdErr = console.ReadErrorString().Trim();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stderr.Should().NotBeNullOrWhiteSpace(); stdErr.Should().NotBeNullOrWhiteSpace();
if (expectedExitCode != null) if (expectedExitCode != null)
exitCode.Should().Be(expectedExitCode); exitCode.Should().Be(expectedExitCode);
if (expectedStdErr != null) if (expectedStdErr != null)
stderr.Should().Be(expectedStdErr); stdErr.Should().Be(expectedStdErr);
Console.WriteLine(stderr); Console.WriteLine(stdErr);
} }
[TestCaseSource(nameof(GetTestCases_RunAsync_Help))] [TestCaseSource(nameof(GetTestCases_RunAsync_Help))]
@@ -389,8 +387,7 @@ namespace CliFx.Tests
IReadOnlyList<string>? expectedSubstrings = null) IReadOnlyList<string>? expectedSubstrings = null)
{ {
// Arrange // Arrange
await using var stdOutStream = new StringWriter(); using var console = new VirtualConsole();
var console = new VirtualConsole(stdOutStream);
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
@@ -404,7 +401,7 @@ namespace CliFx.Tests
// Act // Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim(); var stdOut = console.ReadOutputString().Trim();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
@@ -420,11 +417,7 @@ namespace CliFx.Tests
public async Task RunAsync_Cancellation_Test() public async Task RunAsync_Cancellation_Test()
{ {
// Arrange // Arrange
using var cancellationTokenSource = new CancellationTokenSource(); using var console = new VirtualConsole();
await using var stdOutStream = new StringWriter();
await using var stdErrStream = new StringWriter();
var console = new VirtualConsole(stdOutStream, stdErrStream, cancellationTokenSource.Token);
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand)) .AddCommand(typeof(CancellableCommand))
@@ -435,10 +428,11 @@ namespace CliFx.Tests
var environmentVariables = new Dictionary<string, string>(); var environmentVariables = new Dictionary<string, string>();
// Act // Act
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(0.2)); console.CancelAfter(TimeSpan.FromSeconds(0.2));
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim(); var stdOut = console.ReadOutputString().Trim();
var stdErr = stdErrStream.ToString().Trim(); var stdErr = console.ReadErrorString().Trim();
// Assert // Assert
exitCode.Should().NotBe(0); 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.Linq;
using System.IO;
using System.Linq;
using CliFx.Utilities; using CliFx.Utilities;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
@@ -14,31 +12,25 @@ namespace CliFx.Tests.Utilities
public void Report_Test() public void Report_Test()
{ {
// Arrange // Arrange
var formatProvider = CultureInfo.InvariantCulture; using var console = new VirtualConsole(false);
using var stdout = new StringWriter(formatProvider);
var console = new VirtualConsole(TextReader.Null, false, stdout, false, TextWriter.Null, false);
var ticker = console.CreateProgressTicker(); var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray(); 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 // Act
foreach (var progress in progressValues) foreach (var progress in progressValues)
ticker.Report(progress); ticker.Report(progress);
// Assert // Assert
stdout.ToString().Should().ContainAll(progressStringValues); console.ReadOutputString().Should().ContainAll(progressStringValues);
} }
[Test] [Test]
public void Report_Redirected_Test() public void Report_Redirected_Test()
{ {
// Arrange // Arrange
using var stdout = new StringWriter(); using var console = new VirtualConsole();
var console = new VirtualConsole(stdout);
var ticker = console.CreateProgressTicker(); var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray(); var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
@@ -48,7 +40,7 @@ namespace CliFx.Tests.Utilities
ticker.Report(progress); ticker.Report(progress);
// Assert // Assert
stdout.ToString().Should().BeEmpty(); console.ReadOutputString().Should().BeEmpty();
} }
} }
} }

View File

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

View File

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

View File

@@ -12,19 +12,19 @@ namespace CliFx
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
/// <inheritdoc /> /// <inheritdoc />
public TextReader Input => Console.In; public StreamReader Input { get; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsInputRedirected => Console.IsInputRedirected; public bool IsInputRedirected => Console.IsInputRedirected;
/// <inheritdoc /> /// <inheritdoc />
public TextWriter Output => Console.Out; public StreamWriter Output { get; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsOutputRedirected => Console.IsOutputRedirected; public bool IsOutputRedirected => Console.IsOutputRedirected;
/// <inheritdoc /> /// <inheritdoc />
public TextWriter Error => Console.Error; public StreamWriter Error { get; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsErrorRedirected => Console.IsErrorRedirected; public bool IsErrorRedirected => Console.IsErrorRedirected;
@@ -43,6 +43,16 @@ namespace CliFx
set => Console.BackgroundColor = value; 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 /> /// <inheritdoc />
public void ResetColor() => Console.ResetColor(); public void ResetColor() => Console.ResetColor();

View File

@@ -9,24 +9,92 @@ namespace CliFx
/// Does not leak to system console in any way. /// Does not leak to system console in any way.
/// Use this class as a substitute for system console when running tests. /// Use this class as a substitute for system console when running tests.
/// </summary> /// </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 /> /// <inheritdoc />
public TextReader Input { get; } public StreamReader Input { get; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsInputRedirected { get; } public bool IsInputRedirected { get; }
/// <inheritdoc /> /// <inheritdoc />
public TextWriter Output { get; } public StreamWriter Output { get; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsOutputRedirected { get; } public bool IsOutputRedirected { get; }
/// <inheritdoc /> /// <inheritdoc />
public TextWriter Error { get; } public StreamWriter Error { get; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsErrorRedirected { get; } public bool IsErrorRedirected { get; }
@@ -37,50 +105,6 @@ namespace CliFx
/// <inheritdoc /> /// <inheritdoc />
public ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black; 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 /> /// <inheritdoc />
public void ResetColor() public void ResetColor()
{ {
@@ -89,6 +113,21 @@ namespace CliFx
} }
/// <inheritdoc /> /// <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();
}
} }
} }