Files
Andy Jordan 06e3b4bcb6 Add fixture to enable existing tests for screen reader mode
Tests that depend on supported features such as menu completions, list
view, inline predictions, continuation prompt, and colors are skipped.

The "helper" module for running tests from the command-line and in
AppVeyor is minimally updated to not skip the new fixture.

A `BlankRestOfBuffer()` function implements the semantics of the `0J`
control sequence in the mock console.
2025-08-27 17:46:47 -07:00

554 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.PowerShell.Internal;
namespace Test
{
internal struct CHAR_INFO
{
public ushort UnicodeChar;
public ushort Attributes;
public CHAR_INFO(char c, ConsoleColor foreground, ConsoleColor background)
{
UnicodeChar = c;
Attributes = (ushort)(((int)background << 4) | (int)foreground);
}
public ConsoleColor ForegroundColor
{
get => (ConsoleColor)(Attributes & 0xf);
set => Attributes = (ushort)((Attributes & 0xfff0) | ((int)value & 0xf));
}
public ConsoleColor BackgroundColor
{
get => (ConsoleColor)((Attributes & 0xf0) >> 4);
set => Attributes = (ushort)((Attributes & 0xff0f) | (((int)value & 0xf) << 4));
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append((char)UnicodeChar);
if (ForegroundColor != Console.ForegroundColor)
sb.AppendFormat(" fg: {0}", ForegroundColor);
if (BackgroundColor != Console.BackgroundColor)
sb.AppendFormat(" bg: {0}", BackgroundColor);
return sb.ToString();
}
public override bool Equals(object obj)
{
if (!(obj is CHAR_INFO))
{
return false;
}
var other = (CHAR_INFO)obj;
return this.UnicodeChar == other.UnicodeChar && this.Attributes == other.Attributes;
}
public override int GetHashCode()
{
return UnicodeChar.GetHashCode() + Attributes.GetHashCode();
}
}
internal class TestConsole : IConsole
{
internal int index;
internal object[] inputOrValidateItems;
internal Exception validationFailure;
protected readonly CHAR_INFO[] buffer;
protected readonly int _bufferWidth;
protected readonly int _windowWidth;
protected readonly int _bufferHeight;
protected readonly int _windowHeight;
protected dynamic _keyboardLayout;
private bool _ignoreNextNewline;
private bool _negative;
private ConsoleColor _foregroundColor;
private ConsoleColor _backgroundColor;
/// <summary>
/// Use big enough window/buffer to avoid the need to implement scrolling.
/// </summary>
internal TestConsole(dynamic keyboardLayout)
: this(width: 60, height: 1000, mimicScrolling: false)
{
_keyboardLayout = keyboardLayout;
}
/// <summary>
/// Use specific window width and height without scrolling capability.
/// </summary>
internal TestConsole(dynamic keyboardLayout, int width, int height)
: this(width, height, mimicScrolling: false)
{
_keyboardLayout = keyboardLayout;
}
protected TestConsole(int width, int height, bool mimicScrolling)
{
BackgroundColor = ReadLine.BackgroundColors[0];
ForegroundColor = ReadLine.Colors[0];
CursorLeft = 0;
CursorTop = 0;
_bufferWidth = _windowWidth = width;
_bufferHeight = _windowHeight = height;
// Use a big enough buffer when we are mimicking scrolling.
int bufferSize = mimicScrolling ? BufferWidth * 1000 : BufferWidth * BufferHeight;
buffer = new CHAR_INFO[bufferSize];
ClearBuffer();
}
internal void Init(object[] items)
{
this.index = 0;
this.inputOrValidateItems = items;
this.validationFailure = null;
}
public virtual ConsoleKeyInfo ReadKey()
{
while (index < inputOrValidateItems.Length)
{
var item = inputOrValidateItems[index++];
if (item is ConsoleKeyInfo consoleKeyInfo)
{
return consoleKeyInfo;
}
try
{
((Action)item)();
}
catch (Exception e)
{
// Just remember the first exception
if (validationFailure == null)
{
validationFailure = e;
}
// In the hopes of avoiding additional failures, try cancelling via Ctrl+C.
return _keyboardLayout.Ctrl_c;
}
}
validationFailure = new Exception("Shouldn't call ReadKey when there are no more keys");
return _keyboardLayout.Ctrl_c;
}
public virtual bool KeyAvailable => index < inputOrValidateItems.Length && inputOrValidateItems[index] is ConsoleKeyInfo;
public virtual int CursorLeft { get; set; }
public virtual int CursorTop { get; set; }
public virtual int CursorSize { get; set; }
public virtual bool CursorVisible { get; set; }
public virtual int BufferWidth
{
get => _bufferWidth;
set => throw new NotImplementedException();
}
public virtual int BufferHeight
{
get => _bufferHeight;
set => throw new NotImplementedException();
}
public virtual int WindowWidth
{
get => _windowWidth;
set => throw new NotImplementedException();
}
public virtual int WindowHeight
{
get => _windowHeight;
set => throw new NotImplementedException();
}
public virtual int WindowTop { get; set; }
public virtual ConsoleColor BackgroundColor
{
get => _backgroundColor;
set => _backgroundColor = _negative ? (ConsoleColor)((int)value ^ 7) : value;
}
public virtual ConsoleColor ForegroundColor
{
get => _foregroundColor;
set => _foregroundColor = _negative ? (ConsoleColor)((int)value ^ 7) : value;
}
public virtual Encoding OutputEncoding
{
get => Encoding.Default;
set { }
}
public virtual void SetWindowPosition(int left, int top)
{
}
public virtual void SetCursorPosition(int left, int top)
{
if (left != CursorLeft || top != CursorTop)
{
_ignoreNextNewline = false;
}
CursorLeft = left;
CursorTop = top;
}
public virtual void WriteLine(string s)
{
// Crappy code here - no checks for a string that's too long, no scrolling.
Write(s);
CursorLeft = 0;
CursorTop += 1;
}
public virtual void Write(string s)
{
// Crappy code here - no checks for a string that's too long, no scrolling.
var writePos = CursorTop * BufferWidth + CursorLeft;
for (int i = 0; i < s.Length; i++)
{
if (s[i] == (char) 0x1b)
{
// Escape sequence - limited support here, and assumed to be well formed.
if (s[i+1] != '[') throw new ArgumentException("Unexpected escape sequence", nameof(s));
var endSequence = s.IndexOfAny(endEscapeChars, i);
var len = endSequence - i - (s[endSequence] != 'm' ? 1 : 2);
var escapeSequence = s.Substring(i + 2, len);
foreach (var subsequence in escapeSequence.Split(';'))
{
if (subsequence is "1" or "2" or "3")
{
// Ignore the font effect sequence: 1 - bold; 2 - dimmed color; 3 - italics
// They are used in the metadata line of the list view.
continue;
}
EscapeSequenceActions[subsequence](this);
}
i = endSequence;
continue;
}
if (s[i] == '\b')
{
CursorLeft -= 1;
if (CursorLeft < 0)
{
CursorTop -= 1;
CursorLeft = BufferWidth - 1;
}
_ignoreNextNewline = false;
}
else if (s[i] == '\n')
{
if (!_ignoreNextNewline)
{
CursorTop += 1;
CursorLeft = 0;
writePos = CursorTop * BufferWidth;
}
_ignoreNextNewline = false;
}
else
{
_ignoreNextNewline = false;
CursorLeft += 1;
if (CursorLeft == BufferWidth)
{
CursorLeft = 0;
CursorTop += 1;
_ignoreNextNewline = true;
}
buffer[writePos].UnicodeChar = s[i];
buffer[writePos].BackgroundColor = BackgroundColor;
buffer[writePos].ForegroundColor = ForegroundColor;
writePos += 1;
}
}
}
public virtual void BlankRestOfLine()
{
var writePos = CursorTop * BufferWidth + CursorLeft;
for (int i = 0; i < BufferWidth - CursorLeft; i++)
{
buffer[writePos + i].UnicodeChar = ' ';
buffer[writePos + i].BackgroundColor = BackgroundColor;
buffer[writePos + i].ForegroundColor = ForegroundColor;
}
}
public virtual void BlankRestOfBuffer()
{
var writePos = CursorTop * BufferWidth + CursorLeft;
for (; writePos < buffer.Length; writePos++)
{
buffer[writePos].UnicodeChar = ' ';
buffer[writePos].BackgroundColor = BackgroundColor;
buffer[writePos].ForegroundColor = ForegroundColor;
}
}
public virtual void Clear()
{
SetCursorPosition(0, 0);
ClearBuffer();
}
protected void ClearBuffer()
{
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = new CHAR_INFO(' ', ForegroundColor, BackgroundColor);
}
}
public CHAR_INFO[] ReadBufferLines(int top, int count)
{
var toCopy = BufferWidth * count;
var result = new CHAR_INFO[toCopy];
Array.Copy(buffer, top * BufferWidth, result, 0, toCopy);
return result;
}
protected static readonly char[] endEscapeChars = { 'm', 'J' };
private static readonly ConsoleColor DefaultForeground = ReadLine.Colors[0];
private static readonly ConsoleColor DefaultBackground = ReadLine.BackgroundColors[0];
private static void ToggleNegative(TestConsole c, bool b)
{
c._negative = false;
c.ForegroundColor = (ConsoleColor)((int)c.ForegroundColor ^ 7);
c.BackgroundColor = (ConsoleColor)((int)c.BackgroundColor ^ 7);
c._negative = b;
}
protected static readonly Dictionary<string, Action<TestConsole>> EscapeSequenceActions = new()
{
{"7", c => ToggleNegative(c, true) },
{"27", c => ToggleNegative(c, false) },
{"40", c => c.BackgroundColor = ConsoleColor.Black},
{"44", c => c.BackgroundColor = ConsoleColor.DarkBlue },
{"42", c => c.BackgroundColor = ConsoleColor.DarkGreen},
{"46", c => c.BackgroundColor = ConsoleColor.DarkCyan},
{"41", c => c.BackgroundColor = ConsoleColor.DarkRed},
{"45", c => c.BackgroundColor = ConsoleColor.DarkMagenta},
{"43", c => c.BackgroundColor = ConsoleColor.DarkYellow},
{"47", c => c.BackgroundColor = ConsoleColor.Gray},
{"49", c => c.BackgroundColor = DefaultBackground},
{"100", c => c.BackgroundColor = ConsoleColor.DarkGray},
{"104", c => c.BackgroundColor = ConsoleColor.Blue},
{"102", c => c.BackgroundColor = ConsoleColor.Green},
{"106", c => c.BackgroundColor = ConsoleColor.Cyan},
{"101", c => c.BackgroundColor = ConsoleColor.Red},
{"105", c => c.BackgroundColor = ConsoleColor.Magenta},
{"103", c => c.BackgroundColor = ConsoleColor.Yellow},
{"107", c => c.BackgroundColor = ConsoleColor.White},
{"30", c => c.ForegroundColor = ConsoleColor.Black},
{"34", c => c.ForegroundColor = ConsoleColor.DarkBlue},
{"32", c => c.ForegroundColor = ConsoleColor.DarkGreen},
{"36", c => c.ForegroundColor = ConsoleColor.DarkCyan},
{"31", c => c.ForegroundColor = ConsoleColor.DarkRed},
{"35", c => c.ForegroundColor = ConsoleColor.DarkMagenta},
{"33", c => c.ForegroundColor = ConsoleColor.DarkYellow},
{"37", c => c.ForegroundColor = ConsoleColor.Gray},
{"39", c => c.ForegroundColor = DefaultForeground},
{"90", c => c.ForegroundColor = ConsoleColor.DarkGray},
{"94", c => c.ForegroundColor = ConsoleColor.Blue},
{"92", c => c.ForegroundColor = ConsoleColor.Green},
{"96", c => c.ForegroundColor = ConsoleColor.Cyan},
{"91", c => c.ForegroundColor = ConsoleColor.Red},
{"95", c => c.ForegroundColor = ConsoleColor.Magenta},
{"93", c => c.ForegroundColor = ConsoleColor.Yellow},
{"97", c => c.ForegroundColor = ConsoleColor.White},
{"0", c => {
c.ForegroundColor = DefaultForeground;
c.BackgroundColor = DefaultBackground;
}},
{ "0J", c => c.BlankRestOfBuffer() },
{ "2J", c => c.SetCursorPosition(0, 0) },
};
}
internal class BasicScrollingConsole : TestConsole
{
private int _offset;
internal BasicScrollingConsole(dynamic keyboardLayout, int width, int height)
: base(width, height, mimicScrolling: true)
{
_keyboardLayout = keyboardLayout;
_offset = 0;
}
private void AdjustCursorWhenNeeded()
{
// If the last character written out happened to be in the last cell
// of a physical line, we need to adjust the cursor.
if (CursorLeft == BufferWidth)
{
CursorLeft = 0;
CursorTop++;
}
// After adjusting the cursor, we may need to handle scrolling in case
// that the cursor top went beyond the buffer height.
if (CursorTop == BufferHeight)
{
_offset++;
CursorTop--;
}
}
public override void SetCursorPosition(int left, int top)
{
if (left < 0 || left >= _bufferWidth)
{
throw new ArgumentOutOfRangeException(nameof(left), $"Value should be >= 0 and < BufferWidth({_bufferWidth}), but it's {left}.");
}
if (top < 0 || top >= _bufferHeight)
{
throw new ArgumentOutOfRangeException(nameof(top), $"Value should be >= 0 and < BufferHeight({_bufferHeight}), but it's {top}.");
}
CursorLeft = left;
CursorTop = top;
}
public override void WriteLine(string s)
{
Write(s);
CursorLeft = 0;
CursorTop += 1;
AdjustCursorWhenNeeded();
}
public override void Write(string s)
{
// Crappy code here - no checks for a string that's too long, basic scrolling handling.
var writePos = (_offset + CursorTop) * BufferWidth + CursorLeft;
for (int i = 0; i < s.Length; i++)
{
if (s[i] == (char)0x1b)
{
// Escape sequence - limited support here, and assumed to be well formed.
if (s[i + 1] != '[') throw new ArgumentException("Unexpected escape sequence", nameof(s));
var endSequence = s.IndexOfAny(endEscapeChars, i);
var len = endSequence - i - (s[endSequence] != 'm' ? 1 : 2);
var escapeSequence = s.Substring(i + 2, len);
foreach (var subsequence in escapeSequence.Split(';'))
{
if (subsequence is "1" or "2" or "3")
{
// Ignore the font effect sequence: 1 - bold; 2 - dimmed color; 3 - italics
// They are used in the metadata line of the list view.
continue;
}
EscapeSequenceActions[subsequence](this);
}
i = endSequence;
continue;
}
if (s[i] == '\b')
{
CursorLeft -= 1;
if (CursorLeft < 0)
{
CursorTop -= 1;
CursorLeft = BufferWidth - 1;
}
}
else if (s[i] == '\n')
{
CursorTop += 1;
CursorLeft = 0;
// Explicitly writing a new-line may trigger scrolling.
AdjustCursorWhenNeeded();
writePos = (_offset + CursorTop) * BufferWidth;
}
else
{
AdjustCursorWhenNeeded();
CursorLeft += 1;
// When 'CursorLeft == BufferWidth', it means the current character will take up the last cell in the current physical line.
// Assuming the current physical line is 'Y'.
// In such a case, Windows Terminal will set the cursor position to be the following after writing out the character in the
// last cell of the current physical line:
// CursorLeft: BufferWidth - 1
// CursorTop: Y
// It doesn't directly set the cursor to be (0, Y+1) in this case, but when there are more visible characters (non-control chars)
// to be written, it will automatically adjust the cursor position to be (2, Y+1) after writing out the next visible character.
//
// This behavior makes handling the new-line character '\n' much easier -- you can simply set the cursor to be (0, top+1), and
// then take care of scrolling if needed.
// So, we mimic that behavior here: we allow 'CursorLeft' to be 'BufferWidth' after 'CursorLeft += 1' above, and we adjust the
// cursor when we are about to write a new visible character.
buffer[writePos].UnicodeChar = s[i];
buffer[writePos].BackgroundColor = BackgroundColor;
buffer[writePos].ForegroundColor = ForegroundColor;
writePos += 1;
}
}
}
public override void BlankRestOfLine()
{
var writePos = (_offset + CursorTop) * BufferWidth + CursorLeft;
for (int i = 0; i < BufferWidth - CursorLeft; i++)
{
buffer[writePos + i].UnicodeChar = ' ';
buffer[writePos + i].BackgroundColor = BackgroundColor;
buffer[writePos + i].ForegroundColor = ForegroundColor;
}
}
public override void BlankRestOfBuffer()
{
var writePos = (_offset + CursorTop) * BufferWidth + CursorLeft;
for (; writePos < buffer.Length; writePos++)
{
buffer[writePos].UnicodeChar = ' ';
buffer[writePos].BackgroundColor = BackgroundColor;
buffer[writePos].ForegroundColor = ForegroundColor;
}
}
public override void Clear()
{
_offset = 0;
base.Clear();
}
}
}