// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include #include "../../inc/consoletaeftemplates.hpp" #include "../../types/inc/Viewport.hpp" #include "../../renderer/vt/Xterm256Engine.hpp" #include "../../renderer/vt/XtermEngine.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; using namespace Microsoft::Console::VirtualTerminal::DispatchTypes; 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 Microsoft::Console::Render::VtRendererTest { TEST_CLASS(VtRendererTest); TEST_CLASS_SETUP(ClassSetup) { return true; } TEST_CLASS_CLEANUP(ClassCleanup) { return true; } TEST_METHOD_SETUP(MethodSetup) { qExpectedInput.clear(); 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(Xterm256TestExtendedAttributes); TEST_METHOD(Xterm256TestAttributesAcrossReset); TEST_METHOD(XtermTestInvalidate); TEST_METHOD(XtermTestColors); TEST_METHOD(XtermTestCursor); TEST_METHOD(XtermTestAttributesAcrossReset); TEST_METHOD(FormattedString); TEST_METHOD(TestWrapping); TEST_METHOD(TestResize); TEST_METHOD(TestCursorVisibility); void Test16Colors(VtEngine* engine); std::deque qExpectedInput; bool WriteCallback(const char* const pch, size_t const cch); void TestPaint(VtEngine& engine, std::function pfn); Viewport SetUpViewport(); void VerifyExpectedInputsDrained(); }; Viewport VtRendererTest::SetUpViewport() { SMALL_RECT view = {}; view.Top = view.Left = 0; view.Bottom = 31; view.Right = 79; return Viewport::FromInclusive(view); } void VtRendererTest::VerifyExpectedInputsDrained() { if (!qExpectedInput.empty()) { for (const auto& exp : qExpectedInput) { Log::Error(NoThrowString().Format(L"EXPECTED INPUT NEVER RECEIVED: %hs", exp.c_str())); } VERIFY_FAIL(L"there should be no remaining un-drained expected input"); } } 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(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: // - void VtRendererTest::TestPaint(VtEngine& engine, std::function 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 engine = std::make_unique(std::move(hFile), SetUpViewport()); 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 engine = std::make_unique(std::move(hFile), SetUpViewport()); 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); }); const Viewport view = SetUpViewport(); Log::Comment(NoThrowString().Format( L"Make sure that invalidating all invalidates the whole viewport.")); VERIFY_SUCCEEDED(engine->InvalidateAll()); TestPaint(*engine, [&]() { VERIFY_IS_TRUE(engine->_invalidMap.all()); }); Log::Comment(NoThrowString().Format( L"Make sure that invalidating anything only invalidates that portion")); SMALL_RECT invalid = { 1, 1, 2, 2 }; VERIFY_SUCCEEDED(engine->Invalidate(&invalid)); TestPaint(*engine, [&]() { VERIFY_IS_TRUE(engine->_invalidMap.one()); VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, *(engine->_invalidMap.begin())); }); 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; const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(1u, runs.size()); VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front()); 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; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(3u, runs.size()); auto invalidRect = runs.front(); for (size_t i = 1; i < runs.size(); ++i) { invalidRect |= runs[i]; } // verify the rect matches the invalid one. VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); // 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; const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(1u, runs.size()); VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front()); 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; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(3u, runs.size()); auto invalidRect = runs.front(); for (size_t i = 1; i < runs.size(); ++i) { invalidRect |= runs[i]; } // verify the rect matches the invalid one. VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); // 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; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(3u, runs.size()); auto invalidRect = runs.front(); for (size_t i = 1; i < runs.size(); ++i) { invalidRect |= runs[i]; } // verify the rect matches the invalid one. VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); 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(engine->_invalidMap.to_string().c_str()); scrollDelta = { 0, -1 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); Log::Comment(engine->_invalidMap.to_string().c_str()); 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")); const auto runs = engine->_invalidMap.runs(); auto invalidRect = runs.front(); for (size_t i = 1; i < runs.size(); ++i) { invalidRect |= runs[i]; } // only the bottom line should be dirty. // When we scrolled down, the bitmap looked like this: // 1111 // 0000 // 0000 // 0000 // And then we scrolled up and the top line fell off and a bottom // line was filled in like this: // 0000 // 0000 // 0000 // 1111 const til::rectangle expected{ til::point{ view.Left(), view.BottomInclusive() }, til::size{ view.Width(), 1 } }; VERIFY_ARE_EQUAL(expected, invalidRect); VERIFY_SUCCEEDED(engine->ScrollFrame()); }); } void VtRendererTest::Xterm256TestColors() { wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); std::unique_ptr engine = std::make_unique(std::move(hFile), SetUpViewport()); auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); engine->SetTestCallback(pfn); RenderData renderData; // 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 }, &renderData, false, 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 }, &renderData, false, 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 }, &renderData, false, 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 }, &renderData, false, 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. // However, instead of using a closest match ANSI color, we can reproduce // the exact RGB or 256-color index value stored in the TextAttribute. Log::Comment(NoThrowString().Format( L"Begin by setting the default colors")); qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, false, false)); TestPaint(*engine, [&]() { TextAttribute textAttributes; Log::Comment(NoThrowString().Format( L"----Change only the BG----")); textAttributes.SetIndexedBackground(TextColor::DARK_RED); qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the FG----")); textAttributes.SetIndexedForeground(TextColor::DARK_WHITE); qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the BG to an RGB value----")); textAttributes.SetBackground(RGB(19, 161, 14)); qExpectedInput.push_back("\x1b[48;2;19;161;14m"); // Background RGB(19,161,14) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the FG to an RGB value----")); textAttributes.SetForeground(RGB(193, 156, 0)); qExpectedInput.push_back("\x1b[38;2;193;156;0m"); // Foreground RGB(193,156,0) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the BG to the 'Default' background----")); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[49m"); // Background default VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the FG to a 256-color index----")); textAttributes.SetIndexedForeground256(TextColor::DARK_WHITE); qExpectedInput.push_back("\x1b[38;5;7m"); // Foreground DARK_WHITE (256-Color Index) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the BG to a 256-color index----")); textAttributes.SetIndexedBackground256(TextColor::DARK_RED); qExpectedInput.push_back("\x1b[48;5;1m"); // Background DARK_RED (256-Color Index) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the FG to the 'Default' foreground----")); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[39m"); // Background default VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Back to defaults----")); textAttributes = {}; qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, 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({}, &renderData, false, 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 engine = std::make_unique(std::move(hFile), SetUpViewport()); 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 sequences----")); 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 clusters; for (size_t i = 0; i < wcslen(line); i++) { clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast(rgWidths[i])); } VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false, 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::Xterm256TestExtendedAttributes() { // Run this test for each and every possible combination of states. BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:faint", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:underlined", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:doublyUnderlined", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:italics", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:blink", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:invisible", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:crossedOut", L"{false, true}") END_TEST_METHOD_PROPERTIES() bool faint, underlined, doublyUnderlined, italics, blink, invisible, crossedOut; VERIFY_SUCCEEDED(TestData::TryGetValue(L"faint", faint)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"underlined", underlined)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"doublyUnderlined", doublyUnderlined)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"italics", italics)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"blink", blink)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"invisible", invisible)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"crossedOut", crossedOut)); TextAttribute desiredAttrs; std::vector onSequences, offSequences; // Collect up a VT sequence to set the state given the method properties if (faint) { desiredAttrs.SetFaint(true); onSequences.push_back("\x1b[2m"); offSequences.push_back("\x1b[22m"); } if (underlined) { desiredAttrs.SetUnderlined(true); onSequences.push_back("\x1b[4m"); offSequences.push_back("\x1b[24m"); } if (doublyUnderlined) { desiredAttrs.SetDoublyUnderlined(true); onSequences.push_back("\x1b[21m"); // The two underlines share the same off sequence, so we // only add it here if that hasn't already been done. if (!underlined) { offSequences.push_back("\x1b[24m"); } } if (italics) { desiredAttrs.SetItalic(true); onSequences.push_back("\x1b[3m"); offSequences.push_back("\x1b[23m"); } if (blink) { desiredAttrs.SetBlinking(true); onSequences.push_back("\x1b[5m"); offSequences.push_back("\x1b[25m"); } if (invisible) { desiredAttrs.SetInvisible(true); onSequences.push_back("\x1b[8m"); offSequences.push_back("\x1b[28m"); } if (crossedOut) { desiredAttrs.SetCrossedOut(true); onSequences.push_back("\x1b[9m"); offSequences.push_back("\x1b[29m"); } wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); std::unique_ptr engine = std::make_unique(std::move(hFile), SetUpViewport()); 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"----Turn the extended attributes on----")); TestPaint(*engine, [&]() { // Merge the "on" sequences into expected input. std::copy(onSequences.cbegin(), onSequences.cend(), std::back_inserter(qExpectedInput)); VERIFY_SUCCEEDED(engine->_UpdateExtendedAttrs(desiredAttrs)); }); Log::Comment(NoThrowString().Format( L"----Turn the extended attributes off----")); TestPaint(*engine, [&]() { std::copy(offSequences.cbegin(), offSequences.cend(), std::back_inserter(qExpectedInput)); VERIFY_SUCCEEDED(engine->_UpdateExtendedAttrs({})); }); Log::Comment(NoThrowString().Format( L"----Turn the extended attributes back on----")); TestPaint(*engine, [&]() { std::copy(onSequences.cbegin(), onSequences.cend(), std::back_inserter(qExpectedInput)); VERIFY_SUCCEEDED(engine->_UpdateExtendedAttrs(desiredAttrs)); }); VerifyExpectedInputsDrained(); } void VtRendererTest::Xterm256TestAttributesAcrossReset() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:renditionAttribute", L"{1, 2, 3, 4, 5, 7, 8, 9, 21, 53}") END_TEST_METHOD_PROPERTIES() int renditionAttribute; VERIFY_SUCCEEDED(TestData::TryGetValue(L"renditionAttribute", renditionAttribute)); std::stringstream renditionSequence; renditionSequence << "\x1b[" << renditionAttribute << "m"; wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); std::unique_ptr engine = std::make_unique(std::move(hFile), SetUpViewport()); auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); engine->SetTestCallback(pfn); RenderData renderData; Log::Comment(L"Make sure rendition attributes are retained when colors are reset"); Log::Comment(L"----Start With All Attributes Reset----"); TextAttribute textAttributes = {}; qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); switch (renditionAttribute) { case GraphicsOptions::BoldBright: Log::Comment(L"----Set Bold Attribute----"); textAttributes.SetBold(true); break; case GraphicsOptions::RGBColorOrFaint: Log::Comment(L"----Set Faint Attribute----"); textAttributes.SetFaint(true); break; case GraphicsOptions::Italics: Log::Comment(L"----Set Italics Attribute----"); textAttributes.SetItalic(true); break; case GraphicsOptions::Underline: Log::Comment(L"----Set Underline Attribute----"); textAttributes.SetUnderlined(true); break; case GraphicsOptions::DoublyUnderlined: Log::Comment(L"----Set Doubly Underlined Attribute----"); textAttributes.SetDoublyUnderlined(true); break; case GraphicsOptions::Overline: Log::Comment(L"----Set Overline Attribute----"); textAttributes.SetOverlined(true); break; case GraphicsOptions::BlinkOrXterm256Index: Log::Comment(L"----Set Blink Attribute----"); textAttributes.SetBlinking(true); break; case GraphicsOptions::Negative: Log::Comment(L"----Set Negative Attribute----"); textAttributes.SetReverseVideo(true); break; case GraphicsOptions::Invisible: Log::Comment(L"----Set Invisible Attribute----"); textAttributes.SetInvisible(true); break; case GraphicsOptions::CrossedOut: Log::Comment(L"----Set Crossed Out Attribute----"); textAttributes.SetCrossedOut(true); break; } qExpectedInput.push_back(renditionSequence.str()); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Foreground----"); textAttributes.SetIndexedForeground(TextColor::DARK_GREEN); qExpectedInput.push_back("\x1b[32m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Foreground and Retain Rendition----"); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Background----"); textAttributes.SetIndexedBackground(TextColor::DARK_GREEN); qExpectedInput.push_back("\x1b[42m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Background and Retain Rendition----"); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); VerifyExpectedInputsDrained(); } void VtRendererTest::XtermTestInvalidate() { wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); std::unique_ptr engine = std::make_unique(std::move(hFile), SetUpViewport(), 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()); TestPaint(*engine, [&]() { VERIFY_IS_TRUE(engine->_invalidMap.all()); }); Log::Comment(NoThrowString().Format( L"Make sure that invalidating anything only invalidates that portion")); SMALL_RECT invalid = { 1, 1, 2, 2 }; VERIFY_SUCCEEDED(engine->Invalidate(&invalid)); TestPaint(*engine, [&]() { VERIFY_IS_TRUE(engine->_invalidMap.one()); VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, *(engine->_invalidMap.begin())); }); 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; const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(1u, runs.size()); VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front()); 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; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(3u, runs.size()); auto invalidRect = runs.front(); for (size_t i = 1; i < runs.size(); ++i) { invalidRect |= runs[i]; } // verify the rect matches the invalid one. VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); // 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; const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(1u, runs.size()); VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front()); 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; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(3u, runs.size()); auto invalidRect = runs.front(); for (size_t i = 1; i < runs.size(); ++i) { invalidRect |= runs[i]; } // verify the rect matches the invalid one. VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); // 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; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(3u, runs.size()); auto invalidRect = runs.front(); for (size_t i = 1; i < runs.size(); ++i) { invalidRect |= runs[i]; } // verify the rect matches the invalid one. VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); 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(engine->_invalidMap.to_string().c_str()); scrollDelta = { 0, -1 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); Log::Comment(engine->_invalidMap.to_string().c_str()); 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")); const auto runs = engine->_invalidMap.runs(); auto invalidRect = runs.front(); for (size_t i = 1; i < runs.size(); ++i) { invalidRect |= runs[i]; } // only the bottom line should be dirty. // When we scrolled down, the bitmap looked like this: // 1111 // 0000 // 0000 // 0000 // And then we scrolled up and the top line fell off and a bottom // line was filled in like this: // 0000 // 0000 // 0000 // 1111 const til::rectangle expected{ til::point{ view.Left(), view.BottomInclusive() }, til::size{ view.Width(), 1 } }; VERIFY_ARE_EQUAL(expected, invalidRect); VERIFY_SUCCEEDED(engine->ScrollFrame()); }); } void VtRendererTest::XtermTestColors() { wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); std::unique_ptr engine = std::make_unique(std::move(hFile), SetUpViewport(), false); auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); engine->SetTestCallback(pfn); RenderData renderData; // 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")); qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, false, false)); TestPaint(*engine, [&]() { TextAttribute textAttributes; Log::Comment(NoThrowString().Format( L"----Change only the BG----")); textAttributes.SetIndexedBackground(TextColor::DARK_RED); qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the FG----")); textAttributes.SetIndexedForeground(TextColor::DARK_WHITE); qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the BG to an RGB value----")); textAttributes.SetBackground(RGB(19, 161, 14)); qExpectedInput.push_back("\x1b[42m"); // Background DARK_GREEN VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the FG to an RGB value----")); textAttributes.SetForeground(RGB(193, 156, 0)); qExpectedInput.push_back("\x1b[33m"); // Foreground DARK_YELLOW VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the BG to the 'Default' background----")); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[m"); // Both foreground and background default qExpectedInput.push_back("\x1b[33m"); // Reapply foreground DARK_YELLOW VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the FG to a 256-color index----")); textAttributes.SetIndexedForeground256(TextColor::DARK_WHITE); qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the BG to a 256-color index----")); textAttributes.SetIndexedBackground256(TextColor::DARK_RED); qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Change only the FG to the 'Default' foreground----")); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[m"); // Both foreground and background default qExpectedInput.push_back("\x1b[41m"); // Reapply background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(NoThrowString().Format( L"----Back to defaults----")); textAttributes = {}; qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, 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({}, &renderData, false, 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 engine = std::make_unique(std::move(hFile), SetUpViewport(), 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 sequences----")); 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 clusters; for (size_t i = 0; i < wcslen(line); i++) { clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast(rgWidths[i])); } VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false, 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::XtermTestAttributesAcrossReset() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:renditionAttribute", L"{1, 4, 7}") END_TEST_METHOD_PROPERTIES() int renditionAttribute; VERIFY_SUCCEEDED(TestData::TryGetValue(L"renditionAttribute", renditionAttribute)); std::stringstream renditionSequence; renditionSequence << "\x1b[" << renditionAttribute << "m"; wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); std::unique_ptr engine = std::make_unique(std::move(hFile), SetUpViewport(), false); auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); engine->SetTestCallback(pfn); RenderData renderData; Log::Comment(L"Make sure rendition attributes are retained when colors are reset"); Log::Comment(L"----Start With All Attributes Reset----"); TextAttribute textAttributes = {}; qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); switch (renditionAttribute) { case GraphicsOptions::BoldBright: Log::Comment(L"----Set Bold Attribute----"); textAttributes.SetBold(true); break; case GraphicsOptions::Underline: Log::Comment(L"----Set Underline Attribute----"); textAttributes.SetUnderlined(true); break; case GraphicsOptions::Negative: Log::Comment(L"----Set Negative Attribute----"); textAttributes.SetReverseVideo(true); break; } qExpectedInput.push_back(renditionSequence.str()); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Foreground----"); textAttributes.SetIndexedForeground(TextColor::DARK_GREEN); qExpectedInput.push_back("\x1b[32m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Foreground and Retain Rendition----"); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Background----"); textAttributes.SetIndexedBackground(TextColor::DARK_GREEN); qExpectedInput.push_back("\x1b[42m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Background and Retain Rendition----"); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); VerifyExpectedInputsDrained(); } void VtRendererTest::TestWrapping() { wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); std::unique_ptr engine = std::make_unique(std::move(hFile), SetUpViewport()); 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 clusters1; for (size_t i = 0; i < wcslen(line1); i++) { clusters1.emplace_back(std::wstring_view{ &line1[i], 1 }, static_cast(rgWidths[i])); } std::vector clusters2; for (size_t i = 0; i < wcslen(line2); i++) { clusters2.emplace_back(std::wstring_view{ &line2[i], 1 }, static_cast(rgWidths[i])); } VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters1.data(), clusters1.size() }, { 0, 0 }, false, false)); VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters2.data(), clusters2.size() }, { 0, 1 }, false, false)); }); } void VtRendererTest::TestResize() { Viewport view = SetUpViewport(); wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); auto engine = std::make_unique(std::move(hFile), view); 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_IS_TRUE(engine->_invalidMap.all()); 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(std::move(hFile), view); 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); 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); } void VtRendererTest::FormattedString() { // This test works with a static cache variable that // can be affected by other tests BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") END_TEST_METHOD_PROPERTIES(); static const auto format = FMT_COMPILE("\x1b[{}m"); const auto value = 12; Viewport view = SetUpViewport(); wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); auto engine = std::make_unique(std::move(hFile), view); auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); engine->SetTestCallback(pfn); Log::Comment(L"1.) Write it once. It should resize itself."); qExpectedInput.push_back("\x1b[12m"); VERIFY_SUCCEEDED(engine->_WriteFormatted(format, value)); Log::Comment(L"2.) Write the same thing again, should be fine."); qExpectedInput.push_back("\x1b[12m"); VERIFY_SUCCEEDED(engine->_WriteFormatted(format, value)); Log::Comment(L"3.) Now write something huge. Should resize itself and still be fine."); static const auto bigFormat = FMT_COMPILE("\x1b[28;3;{};{};{}m"); const auto bigValue = 500; qExpectedInput.push_back("\x1b[28;3;500;500;500m"); VERIFY_SUCCEEDED(engine->_WriteFormatted(bigFormat, bigValue, bigValue, bigValue)); }