mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-07-06 04:28:15 +08:00
Add new test framework for consoles
This commit is contained in:

committed by
Phil Scott

parent
f5a9c0ca26
commit
04efd1719c
@ -1,17 +1,3 @@
|
||||
root = false
|
||||
|
||||
[*.cs]
|
||||
# CS1591: Missing XML comment for publicly visible type or member
|
||||
dotnet_diagnostic.CS1591.severity = none
|
||||
|
||||
# SA1600: Elements should be documented
|
||||
dotnet_diagnostic.SA1600.severity = none
|
||||
|
||||
# SA1200: Using directives should be placed correctly
|
||||
dotnet_diagnostic.SA1200.severity = none
|
||||
|
||||
# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.OrderingRules'
|
||||
dotnet_analyzer_diagnostic.category-StyleCop.CSharp.OrderingRules.severity = none
|
||||
|
||||
# CA1819: Properties should not return arrays
|
||||
dotnet_diagnostic.CA1819.severity = none
|
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ICommandInterceptor"/> that triggers a callback when invoked.
|
||||
/// </summary>
|
||||
public sealed class CallbackCommandInterceptor : ICommandInterceptor
|
||||
{
|
||||
private readonly Action<CommandContext, CommandSettings> _callback;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CallbackCommandInterceptor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback to call when the interceptor is invoked.</param>
|
||||
public CallbackCommandInterceptor(Action<CommandContext, CommandSettings> callback)
|
||||
{
|
||||
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Intercept(CommandContext context, CommandSettings settings)
|
||||
{
|
||||
_callback(context, settings);
|
||||
}
|
||||
}
|
||||
}
|
29
src/Spectre.Console.Testing/Cli/CommandAppFailure.cs
Normal file
29
src/Spectre.Console.Testing/Cli/CommandAppFailure.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a <see cref="CommandApp"/> runtime failure.
|
||||
/// </summary>
|
||||
public sealed class CommandAppFailure
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the exception that was thrown.
|
||||
/// </summary>
|
||||
public Exception Exception { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the console output.
|
||||
/// </summary>
|
||||
public string Output { get; }
|
||||
|
||||
internal CommandAppFailure(Exception exception, string output)
|
||||
{
|
||||
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
|
||||
Output = output.NormalizeLineEndings()
|
||||
.TrimLines()
|
||||
.Trim();
|
||||
}
|
||||
}
|
||||
}
|
43
src/Spectre.Console.Testing/Cli/CommandAppResult.cs
Normal file
43
src/Spectre.Console.Testing/Cli/CommandAppResult.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of a completed <see cref="CommandApp"/> run.
|
||||
/// </summary>
|
||||
public sealed class CommandAppResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the exit code.
|
||||
/// </summary>
|
||||
public int ExitCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the console output.
|
||||
/// </summary>
|
||||
public string Output { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command context.
|
||||
/// </summary>
|
||||
public CommandContext? Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command settings.
|
||||
/// </summary>
|
||||
public CommandSettings? Settings { get; }
|
||||
|
||||
internal CommandAppResult(int exitCode, string output, CommandContext? context, CommandSettings? settings)
|
||||
{
|
||||
ExitCode = exitCode;
|
||||
Output = output ?? string.Empty;
|
||||
Context = context;
|
||||
Settings = settings;
|
||||
|
||||
Output = Output
|
||||
.NormalizeLineEndings()
|
||||
.TrimLines()
|
||||
.Trim();
|
||||
}
|
||||
}
|
||||
}
|
112
src/Spectre.Console.Testing/Cli/CommandAppTester.cs
Normal file
112
src/Spectre.Console.Testing/Cli/CommandAppTester.cs
Normal file
@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="CommandApp"/> test harness.
|
||||
/// </summary>
|
||||
public sealed class CommandAppTester
|
||||
{
|
||||
private Action<CommandApp>? _appConfiguration;
|
||||
private Action<IConfigurator>? _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default command.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The default command type.</typeparam>
|
||||
public void SetDefaultCommand<T>()
|
||||
where T : class, ICommand
|
||||
{
|
||||
_appConfiguration = (app) => app.SetDefaultCommand<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the command application.
|
||||
/// </summary>
|
||||
/// <param name="action">The configuration action.</param>
|
||||
public void Configure(Action<IConfigurator> action)
|
||||
{
|
||||
if (_configuration != null)
|
||||
{
|
||||
throw new InvalidOperationException("The command app harnest have already been configured.");
|
||||
}
|
||||
|
||||
_configuration = action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the command application and expects an exception of a specific type to be thrown.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The expected exception type.</typeparam>
|
||||
/// <param name="args">The arguments.</param>
|
||||
/// <returns>The information about the failure.</returns>
|
||||
public CommandAppFailure RunAndCatch<T>(params string[] args)
|
||||
where T : Exception
|
||||
{
|
||||
var console = new TestConsole().Width(int.MaxValue);
|
||||
|
||||
try
|
||||
{
|
||||
Run(args, console, c => c.PropagateExceptions());
|
||||
throw new InvalidOperationException("Expected an exception to be thrown, but there was none.");
|
||||
}
|
||||
catch (T ex)
|
||||
{
|
||||
return new CommandAppFailure(ex, console.Output);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Expected an exception of type '{typeof(T).FullName}' to be thrown, "
|
||||
+ $"but received {ex.GetType().FullName}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the command application.
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments.</param>
|
||||
/// <returns>The result.</returns>
|
||||
public CommandAppResult Run(params string[] args)
|
||||
{
|
||||
var console = new TestConsole().Width(int.MaxValue);
|
||||
return Run(args, console);
|
||||
}
|
||||
|
||||
private CommandAppResult Run(string[] args, TestConsole console, Action<IConfigurator>? config = null)
|
||||
{
|
||||
CommandContext? context = null;
|
||||
CommandSettings? settings = null;
|
||||
|
||||
var app = new CommandApp();
|
||||
_appConfiguration?.Invoke(app);
|
||||
|
||||
if (_configuration != null)
|
||||
{
|
||||
app.Configure(_configuration);
|
||||
}
|
||||
|
||||
if (config != null)
|
||||
{
|
||||
app.Configure(config);
|
||||
}
|
||||
|
||||
app.Configure(c => c.ConfigureConsole(console));
|
||||
app.Configure(c => c.SetInterceptor(new CallbackCommandInterceptor((ctx, s) =>
|
||||
{
|
||||
context = ctx;
|
||||
settings = s;
|
||||
})));
|
||||
|
||||
var result = app.Run(args);
|
||||
|
||||
var output = console.Output
|
||||
.NormalizeLineEndings()
|
||||
.TrimLines()
|
||||
.Trim();
|
||||
|
||||
return new CommandAppResult(result, output, context, settings);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
using System;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class CommandAppFixture
|
||||
{
|
||||
private Action<CommandApp> _appConfiguration = _ => { };
|
||||
private Action<IConfigurator> _configuration;
|
||||
|
||||
public CommandAppFixture()
|
||||
{
|
||||
_configuration = (_) => { };
|
||||
}
|
||||
|
||||
public CommandAppFixture WithDefaultCommand<T>()
|
||||
where T : class, ICommand
|
||||
{
|
||||
_appConfiguration = (app) => app.SetDefaultCommand<T>();
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Configure(Action<IConfigurator> action)
|
||||
{
|
||||
_configuration = action;
|
||||
}
|
||||
|
||||
public (string Message, string Output) RunAndCatch<T>(params string[] args)
|
||||
where T : Exception
|
||||
{
|
||||
CommandContext context = null;
|
||||
CommandSettings settings = null;
|
||||
|
||||
using var console = new FakeConsole();
|
||||
|
||||
var app = new CommandApp();
|
||||
_appConfiguration?.Invoke(app);
|
||||
|
||||
app.Configure(_configuration);
|
||||
app.Configure(c => c.ConfigureConsole(console));
|
||||
app.Configure(c => c.SetInterceptor(new FakeCommandInterceptor((ctx, s) =>
|
||||
{
|
||||
context = ctx;
|
||||
settings = s;
|
||||
})));
|
||||
|
||||
try
|
||||
{
|
||||
app.Run(args);
|
||||
}
|
||||
catch (T ex)
|
||||
{
|
||||
var output = console.Output
|
||||
.NormalizeLineEndings()
|
||||
.TrimLines()
|
||||
.Trim();
|
||||
|
||||
return (ex.Message, output);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No exception was thrown");
|
||||
}
|
||||
|
||||
public (int ExitCode, string Output, CommandContext Context, CommandSettings Settings) Run(params string[] args)
|
||||
{
|
||||
CommandContext context = null;
|
||||
CommandSettings settings = null;
|
||||
|
||||
using var console = new FakeConsole(width: int.MaxValue);
|
||||
|
||||
var app = new CommandApp();
|
||||
_appConfiguration?.Invoke(app);
|
||||
|
||||
app.Configure(_configuration);
|
||||
app.Configure(c => c.ConfigureConsole(console));
|
||||
app.Configure(c => c.SetInterceptor(new FakeCommandInterceptor((ctx, s) =>
|
||||
{
|
||||
context = ctx;
|
||||
settings = s;
|
||||
})));
|
||||
|
||||
var result = app.Run(args);
|
||||
|
||||
var output = console.Output
|
||||
.NormalizeLineEndings()
|
||||
.TrimLines()
|
||||
.Trim();
|
||||
|
||||
return (result, output, context, settings);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Spectre.Console.Tests
|
||||
{
|
||||
public static class EmbeddedResourceReader
|
||||
{
|
||||
public static Stream LoadResourceStream(string resourceName)
|
||||
{
|
||||
if (resourceName is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(resourceName));
|
||||
}
|
||||
|
||||
var assembly = Assembly.GetCallingAssembly();
|
||||
resourceName = resourceName.ReplaceExact("/", ".");
|
||||
|
||||
return assembly.GetManifestResourceStream(resourceName);
|
||||
}
|
||||
|
||||
public static Stream LoadResourceStream(Assembly assembly, string resourceName)
|
||||
{
|
||||
if (assembly is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
}
|
||||
|
||||
if (resourceName is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(resourceName));
|
||||
}
|
||||
|
||||
resourceName = resourceName.ReplaceExact("/", ".");
|
||||
return assembly.GetManifestResourceStream(resourceName);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Shouldly;
|
||||
|
||||
namespace Spectre.Console.Cli
|
||||
{
|
||||
public static class CommandContextExtensions
|
||||
{
|
||||
public static void ShouldHaveRemainingArgument(this CommandContext context, string name, string[] values)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (values == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
}
|
||||
|
||||
context.Remaining.Parsed.Contains(name).ShouldBeTrue();
|
||||
context.Remaining.Parsed[name].Count().ShouldBe(values.Length);
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
context.Remaining.Parsed[name].ShouldContain(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Shouldly;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
public static class ShouldlyExtensions
|
||||
{
|
||||
[DebuggerStepThrough]
|
||||
public static T And<T>(this T item, Action<T> action)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
action(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public static void As<T>(this T item, Action<T> action)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
action(item);
|
||||
}
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public static void As<T>(this object item, Action<T> action)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
action((T)item);
|
||||
}
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public static void ShouldBe<T>(this Type item)
|
||||
{
|
||||
item.ShouldBe(typeof(T));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Spectre.Console
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extensions for <see cref="string"/>.
|
||||
/// </summary>
|
||||
public static class StringExtensions
|
||||
{
|
||||
private static readonly Regex _lineNumberRegex = new Regex(":\\d+", RegexOptions.Singleline);
|
||||
private static readonly Regex _filenameRegex = new Regex("\\sin\\s.*cs:nn", RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new string with all lines trimmed of trailing whitespace.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to trim.</param>
|
||||
/// <returns>A new string with all lines trimmed of trailing whitespace.</returns>
|
||||
public static string TrimLines(this string value)
|
||||
{
|
||||
if (value is null)
|
||||
@ -16,24 +21,19 @@ namespace Spectre.Console
|
||||
}
|
||||
|
||||
var result = new List<string>();
|
||||
var lines = value.Split(new[] { '\n' });
|
||||
|
||||
foreach (var line in lines)
|
||||
foreach (var line in value.Split(new[] { '\n' }))
|
||||
{
|
||||
var current = line.TrimEnd();
|
||||
if (string.IsNullOrWhiteSpace(current))
|
||||
{
|
||||
result.Add(string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(current);
|
||||
}
|
||||
result.Add(line.TrimEnd());
|
||||
}
|
||||
|
||||
return string.Join("\n", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new string with normalized line endings.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to normalize line endings for.</param>
|
||||
/// <returns>A new string with normalized line endings.</returns>
|
||||
public static string NormalizeLineEndings(this string value)
|
||||
{
|
||||
if (value != null)
|
||||
@ -44,36 +44,5 @@ namespace Spectre.Console
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static string NormalizeStackTrace(this string text)
|
||||
{
|
||||
text = _lineNumberRegex.Replace(text, match =>
|
||||
{
|
||||
return ":nn";
|
||||
});
|
||||
|
||||
return _filenameRegex.Replace(text, match =>
|
||||
{
|
||||
var value = match.Value;
|
||||
var index = value.LastIndexOfAny(new[] { '\\', '/' });
|
||||
var filename = value.Substring(index + 1, value.Length - index - 1);
|
||||
|
||||
return $" in /xyz/{filename}";
|
||||
});
|
||||
}
|
||||
|
||||
internal static string ReplaceExact(this string text, string oldValue, string newValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newValue))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
#if NET5_0
|
||||
return text.Replace(oldValue, newValue, StringComparison.Ordinal);
|
||||
#else
|
||||
return text.Replace(oldValue, newValue);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,17 @@
|
||||
namespace Spectre.Console.Tests
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extensions for <see cref="Style"/>.
|
||||
/// </summary>
|
||||
public static class StyleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the foreground or background color of the specified style.
|
||||
/// </summary>
|
||||
/// <param name="style">The style.</param>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="foreground">Whether or not to set the foreground color.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static Style SetColor(this Style style, Color color, bool foreground)
|
||||
{
|
||||
if (foreground)
|
||||
|
@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
public static class XmlElementExtensions
|
||||
{
|
||||
public static void SetNullableAttribute(this XmlElement element, string name, string value)
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
element.SetAttribute(name, value ?? "NULL");
|
||||
}
|
||||
|
||||
public static void SetNullableAttribute(this XmlElement element, string name, IEnumerable<string> values)
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
if (values?.Any() != true)
|
||||
{
|
||||
element.SetAttribute(name, "NULL");
|
||||
}
|
||||
|
||||
element.SetAttribute(name, string.Join(",", values));
|
||||
}
|
||||
|
||||
public static void SetBooleanAttribute(this XmlElement element, string name, bool value)
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
element.SetAttribute(name, value ? "true" : "false");
|
||||
}
|
||||
|
||||
public static void SetEnumAttribute(this XmlElement element, string name, Enum value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
if (element == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
var field = value.GetType().GetField(value.ToString());
|
||||
var attribute = field.GetCustomAttribute<DescriptionAttribute>(false);
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new InvalidOperationException("Enum is missing description.");
|
||||
}
|
||||
|
||||
element.SetAttribute(name, attribute.Description);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class FakeAnsiConsole : IDisposable, IAnsiConsole
|
||||
{
|
||||
private readonly StringWriter _writer;
|
||||
private readonly IAnsiConsole _console;
|
||||
private readonly FakeExclusivityMode _exclusivityLock;
|
||||
|
||||
public string Output => _writer.ToString();
|
||||
|
||||
public Profile Profile => _console.Profile;
|
||||
public IAnsiConsoleCursor Cursor => _console.Cursor;
|
||||
public FakeConsoleInput Input { get; }
|
||||
public IExclusivityMode ExclusivityMode => _exclusivityLock;
|
||||
public RenderPipeline Pipeline => _console.Pipeline;
|
||||
|
||||
IAnsiConsoleInput IAnsiConsole.Input => Input;
|
||||
|
||||
public FakeAnsiConsole(
|
||||
ColorSystem colors,
|
||||
AnsiSupport ansi = AnsiSupport.Yes,
|
||||
int width = 80)
|
||||
{
|
||||
_exclusivityLock = new FakeExclusivityMode();
|
||||
_writer = new StringWriter();
|
||||
|
||||
var factory = new AnsiConsoleFactory();
|
||||
_console = factory.Create(new AnsiConsoleSettings
|
||||
{
|
||||
Ansi = ansi,
|
||||
ColorSystem = (ColorSystemSupport)colors,
|
||||
Out = new AnsiConsoleOutput(_writer),
|
||||
Enrichment = new ProfileEnrichment
|
||||
{
|
||||
UseDefaultEnrichers = false,
|
||||
},
|
||||
});
|
||||
|
||||
_console.Profile.Width = width;
|
||||
_console.Profile.Capabilities.Unicode = true;
|
||||
|
||||
Input = new FakeConsoleInput();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_writer?.Dispose();
|
||||
}
|
||||
|
||||
public void Clear(bool home)
|
||||
{
|
||||
_console.Clear(home);
|
||||
}
|
||||
|
||||
public void Write(IRenderable renderable)
|
||||
{
|
||||
_console.Write(renderable);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class FakeCapabilities : IReadOnlyCapabilities
|
||||
{
|
||||
public ColorSystem ColorSystem { get; set; } = ColorSystem.TrueColor;
|
||||
|
||||
public bool Ansi { get; set; }
|
||||
|
||||
public bool Links { get; set; }
|
||||
|
||||
public bool Legacy { get; set; }
|
||||
|
||||
public bool IsTerminal { get; set; }
|
||||
|
||||
public bool Interactive { get; set; }
|
||||
|
||||
public bool Unicode { get; set; }
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
using System;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class FakeCommandInterceptor : ICommandInterceptor
|
||||
{
|
||||
private readonly Action<CommandContext, CommandSettings> _action;
|
||||
|
||||
public FakeCommandInterceptor(Action<CommandContext, CommandSettings> action)
|
||||
{
|
||||
_action = action ?? throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
public void Intercept(CommandContext context, CommandSettings settings)
|
||||
{
|
||||
_action(context, settings);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class FakeConsole : IAnsiConsole, IDisposable
|
||||
{
|
||||
public Profile Profile { get; }
|
||||
public IAnsiConsoleCursor Cursor => new FakeAnsiConsoleCursor();
|
||||
IAnsiConsoleInput IAnsiConsole.Input => Input;
|
||||
public IExclusivityMode ExclusivityMode { get; }
|
||||
public RenderPipeline Pipeline { get; }
|
||||
|
||||
public FakeConsoleInput Input { get; }
|
||||
public string Output => Profile.Out.Writer.ToString();
|
||||
public IReadOnlyList<string> Lines => Output.TrimEnd('\n').Split(new char[] { '\n' });
|
||||
|
||||
public FakeConsole(
|
||||
int width = 80, int height = 9000, Encoding encoding = null,
|
||||
bool supportsAnsi = true, ColorSystem colorSystem = ColorSystem.Standard,
|
||||
bool legacyConsole = false, bool interactive = true)
|
||||
{
|
||||
Input = new FakeConsoleInput();
|
||||
ExclusivityMode = new FakeExclusivityMode();
|
||||
Pipeline = new RenderPipeline();
|
||||
|
||||
Profile = new Profile(new AnsiConsoleOutput(new StringWriter()), encoding ?? Encoding.UTF8);
|
||||
Profile.Width = width;
|
||||
Profile.Height = height;
|
||||
Profile.Capabilities.ColorSystem = colorSystem;
|
||||
Profile.Capabilities.Ansi = supportsAnsi;
|
||||
Profile.Capabilities.Legacy = legacyConsole;
|
||||
Profile.Capabilities.Interactive = interactive;
|
||||
Profile.Capabilities.Links = true;
|
||||
Profile.Capabilities.Unicode = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Profile.Out.Writer.Dispose();
|
||||
}
|
||||
|
||||
public void Clear(bool home)
|
||||
{
|
||||
}
|
||||
|
||||
public void Write(IRenderable renderable)
|
||||
{
|
||||
foreach (var segment in renderable.GetSegments(this))
|
||||
{
|
||||
Profile.Out.Writer.Write(segment.Text);
|
||||
}
|
||||
}
|
||||
|
||||
public string WriteNormalizedException(Exception ex, ExceptionFormats formats = ExceptionFormats.Default)
|
||||
{
|
||||
this.WriteException(ex, formats);
|
||||
return string.Join("\n", Output.NormalizeStackTrace()
|
||||
.NormalizeLineEndings()
|
||||
.Split(new char[] { '\n' })
|
||||
.Select(line => line.TrimEnd()));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class FakeConsoleInput : IAnsiConsoleInput
|
||||
{
|
||||
private readonly Queue<ConsoleKeyInfo> _input;
|
||||
|
||||
public FakeConsoleInput()
|
||||
{
|
||||
_input = new Queue<ConsoleKeyInfo>();
|
||||
}
|
||||
|
||||
public void PushText(string input)
|
||||
{
|
||||
if (input is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
}
|
||||
|
||||
foreach (var character in input)
|
||||
{
|
||||
PushCharacter(character);
|
||||
}
|
||||
}
|
||||
|
||||
public void PushTextWithEnter(string input)
|
||||
{
|
||||
PushText(input);
|
||||
PushKey(ConsoleKey.Enter);
|
||||
}
|
||||
|
||||
public void PushCharacter(char character)
|
||||
{
|
||||
var control = char.IsUpper(character);
|
||||
_input.Enqueue(new ConsoleKeyInfo(character, (ConsoleKey)character, false, false, control));
|
||||
}
|
||||
|
||||
public void PushKey(ConsoleKey key)
|
||||
{
|
||||
_input.Enqueue(new ConsoleKeyInfo((char)key, key, false, false, false));
|
||||
}
|
||||
|
||||
public ConsoleKeyInfo ReadKey(bool intercept)
|
||||
{
|
||||
if (_input.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No input available.");
|
||||
}
|
||||
|
||||
return _input.Dequeue();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class FakeTypeRegistrar : ITypeRegistrar
|
||||
{
|
||||
private readonly ITypeResolver _resolver;
|
||||
public Dictionary<Type, List<Type>> Registrations { get; }
|
||||
public Dictionary<Type, List<object>> Instances { get; }
|
||||
|
||||
public FakeTypeRegistrar(ITypeResolver resolver = null)
|
||||
{
|
||||
_resolver = resolver;
|
||||
Registrations = new Dictionary<Type, List<Type>>();
|
||||
Instances = new Dictionary<Type, List<object>>();
|
||||
}
|
||||
|
||||
public void Register(Type service, Type implementation)
|
||||
{
|
||||
if (!Registrations.ContainsKey(service))
|
||||
{
|
||||
Registrations.Add(service, new List<Type> { implementation });
|
||||
}
|
||||
else
|
||||
{
|
||||
Registrations[service].Add(implementation);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterInstance(Type service, object implementation)
|
||||
{
|
||||
if (!Instances.ContainsKey(service))
|
||||
{
|
||||
Instances.Add(service, new List<object> { implementation });
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterLazy(Type service, Func<object> factory)
|
||||
{
|
||||
if (factory is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
if (!Instances.ContainsKey(service))
|
||||
{
|
||||
Instances.Add(service, new List<object> { factory() });
|
||||
}
|
||||
}
|
||||
|
||||
public ITypeResolver Build()
|
||||
{
|
||||
return _resolver;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class FakeTypeResolver : ITypeResolver
|
||||
{
|
||||
private readonly IDictionary<Type, object> _lookup;
|
||||
|
||||
public FakeTypeResolver()
|
||||
{
|
||||
_lookup = new Dictionary<Type, object>();
|
||||
}
|
||||
|
||||
public void Register<T>(T instance)
|
||||
{
|
||||
_lookup[typeof(T)] = instance;
|
||||
}
|
||||
|
||||
public object Resolve(Type type)
|
||||
{
|
||||
if (_lookup.TryGetValue(type, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return Activator.CreateInstance(type);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class FakeAnsiConsoleCursor : IAnsiConsoleCursor
|
||||
internal sealed class NoopCursor : IAnsiConsoleCursor
|
||||
{
|
||||
public void Move(CursorDirection direction, int steps)
|
||||
{
|
@ -3,7 +3,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class FakeExclusivityMode : IExclusivityMode
|
||||
internal sealed class NoopExclusivityMode : IExclusivityMode
|
||||
{
|
||||
public T Run<T>(Func<T> func)
|
||||
{
|
@ -1,18 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||
<TargetFrameworks>net5.0;netstandard2.0</TargetFrameworks>
|
||||
<IsTestProject>false</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Description>Contains testing utilities for Spectre.Console.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
|
||||
<None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Shouldly" Version="4.0.3" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,5 +0,0 @@
|
||||
<ProjectConfiguration>
|
||||
<Settings>
|
||||
<XUnit2Enabled>False</XUnit2Enabled>
|
||||
</Settings>
|
||||
</ProjectConfiguration>
|
40
src/Spectre.Console.Testing/TestCapabilities.cs
Normal file
40
src/Spectre.Console.Testing/TestCapabilities.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents fake capabilities useful in tests.
|
||||
/// </summary>
|
||||
public sealed class TestCapabilities : IReadOnlyCapabilities
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public ColorSystem ColorSystem { get; set; } = ColorSystem.TrueColor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ansi { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Links { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Legacy { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsTerminal { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Interactive { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Unicode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RenderContext"/> with the same capabilities as this instace.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="RenderContext"/> with the same capabilities as this instace.</returns>
|
||||
public RenderContext CreateRenderContext()
|
||||
{
|
||||
return new RenderContext(this);
|
||||
}
|
||||
}
|
||||
}
|
122
src/Spectre.Console.Testing/TestConsole.cs
Normal file
122
src/Spectre.Console.Testing/TestConsole.cs
Normal file
@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// A testable console.
|
||||
/// </summary>
|
||||
public sealed class TestConsole : IAnsiConsole, IDisposable
|
||||
{
|
||||
private readonly IAnsiConsole _console;
|
||||
private readonly StringWriter _writer;
|
||||
private IAnsiConsoleCursor? _cursor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Profile Profile => _console.Profile;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IExclusivityMode ExclusivityMode => _console.ExclusivityMode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the console input.
|
||||
/// </summary>
|
||||
public TestConsoleInput Input { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RenderPipeline Pipeline => _console.Pipeline;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IAnsiConsoleCursor Cursor => _cursor ?? _console.Cursor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
IAnsiConsoleInput IAnsiConsole.Input => Input;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the console output.
|
||||
/// </summary>
|
||||
public string Output => _writer.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the console output lines.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Lines => Output.NormalizeLineEndings().TrimEnd('\n').Split(new char[] { '\n' });
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not VT/ANSI sequences
|
||||
/// should be emitted to the console.
|
||||
/// </summary>
|
||||
public bool EmitAnsiSequences { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TestConsole"/> class.
|
||||
/// </summary>
|
||||
public TestConsole()
|
||||
{
|
||||
_writer = new StringWriter();
|
||||
_cursor = new NoopCursor();
|
||||
|
||||
Input = new TestConsoleInput();
|
||||
EmitAnsiSequences = false;
|
||||
|
||||
var factory = new AnsiConsoleFactory();
|
||||
_console = factory.Create(new AnsiConsoleSettings
|
||||
{
|
||||
Ansi = AnsiSupport.Yes,
|
||||
ColorSystem = (ColorSystemSupport)ColorSystem.TrueColor,
|
||||
Out = new AnsiConsoleOutput(_writer),
|
||||
Interactive = InteractionSupport.No,
|
||||
ExclusivityMode = new NoopExclusivityMode(),
|
||||
Enrichment = new ProfileEnrichment
|
||||
{
|
||||
UseDefaultEnrichers = false,
|
||||
},
|
||||
});
|
||||
|
||||
_console.Profile.Width = 80;
|
||||
_console.Profile.Height = 24;
|
||||
_console.Profile.Capabilities.Ansi = true;
|
||||
_console.Profile.Capabilities.Unicode = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
_writer.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Clear(bool home)
|
||||
{
|
||||
_console.Clear(home);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Write(IRenderable renderable)
|
||||
{
|
||||
if (EmitAnsiSequences)
|
||||
{
|
||||
_console.Write(renderable);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var segment in renderable.GetSegments(this))
|
||||
{
|
||||
if (segment.IsControlCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Profile.Out.Writer.Write(segment.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetCursor(IAnsiConsoleCursor? cursor)
|
||||
{
|
||||
_cursor = cursor;
|
||||
}
|
||||
}
|
||||
}
|
55
src/Spectre.Console.Testing/TestConsoleExtensions.cs
Normal file
55
src/Spectre.Console.Testing/TestConsoleExtensions.cs
Normal file
@ -0,0 +1,55 @@
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extensions for <see cref="TestConsole"/>.
|
||||
/// </summary>
|
||||
public static class TestConsoleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the console's color system.
|
||||
/// </summary>
|
||||
/// <param name="console">The console.</param>
|
||||
/// <param name="colors">The color system to use.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static TestConsole Colors(this TestConsole console, ColorSystem colors)
|
||||
{
|
||||
console.Profile.Capabilities.ColorSystem = colors;
|
||||
return console;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the console interactive.
|
||||
/// </summary>
|
||||
/// <param name="console">The console.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static TestConsole Interactive(this TestConsole console)
|
||||
{
|
||||
console.Profile.Capabilities.Interactive = true;
|
||||
return console;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the console width.
|
||||
/// </summary>
|
||||
/// <param name="console">The console.</param>
|
||||
/// <param name="width">The console width.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static TestConsole Width(this TestConsole console, int width)
|
||||
{
|
||||
console.Profile.Width = width;
|
||||
return console;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns on emitting of VT/ANSI sequences.
|
||||
/// </summary>
|
||||
/// <param name="console">The console.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static TestConsole EmitAnsiSequences(this TestConsole console)
|
||||
{
|
||||
console.SetCursor(null);
|
||||
console.EmitAnsiSequences = true;
|
||||
return console;
|
||||
}
|
||||
}
|
||||
}
|
78
src/Spectre.Console.Testing/TestConsoleInput.cs
Normal file
78
src/Spectre.Console.Testing/TestConsoleInput.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a testable console input mechanism.
|
||||
/// </summary>
|
||||
public sealed class TestConsoleInput : IAnsiConsoleInput
|
||||
{
|
||||
private readonly Queue<ConsoleKeyInfo> _input;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TestConsoleInput"/> class.
|
||||
/// </summary>
|
||||
public TestConsoleInput()
|
||||
{
|
||||
_input = new Queue<ConsoleKeyInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the specified text to the input queue.
|
||||
/// </summary>
|
||||
/// <param name="input">The input string.</param>
|
||||
public void PushText(string input)
|
||||
{
|
||||
if (input is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
}
|
||||
|
||||
foreach (var character in input)
|
||||
{
|
||||
PushCharacter(character);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the specified text followed by 'Enter' to the input queue.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
public void PushTextWithEnter(string input)
|
||||
{
|
||||
PushText(input);
|
||||
PushKey(ConsoleKey.Enter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the specified character to the input queue.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
public void PushCharacter(char input)
|
||||
{
|
||||
var control = char.IsUpper(input);
|
||||
_input.Enqueue(new ConsoleKeyInfo(input, (ConsoleKey)input, false, false, control));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the specified key to the input queue.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
public void PushKey(ConsoleKey input)
|
||||
{
|
||||
_input.Enqueue(new ConsoleKeyInfo((char)input, input, false, false, false));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConsoleKeyInfo ReadKey(bool intercept)
|
||||
{
|
||||
if (_input.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No input available.");
|
||||
}
|
||||
|
||||
return _input.Dequeue();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class DummySpinner1 : Spinner
|
||||
{
|
||||
public override TimeSpan Interval => TimeSpan.FromMilliseconds(100);
|
||||
public override bool IsUnicode => true;
|
||||
public override IReadOnlyList<string> Frames => new List<string> { "*", };
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Testing
|
||||
{
|
||||
public sealed class DummySpinner2 : Spinner
|
||||
{
|
||||
public override TimeSpan Interval => TimeSpan.FromMilliseconds(100);
|
||||
public override bool IsUnicode => true;
|
||||
public override IReadOnlyList<string> Frames => new List<string> { "-", };
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user