Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions Sharprompt.Tests/PromptTests.cs
Original file line number Diff line number Diff line change
@@ -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<string>("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<int>("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<int>("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());
}
}
}
60 changes: 60 additions & 0 deletions Sharprompt.Tests/Tools/InputBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Sharprompt.Tests
{
public class InputBuffer : ConcurrentQueue<ConsoleKeyInfo>
{
private Dictionary<char, ConsoleKeyInfo> _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<char, ConsoleKeyInfo> SetupKeyMapping()
{
var keyMapping = new Dictionary<char, ConsoleKeyInfo>()
{
{(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;
}
}
}
107 changes: 107 additions & 0 deletions Sharprompt.Tests/Tools/MockConsoleDriver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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;

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 bool IsUnicodeSupported { get; } = false;

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<string> lines = new List<string>();
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));
}
}
}
20 changes: 20 additions & 0 deletions Sharprompt.Tests/Tools/MockConsoleDriverFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using Sharprompt.Drivers;

namespace Sharprompt.Tests
{
public class MockConsoleDriverFactory : IConsoleDriverFactory
{
private readonly Func<IConsoleDriver> _consoleDriverFn;

/// <summary>
/// Initializes a new instance of the <see cref="MockConsoleDriverFactory" /> class.
/// </summary>
public MockConsoleDriverFactory(Func<IConsoleDriver> consoleDriverFn)
{
_consoleDriverFn = consoleDriverFn;
}

public IConsoleDriver Create() => _consoleDriverFn();
}
}
7 changes: 7 additions & 0 deletions Sharprompt/Drivers/ConsoleDriverFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Sharprompt.Drivers
{
public static class ConsoleDriverFactory
{
public static IConsoleDriverFactory Instance { get; set; } = new DefaultConsoleDriverFactory();
}
}
28 changes: 4 additions & 24 deletions Sharprompt/Drivers/DefaultConsoleDriver.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,10 @@
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();
Expand Down Expand Up @@ -93,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
}
}
44 changes: 44 additions & 0 deletions Sharprompt/Drivers/DefaultConsoleDriverFactory.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
3 changes: 2 additions & 1 deletion Sharprompt/Drivers/IConsoleDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Sharprompt.Drivers
{
internal interface IConsoleDriver : IDisposable
public interface IConsoleDriver : IDisposable
{
void Beep();
void Reset();
Expand All @@ -18,5 +18,6 @@ internal interface IConsoleDriver : IDisposable
int BufferWidth { get; }
int BufferHeight { get; }
Action CancellationCallback { get; set; }
bool IsUnicodeSupported { get; }
}
}
7 changes: 7 additions & 0 deletions Sharprompt/Drivers/IConsoleDriverFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Sharprompt.Drivers
{
public interface IConsoleDriverFactory
{
IConsoleDriver Create();
}
}
5 changes: 1 addition & 4 deletions Sharprompt/Forms/FormBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ internal abstract class FormBase<T> : IDisposable
{
protected FormBase()
{
ConsoleDriver = new DefaultConsoleDriver
{
CancellationCallback = CancellationHandler
};
ConsoleDriver = ConsoleDriverFactory.Instance.Create();

_formRenderer = new FormRenderer(ConsoleDriver);
}
Expand Down
Loading