This commit is contained in:
Tyrrrz
2024-01-23 23:30:27 +02:00
parent 6aa72e45e8
commit 4bdd3ccc6c
8 changed files with 91 additions and 98 deletions

View File

@@ -8,6 +8,16 @@ namespace CliFx.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class CommandAttribute : Attribute public sealed class CommandAttribute : Attribute
{ {
/// <summary>
/// Initializes an instance of <see cref="CommandAttribute" />.
/// </summary>
public CommandAttribute(string name) => Name = name;
/// <summary>
/// Initializes an instance of <see cref="CommandAttribute" />.
/// </summary>
public CommandAttribute() { }
/// <summary> /// <summary>
/// Command name. /// Command name.
/// </summary> /// </summary>
@@ -23,17 +33,4 @@ public sealed class CommandAttribute : Attribute
/// This is shown to the user in the help text. /// This is shown to the user in the help text.
/// </summary> /// </summary>
public string? Description { get; set; } public string? Description { get; set; }
/// <summary>
/// Initializes an instance of <see cref="CommandAttribute" />.
/// </summary>
public CommandAttribute(string name)
{
Name = name;
}
/// <summary>
/// Initializes an instance of <see cref="CommandAttribute" />.
/// </summary>
public CommandAttribute() { }
} }

View File

@@ -9,6 +9,33 @@ namespace CliFx.Attributes;
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public sealed class CommandOptionAttribute : Attribute public sealed class CommandOptionAttribute : Attribute
{ {
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
/// </summary>
private CommandOptionAttribute(string? name, char? shortName)
{
Name = name;
ShortName = shortName;
}
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
/// </summary>
public CommandOptionAttribute(string name, char shortName)
: this(name, (char?)shortName) { }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
/// </summary>
public CommandOptionAttribute(string name)
: this(name, null) { }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
/// </summary>
public CommandOptionAttribute(char shortName)
: this(null, (char?)shortName) { }
/// <summary> /// <summary>
/// Option name. /// Option name.
/// </summary> /// </summary>
@@ -67,31 +94,4 @@ public sealed class CommandOptionAttribute : Attribute
/// Validators must derive from <see cref="BindingValidator{T}" />. /// Validators must derive from <see cref="BindingValidator{T}" />.
/// </remarks> /// </remarks>
public Type[] Validators { get; set; } = Array.Empty<Type>(); public Type[] Validators { get; set; } = Array.Empty<Type>();
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
/// </summary>
private CommandOptionAttribute(string? name, char? shortName)
{
Name = name;
ShortName = shortName;
}
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
/// </summary>
public CommandOptionAttribute(string name, char shortName)
: this(name, (char?)shortName) { }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
/// </summary>
public CommandOptionAttribute(string name)
: this(name, null) { }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
/// </summary>
public CommandOptionAttribute(char shortName)
: this(null, (char?)shortName) { }
} }

View File

@@ -11,8 +11,8 @@ namespace CliFx.Infrastructure;
/// </summary> /// </summary>
// Both the underlying stream AND the stream reader must be synchronized! // Both the underlying stream AND the stream reader must be synchronized!
// https://github.com/Tyrrrz/CliFx/issues/123 // https://github.com/Tyrrrz/CliFx/issues/123
public partial class ConsoleReader(IConsole console, Stream stream, Encoding encoding) public class ConsoleReader(IConsole console, Stream stream, Encoding encoding)
: StreamReader(stream, encoding, false, 4096) : StreamReader(Stream.Synchronized(stream), encoding, false, 4096)
{ {
/// <summary> /// <summary>
/// Initializes an instance of <see cref="ConsoleReader" />. /// Initializes an instance of <see cref="ConsoleReader" />.
@@ -86,9 +86,3 @@ public partial class ConsoleReader(IConsole console, Stream stream, Encoding enc
[ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)] [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
protected override void Dispose(bool disposing) => base.Dispose(disposing); protected override void Dispose(bool disposing) => base.Dispose(disposing);
} }
public partial class ConsoleReader
{
internal static ConsoleReader Create(IConsole console, Stream stream) =>
new(console, Stream.Synchronized(stream));
}

View File

@@ -12,9 +12,18 @@ namespace CliFx.Infrastructure;
/// </summary> /// </summary>
// Both the underlying stream AND the stream writer must be synchronized! // Both the underlying stream AND the stream writer must be synchronized!
// https://github.com/Tyrrrz/CliFx/issues/123 // https://github.com/Tyrrrz/CliFx/issues/123
public partial class ConsoleWriter(IConsole console, Stream stream, Encoding encoding) public class ConsoleWriter : StreamWriter
: StreamWriter(stream, encoding.WithoutPreamble(), 256)
{ {
/// <summary>
/// Initializes an instance of <see cref="ConsoleWriter" />.
/// </summary>
public ConsoleWriter(IConsole console, Stream stream, Encoding encoding)
: base(Stream.Synchronized(stream), encoding.WithoutPreamble(), 256)
{
Console = console;
AutoFlush = true;
}
/// <summary> /// <summary>
/// Initializes an instance of <see cref="ConsoleWriter" />. /// Initializes an instance of <see cref="ConsoleWriter" />.
/// </summary> /// </summary>
@@ -24,7 +33,7 @@ public partial class ConsoleWriter(IConsole console, Stream stream, Encoding enc
/// <summary> /// <summary>
/// Console that owns this stream. /// Console that owns this stream.
/// </summary> /// </summary>
public IConsole Console { get; } = console; public IConsole Console { get; }
// The following overrides are required to establish thread-safe behavior // The following overrides are required to establish thread-safe behavior
// in methods deriving from StreamWriter. // in methods deriving from StreamWriter.
@@ -260,9 +269,3 @@ public partial class ConsoleWriter(IConsole console, Stream stream, Encoding enc
[ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)] [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
protected override void Dispose(bool disposing) => base.Dispose(disposing); protected override void Dispose(bool disposing) => base.Dispose(disposing);
} }
public partial class ConsoleWriter
{
internal static ConsoleWriter Create(IConsole console, Stream stream) =>
new(console, Stream.Synchronized(stream)) { AutoFlush = true };
}

View File

@@ -6,15 +6,24 @@ using System.Threading;
namespace CliFx.Infrastructure; namespace CliFx.Infrastructure;
/// <summary> /// <summary>
/// Implementation of <see cref="IConsole" /> that uses the provided fake /// Implementation of <see cref="IConsole" /> that uses the provided fake standard input, output, and error streams.
/// standard input, output, and error streams. /// Use this implementation in tests to verify how a command interacts with the console.
/// Use this implementation in tests to verify command interactions with the console.
/// </summary> /// </summary>
public class FakeConsole : IConsole, IDisposable public class FakeConsole : IConsole, IDisposable
{ {
private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly ConcurrentQueue<ConsoleKeyInfo> _keys = new(); private readonly ConcurrentQueue<ConsoleKeyInfo> _keys = new();
/// <summary>
/// Initializes an instance of <see cref="FakeConsole" />.
/// </summary>
public FakeConsole(Stream? input = null, Stream? output = null, Stream? error = null)
{
Input = new ConsoleReader(this, input ?? Stream.Null);
Output = new ConsoleWriter(this, output ?? Stream.Null);
Error = new ConsoleWriter(this, error ?? Stream.Null);
}
/// <inheritdoc /> /// <inheritdoc />
public ConsoleReader Input { get; } public ConsoleReader Input { get; }
@@ -52,14 +61,9 @@ public class FakeConsole : IConsole, IDisposable
public int CursorTop { get; set; } public int CursorTop { get; set; }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="FakeConsole" />. /// Enqueues a simulated key press, which can then be read by calling <see cref="ReadKey" />.
/// </summary> /// </summary>
public FakeConsole(Stream? input = null, Stream? output = null, Stream? error = null) public void EnqueueKey(ConsoleKeyInfo key) => _keys.Enqueue(key);
{
Input = ConsoleReader.Create(this, input ?? Stream.Null);
Output = ConsoleWriter.Create(this, output ?? Stream.Null);
Error = ConsoleWriter.Create(this, error ?? Stream.Null);
}
/// <inheritdoc /> /// <inheritdoc />
public ConsoleKeyInfo ReadKey(bool intercept = false) => public ConsoleKeyInfo ReadKey(bool intercept = false) =>
@@ -70,11 +74,6 @@ public class FakeConsole : IConsole, IDisposable
+ $"Use the `{nameof(EnqueueKey)}(...)` method to simulate a key press." + $"Use the `{nameof(EnqueueKey)}(...)` method to simulate a key press."
); );
/// <summary>
/// Enqueues a simulated key press, which can then be read by calling <see cref="ReadKey" />.
/// </summary>
public void EnqueueKey(ConsoleKeyInfo key) => _keys.Enqueue(key);
/// <inheritdoc /> /// <inheritdoc />
public void ResetColor() public void ResetColor()
{ {
@@ -85,11 +84,8 @@ public class FakeConsole : IConsole, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public void Clear() { } public void Clear() { }
/// <inheritdoc />
public CancellationToken RegisterCancellationHandler() => _cancellationTokenSource.Token;
/// <summary> /// <summary>
/// Sends a cancellation signal to the currently executing command. /// Simulates a cancellation request.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If the command is not cancellation-aware (i.e. it doesn't call <see cref="IConsole.RegisterCancellationHandler" />), /// If the command is not cancellation-aware (i.e. it doesn't call <see cref="IConsole.RegisterCancellationHandler" />),
@@ -108,6 +104,9 @@ public class FakeConsole : IConsole, IDisposable
} }
} }
/// <inheritdoc />
public CancellationToken RegisterCancellationHandler() => _cancellationTokenSource.Token;
/// <inheritdoc /> /// <inheritdoc />
public virtual void Dispose() => _cancellationTokenSource.Dispose(); public virtual void Dispose() => _cancellationTokenSource.Dispose();
} }

View File

@@ -3,9 +3,9 @@
namespace CliFx.Infrastructure; namespace CliFx.Infrastructure;
/// <summary> /// <summary>
/// Implementation of <see cref="IConsole" /> that uses fake /// Implementation of <see cref="IConsole" /> that uses fake standard input, output, and error streams
/// standard input, output, and error streams backed by in-memory stores. /// backed by in-memory stores.
/// Use this implementation in tests to verify command interactions with the console. /// Use this implementation in tests to verify how a command interacts with the console.
/// </summary> /// </summary>
public class FakeInMemoryConsole : FakeConsole public class FakeInMemoryConsole : FakeConsole
{ {

View File

@@ -5,47 +5,47 @@ using CliFx.Utils;
namespace CliFx.Infrastructure; namespace CliFx.Infrastructure;
/// <summary> /// <summary>
/// Abstraction for the console layer. /// Abstraction for interacting with the console layer.
/// </summary> /// </summary>
public interface IConsole public interface IConsole
{ {
/// <summary> /// <summary>
/// Input stream (stdin). /// Console's standard input stream.
/// </summary> /// </summary>
ConsoleReader Input { get; } ConsoleReader Input { get; }
/// <summary> /// <summary>
/// Gets a value that indicates whether input has been redirected from the standard input stream. /// Whether the input stream has been redirected.
/// </summary> /// </summary>
bool IsInputRedirected { get; } bool IsInputRedirected { get; }
/// <summary> /// <summary>
/// Output stream (stdout). /// Console's standard output stream.
/// </summary> /// </summary>
ConsoleWriter Output { get; } ConsoleWriter Output { get; }
/// <summary> /// <summary>
/// Gets a value that indicates whether output has been redirected from the standard output stream. /// Whether the output stream has been redirected.
/// </summary> /// </summary>
bool IsOutputRedirected { get; } bool IsOutputRedirected { get; }
/// <summary> /// <summary>
/// Error stream (stderr). /// Console's standard error stream.
/// </summary> /// </summary>
ConsoleWriter Error { get; } ConsoleWriter Error { get; }
/// <summary> /// <summary>
/// Gets a value that indicates whether error output has been redirected from the standard error stream. /// Whether the error stream has been redirected.
/// </summary> /// </summary>
bool IsErrorRedirected { get; } bool IsErrorRedirected { get; }
/// <summary> /// <summary>
/// Gets or sets the foreground color of the console /// Gets or sets the current foreground color of the console.
/// </summary> /// </summary>
ConsoleColor ForegroundColor { get; set; } ConsoleColor ForegroundColor { get; set; }
/// <summary> /// <summary>
/// Gets or sets the background color of the console. /// Gets or sets the current background color of the console.
/// </summary> /// </summary>
ConsoleColor BackgroundColor { get; set; } ConsoleColor BackgroundColor { get; set; }

View File

@@ -10,6 +10,16 @@ public class SystemConsole : IConsole, IDisposable
{ {
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
/// <summary>
/// Initializes an instance of <see cref="SystemConsole" />.
/// </summary>
public SystemConsole()
{
Input = new ConsoleReader(this, Console.OpenStandardInput());
Output = new ConsoleWriter(this, Console.OpenStandardOutput());
Error = new ConsoleWriter(this, Console.OpenStandardError());
}
/// <inheritdoc /> /// <inheritdoc />
public ConsoleReader Input { get; } public ConsoleReader Input { get; }
@@ -70,16 +80,6 @@ public class SystemConsole : IConsole, IDisposable
set => Console.CursorTop = value; set => Console.CursorTop = value;
} }
/// <summary>
/// Initializes an instance of <see cref="SystemConsole" />.
/// </summary>
public SystemConsole()
{
Input = ConsoleReader.Create(this, Console.OpenStandardInput());
Output = ConsoleWriter.Create(this, Console.OpenStandardOutput());
Error = ConsoleWriter.Create(this, Console.OpenStandardError());
}
/// <inheritdoc /> /// <inheritdoc />
public ConsoleKeyInfo ReadKey(bool intercept = false) => Console.ReadKey(intercept); public ConsoleKeyInfo ReadKey(bool intercept = false) => Console.ReadKey(intercept);