Files
PowerShell-PSReadLine/test/UnitTestReadLine.cs
T

659 lines
24 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Management.Automation.Runspaces;
using System.Management.Automation.Subsystem.Prediction;
using System.Threading.Tasks;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.PowerShell;
using Microsoft.PowerShell.Internal;
using Xunit;
using Xunit.Abstractions;
[assembly: CollectionBehavior(DisableTestParallelization = true)]
namespace Test
{
internal class MockedMethods : IPSConsoleReadLineMockableMethods
{
internal bool didDing;
internal IReadOnlyList<string> commandHistory;
internal bool? lastCommandRunStatus;
internal Guid acceptedPredictorId;
internal string acceptedSuggestion;
internal string helpContentRendered;
internal Dictionary<Guid, Tuple<uint, int>> displayedSuggestions = new Dictionary<Guid, Tuple<uint, int>>();
internal void ClearPredictionFields()
{
commandHistory = null;
lastCommandRunStatus = null;
acceptedPredictorId = Guid.Empty;
acceptedSuggestion = null;
displayedSuggestions.Clear();
}
public void Ding()
{
didDing = true;
}
public CommandCompletion CompleteInput(string input, int cursorIndex, Hashtable options, PowerShell powershell)
{
return ReadLine.MockedCompleteInput(input, cursorIndex, options, powershell);
}
public bool RunspaceIsRemote(Runspace runspace)
{
return false;
}
public Task<List<PredictionResult>> PredictInputAsync(Ast ast, Token[] tokens)
{
var result = ReadLine.MockedPredictInput(ast, tokens);
var source = new TaskCompletionSource<List<PredictionResult>>();
source.SetResult(result);
return source.Task;
}
public void OnCommandLineAccepted(IReadOnlyList<string> history)
{
commandHistory = history;
}
public void OnCommandLineExecuted(string commandLine, bool success)
{
lastCommandRunStatus = success;
}
public void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex)
{
displayedSuggestions[predictorId] = Tuple.Create(session, countOrIndex);
}
public void OnSuggestionAccepted(Guid predictorId, uint session, string suggestionText)
{
acceptedPredictorId = predictorId;
acceptedSuggestion = suggestionText;
}
public object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp)
{
return ReadLine.GetDynamicHelpContent(commandName, parameterName, isFullHelp);
}
public void RenderFullHelp(string content, string regexPatternToScrollTo)
{
helpContentRendered = content;
}
}
public enum TokenClassification
{
None,
Comment,
Keyword,
String,
Operator,
Variable,
Command,
Parameter,
Type,
Number,
Member,
Selection,
InlinePrediction,
ListPrediction,
ListPredictionSelected,
}
public abstract partial class ReadLine
{
protected ReadLine(ConsoleFixture fixture, ITestOutputHelper output, string lang, string os)
{
Output = output;
Fixture = fixture;
Fixture.Initialize(lang, os);
}
internal dynamic _ => Fixture.KbLayout;
internal ConsoleFixture Fixture { get; }
internal ITestOutputHelper Output { get; }
internal virtual bool KeyboardHasLessThan => true;
internal virtual bool KeyboardHasGreaterThan => true;
internal virtual bool KeyboardHasCtrlRBracket => true;
internal virtual bool KeyboardHasCtrlAt => true;
static ReadLine()
{
var iss = InitialSessionState.CreateDefault();
var rs = RunspaceFactory.CreateRunspace(iss);
rs.Open();
Runspace.DefaultRunspace = rs;
}
private enum KeyMode
{
Cmd,
Emacs,
Vi
};
// These colors are random - we just use these colors instead of the defaults
// so the tests aren't sensitive to tweaks to the default colors.
internal static readonly ConsoleColor[] Colors = new []
{
/*None*/ ConsoleColor.DarkRed,
/*Comment*/ ConsoleColor.Blue,
/*Keyword*/ ConsoleColor.Cyan,
/*String*/ ConsoleColor.Gray,
/*Operator*/ ConsoleColor.Green,
/*Variable*/ ConsoleColor.Magenta,
/*Command*/ ConsoleColor.Red,
/*Parameter*/ ConsoleColor.White,
/*Type*/ ConsoleColor.Yellow,
/*Number*/ ConsoleColor.DarkBlue,
/*Member*/ ConsoleColor.DarkMagenta,
/*Selection*/ ConsoleColor.Black,
/*InlinePrediction*/ ConsoleColor.DarkGreen,
/*ListPrediction*/ ConsoleColor.Yellow,
/*ListPredictionSelected*/ ConsoleColor.Gray,
};
internal static readonly ConsoleColor[] BackgroundColors = new[]
{
/*None*/ ConsoleColor.DarkGray,
/*Comment*/ ConsoleColor.DarkBlue,
/*Keyword*/ ConsoleColor.DarkCyan,
/*String*/ ConsoleColor.DarkGray,
/*Operator*/ ConsoleColor.DarkGreen,
/*Variable*/ ConsoleColor.DarkMagenta,
/*Command*/ ConsoleColor.DarkRed,
/*Parameter*/ ConsoleColor.DarkYellow,
/*Type*/ ConsoleColor.Black,
/*Number*/ ConsoleColor.Gray,
/*Member*/ ConsoleColor.Yellow,
/*Selection*/ ConsoleColor.Gray,
/*InlinePrediction*/ ConsoleColor.Cyan,
/*ListPrediction*/ ConsoleColor.Red,
/*ListPredictionSelected*/ ConsoleColor.DarkBlue,
};
class KeyHandler
{
public KeyHandler(string chord, Action<ConsoleKeyInfo?, object> handler)
{
this.Chord = chord;
this.Handler = handler;
}
public string Chord { get; }
public Action<ConsoleKeyInfo?, object> Handler { get; }
}
private void AssertCursorLeftTopIs(int left, int top)
{
AssertCursorLeftIs(left);
AssertCursorTopIs(top);
}
private void AssertCursorLeftIs(int expected)
{
Assert.Equal(expected, _console.CursorLeft);
}
private void AssertCursorTopIs(int expected)
{
Assert.Equal(expected, _console.CursorTop);
}
private void AssertLineIs(string expected)
{
PSConsoleReadLine.GetBufferState(out var input, out var unused);
Assert.Equal(expected, input);
}
static readonly MethodInfo ClipboardGetTextMethod =
typeof(PSConsoleReadLine).Assembly.GetType("Microsoft.PowerShell.Internal.Clipboard")
.GetMethod("GetText", BindingFlags.Static | BindingFlags.Public);
static readonly MethodInfo ClipboardSetTextMethod =
typeof(PSConsoleReadLine).Assembly.GetType("Microsoft.PowerShell.Internal.Clipboard")
.GetMethod("SetText", BindingFlags.Static | BindingFlags.Public);
private static string GetClipboardText()
{
return (string)ClipboardGetTextMethod.Invoke(null, null);
}
private static void SetClipboardText(string text)
{
ClipboardSetTextMethod.Invoke(null, new[] { text });
}
private void AssertClipboardTextIs(string text)
{
var fromClipboard = GetClipboardText();
Assert.Equal(text, fromClipboard);
}
private void AssertScreenCaptureClipboardIs(params string[] lines)
{
var fromClipboard = GetClipboardText();
var newline = Environment.NewLine;
var text = string.Join(Environment.NewLine, lines);
if (!text.EndsWith(newline))
{
text = text + newline;
}
Assert.Equal(text, fromClipboard);
}
private class NextLineToken { }
static readonly NextLineToken NextLine = new NextLineToken();
private class SelectionToken { public string _text; }
private static SelectionToken Selected(string s) { return new SelectionToken {_text = s}; }
private CHAR_INFO[] CreateCharInfoBuffer(int lines, params object[] items)
{
var result = new List<CHAR_INFO>();
var fg = _console.ForegroundColor;
var bg = _console.BackgroundColor;
bool isLastItemNextLineToken = false;
foreach (var i in items)
{
var item = i;
if (item is not NextLineToken)
{
isLastItemNextLineToken = false;
}
if (item is char c1)
{
result.Add(new CHAR_INFO(c1, fg, bg));
continue;
}
if (item is SelectionToken st)
{
result.AddRange(st._text.Select(c => new CHAR_INFO(c, ConsoleColor.Black, ConsoleColor.Gray)));
continue;
}
if (item is NextLineToken)
{
fg = _console.ForegroundColor;
bg = _console.BackgroundColor;
bool localCopy = isLastItemNextLineToken;
isLastItemNextLineToken = true;
if (localCopy)
{
// So this is the 2nd (or 3rd, 4th, and etc.) 'NextLineToken' in a row,
// and that means an empty line is requested.
item = _emptyLine;
}
else
{
int lineLen = result.Count % _console.BufferWidth;
if (lineLen == 0 && result.Count > 0)
{
// The existing content is right at the end of a physical line,
// so there is no need to pad.
continue;
}
// Padding is needed. Fall through to the string case.
item = new string(' ', _console.BufferWidth - lineLen);
}
}
if (item is string str)
{
result.AddRange(str.Select(c => new CHAR_INFO(c, fg, bg)));
continue;
}
if (item is TokenClassification)
{
fg = Colors[(int)(TokenClassification)item];
bg = BackgroundColors[(int)(TokenClassification)item];
continue;
}
if (item is Tuple<ConsoleColor, ConsoleColor> tuple)
{
fg = tuple.Item1;
bg = tuple.Item2;
continue;
}
throw new ArgumentException("Unexpected type");
}
var extraSpacesNeeded = (lines * _console.BufferWidth) - result.Count;
if (extraSpacesNeeded > 0)
{
var space = new CHAR_INFO(' ', _console.ForegroundColor, _console.BackgroundColor);
result.AddRange(Enumerable.Repeat(space, extraSpacesNeeded));
}
return result.ToArray();
}
static Action CheckThat(Action action)
{
// Syntactic sugar - saves a couple parens when calling Keys
return action;
}
private class KeyPlaceholder {}
private static readonly KeyPlaceholder InputAcceptedNow = new KeyPlaceholder();
private object[] Keys(params object[] input)
{
bool autoAddEnter = true;
var list = new List<object>();
foreach (var t in input)
{
if (t is IEnumerable enumerable && !(t is string))
{
foreach (var i in enumerable)
{
NewAddSingleKeyToList(i, list);
}
continue;
}
if (t == InputAcceptedNow)
{
autoAddEnter = false;
continue;
}
NewAddSingleKeyToList(t, list);
}
if (autoAddEnter)
{
// Make sure we have Enter as the last key.
for (int i = list.Count - 1; i >= 0; i--)
{
// Actions after the last key are fine, they'll
// get called.
if (list[i] is Action)
{
continue;
}
// We've skipped any actions at the end, this is
// the last key. If it's not Enter, add Enter at the
// end for convenience.
var consoleKeyInfo = (ConsoleKeyInfo) list[i];
if (consoleKeyInfo.Key != ConsoleKey.Enter || consoleKeyInfo.Modifiers != 0) {
list.Add(_.Enter);
}
break;
}
}
return list.ToArray();
}
private void NewAddSingleKeyToList(object t, List<object> list)
{
switch (t)
{
case string s:
foreach (var c in s)
{
list.Add(_[c]);
}
break;
case ConsoleKeyInfo _:
list.Add(t);
break;
case char _:
list.Add(_[(char)t]);
break;
default:
Assert.IsAssignableFrom<Action>(t);
list.Add(t);
break;
}
}
private void AssertScreenIs(int lines, params object[] items)
{
AssertScreenIs(top: 0, lines, items);
}
private void AssertScreenIs(int top, int lines, params object[] items)
{
var consoleBuffer = _console.ReadBufferLines(top, lines);
var expectedBuffer = CreateCharInfoBuffer(lines, items);
Assert.Equal(expectedBuffer.Length, consoleBuffer.Length);
for (var i = 0; i < expectedBuffer.Length; i++)
{
// Comparing CHAR_INFO should work, but randomly some attributes are set
// that shouldn't be and aren't ever set by any code in PSReadLine, so we'll
// ignore those bits and just check the stuff we do set.
Assert.Equal(expectedBuffer[i].UnicodeChar, consoleBuffer[i].UnicodeChar);
Assert.Equal(expectedBuffer[i].ForegroundColor, consoleBuffer[i].ForegroundColor);
Assert.Equal(expectedBuffer[i].BackgroundColor, consoleBuffer[i].BackgroundColor);
}
}
private void SetPrompt(string prompt)
{
var options = new SetPSReadLineOption {ExtraPromptLineCount = 0};
if (string.IsNullOrEmpty(prompt))
{
options.PromptText = new [] {""};
PSConsoleReadLine.SetOptions(options);
return;
}
int i;
for (i = prompt.Length - 1; i >= 0; i--)
{
if (!char.IsWhiteSpace(prompt[i])) break;
}
options.PromptText = new [] { prompt.Substring(i) };
var lineCount = 1 + prompt.Count(c => c == '\n');
if (lineCount > 1)
{
options.ExtraPromptLineCount = lineCount - 1;
}
PSConsoleReadLine.SetOptions(options);
_console.Write(prompt);
}
[ExcludeFromCodeCoverage]
private void Test(string expectedResult, object[] items)
{
Test(expectedResult, items, resetCursor: true, prompt: null, mustDing: false);
}
[ExcludeFromCodeCoverage]
private void Test(string expectedResult, object[] items, string prompt)
{
Test(expectedResult, items, resetCursor: true, prompt: prompt, mustDing: false);
}
[ExcludeFromCodeCoverage]
private void Test(string expectedResult, object[] items, bool resetCursor)
{
Test(expectedResult, items, resetCursor: resetCursor, prompt: null, mustDing: false);
}
[ExcludeFromCodeCoverage]
private void Test(string expectedResult, object[] items, bool resetCursor, string prompt, bool mustDing)
{
if (resetCursor)
{
_console.Clear();
}
SetPrompt(prompt);
_console.Init(items);
var result = PSConsoleReadLine.ReadLine(
runspace: null,
engineIntrinsics: null,
lastRunStatus: true);
if (_console.validationFailure != null)
{
throw new Exception("", _console.validationFailure);
}
while (_console.index < _console.inputOrValidateItems.Length)
{
var item = _console.inputOrValidateItems[_console.index++];
((Action)item)();
}
Assert.Equal(expectedResult, result);
if (mustDing)
{
Assert.True(_mockedMethods.didDing);
}
}
private void TestMustDing(string expectedResult, object[] items)
{
Test(expectedResult, items, resetCursor: true, prompt: null, mustDing: true);
}
private string _emptyLine;
private TestConsole _console;
private MockedMethods _mockedMethods;
private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg)
=> VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true);
private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers)
{
TestSetup(console: null, keyMode, keyHandlers);
}
private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] keyHandlers)
{
Skip.If(WindowsConsoleFixtureHelper.GetKeyboardLayout() != this.Fixture.Lang,
$"Keyboard layout must be set to {this.Fixture.Lang}");
_console = console ?? new TestConsole(_);
_mockedMethods = new MockedMethods();
var instance = (PSConsoleReadLine)typeof(PSConsoleReadLine)
.GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
typeof(PSConsoleReadLine)
.GetField("_mockableMethods", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(instance, _mockedMethods);
typeof(PSConsoleReadLine)
.GetField("_console", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(instance, _console);
_emptyLine ??= new string(' ', _console.BufferWidth);
PSConsoleReadLine.ClearHistory();
PSConsoleReadLine.ClearKillRing();
var options = new SetPSReadLineOption
{
AddToHistoryHandler = PSConsoleReadLineOptions.DefaultAddToHistoryHandler,
AnsiEscapeTimeout = 0,
BellStyle = PSConsoleReadLineOptions.DefaultBellStyle,
CompletionQueryItems = PSConsoleReadLineOptions.DefaultCompletionQueryItems,
ContinuationPrompt = PSConsoleReadLineOptions.DefaultContinuationPrompt,
DingDuration = 1, // Make tests virtually silent when they ding
DingTone = 37, // Make tests virtually silent when they ding
ExtraPromptLineCount = PSConsoleReadLineOptions.DefaultExtraPromptLineCount,
HistoryNoDuplicates = PSConsoleReadLineOptions.DefaultHistoryNoDuplicates,
HistorySaveStyle = HistorySaveStyle.SaveNothing,
HistorySearchCaseSensitive = PSConsoleReadLineOptions.DefaultHistorySearchCaseSensitive,
HistorySearchCursorMovesToEnd = PSConsoleReadLineOptions.DefaultHistorySearchCursorMovesToEnd,
MaximumHistoryCount = PSConsoleReadLineOptions.DefaultMaximumHistoryCount,
MaximumKillRingCount = PSConsoleReadLineOptions.DefaultMaximumKillRingCount,
ShowToolTips = PSConsoleReadLineOptions.DefaultShowToolTips,
WordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters,
PromptText = new [] {""},
Colors = new Hashtable {
{ "ContinuationPrompt", MakeCombinedColor(_console.ForegroundColor, _console.BackgroundColor) },
{ "Emphasis", MakeCombinedColor(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor) },
{ "Error", MakeCombinedColor(ConsoleColor.Red, ConsoleColor.DarkRed) },
}
};
switch (keyMode)
{
case KeyMode.Cmd:
options.EditMode = EditMode.Windows;
break;
case KeyMode.Emacs:
options.EditMode = EditMode.Emacs;
break;
case KeyMode.Vi:
options.EditMode = EditMode.Vi;
break;
}
PSConsoleReadLine.SetOptions(options);
foreach (var keyHandler in keyHandlers)
{
PSConsoleReadLine.SetKeyHandler(new [] {keyHandler.Chord}, keyHandler.Handler, "", "");
}
var tokenTypes = new[]
{
"Default", "Comment", "Keyword", "String", "Operator", "Variable",
"Command", "Parameter", "Type", "Number", "Member", "Selection",
"InlinePrediction", "ListPrediction", "ListPredictionSelected"
};
var colors = new Hashtable();
for (var i = 0; i < tokenTypes.Length; i++)
{
colors.Add(tokenTypes[i], MakeCombinedColor(Colors[i], BackgroundColors[i]));
}
var colorOptions = new SetPSReadLineOption {Colors = colors};
PSConsoleReadLine.SetOptions(colorOptions);
}
}
public class en_US_Windows : Test.ReadLine, IClassFixture<ConsoleFixture>
{
public en_US_Windows(ConsoleFixture fixture, ITestOutputHelper output)
: base(fixture, output, "en-US", "windows")
{
}
}
public class fr_FR_Windows : Test.ReadLine, IClassFixture<ConsoleFixture>
{
public fr_FR_Windows(ConsoleFixture fixture, ITestOutputHelper output)
: base(fixture, output, "fr-FR", "windows")
{
}
// I don't think this is actually true for real French keyboard, but on my US keyboard,
// I have to use Alt 6 0 for `<` and Alt 6 2 for `>` and that means the Alt+< and Alt+>
// bindings can't work.
internal override bool KeyboardHasLessThan => false;
internal override bool KeyboardHasGreaterThan => false;
// These are most likely an issue with .Net on Windows - AltGr turns into Ctrl+Alt and `]` or `@`
// requires AltGr, so you can't tell the difference b/w `]` and `Ctrl+]`.
internal override bool KeyboardHasCtrlRBracket => false;
internal override bool KeyboardHasCtrlAt => false;
}
}