From 69b656382ea5e7fd64a4b03e73b15be98cc14a84 Mon Sep 17 00:00:00 2001 From: Jason Tremper Date: Wed, 5 Jan 2022 14:58:55 -0500 Subject: [PATCH 1/2] Exposing public IConsoleDriver for additional input options and better testing --- Sharprompt.Tests/PromptTests.cs | 72 ++++++++++++ Sharprompt.Tests/Tools/InputBuffer.cs | 60 ++++++++++ Sharprompt.Tests/Tools/MockConsoleDriver.cs | 109 ++++++++++++++++++ .../Tools/MockConsoleDriverFactory.cs | 20 ++++ Sharprompt/Drivers/ConsoleDriverFactory.cs | 7 ++ Sharprompt/Drivers/DefaultConsoleDriver.cs | 25 ---- .../Drivers/DefaultConsoleDriverFactory.cs | 44 +++++++ Sharprompt/Drivers/IConsoleDriver.cs | 2 +- Sharprompt/Drivers/IConsoleDriverFactory.cs | 7 ++ Sharprompt/Forms/FormBase.cs | 5 +- 10 files changed, 321 insertions(+), 30 deletions(-) create mode 100644 Sharprompt.Tests/PromptTests.cs create mode 100644 Sharprompt.Tests/Tools/InputBuffer.cs create mode 100644 Sharprompt.Tests/Tools/MockConsoleDriver.cs create mode 100644 Sharprompt.Tests/Tools/MockConsoleDriverFactory.cs create mode 100644 Sharprompt/Drivers/ConsoleDriverFactory.cs create mode 100644 Sharprompt/Drivers/DefaultConsoleDriverFactory.cs create mode 100644 Sharprompt/Drivers/IConsoleDriverFactory.cs diff --git a/Sharprompt.Tests/PromptTests.cs b/Sharprompt.Tests/PromptTests.cs new file mode 100644 index 0000000..628fc6d --- /dev/null +++ b/Sharprompt.Tests/PromptTests.cs @@ -0,0 +1,72 @@ +using System.Threading; +using System.Threading.Tasks; +using Sharprompt.Drivers; +using Xunit; + +namespace Sharprompt.Tests +{ + public class PromptTests + { + private MockConsoleDriver ConsoleDriver { get; } + + public PromptTests() + { + ConsoleDriver = new MockConsoleDriver(); + ConsoleDriverFactory.Instance = new MockConsoleDriverFactory(() => ConsoleDriver); + } + + [Fact] + public async Task InputString() + { + var keyWait = new AutoResetEvent(false); + ConsoleDriver.AwaitingKeyPress += (sender, args) => keyWait.Set(); + + var task = Task.Run(() => Prompt.Input("Get")); + + keyWait.WaitOne(); + Assert.Equal("? Get:", ConsoleDriver.GetOutputBuffer()); + + ConsoleDriver.InputBuffer.WriteLine("PASS"); + var result = await task; + + Assert.Equal("PASS", result); + Assert.Equal("V Get: PASS", ConsoleDriver.GetOutputBuffer()); + } + + [Fact] + public async Task InputInteger() + { + var keyWait = new AutoResetEvent(false); + ConsoleDriver.AwaitingKeyPress += (sender, args) => keyWait.Set(); + + var task = Task.Run(() => Prompt.Input("Get")); + + keyWait.WaitOne(); + Assert.Equal("? Get:", ConsoleDriver.GetOutputBuffer()); + + ConsoleDriver.InputBuffer.WriteLine("0"); + var result = await task; + + Assert.Equal(0, result); + Assert.Equal("V Get: 0", ConsoleDriver.GetOutputBuffer()); + } + + [Fact] + public void InputInteger_ValidationFailed() + { + var keyWait = new AutoResetEvent(false); + + ConsoleDriver.AwaitingKeyPress += (sender, args) => keyWait.Set(); + + Task.Run(() => Prompt.Input("Get")); + + keyWait.WaitOne(); + Assert.Equal("? Get:", ConsoleDriver.GetOutputBuffer()); + + ConsoleDriver.InputBuffer.WriteLine("PASS"); + + keyWait.WaitOne(); + Assert.Equal("? Get: PASS\n>> PASS is not a valid value for Int32. (Parameter 'value')", ConsoleDriver.GetOutputBuffer()); + } + } +} diff --git a/Sharprompt.Tests/Tools/InputBuffer.cs b/Sharprompt.Tests/Tools/InputBuffer.cs new file mode 100644 index 0000000..f383998 --- /dev/null +++ b/Sharprompt.Tests/Tools/InputBuffer.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Sharprompt.Tests +{ + public class InputBuffer : ConcurrentQueue + { + private Dictionary _inputList = SetupKeyMapping(); + + public void Write(string input) + { + foreach (var keyChar in input) + { + if (_inputList.TryGetValue(keyChar, out var keyInfo)) + { + Enqueue(keyInfo); + } + else + { + throw new InvalidOperationException("Unknown character to key mapping"); + } + } + } + + public void WriteLine(string input) + { + Write(input); + Write("\n"); + } + + private static Dictionary SetupKeyMapping() + { + var keyMapping = new Dictionary() + { + {(char)3, new ConsoleKeyInfo((char)3, ConsoleKey.C, false, false, true)}, + {' ', new ConsoleKeyInfo(' ', ConsoleKey.Spacebar, false, false, false)}, + {'\n', new ConsoleKeyInfo('\n', ConsoleKey.Enter, false, false, false)}, + {'\b', new ConsoleKeyInfo('\b', ConsoleKey.Backspace, false, false, false)} + }; + + for (char c = 'a'; c < 'z'; c++) + { + keyMapping.Add(c, new ConsoleKeyInfo(c, (c - 'a') + ConsoleKey.A, false, false, false)); + } + + for (char c = 'A'; c < 'Z'; c++) + { + keyMapping.Add(c, new ConsoleKeyInfo(c, (c - 'Z') + ConsoleKey.A, true, false, false)); + } + + for (char c = '0'; c < '9'; c++) + { + keyMapping.Add(c, new ConsoleKeyInfo(c, (c - '0') + ConsoleKey.D0, false, false, false)); + } + + return keyMapping; + } + } +} diff --git a/Sharprompt.Tests/Tools/MockConsoleDriver.cs b/Sharprompt.Tests/Tools/MockConsoleDriver.cs new file mode 100644 index 0000000..fe73611 --- /dev/null +++ b/Sharprompt.Tests/Tools/MockConsoleDriver.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using Sharprompt.Drivers; + +namespace Sharprompt.Tests +{ + public class MockConsoleDriver : IConsoleDriver + { + private char[,] _buffer; + public InputBuffer InputBuffer { get; } = new InputBuffer(); + + public event EventHandler AwaitingKeyPress; + + /// + /// Initializes a new instance of the class. + /// + public MockConsoleDriver() + { + Reset(); + } + + public bool KeyAvailable => InputBuffer.Any(); + public bool CursorVisible { get; set; } = true; + public int CursorLeft { get; set; } = 0; + public int CursorTop { get; set; } = 0; + public int BufferWidth { get; } = 120; + public int BufferHeight { get; } = 30; + public Action CancellationCallback { get; set; } + + public void Dispose() + { + } + + public void Beep() + { + } + + public void Reset() + { + _buffer = new char[BufferHeight, BufferWidth]; + CursorLeft = CursorTop = 0; + } + + public void ClearLine(int top) + { + for (var x = 0; x < BufferWidth; x++) + { + _buffer[top, x] = ' '; + } + + if (CursorTop == top) CursorLeft = 0; + } + + public ConsoleKeyInfo ReadKey() + { + if (!KeyAvailable) + { + AwaitingKeyPress?.Invoke(this, EventArgs.Empty); + } + + var consoleKey = new ConsoleKeyInfo(); + SpinWait.SpinUntil(() => KeyAvailable && InputBuffer.TryDequeue(out consoleKey), 1000); + return consoleKey; + } + + public void Write(string value, ConsoleColor color) + { + for (int x = 0; x < value.Length; x++, CursorLeft++) + { + _buffer[CursorTop, CursorLeft] = value[x]; + } + } + + public void WriteLine() + { + Write("\n", ConsoleColor.Black); + CursorLeft = 0; + CursorTop++; + } + + public void SetCursorPosition(int left, int top) + { + CursorLeft = left; + CursorTop = top; + } + + public string GetOutputBuffer() + { + List lines = new List(); + for (int y = 0; y < BufferHeight; y++) + { + var stringBuilder = new StringBuilder(); + for (int x = 0; x < BufferWidth && _buffer[y,x] != '\0'; x++) + { + stringBuilder.Append(_buffer[y,x]); + } + + lines.Add(stringBuilder.ToString().TrimEnd()); + } + + var idx = lines.FindLastIndex((l) => !string.IsNullOrEmpty(l)); + + return string.Join('\n', lines.Take(idx + 1)); + } + } +} diff --git a/Sharprompt.Tests/Tools/MockConsoleDriverFactory.cs b/Sharprompt.Tests/Tools/MockConsoleDriverFactory.cs new file mode 100644 index 0000000..9f02393 --- /dev/null +++ b/Sharprompt.Tests/Tools/MockConsoleDriverFactory.cs @@ -0,0 +1,20 @@ +using System; +using Sharprompt.Drivers; + +namespace Sharprompt.Tests +{ + public class MockConsoleDriverFactory : IConsoleDriverFactory + { + private readonly Func _consoleDriverFn; + + /// + /// Initializes a new instance of the class. + /// + public MockConsoleDriverFactory(Func consoleDriverFn) + { + _consoleDriverFn = consoleDriverFn; + } + + public IConsoleDriver Create() => _consoleDriverFn(); + } +} diff --git a/Sharprompt/Drivers/ConsoleDriverFactory.cs b/Sharprompt/Drivers/ConsoleDriverFactory.cs new file mode 100644 index 0000000..8b17272 --- /dev/null +++ b/Sharprompt/Drivers/ConsoleDriverFactory.cs @@ -0,0 +1,7 @@ +namespace Sharprompt.Drivers +{ + public static class ConsoleDriverFactory + { + public static IConsoleDriverFactory Instance { get; set; } = new DefaultConsoleDriverFactory(); + } +} diff --git a/Sharprompt/Drivers/DefaultConsoleDriver.cs b/Sharprompt/Drivers/DefaultConsoleDriver.cs index ba650ca..8ee4f53 100644 --- a/Sharprompt/Drivers/DefaultConsoleDriver.cs +++ b/Sharprompt/Drivers/DefaultConsoleDriver.cs @@ -1,34 +1,9 @@ using System; -using System.Runtime.InteropServices; - -using Sharprompt.Internal; namespace Sharprompt.Drivers { internal sealed class DefaultConsoleDriver : IConsoleDriver { - static DefaultConsoleDriver() - { - if (Console.IsInputRedirected || Console.IsOutputRedirected) - { - throw new InvalidOperationException("Sharprompt requires an interactive environment."); - } - - Console.TreatControlCAsInput = true; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var hConsole = NativeMethods.GetStdHandle(NativeMethods.STD_OUTPUT_HANDLE); - - if (!NativeMethods.GetConsoleMode(hConsole, out var mode)) - { - return; - } - - NativeMethods.SetConsoleMode(hConsole, mode | NativeMethods.ENABLE_VIRTUAL_TERMINAL_PROCESSING); - } - } - #region IDisposable public void Dispose() => Reset(); diff --git a/Sharprompt/Drivers/DefaultConsoleDriverFactory.cs b/Sharprompt/Drivers/DefaultConsoleDriverFactory.cs new file mode 100644 index 0000000..71f7918 --- /dev/null +++ b/Sharprompt/Drivers/DefaultConsoleDriverFactory.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices; + +using Sharprompt.Internal; + +namespace Sharprompt.Drivers +{ + internal sealed class DefaultConsoleDriverFactory : IConsoleDriverFactory + { + private bool isInitialized = false; + + private void InitializeDefaultConsoleDriver() + { + if (isInitialized) return; + + if (Console.IsInputRedirected || Console.IsOutputRedirected) + { + throw new InvalidOperationException("Sharprompt requires an interactive environment."); + } + + Console.TreatControlCAsInput = true; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var hConsole = NativeMethods.GetStdHandle(NativeMethods.STD_OUTPUT_HANDLE); + + if (!NativeMethods.GetConsoleMode(hConsole, out var mode)) + { + return; + } + + NativeMethods.SetConsoleMode(hConsole, mode | NativeMethods.ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } + + isInitialized = true; + } + + public IConsoleDriver Create() + { + InitializeDefaultConsoleDriver(); + return new DefaultConsoleDriver(); + } + } +} diff --git a/Sharprompt/Drivers/IConsoleDriver.cs b/Sharprompt/Drivers/IConsoleDriver.cs index 7b0eac7..6e79a8a 100644 --- a/Sharprompt/Drivers/IConsoleDriver.cs +++ b/Sharprompt/Drivers/IConsoleDriver.cs @@ -2,7 +2,7 @@ namespace Sharprompt.Drivers { - internal interface IConsoleDriver : IDisposable + public interface IConsoleDriver : IDisposable { void Beep(); void Reset(); diff --git a/Sharprompt/Drivers/IConsoleDriverFactory.cs b/Sharprompt/Drivers/IConsoleDriverFactory.cs new file mode 100644 index 0000000..169c934 --- /dev/null +++ b/Sharprompt/Drivers/IConsoleDriverFactory.cs @@ -0,0 +1,7 @@ +namespace Sharprompt.Drivers +{ + public interface IConsoleDriverFactory + { + IConsoleDriver Create(); + } +} diff --git a/Sharprompt/Forms/FormBase.cs b/Sharprompt/Forms/FormBase.cs index 55b93b8..cf2b94e 100644 --- a/Sharprompt/Forms/FormBase.cs +++ b/Sharprompt/Forms/FormBase.cs @@ -11,10 +11,7 @@ internal abstract class FormBase : IDisposable { protected FormBase() { - ConsoleDriver = new DefaultConsoleDriver - { - CancellationCallback = CancellationHandler - }; + ConsoleDriver = ConsoleDriverFactory.Instance.Create(); _formRenderer = new FormRenderer(ConsoleDriver); } From 5c5a81139ebc746aef03bc5836f78e61d8bf1cdd Mon Sep 17 00:00:00 2001 From: Jason Tremper Date: Thu, 6 Jan 2022 10:20:57 -0500 Subject: [PATCH 2/2] Fixing issue where System.Console referenced outside IConsoleDriver --- Sharprompt.Tests/Tools/MockConsoleDriver.cs | 4 +--- Sharprompt/Drivers/DefaultConsoleDriver.cs | 5 +++++ Sharprompt/Drivers/IConsoleDriver.cs | 1 + Sharprompt/Symbol.cs | 11 ++++++++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Sharprompt.Tests/Tools/MockConsoleDriver.cs b/Sharprompt.Tests/Tools/MockConsoleDriver.cs index fe73611..57b9c44 100644 --- a/Sharprompt.Tests/Tools/MockConsoleDriver.cs +++ b/Sharprompt.Tests/Tools/MockConsoleDriver.cs @@ -14,9 +14,6 @@ public class MockConsoleDriver : IConsoleDriver public event EventHandler AwaitingKeyPress; - /// - /// Initializes a new instance of the class. - /// public MockConsoleDriver() { Reset(); @@ -29,6 +26,7 @@ public MockConsoleDriver() public int BufferWidth { get; } = 120; public int BufferHeight { get; } = 30; public Action CancellationCallback { get; set; } + public bool IsUnicodeSupported { get; } = false; public void Dispose() { diff --git a/Sharprompt/Drivers/DefaultConsoleDriver.cs b/Sharprompt/Drivers/DefaultConsoleDriver.cs index 8ee4f53..0f8721f 100644 --- a/Sharprompt/Drivers/DefaultConsoleDriver.cs +++ b/Sharprompt/Drivers/DefaultConsoleDriver.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; namespace Sharprompt.Drivers { @@ -68,6 +69,10 @@ public bool CursorVisible public Action CancellationCallback { get; set; } + public bool IsUnicodeSupported + { + get => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || Console.OutputEncoding.CodePage is 1200 or 65001; + } #endregion } } diff --git a/Sharprompt/Drivers/IConsoleDriver.cs b/Sharprompt/Drivers/IConsoleDriver.cs index 6e79a8a..0a9c1f3 100644 --- a/Sharprompt/Drivers/IConsoleDriver.cs +++ b/Sharprompt/Drivers/IConsoleDriver.cs @@ -18,5 +18,6 @@ public interface IConsoleDriver : IDisposable int BufferWidth { get; } int BufferHeight { get; } Action CancellationCallback { get; set; } + bool IsUnicodeSupported { get; } } } diff --git a/Sharprompt/Symbol.cs b/Sharprompt/Symbol.cs index 66c716f..5976b68 100644 --- a/Sharprompt/Symbol.cs +++ b/Sharprompt/Symbol.cs @@ -1,6 +1,8 @@ using System; using System.Runtime.InteropServices; +using Sharprompt.Drivers; + namespace Sharprompt { public class Symbol @@ -18,6 +20,13 @@ public Symbol(string value, string fallbackValue) public static implicit operator string(Symbol symbol) => symbol.ToString(); - private static bool IsUnicodeSupported => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || Console.OutputEncoding.CodePage is 1200 or 65001; + private static bool IsUnicodeSupported + { + get + { + var consoleDriver = ConsoleDriverFactory.Instance.Create(); + return consoleDriver.IsUnicodeSupported; + } + } } }