Separate Spectre.Console.Cli from Spectre.Console (#1850)

This commit is contained in:
Frank Ray
2025-07-23 22:11:07 +01:00
committed by GitHub
parent 6ad814cab0
commit 8b59ddfd41
13 changed files with 179 additions and 139 deletions

View File

@@ -1,7 +1,20 @@
namespace Spectre.Console;
namespace Spectre.Console.Testing;
internal static class ShouldlyExtensions
/// <summary>
/// Provides extensions for testing using the Shouldly-style fluent assertions.
/// </summary>
public static class ShouldlyExtensions
{
/// <summary>
/// Performs the specified action on the given object and then returns the object.
/// Useful for fluent testing patterns where additional assertions or operations
/// are chained together in a readable manner.
/// </summary>
/// <typeparam name="T">The type of the object.</typeparam>
/// <param name="item">The object to operate on.</param>
/// <param name="action">An action to perform on the object.</param>
/// <returns>The original object, to allow further chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
[DebuggerStepThrough]
public static T And<T>(this T item, Action<T> action)
{

View File

@@ -0,0 +1,59 @@
namespace Spectre.Console.Testing;
/// <summary>
/// Provides extension methods for working with <see cref="TestConsole"/> in a testing context,
/// including stack trace normalization for consistent and deterministic test output.
/// </summary>
public static partial class TestConsoleExtensions
{
private static readonly Regex _lineNumberRegex = new Regex(":\\d+", RegexOptions.Singleline);
private static readonly Regex _filenameRegex = new Regex("\\sin\\s.*cs:nn", RegexOptions.Multiline);
/// <summary>
/// Writes the given exception to the <see cref="TestConsole"/> and returns a normalized string
/// representation of the exception, with file paths and line numbers sanitized.
/// </summary>
/// <param name="console">The <see cref="TestConsole"/> to write to.</param>
/// <param name="ex">The exception to write and normalize.</param>
/// <param name="formats">Optional formatting options for exception output.</param>
/// <returns>A normalized string of the exception's output, safe for snapshot testing.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown if the console's output buffer is not empty before writing the exception.
/// </exception>
public static string WriteNormalizedException(this TestConsole console, Exception ex, ExceptionFormats formats = ExceptionFormats.Default)
{
if (!string.IsNullOrWhiteSpace(console.Output))
{
throw new InvalidOperationException("Output buffer is not empty.");
}
console.WriteException(ex, formats);
return string.Join("\n", NormalizeStackTrace(console.Output)
.NormalizeLineEndings()
.Split(new char[] { '\n' })
.Select(line => line.TrimEnd()));
}
/// <summary>
/// Normalizes a stack trace string by replacing line numbers with ":nn"
/// and converting full file paths to a fixed placeholder path ("/xyz/filename.cs").
/// </summary>
/// <param name="text">The stack trace text to normalize.</param>
/// <returns>A sanitized stack trace suitable for stable testing output.</returns>
public static string NormalizeStackTrace(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}";
});
}
}

View File

@@ -0,0 +1,91 @@
namespace Spectre.Console.Testing;
/// <summary>
/// Contains extensions for <see cref="TestConsole"/>.
/// </summary>
public static partial 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>
/// Sets whether or not ANSI is supported.
/// </summary>
/// <param name="console">The console.</param>
/// <param name="enable">Whether or not VT/ANSI control codes are supported.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TestConsole SupportsAnsi(this TestConsole console, bool enable)
{
console.Profile.Capabilities.Ansi = enable;
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>
/// Sets the console height.
/// </summary>
/// <param name="console">The console.</param>
/// <param name="width">The console height.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TestConsole Height(this TestConsole console, int width)
{
console.Profile.Height = width;
return console;
}
/// <summary>
/// Sets the console size.
/// </summary>
/// <param name="console">The console.</param>
/// <param name="size">The console size.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TestConsole Size(this TestConsole console, Size size)
{
console.Profile.Width = size.Width;
console.Profile.Height = size.Height;
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;
}
}