terminal/src/host/ut_host/VtRendererTests.cpp

1378 lines
58 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include <wextestclass.h>
#include "../../inc/consoletaeftemplates.hpp"
#include "../../types/inc/Viewport.hpp"
#include "../../renderer/vt/Xterm256Engine.hpp"
#include "../../renderer/vt/XtermEngine.hpp"
#include "../../renderer/vt/WinTelnetEngine.hpp"
#include "../Settings.hpp"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
namespace Microsoft
{
namespace Console
{
namespace Render
{
class VtRendererTest;
};
};
};
using namespace Microsoft::Console;
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Types;
COLORREF g_ColorTable[COLOR_TABLE_SIZE];
static const std::string CLEAR_SCREEN = "\x1b[2J";
static const std::string CURSOR_HOME = "\x1b[H";
// Sometimes when we're expecting the renderengine to not write anything,
// we'll add this to the expected input, and manually write this to the callback
// to make sure nothing else gets written.
// We don't use null because that will confuse the VERIFY macros re: string length.
const char* const EMPTY_CALLBACK_SENTINEL = "\xff";
class VtRenderTestColorProvider : public Microsoft::Console::IDefaultColorProvider
{
public:
virtual ~VtRenderTestColorProvider() = default;
COLORREF GetDefaultForeground() const
{
return g_ColorTable[15];
}
COLORREF GetDefaultBackground() const
{
return g_ColorTable[0];
}
};
VtRenderTestColorProvider p;
class Microsoft::Console::Render::VtRendererTest
{
TEST_CLASS(VtRendererTest);
wil::shared_event _shutdownEvent;
TEST_CLASS_SETUP(ClassSetup)
{
// clang-format off
g_ColorTable[0] = RGB( 12, 12, 12); // Black
g_ColorTable[1] = RGB( 0, 55, 218); // Dark Blue
g_ColorTable[2] = RGB( 19, 161, 14); // Dark Green
g_ColorTable[3] = RGB( 58, 150, 221); // Dark Cyan
g_ColorTable[4] = RGB(197, 15, 31); // Dark Red
g_ColorTable[5] = RGB(136, 23, 152); // Dark Magenta
g_ColorTable[6] = RGB(193, 156, 0); // Dark Yellow
g_ColorTable[7] = RGB(204, 204, 204); // Dark White
g_ColorTable[8] = RGB(118, 118, 118); // Bright Black
g_ColorTable[9] = RGB( 59, 120, 255); // Bright Blue
g_ColorTable[10] = RGB( 22, 198, 12); // Bright Green
g_ColorTable[11] = RGB( 97, 214, 214); // Bright Cyan
g_ColorTable[12] = RGB(231, 72, 86); // Bright Red
g_ColorTable[13] = RGB(180, 0, 158); // Bright Magenta
g_ColorTable[14] = RGB(249, 241, 165); // Bright Yellow
g_ColorTable[15] = RGB(242, 242, 242); // White
// clang-format on
_shutdownEvent.create(wil::EventOptions::ManualReset);
return true;
}
TEST_CLASS_CLEANUP(ClassCleanup)
{
return true;
}
TEST_METHOD_SETUP(MethodSetup)
{
_shutdownEvent.ResetEvent();
return true;
}
// Defining a TEST_METHOD_CLEANUP seemed to break x86 test pass. Not sure why,
// something about the clipboard tests and
// YOU_CAN_ONLY_DESIGNATE_ONE_CLASS_METHOD_TO_BE_A_TEST_METHOD_SETUP_METHOD
// It's probably more correct to leave it out anyways.
TEST_METHOD(VtSequenceHelperTests);
TEST_METHOD(Xterm256TestInvalidate);
TEST_METHOD(Xterm256TestColors);
TEST_METHOD(Xterm256TestCursor);
TEST_METHOD(XtermTestInvalidate);
TEST_METHOD(XtermTestColors);
TEST_METHOD(XtermTestCursor);
TEST_METHOD(WinTelnetTestInvalidate);
TEST_METHOD(WinTelnetTestColors);
TEST_METHOD(WinTelnetTestCursor);
TEST_METHOD(TestWrapping);
TEST_METHOD(TestResize);
TEST_METHOD(TestCursorVisibility);
void Test16Colors(VtEngine* engine);
std::deque<std::string> qExpectedInput;
bool WriteCallback(const char* const pch, size_t const cch);
void TestPaint(VtEngine& engine, std::function<void()> pfn);
Viewport SetUpViewport();
};
Viewport VtRendererTest::SetUpViewport()
{
SMALL_RECT view = {};
view.Top = view.Left = 0;
view.Bottom = 31;
view.Right = 79;
return Viewport::FromInclusive(view);
}
bool VtRendererTest::WriteCallback(const char* const pch, size_t const cch)
{
std::string actualString = std::string(pch, cch);
VERIFY_IS_GREATER_THAN(qExpectedInput.size(),
static_cast<size_t>(0),
NoThrowString().Format(L"writing=\"%hs\", expecting %u strings", actualString.c_str(), qExpectedInput.size()));
std::string first = qExpectedInput.front();
qExpectedInput.pop_front();
Log::Comment(NoThrowString().Format(L"Expected =\t\"%hs\"", first.c_str()));
Log::Comment(NoThrowString().Format(L"Actual =\t\"%hs\"", actualString.c_str()));
VERIFY_ARE_EQUAL(first.length(), cch);
VERIFY_ARE_EQUAL(first, actualString);
return true;
}
// Function Description:
// - Small helper to do a series of testing wrapped by StartPaint/EndPaint calls
// Arguments:
// - engine: the engine to operate on
// - pfn: A function pointer to some test code to run.
// Return Value:
// - <none>
void VtRendererTest::TestPaint(VtEngine& engine, std::function<void()> pfn)
{
VERIFY_SUCCEEDED(engine.StartPaint());
pfn();
VERIFY_SUCCEEDED(engine.EndPaint());
}
void VtRendererTest::VtSequenceHelperTests()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
qExpectedInput.push_back("\x1b[?12l");
VERIFY_SUCCEEDED(engine->_StopCursorBlinking());
qExpectedInput.push_back("\x1b[?12h");
VERIFY_SUCCEEDED(engine->_StartCursorBlinking());
qExpectedInput.push_back("\x1b[?25l");
VERIFY_SUCCEEDED(engine->_HideCursor());
qExpectedInput.push_back("\x1b[?25h");
VERIFY_SUCCEEDED(engine->_ShowCursor());
qExpectedInput.push_back("\x1b[K");
VERIFY_SUCCEEDED(engine->_EraseLine());
qExpectedInput.push_back("\x1b[M");
VERIFY_SUCCEEDED(engine->_DeleteLine(1));
qExpectedInput.push_back("\x1b[2M");
VERIFY_SUCCEEDED(engine->_DeleteLine(2));
qExpectedInput.push_back("\x1b[L");
VERIFY_SUCCEEDED(engine->_InsertLine(1));
qExpectedInput.push_back("\x1b[2L");
VERIFY_SUCCEEDED(engine->_InsertLine(2));
qExpectedInput.push_back("\x1b[2X");
VERIFY_SUCCEEDED(engine->_EraseCharacter(2));
qExpectedInput.push_back("\x1b[2;3H");
VERIFY_SUCCEEDED(engine->_CursorPosition({ 2, 1 }));
qExpectedInput.push_back("\x1b[1;1H");
VERIFY_SUCCEEDED(engine->_CursorPosition({ 0, 0 }));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->_CursorHome());
qExpectedInput.push_back("\x1b[8;32;80t");
VERIFY_SUCCEEDED(engine->_ResizeWindow(80, 32));
qExpectedInput.push_back("\x1b[2J");
VERIFY_SUCCEEDED(engine->_ClearScreen());
qExpectedInput.push_back("\x1b[10C");
VERIFY_SUCCEEDED(engine->_CursorForward(10));
}
void VtRendererTest::Xterm256TestInvalidate()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating all invalidates the whole viewport."));
VERIFY_SUCCEEDED(engine->InvalidateAll());
qExpectedInput.push_back("\x1b[2J");
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
});
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating anything only invalidates that portion"));
SMALL_RECT invalid = { 1, 1, 1, 1 };
VERIFY_SUCCEEDED(engine->Invalidate(&invalid));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
});
Log::Comment(NoThrowString().Format(
L"Make sure that scrolling only invalidates part of the viewport, and sends the right sequences"));
COORD scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one down, only top line is invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 1;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
qExpectedInput.push_back("\x1b[H"); // Go Home
qExpectedInput.push_back("\x1b[L"); // insert a line
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, 3 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three down, only top 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 3;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
// We would expect a CUP here, but the cursor is already at the home position
qExpectedInput.push_back("\x1b[3L"); // insert 3 lines
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one up, only bottom line is invalid. ----"));
invalid = view.ToExclusive();
invalid.Top = invalid.Bottom - 1;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
qExpectedInput.push_back("\x1b[32;1H"); // Bottom of buffer
qExpectedInput.push_back("\n"); // Scroll down once
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, -3 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three up, only bottom 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Top = invalid.Bottom - 3;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
// We would expect a CUP here, but we're already at the bottom from the last call.
qExpectedInput.push_back("\n\n\n"); // Scroll down three times
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
Log::Comment(NoThrowString().Format(
L"Multiple scrolls are coalesced"));
scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
scrollDelta = { 0, 2 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three down, only top 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 3;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
qExpectedInput.push_back("\x1b[H"); // Go to home
qExpectedInput.push_back("\x1b[3L"); // insert 3 lines
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
Log::Comment(NoThrowString().Format(
VerifyOutputTraits<SMALL_RECT>::ToString(engine->_invalidRect.ToExclusive())));
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
Log::Comment(NoThrowString().Format(
VerifyOutputTraits<SMALL_RECT>::ToString(engine->_invalidRect.ToExclusive())));
qExpectedInput.push_back("\x1b[2J");
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one down and one up, nothing should change ----"
L" But it still does for now MSFT:14169294"));
invalid = view.ToExclusive();
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
}
void VtRendererTest::Xterm256TestColors()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test changing the text attributes"));
Log::Comment(NoThrowString().Format(
L"Begin by setting some test values - FG,BG = (1,2,3), (4,5,6) to start"
L"These values were picked for ease of formatting raw COLORREF values."));
qExpectedInput.push_back("\x1b[38;2;1;2;3m");
qExpectedInput.push_back("\x1b[48;2;5;6;7m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x00030201,
0x00070605,
0,
ExtendedAttributes::Normal,
false));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
qExpectedInput.push_back("\x1b[48;2;7;8;9m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x00030201,
0x00090807,
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
qExpectedInput.push_back("\x1b[38;2;10;11;12m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x000c0b0a,
0x00090807,
0,
ExtendedAttributes::Normal,
false));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure that color setting persists across EndPaint/StartPaint"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x000c0b0a,
0x00090807,
0,
ExtendedAttributes::Normal,
false));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
// Now also do the body of the 16color test as well.
// The only change is that the "Change only the BG to something not in the table"
// test actually uses an RGB value instead of the closest match.
Log::Comment(NoThrowString().Format(
L"Begin by setting the default colors - FG,BG = BRIGHT_WHITE,DARK_BLACK"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to something not in the table----"));
qExpectedInput.push_back("\x1b[48;2;1;1;1m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
0x010101,
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to the 'Default' background----"));
qExpectedInput.push_back("\x1b[49m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Back to defaults----"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure that color setting persists across EndPaint/StartPaint"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
}
void VtRendererTest::Xterm256TestCursor()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test moving the cursor around. Every sequence should have both params to CUP explicitly."));
TestPaint(*engine, [&]() {
qExpectedInput.push_back("\x1b[2;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 1 }));
Log::Comment(NoThrowString().Format(
L"----Only move Y coord----"));
qExpectedInput.push_back("\x1b[31;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 30 }));
Log::Comment(NoThrowString().Format(
L"----Only move X coord----"));
qExpectedInput.push_back("\x1b[29C");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
Log::Comment(NoThrowString().Format(
L"----Sending the same move sends nothing----"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"----moving home sends a simple sequence----"));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 0 }));
Log::Comment(NoThrowString().Format(
L"----move into the line to test some other sequences----"));
qExpectedInput.push_back("\x1b[7C");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 0 }));
Log::Comment(NoThrowString().Format(
L"----move down one line (x stays the same)----"));
qExpectedInput.push_back("\n");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 1 }));
Log::Comment(NoThrowString().Format(
L"----move to the start of the next line----"));
qExpectedInput.push_back("\r\n");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 2 }));
Log::Comment(NoThrowString().Format(
L"----move into the line to test some other sequnces----"));
qExpectedInput.push_back("\x1b[2;8H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 1 }));
Log::Comment(NoThrowString().Format(
L"----move to the start of this line (y stays the same)----"));
qExpectedInput.push_back("\r");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 1 }));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."
L"The cursor's last \"real\" position was 0,0"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"Paint some text at 0,0, then try moving the cursor to where it currently is."));
qExpectedInput.push_back("\x1b[1C");
qExpectedInput.push_back("asdfghjkl");
const wchar_t* const line = L"asdfghjkl";
const unsigned char rgWidths[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
std::vector<Cluster> clusters;
for (size_t i = 0; i < wcslen(line); i++)
{
clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
// Note that only PaintBufferLine updates the "Real" cursor position, which
// the cursor is moved back to at the end of each paint
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
}
void VtRendererTest::XtermTestInvalidate()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<XtermEngine> engine = std::make_unique<XtermEngine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE), false);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating all invalidates the whole viewport."));
VERIFY_SUCCEEDED(engine->InvalidateAll());
qExpectedInput.push_back("\x1b[2J");
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
});
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating anything only invalidates that portion"));
SMALL_RECT invalid = { 1, 1, 1, 1 };
VERIFY_SUCCEEDED(engine->Invalidate(&invalid));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
});
Log::Comment(NoThrowString().Format(
L"Make sure that scrolling only invalidates part of the viewport, and sends the right sequences"));
COORD scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one down, only top line is invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 1;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
qExpectedInput.push_back("\x1b[H"); // Go Home
qExpectedInput.push_back("\x1b[L"); // insert a line
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, 3 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three down, only top 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 3;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
// We would expect a CUP here, but the cursor is already at the home position
qExpectedInput.push_back("\x1b[3L"); // insert 3 lines
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one up, only bottom line is invalid. ----"));
invalid = view.ToExclusive();
invalid.Top = invalid.Bottom - 1;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
qExpectedInput.push_back("\x1b[32;1H"); // Bottom of buffer
qExpectedInput.push_back("\n"); // Scroll down once
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, -3 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three up, only bottom 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Top = invalid.Bottom - 3;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
// We would expect a CUP here, but we're already at the bottom from the last call.
qExpectedInput.push_back("\n\n\n"); // Scroll down three times
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
Log::Comment(NoThrowString().Format(
L"Multiple scrolls are coalesced"));
scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
scrollDelta = { 0, 2 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three down, only top 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 3;
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
qExpectedInput.push_back("\x1b[H"); // Go to home
qExpectedInput.push_back("\x1b[3L"); // insert 3 lines
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
Log::Comment(NoThrowString().Format(
VerifyOutputTraits<SMALL_RECT>::ToString(engine->_invalidRect.ToExclusive())));
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
Log::Comment(NoThrowString().Format(
VerifyOutputTraits<SMALL_RECT>::ToString(engine->_invalidRect.ToExclusive())));
qExpectedInput.push_back("\x1b[2J");
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one down and one up, nothing should change ----"
L" But it still does for now MSFT:14169294"));
invalid = view.ToExclusive();
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
}
void VtRendererTest::XtermTestColors()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<XtermEngine> engine = std::make_unique<XtermEngine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE), false);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test changing the text attributes"));
Log::Comment(NoThrowString().Format(
L"Begin by setting the default colors - FG,BG = BRIGHT_WHITE,DARK_BLACK"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to something not in the table----"));
qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], 0x010101, 0, ExtendedAttributes::Normal, false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to the 'Default' background----"));
qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Back to defaults----"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure that color setting persists across EndPaint/StartPaint"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
}
void VtRendererTest::XtermTestCursor()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<XtermEngine> engine = std::make_unique<XtermEngine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE), false);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test moving the cursor around. Every sequence should have both params to CUP explicitly."));
TestPaint(*engine, [&]() {
qExpectedInput.push_back("\x1b[2;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 1 }));
Log::Comment(NoThrowString().Format(
L"----Only move Y coord----"));
qExpectedInput.push_back("\x1b[31;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 30 }));
Log::Comment(NoThrowString().Format(
L"----Only move X coord----"));
qExpectedInput.push_back("\x1b[29C");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
Log::Comment(NoThrowString().Format(
L"----Sending the same move sends nothing----"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"----moving home sends a simple sequence----"));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 0 }));
Log::Comment(NoThrowString().Format(
L"----move into the line to test some other sequences----"));
qExpectedInput.push_back("\x1b[7C");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 0 }));
Log::Comment(NoThrowString().Format(
L"----move down one line (x stays the same)----"));
qExpectedInput.push_back("\n");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 1 }));
Log::Comment(NoThrowString().Format(
L"----move to the start of the next line----"));
qExpectedInput.push_back("\r\n");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 2 }));
Log::Comment(NoThrowString().Format(
L"----move into the line to test some other sequnces----"));
qExpectedInput.push_back("\x1b[2;8H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 1 }));
Log::Comment(NoThrowString().Format(
L"----move to the start of this line (y stays the same)----"));
qExpectedInput.push_back("\r");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 1 }));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."
L"The cursor's last \"real\" position was 0,0"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"Paint some text at 0,0, then try moving the cursor to where it currently is."));
qExpectedInput.push_back("\x1b[1C");
qExpectedInput.push_back("asdfghjkl");
const wchar_t* const line = L"asdfghjkl";
const unsigned char rgWidths[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
std::vector<Cluster> clusters;
for (size_t i = 0; i < wcslen(line); i++)
{
clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
// Note that only PaintBufferLine updates the "Real" cursor position, which
// the cursor is moved back to at the end of each paint
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
}
void VtRendererTest::WinTelnetTestInvalidate()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<WinTelnetEngine> engine = std::make_unique<WinTelnetEngine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating all invalidates the whole viewport."));
VERIFY_SUCCEEDED(engine->InvalidateAll());
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
});
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating anything only invalidates that portion"));
SMALL_RECT invalid = { 1, 1, 1, 1 };
VERIFY_SUCCEEDED(engine->Invalidate(&invalid));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive());
});
Log::Comment(NoThrowString().Format(
L"Make sure that scrolling invalidates the whole viewport, and sends no VT sequences"));
COORD scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); // sentinel
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
scrollDelta = { 1, 0 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
scrollDelta = { -1, 0 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
scrollDelta = { 1, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
}
void VtRendererTest::WinTelnetTestColors()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<WinTelnetEngine> engine = std::make_unique<WinTelnetEngine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test changing the text attributes"));
Log::Comment(NoThrowString().Format(
L"Begin by setting the default colors - FG,BG = BRIGHT_WHITE,DARK_BLACK"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to something not in the table----"));
qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], 0x010101, 0, ExtendedAttributes::Normal, false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to the 'Default' background----"));
qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Back to defaults----"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure that color setting persists across EndPaint/StartPaint"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
}
void VtRendererTest::WinTelnetTestCursor()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<WinTelnetEngine> engine = std::make_unique<WinTelnetEngine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test moving the cursor around. Every sequence should have both params to CUP explicitly."));
TestPaint(*engine, [&]() {
qExpectedInput.push_back("\x1b[2;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 1 }));
Log::Comment(NoThrowString().Format(
L"----Only move X coord----"));
qExpectedInput.push_back("\x1b[31;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 30 }));
Log::Comment(NoThrowString().Format(
L"----Only move Y coord----"));
qExpectedInput.push_back("\x1b[31;31H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
Log::Comment(NoThrowString().Format(
L"----Sending the same move sends nothing----"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
// The "real" location is the last place the cursor was moved to not
// during the course of VT operations - eg the last place text was written,
// or the cursor was manually painted at (MSFT 13310327)
Log::Comment(NoThrowString().Format(
L"Make sure the cursor gets moved back to the last real location it was at"));
qExpectedInput.push_back("\x1b[1;1H");
// EndPaint will send this sequence for us.
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."
L"The cursor's last \"real\" position was 0,0"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 0 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"Paint some text at 0,0, then try moving the cursor to where it currently is."));
qExpectedInput.push_back("\x1b[2;2H");
qExpectedInput.push_back("asdfghjkl");
const wchar_t* const line = L"asdfghjkl";
const unsigned char rgWidths[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
std::vector<Cluster> clusters;
for (size_t i = 0; i < wcslen(line); i++)
{
clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
// Note that only PaintBufferLine updates the "Real" cursor position, which
// the cursor is moved back to at the end of each paint
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
}
void VtRendererTest::TestWrapping()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), _shutdownEvent, p, SetUpViewport(), g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure the cursor is at 0,0"));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 0 }));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Painting a line that wrapped, then painting another line, and "
L"making sure we don't manually move the cursor between those paints."));
qExpectedInput.push_back("asdfghjkl");
// TODO: Undoing this behavior due to 18123777. Will come back in MSFT:16485846
qExpectedInput.push_back("\r\n");
qExpectedInput.push_back("zxcvbnm,.");
const wchar_t* const line1 = L"asdfghjkl";
const wchar_t* const line2 = L"zxcvbnm,.";
const unsigned char rgWidths[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
std::vector<Cluster> clusters1;
for (size_t i = 0; i < wcslen(line1); i++)
{
clusters1.emplace_back(std::wstring_view{ &line1[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
std::vector<Cluster> clusters2;
for (size_t i = 0; i < wcslen(line2); i++)
{
clusters2.emplace_back(std::wstring_view{ &line2[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters1.data(), clusters1.size() }, { 0, 0 }, false));
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters2.data(), clusters2.size() }, { 0, 1 }, false));
});
}
void VtRendererTest::TestResize()
{
Viewport view = SetUpViewport();
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
auto engine = std::make_unique<Xterm256Engine>(std::move(hFile), _shutdownEvent, p, view, g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
VERIFY_IS_TRUE(engine->_suppressResizeRepaint);
// The renderer (in Renderer@_PaintFrameForEngine..._CheckViewportAndScroll)
// will manually call UpdateViewport once before actually painting the
// first frame. Replicate that behavior here
VERIFY_SUCCEEDED(engine->UpdateViewport(view.ToInclusive()));
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
VERIFY_IS_FALSE(engine->_suppressResizeRepaint);
});
// Resize the viewport to 120x30
// Everything should be invalidated, and a resize message sent.
const auto newView = Viewport::FromDimensions({ 0, 0 }, { 120, 30 });
qExpectedInput.push_back("\x1b[8;30;120t");
VERIFY_SUCCEEDED(engine->UpdateViewport(newView.ToInclusive()));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(newView, engine->_invalidRect);
VERIFY_IS_FALSE(engine->_firstPaint);
VERIFY_IS_FALSE(engine->_suppressResizeRepaint);
});
}
void VtRendererTest::TestCursorVisibility()
{
Viewport view = SetUpViewport();
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
auto engine = std::make_unique<Xterm256Engine>(std::move(hFile), _shutdownEvent, p, view, g_ColorTable, static_cast<WORD>(COLOR_TABLE_SIZE));
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
VERIFY_IS_FALSE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
TestPaint(*engine, [&]() {
// During StartPaint, we'll mark the cursor as off. make sure that happens.
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_firstPaint);
});
// The cursor wasn't painted in the last frame.
VERIFY_IS_FALSE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
COORD origin{ 0, 0 };
VERIFY_ARE_NOT_EQUAL(origin, engine->_lastText);
IRenderEngine::CursorOptions options{};
options.coordCursor = origin;
// Frame 1: Paint the cursor at the home position. At the end of the frame,
// the cursor should be on. Because we're moving the cursor with CUP, we
// need to disable the cursor during this frame.
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
Log::Comment(NoThrowString().Format(L"Make sure the cursor is at 0,0"));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->PaintCursor(options));
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_TRUE(engine->_needToDisableCursor);
qExpectedInput.push_back("\x1b[?25h");
});
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
// Frame 2: Paint the cursor again at the home position. At the end of the
// frame, the cursor should be on, the same as before. We aren't moving the
// cursor during this frame, so _needToDisableCursor will stay false.
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
Log::Comment(NoThrowString().Format(L"If we just paint the cursor again at the same position, the cursor should not need to be disabled"));
VERIFY_SUCCEEDED(engine->PaintCursor(options));
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
});
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
// Frame 3: Paint the cursor at 2,2. At the end of the frame, the cursor
// should be on, the same as before. Because we're moving the cursor with
// CUP, we need to disable the cursor during this frame.
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
Log::Comment(NoThrowString().Format(L"Move the cursor to 2,2"));
qExpectedInput.push_back("\x1b[3;3H");
options.coordCursor = { 2, 2 };
VERIFY_SUCCEEDED(engine->PaintCursor(options));
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_TRUE(engine->_needToDisableCursor);
// Because _needToDisableCursor is true, we'll insert a ?25l at the
// start of the frame. Unfortunately, we can't test to make sure that
// it's there, but we can ensure that the matching ?25h is printed:
qExpectedInput.push_back("\x1b[?25h");
});
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
// Frame 4: Don't paint the cursor. At the end of the frame, the cursor
// should be off.
Log::Comment(NoThrowString().Format(L"Painting without calling PaintCursor will hide the cursor"));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
qExpectedInput.push_back("\x1b[?25l");
});
VERIFY_IS_FALSE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
}