// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "WexTestClass.h" #include "../../inc/consoletaeftemplates.hpp" #include "CommonState.hpp" #include "globals.h" #include "screenInfo.hpp" #include "input.h" #include "getset.h" #include "_stream.h" // For WriteCharsLegacy #include "../interactivity/inc/ServiceLocator.hpp" #include "../../inc/conattrs.hpp" #include "../../types/inc/Viewport.hpp" #include using namespace WEX::Common; using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace Microsoft::Console::Types; using namespace Microsoft::Console::Interactivity; using namespace Microsoft::Console::VirtualTerminal; class ScreenBufferTests { CommonState* m_state; TEST_CLASS(ScreenBufferTests); TEST_CLASS_SETUP(ClassSetup) { m_state = new CommonState(); m_state->InitEvents(); m_state->PrepareGlobalFont(); m_state->PrepareGlobalScreenBuffer(); m_state->PrepareGlobalInputBuffer(); return true; } TEST_CLASS_CLEANUP(ClassCleanup) { m_state->CleanupGlobalScreenBuffer(); m_state->CleanupGlobalFont(); m_state->CleanupGlobalInputBuffer(); delete m_state; return true; } TEST_METHOD_SETUP(MethodSetup) { // Set up some sane defaults CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, INVALID_COLOR); gci.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, INVALID_COLOR); gci.SetFillAttribute(0x07); // DARK_WHITE on DARK_BLACK gci.CalculateDefaultColorIndices(); m_state->PrepareNewTextBufferInfo(); auto& currentBuffer = gci.GetActiveOutputBuffer(); // Make sure a test hasn't left us in the alt buffer on accident VERIFY_IS_FALSE(currentBuffer._IsAltBuffer()); VERIFY_SUCCEEDED(currentBuffer.SetViewportOrigin(true, { 0, 0 }, true)); // Make sure the viewport always starts off at the default size. auto defaultSize = COORD{ CommonState::s_csWindowWidth, CommonState::s_csWindowHeight }; currentBuffer.SetViewport(Viewport::FromDimensions(defaultSize), true); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), currentBuffer.GetTextBuffer().GetCursor().GetPosition()); return true; } TEST_METHOD_CLEANUP(MethodCleanup) { m_state->CleanupNewTextBufferInfo(); return true; } TEST_METHOD(SingleAlternateBufferCreationTest); TEST_METHOD(MultipleAlternateBufferCreationTest); TEST_METHOD(MultipleAlternateBuffersFromMainCreationTest); TEST_METHOD(AlternateBufferCursorInheritanceTest); TEST_METHOD(TestReverseLineFeed); TEST_METHOD(TestResetClearTabStops); TEST_METHOD(TestAddTabStop); TEST_METHOD(TestClearTabStop); TEST_METHOD(TestGetForwardTab); TEST_METHOD(TestGetReverseTab); TEST_METHOD(TestAltBufferTabStops); TEST_METHOD(EraseAllTests); TEST_METHOD(OutputNULTest); TEST_METHOD(VtResize); TEST_METHOD(VtResizeComprehensive); TEST_METHOD(VtResizeDECCOLM); TEST_METHOD(VtSoftResetCursorPosition); TEST_METHOD(VtScrollMarginsNewlineColor); TEST_METHOD(VtNewlinePastViewport); TEST_METHOD(VtNewlinePastEndOfBuffer); TEST_METHOD(VtNewlineOutsideMargins); TEST_METHOD(VtSetColorTable); TEST_METHOD(ResizeTraditionalDoesNotDoubleFreeAttrRows); TEST_METHOD(ResizeCursorUnchanged); TEST_METHOD(ResizeAltBuffer); TEST_METHOD(ResizeAltBufferGetScreenBufferInfo); TEST_METHOD(VtEraseAllPersistCursor); TEST_METHOD(VtEraseAllPersistCursorFillColor); TEST_METHOD(GetWordBoundary); void GetWordBoundaryTrimZeros(bool on); TEST_METHOD(GetWordBoundaryTrimZerosOn); TEST_METHOD(GetWordBoundaryTrimZerosOff); TEST_METHOD(TestAltBufferCursorState); TEST_METHOD(TestAltBufferVtDispatching); TEST_METHOD(TestAltBufferRIS); TEST_METHOD(SetDefaultsIndividuallyBothDefault); TEST_METHOD(SetDefaultsTogether); TEST_METHOD(ReverseResetWithDefaultBackground); TEST_METHOD(BackspaceDefaultAttrs); TEST_METHOD(BackspaceDefaultAttrsWriteCharsLegacy); TEST_METHOD(BackspaceDefaultAttrsInPrompt); TEST_METHOD(SetGlobalColorTable); TEST_METHOD(SetColorTableThreeDigits); TEST_METHOD(SetDefaultForegroundColor); TEST_METHOD(SetDefaultBackgroundColor); TEST_METHOD(DeleteCharsNearEndOfLine); TEST_METHOD(DeleteCharsNearEndOfLineSimpleFirstCase); TEST_METHOD(DeleteCharsNearEndOfLineSimpleSecondCase); TEST_METHOD(DontResetColorsAboveVirtualBottom); TEST_METHOD(ScrollOperations); TEST_METHOD(InsertChars); TEST_METHOD(DeleteChars); TEST_METHOD(EraseScrollbackTests); TEST_METHOD(EraseTests); TEST_METHOD(ScrollUpInMargins); TEST_METHOD(ScrollDownInMargins); TEST_METHOD(InsertLinesInMargins); TEST_METHOD(DeleteLinesInMargins); TEST_METHOD(ReverseLineFeedInMargins); TEST_METHOD(LineFeedEscapeSequences); TEST_METHOD(ScrollLines256Colors); TEST_METHOD(SetScreenMode); TEST_METHOD(SetOriginMode); TEST_METHOD(SetAutoWrapMode); TEST_METHOD(HardResetBuffer); TEST_METHOD(RestoreDownAltBufferWithTerminalScrolling); TEST_METHOD(SnapCursorWithTerminalScrolling); TEST_METHOD(ClearAlternateBuffer); TEST_METHOD(TestExtendedTextAttributes); TEST_METHOD(TestExtendedTextAttributesWithColors); TEST_METHOD(CursorUpDownAcrossMargins); TEST_METHOD(CursorUpDownOutsideMargins); TEST_METHOD(CursorUpDownExactlyAtMargins); TEST_METHOD(CursorNextPreviousLine); TEST_METHOD(CursorPositionRelative); TEST_METHOD(CursorSaveRestore); TEST_METHOD(ScreenAlignmentPattern); TEST_METHOD(TestCursorIsOn); TEST_METHOD(TestAddHyperlink); TEST_METHOD(TestAddHyperlinkCustomId); TEST_METHOD(TestAddHyperlinkCustomIdDifferentUri); TEST_METHOD(UpdateVirtualBottomWhenCursorMovesBelowIt); TEST_METHOD(RetainHorizontalOffsetWhenMovingToBottom); TEST_METHOD(TestWriteConsoleVTQuirkMode); }; void ScreenBufferTests::SingleAlternateBufferCreationTest() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); Log::Comment(L"Testing creating one alternate buffer, then returning to the main buffer."); SCREEN_INFORMATION* const psiOriginal = &gci.GetActiveOutputBuffer(); VERIFY_IS_NULL(psiOriginal->_psiAlternateBuffer); VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); NTSTATUS Status = psiOriginal->UseAlternateScreenBuffer(); if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) { Log::Comment(L"First alternate buffer successfully created"); SCREEN_INFORMATION* const psiFirstAlternate = &gci.GetActiveOutputBuffer(); VERIFY_ARE_NOT_EQUAL(psiOriginal, psiFirstAlternate); VERIFY_ARE_EQUAL(psiFirstAlternate, psiOriginal->_psiAlternateBuffer); VERIFY_ARE_EQUAL(psiOriginal, psiFirstAlternate->_psiMainBuffer); VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer); psiFirstAlternate->UseMainScreenBuffer(); Log::Comment(L"successfully swapped to the main buffer"); SCREEN_INFORMATION* const psiFinal = &gci.GetActiveOutputBuffer(); VERIFY_ARE_NOT_EQUAL(psiFinal, psiFirstAlternate); VERIFY_ARE_EQUAL(psiFinal, psiOriginal); VERIFY_IS_NULL(psiFinal->_psiMainBuffer); VERIFY_IS_NULL(psiFinal->_psiAlternateBuffer); } } void ScreenBufferTests::MultipleAlternateBufferCreationTest() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); Log::Comment( L"Testing creating one alternate buffer, then creating another " L"alternate from that first alternate, before returning to the " L"main buffer."); SCREEN_INFORMATION* const psiOriginal = &gci.GetActiveOutputBuffer(); NTSTATUS Status = psiOriginal->UseAlternateScreenBuffer(); if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) { Log::Comment(L"First alternate buffer successfully created"); SCREEN_INFORMATION* const psiFirstAlternate = &gci.GetActiveOutputBuffer(); VERIFY_ARE_NOT_EQUAL(psiOriginal, psiFirstAlternate); VERIFY_ARE_EQUAL(psiFirstAlternate, psiOriginal->_psiAlternateBuffer); VERIFY_ARE_EQUAL(psiOriginal, psiFirstAlternate->_psiMainBuffer); VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer); Status = psiFirstAlternate->UseAlternateScreenBuffer(); if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) { Log::Comment(L"Second alternate buffer successfully created"); SCREEN_INFORMATION* psiSecondAlternate = &gci.GetActiveOutputBuffer(); VERIFY_ARE_NOT_EQUAL(psiOriginal, psiSecondAlternate); VERIFY_ARE_NOT_EQUAL(psiSecondAlternate, psiFirstAlternate); VERIFY_ARE_EQUAL(psiSecondAlternate, psiOriginal->_psiAlternateBuffer); VERIFY_ARE_EQUAL(psiOriginal, psiSecondAlternate->_psiMainBuffer); VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); VERIFY_IS_NULL(psiSecondAlternate->_psiAlternateBuffer); psiSecondAlternate->UseMainScreenBuffer(); Log::Comment(L"successfully swapped to the main buffer"); SCREEN_INFORMATION* const psiFinal = &gci.GetActiveOutputBuffer(); VERIFY_ARE_NOT_EQUAL(psiFinal, psiFirstAlternate); VERIFY_ARE_NOT_EQUAL(psiFinal, psiSecondAlternate); VERIFY_ARE_EQUAL(psiFinal, psiOriginal); VERIFY_IS_NULL(psiFinal->_psiMainBuffer); VERIFY_IS_NULL(psiFinal->_psiAlternateBuffer); } } } void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); Log::Comment( L"Testing creating one alternate buffer, then creating another" L" alternate from the main, before returning to the main buffer."); SCREEN_INFORMATION* const psiOriginal = &gci.GetActiveOutputBuffer(); NTSTATUS Status = psiOriginal->UseAlternateScreenBuffer(); if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) { Log::Comment(L"First alternate buffer successfully created"); SCREEN_INFORMATION* const psiFirstAlternate = &gci.GetActiveOutputBuffer(); VERIFY_ARE_NOT_EQUAL(psiOriginal, psiFirstAlternate); VERIFY_ARE_EQUAL(psiFirstAlternate, psiOriginal->_psiAlternateBuffer); VERIFY_ARE_EQUAL(psiOriginal, psiFirstAlternate->_psiMainBuffer); VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer); Status = psiOriginal->UseAlternateScreenBuffer(); if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) { Log::Comment(L"Second alternate buffer successfully created"); SCREEN_INFORMATION* const psiSecondAlternate = &gci.GetActiveOutputBuffer(); VERIFY_ARE_NOT_EQUAL(psiOriginal, psiSecondAlternate); VERIFY_ARE_NOT_EQUAL(psiSecondAlternate, psiFirstAlternate); VERIFY_ARE_EQUAL(psiSecondAlternate, psiOriginal->_psiAlternateBuffer); VERIFY_ARE_EQUAL(psiOriginal, psiSecondAlternate->_psiMainBuffer); VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); VERIFY_IS_NULL(psiSecondAlternate->_psiAlternateBuffer); psiSecondAlternate->UseMainScreenBuffer(); Log::Comment(L"successfully swapped to the main buffer"); SCREEN_INFORMATION* const psiFinal = &gci.GetActiveOutputBuffer(); VERIFY_ARE_NOT_EQUAL(psiFinal, psiFirstAlternate); VERIFY_ARE_NOT_EQUAL(psiFinal, psiSecondAlternate); VERIFY_ARE_EQUAL(psiFinal, psiOriginal); VERIFY_IS_NULL(psiFinal->_psiMainBuffer); VERIFY_IS_NULL(psiFinal->_psiAlternateBuffer); } } } void ScreenBufferTests::AlternateBufferCursorInheritanceTest() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); auto& mainBuffer = gci.GetActiveOutputBuffer(); auto& mainCursor = mainBuffer.GetTextBuffer().GetCursor(); Log::Comment(L"Set the cursor attributes in the main buffer."); auto mainCursorPos = COORD{ 3, 5 }; auto mainCursorVisible = false; auto mainCursorSize = 33u; auto mainCursorType = CursorType::DoubleUnderscore; auto mainCursorBlinking = false; mainCursor.SetPosition(mainCursorPos); mainCursor.SetIsVisible(mainCursorVisible); mainCursor.SetStyle(mainCursorSize, mainCursorType); mainCursor.SetBlinkingAllowed(mainCursorBlinking); Log::Comment(L"Switch to the alternate buffer."); VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); auto& altBuffer = gci.GetActiveOutputBuffer(); auto& altCursor = altBuffer.GetTextBuffer().GetCursor(); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); Log::Comment(L"Confirm the cursor position is inherited from the main buffer."); VERIFY_ARE_EQUAL(mainCursorPos, altCursor.GetPosition()); Log::Comment(L"Confirm the cursor visibility is inherited from the main buffer."); VERIFY_ARE_EQUAL(mainCursorVisible, altCursor.IsVisible()); Log::Comment(L"Confirm the cursor style is inherited from the main buffer."); VERIFY_ARE_EQUAL(mainCursorSize, altCursor.GetSize()); VERIFY_ARE_EQUAL(mainCursorType, altCursor.GetType()); VERIFY_ARE_EQUAL(mainCursorBlinking, altCursor.IsBlinkingAllowed()); Log::Comment(L"Set the cursor attributes in the alt buffer."); auto altCursorPos = COORD{ 5, 3 }; auto altCursorVisible = true; auto altCursorSize = 66u; auto altCursorType = CursorType::EmptyBox; auto altCursorBlinking = true; altCursor.SetPosition(altCursorPos); altCursor.SetIsVisible(altCursorVisible); altCursor.SetStyle(altCursorSize, altCursorType); altCursor.SetBlinkingAllowed(altCursorBlinking); Log::Comment(L"Switch back to the main buffer."); useMain.release(); altBuffer.UseMainScreenBuffer(); VERIFY_ARE_EQUAL(&mainBuffer, &gci.GetActiveOutputBuffer()); Log::Comment(L"Confirm the cursor position is restored to what it was."); VERIFY_ARE_EQUAL(mainCursorPos, mainCursor.GetPosition()); Log::Comment(L"Confirm the cursor visibility is inherited from the alt buffer."); VERIFY_ARE_EQUAL(altCursorVisible, mainCursor.IsVisible()); Log::Comment(L"Confirm the cursor style is inherited from the alt buffer."); VERIFY_ARE_EQUAL(altCursorSize, mainCursor.GetSize()); VERIFY_ARE_EQUAL(altCursorType, mainCursor.GetType()); VERIFY_ARE_EQUAL(altCursorBlinking, mainCursor.IsBlinkingAllowed()); } void ScreenBufferTests::TestReverseLineFeed() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); auto& stateMachine = screenInfo.GetStateMachine(); auto& cursor = screenInfo._textBuffer->GetCursor(); auto viewport = screenInfo.GetViewport(); VERIFY_ARE_EQUAL(viewport.Top(), 0); //////////////////////////////////////////////////////////////////////// Log::Comment(L"Case 1: RI from below top of viewport"); stateMachine.ProcessString(L"foo\nfoo"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1); VERIFY_ARE_EQUAL(viewport.Top(), 0); VERIFY_SUCCEEDED(DoSrvPrivateReverseLineFeed(screenInfo)); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); viewport = screenInfo.GetViewport(); VERIFY_ARE_EQUAL(viewport.Top(), 0); Log::Comment(NoThrowString().Format( L"viewport={L:%d,T:%d,R:%d,B:%d}", viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive())); //////////////////////////////////////////////////////////////////////// Log::Comment(L"Case 2: RI from top of viewport"); cursor.SetPosition({ 0, 0 }); stateMachine.ProcessString(L"123456789"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); VERIFY_ARE_EQUAL(screenInfo.GetViewport().Top(), 0); VERIFY_SUCCEEDED(DoSrvPrivateReverseLineFeed(screenInfo)); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); viewport = screenInfo.GetViewport(); VERIFY_ARE_EQUAL(viewport.Top(), 0); Log::Comment(NoThrowString().Format( L"viewport={L:%d,T:%d,R:%d,B:%d}", viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive())); auto c = screenInfo._textBuffer->GetLastNonSpaceCharacter(); VERIFY_ARE_EQUAL(c.Y, 2); // This is the coordinates of the second "foo" from before. //////////////////////////////////////////////////////////////////////// Log::Comment(L"Case 3: RI from top of viewport, when viewport is below top of buffer"); cursor.SetPosition({ 0, 5 }); VERIFY_SUCCEEDED(screenInfo.SetViewportOrigin(true, { 0, 5 }, true)); stateMachine.ProcessString(L"ABCDEFGH"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 8); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5); VERIFY_ARE_EQUAL(screenInfo.GetViewport().Top(), 5); LOG_IF_FAILED(DoSrvPrivateReverseLineFeed(screenInfo)); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 8); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5); viewport = screenInfo.GetViewport(); VERIFY_ARE_EQUAL(viewport.Top(), 5); Log::Comment(NoThrowString().Format( L"viewport={L:%d,T:%d,R:%d,B:%d}", viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive())); c = screenInfo._textBuffer->GetLastNonSpaceCharacter(); VERIFY_ARE_EQUAL(c.Y, 6); } void _SetTabStops(SCREEN_INFORMATION& screenInfo, std::list columns, bool replace) { auto& stateMachine = screenInfo.GetStateMachine(); auto& cursor = screenInfo.GetTextBuffer().GetCursor(); const auto clearTabStops = L"\033[3g"; const auto addTabStop = L"\033H"; if (replace) { stateMachine.ProcessString(clearTabStops); } for (auto column : columns) { cursor.SetXPosition(column); stateMachine.ProcessString(addTabStop); } } std::list _GetTabStops(SCREEN_INFORMATION& screenInfo) { std::list columns; const auto lastColumn = screenInfo.GetBufferSize().RightInclusive(); auto& stateMachine = screenInfo.GetStateMachine(); auto& cursor = screenInfo.GetTextBuffer().GetCursor(); cursor.SetPosition({ 0, 0 }); for (;;) { stateMachine.ProcessCharacter(L'\t'); auto column = cursor.GetPosition().X; if (column >= lastColumn) { break; } columns.push_back(column); } return columns; } void ScreenBufferTests::TestResetClearTabStops() { // Reset the screen buffer to test the defaults. m_state->CleanupGlobalScreenBuffer(); m_state->PrepareGlobalScreenBuffer(); CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); auto& stateMachine = screenInfo.GetStateMachine(); const auto clearTabStops = L"\033[3g"; const auto resetToInitialState = L"\033c"; Log::Comment(L"Default tabs every 8 columns."); std::list expectedStops{ 8, 16, 24, 32, 40, 48, 56, 64, 72 }; VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(screenInfo)); Log::Comment(L"Clear all tabs."); stateMachine.ProcessString(clearTabStops); expectedStops = {}; VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(screenInfo)); Log::Comment(L"RIS resets tabs to defaults."); stateMachine.ProcessString(resetToInitialState); expectedStops = { 8, 16, 24, 32, 40, 48, 56, 64, 72 }; VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(screenInfo)); } void ScreenBufferTests::TestAddTabStop() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); auto& stateMachine = screenInfo.GetStateMachine(); auto& cursor = screenInfo.GetTextBuffer().GetCursor(); const auto clearTabStops = L"\033[3g"; const auto addTabStop = L"\033H"; Log::Comment(L"Clear all tabs."); stateMachine.ProcessString(clearTabStops); std::list expectedStops{}; VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(screenInfo)); Log::Comment(L"Add tab to empty list."); cursor.SetXPosition(12); stateMachine.ProcessString(addTabStop); expectedStops.push_back(12); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(screenInfo)); Log::Comment(L"Add tab to head of existing list."); cursor.SetXPosition(4); stateMachine.ProcessString(addTabStop); expectedStops.push_front(4); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(screenInfo)); Log::Comment(L"Add tab to tail of existing list."); cursor.SetXPosition(30); stateMachine.ProcessString(addTabStop); expectedStops.push_back(30); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(screenInfo)); Log::Comment(L"Add tab to middle of existing list."); cursor.SetXPosition(24); stateMachine.ProcessString(addTabStop); expectedStops.push_back(24); expectedStops.sort(); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(screenInfo)); Log::Comment(L"Add tab that duplicates an item in the existing list."); cursor.SetXPosition(24); stateMachine.ProcessString(addTabStop); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(screenInfo)); } void ScreenBufferTests::TestClearTabStop() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); auto& stateMachine = screenInfo.GetStateMachine(); auto& cursor = screenInfo.GetTextBuffer().GetCursor(); const auto clearTabStops = L"\033[3g"; const auto clearTabStop = L"\033[0g"; const auto addTabStop = L"\033H"; Log::Comment(L"Start with all tabs cleared."); { stateMachine.ProcessString(clearTabStops); VERIFY_IS_TRUE(_GetTabStops(screenInfo).empty()); } Log::Comment(L"Try to clear nonexistent list."); { cursor.SetXPosition(0); stateMachine.ProcessString(clearTabStop); VERIFY_IS_TRUE(_GetTabStops(screenInfo).empty(), L"List should remain empty"); } Log::Comment(L"Allocate 1 list item and clear it."); { cursor.SetXPosition(0); stateMachine.ProcessString(addTabStop); stateMachine.ProcessString(clearTabStop); VERIFY_IS_TRUE(_GetTabStops(screenInfo).empty()); } Log::Comment(L"Allocate 1 list item and clear nonexistent."); { cursor.SetXPosition(1); stateMachine.ProcessString(addTabStop); Log::Comment(L"Free greater"); cursor.SetXPosition(2); stateMachine.ProcessString(clearTabStop); VERIFY_IS_FALSE(_GetTabStops(screenInfo).empty()); Log::Comment(L"Free less than"); cursor.SetXPosition(0); stateMachine.ProcessString(clearTabStop); VERIFY_IS_FALSE(_GetTabStops(screenInfo).empty()); // clear all tab stops stateMachine.ProcessString(clearTabStops); } Log::Comment(L"Allocate many (5) list items and clear head."); { std::list inputData = { 3, 5, 6, 10, 15, 17 }; _SetTabStops(screenInfo, inputData, false); cursor.SetXPosition(inputData.front()); stateMachine.ProcessString(clearTabStop); inputData.pop_front(); VERIFY_ARE_EQUAL(inputData, _GetTabStops(screenInfo)); // clear all tab stops stateMachine.ProcessString(clearTabStops); } Log::Comment(L"Allocate many (5) list items and clear middle."); { std::list inputData = { 3, 5, 6, 10, 15, 17 }; _SetTabStops(screenInfo, inputData, false); cursor.SetXPosition(*std::next(inputData.begin())); stateMachine.ProcessString(clearTabStop); inputData.erase(std::next(inputData.begin())); VERIFY_ARE_EQUAL(inputData, _GetTabStops(screenInfo)); // clear all tab stops stateMachine.ProcessString(clearTabStops); } Log::Comment(L"Allocate many (5) list items and clear tail."); { std::list inputData = { 3, 5, 6, 10, 15, 17 }; _SetTabStops(screenInfo, inputData, false); cursor.SetXPosition(inputData.back()); stateMachine.ProcessString(clearTabStop); inputData.pop_back(); VERIFY_ARE_EQUAL(inputData, _GetTabStops(screenInfo)); // clear all tab stops stateMachine.ProcessString(clearTabStops); } Log::Comment(L"Allocate many (5) list items and clear nonexistent item."); { std::list inputData = { 3, 5, 6, 10, 15, 17 }; _SetTabStops(screenInfo, inputData, false); cursor.SetXPosition(0); stateMachine.ProcessString(clearTabStop); VERIFY_ARE_EQUAL(inputData, _GetTabStops(screenInfo)); // clear all tab stops stateMachine.ProcessString(clearTabStops); } } void ScreenBufferTests::TestGetForwardTab() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); const auto nextForwardTab = L"\033[I"; std::list inputData = { 3, 5, 6, 10, 15, 17 }; _SetTabStops(si, inputData, true); const COORD coordScreenBufferSize = si.GetBufferSize().Dimensions(); Log::Comment(L"Find next tab from before front."); { cursor.SetXPosition(0); COORD coordCursorExpected = cursor.GetPosition(); coordCursorExpected.X = inputData.front(); stateMachine.ProcessString(nextForwardTab); COORD const coordCursorResult = cursor.GetPosition(); VERIFY_ARE_EQUAL(coordCursorExpected, coordCursorResult, L"Cursor advanced to first tab stop from sample list."); } Log::Comment(L"Find next tab from in the middle."); { cursor.SetXPosition(6); COORD coordCursorExpected = cursor.GetPosition(); coordCursorExpected.X = *std::next(inputData.begin(), 3); stateMachine.ProcessString(nextForwardTab); COORD const coordCursorResult = cursor.GetPosition(); VERIFY_ARE_EQUAL(coordCursorExpected, coordCursorResult, L"Cursor advanced to middle tab stop from sample list."); } Log::Comment(L"Find next tab from end."); { cursor.SetXPosition(30); COORD coordCursorExpected = cursor.GetPosition(); coordCursorExpected.X = coordScreenBufferSize.X - 1; stateMachine.ProcessString(nextForwardTab); COORD const coordCursorResult = cursor.GetPosition(); VERIFY_ARE_EQUAL(coordCursorExpected, coordCursorResult, L"Cursor advanced to end of screen buffer."); } Log::Comment(L"Find next tab from rightmost column."); { cursor.SetXPosition(coordScreenBufferSize.X - 1); COORD coordCursorExpected = cursor.GetPosition(); stateMachine.ProcessString(nextForwardTab); COORD const coordCursorResult = cursor.GetPosition(); VERIFY_ARE_EQUAL(coordCursorExpected, coordCursorResult, L"Cursor remains in rightmost column."); } } void ScreenBufferTests::TestGetReverseTab() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); const auto nextReverseTab = L"\033[Z"; std::list inputData = { 3, 5, 6, 10, 15, 17 }; _SetTabStops(si, inputData, true); Log::Comment(L"Find previous tab from before front."); { cursor.SetXPosition(1); COORD coordCursorExpected = cursor.GetPosition(); coordCursorExpected.X = 0; stateMachine.ProcessString(nextReverseTab); COORD const coordCursorResult = cursor.GetPosition(); VERIFY_ARE_EQUAL(coordCursorExpected, coordCursorResult, L"Cursor adjusted to beginning of the buffer when it started before sample list."); } Log::Comment(L"Find previous tab from in the middle."); { cursor.SetXPosition(6); COORD coordCursorExpected = cursor.GetPosition(); coordCursorExpected.X = *std::next(inputData.begin()); stateMachine.ProcessString(nextReverseTab); COORD const coordCursorResult = cursor.GetPosition(); VERIFY_ARE_EQUAL(coordCursorExpected, coordCursorResult, L"Cursor adjusted back one tab spot from middle of sample list."); } Log::Comment(L"Find next tab from end."); { cursor.SetXPosition(30); COORD coordCursorExpected = cursor.GetPosition(); coordCursorExpected.X = inputData.back(); stateMachine.ProcessString(nextReverseTab); COORD const coordCursorResult = cursor.GetPosition(); VERIFY_ARE_EQUAL(coordCursorExpected, coordCursorResult, L"Cursor adjusted to last item in the sample list from position beyond end."); } } void ScreenBufferTests::TestAltBufferTabStops() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to swap buffers. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer(); // Make sure we're in VT mode WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); Log::Comment(L"Add an initial set of tab in the main buffer."); std::list expectedStops = { 3, 5, 6, 10, 15, 17 }; _SetTabStops(mainBuffer, expectedStops, true); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(mainBuffer)); VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); SCREEN_INFORMATION& altBuffer = gci.GetActiveOutputBuffer(); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); Log::Comment(NoThrowString().Format( L"Manually enable VT mode for the alt buffer - " L"usually the ctor will pick this up from GCI, but not in the tests.")); WI_SetFlag(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); Log::Comment(L"Make sure the tabs are still set in the alt buffer."); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(altBuffer)); Log::Comment(L"Add a new set of tabs in the alt buffer."); expectedStops = { 4, 8, 12, 16 }; _SetTabStops(altBuffer, expectedStops, true); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(altBuffer)); Log::Comment(L"Make sure the tabs are still set in the main buffer."); useMain.release(); altBuffer.UseMainScreenBuffer(); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(mainBuffer)); } void ScreenBufferTests::EraseAllTests() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si._textBuffer->GetCursor(); VERIFY_ARE_EQUAL(si.GetViewport().Top(), 0); //////////////////////////////////////////////////////////////////////// Log::Comment(L"Case 1: Erase a single line of text in the buffer\n"); stateMachine.ProcessString(L"foo"); COORD originalRelativePosition = { 3, 0 }; VERIFY_ARE_EQUAL(si.GetViewport().Top(), 0); VERIFY_ARE_EQUAL(cursor.GetPosition(), originalRelativePosition); VERIFY_SUCCEEDED(si.VtEraseAll()); auto viewport = si._viewport; VERIFY_ARE_EQUAL(viewport.Top(), 1); COORD newRelativePos = originalRelativePosition; viewport.ConvertFromOrigin(&newRelativePos); VERIFY_ARE_EQUAL(cursor.GetPosition(), newRelativePos); Log::Comment(NoThrowString().Format( L"viewport={L:%d,T:%d,R:%d,B:%d}", viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive())); //////////////////////////////////////////////////////////////////////// Log::Comment(L"Case 2: Erase multiple lines, below the top of the buffer\n"); stateMachine.ProcessString(L"bar\nbar\nbar"); viewport = si._viewport; originalRelativePosition = cursor.GetPosition(); viewport.ConvertToOrigin(&originalRelativePosition); VERIFY_ARE_EQUAL(viewport.Top(), 1); Log::Comment(NoThrowString().Format( L"viewport={L:%d,T:%d,R:%d,B:%d}", viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive())); VERIFY_SUCCEEDED(si.VtEraseAll()); viewport = si._viewport; VERIFY_ARE_EQUAL(viewport.Top(), 4); newRelativePos = originalRelativePosition; viewport.ConvertFromOrigin(&newRelativePos); VERIFY_ARE_EQUAL(cursor.GetPosition(), newRelativePos); Log::Comment(NoThrowString().Format( L"viewport={L:%d,T:%d,R:%d,B:%d}", viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive())); //////////////////////////////////////////////////////////////////////// Log::Comment(L"Case 3: multiple lines at the bottom of the buffer\n"); cursor.SetPosition({ 0, 275 }); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 220 }, true)); stateMachine.ProcessString(L"bar\nbar\nbar"); viewport = si._viewport; VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 277); originalRelativePosition = cursor.GetPosition(); viewport.ConvertToOrigin(&originalRelativePosition); Log::Comment(NoThrowString().Format( L"viewport={L:%d,T:%d,R:%d,B:%d}", viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive())); VERIFY_SUCCEEDED(si.VtEraseAll()); viewport = si._viewport; auto heightFromBottom = si.GetBufferSize().Height() - (viewport.Height()); VERIFY_ARE_EQUAL(viewport.Top(), heightFromBottom); newRelativePos = originalRelativePosition; viewport.ConvertFromOrigin(&newRelativePos); VERIFY_ARE_EQUAL(cursor.GetPosition(), newRelativePos); Log::Comment(NoThrowString().Format( L"viewport={L:%d,T:%d,R:%d,B:%d}", viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive())); } void ScreenBufferTests::OutputNULTest() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si._textBuffer->GetCursor(); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); Log::Comment(NoThrowString().Format( L"Writing a single NUL")); stateMachine.ProcessString({ L"\0", 1 }); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); Log::Comment(NoThrowString().Format( L"Writing many NULs")); stateMachine.ProcessString({ L"\0\0\0\0\0\0\0\0", 8 }); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); Log::Comment(NoThrowString().Format( L"Testing a single NUL followed by real text")); stateMachine.ProcessString({ L"\0foo", 4 }); VERIFY_ARE_EQUAL(3, cursor.GetPosition().X); VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); stateMachine.ProcessString(L"\n"); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); Log::Comment(NoThrowString().Format( L"Writing NULs in between other strings")); stateMachine.ProcessString({ L"\0foo\0bar\0", 9 }); VERIFY_ARE_EQUAL(6, cursor.GetPosition().X); VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); } void ScreenBufferTests::VtResize() { // Run this test in isolation - for one reason or another, this breaks other tests. BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") END_TEST_METHOD_PROPERTIES() CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); cursor.SetYPosition(0); auto initialSbHeight = si.GetBufferSize().Height(); auto initialSbWidth = si.GetBufferSize().Width(); auto initialViewHeight = si.GetViewport().Height(); auto initialViewWidth = si.GetViewport().Width(); Log::Comment(NoThrowString().Format( L"Write '\x1b[8;30;80t'" L" The Screen buffer height should remain unchanged, but the width should be 80 columns" L" The viewport should be w,h=80,30")); stateMachine.ProcessString(L"\x1b[8;30;80t"); auto newSbHeight = si.GetBufferSize().Height(); auto newSbWidth = si.GetBufferSize().Width(); auto newViewHeight = si.GetViewport().Height(); auto newViewWidth = si.GetViewport().Width(); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(80, newSbWidth); VERIFY_ARE_EQUAL(30, newViewHeight); VERIFY_ARE_EQUAL(80, newViewWidth); initialSbHeight = newSbHeight; initialSbWidth = newSbWidth; initialViewHeight = newViewHeight; initialViewWidth = newViewWidth; Log::Comment(NoThrowString().Format( L"Write '\x1b[8;40;80t'" L" The Screen buffer height should remain unchanged, but the width should be 80 columns" L" The viewport should be w,h=80,40")); stateMachine.ProcessString(L"\x1b[8;40;80t"); newSbHeight = si.GetBufferSize().Height(); newSbWidth = si.GetBufferSize().Width(); newViewHeight = si.GetViewport().Height(); newViewWidth = si.GetViewport().Width(); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(80, newSbWidth); VERIFY_ARE_EQUAL(40, newViewHeight); VERIFY_ARE_EQUAL(80, newViewWidth); initialSbHeight = newSbHeight; initialSbWidth = newSbWidth; initialViewHeight = newViewHeight; initialViewWidth = newViewWidth; Log::Comment(NoThrowString().Format( L"Write '\x1b[8;40;90t'" L" The Screen buffer height should remain unchanged, but the width should be 90 columns" L" The viewport should be w,h=90,40")); stateMachine.ProcessString(L"\x1b[8;40;90t"); newSbHeight = si.GetBufferSize().Height(); newSbWidth = si.GetBufferSize().Width(); newViewHeight = si.GetViewport().Height(); newViewWidth = si.GetViewport().Width(); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(90, newSbWidth); VERIFY_ARE_EQUAL(40, newViewHeight); VERIFY_ARE_EQUAL(90, newViewWidth); initialSbHeight = newSbHeight; initialSbWidth = newSbWidth; initialViewHeight = newViewHeight; initialViewWidth = newViewWidth; Log::Comment(NoThrowString().Format( L"Write '\x1b[8;12;12t'" L" The Screen buffer height should remain unchanged, but the width should be 12 columns" L" The viewport should be w,h=12,12")); stateMachine.ProcessString(L"\x1b[8;12;12t"); newSbHeight = si.GetBufferSize().Height(); newSbWidth = si.GetBufferSize().Width(); newViewHeight = si.GetViewport().Height(); newViewWidth = si.GetViewport().Width(); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(12, newSbWidth); VERIFY_ARE_EQUAL(12, newViewHeight); VERIFY_ARE_EQUAL(12, newViewWidth); initialSbHeight = newSbHeight; initialSbWidth = newSbWidth; initialViewHeight = newViewHeight; initialViewWidth = newViewWidth; Log::Comment(NoThrowString().Format( L"Write '\x1b[8;0;0t'" L" Nothing should change")); stateMachine.ProcessString(L"\x1b[8;0;0t"); newSbHeight = si.GetBufferSize().Height(); newSbWidth = si.GetBufferSize().Width(); newViewHeight = si.GetViewport().Height(); newViewWidth = si.GetViewport().Width(); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(initialSbWidth, newSbWidth); VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); VERIFY_ARE_EQUAL(initialViewWidth, newViewWidth); } void ScreenBufferTests::VtResizeComprehensive() { // Run this test in isolation - for one reason or another, this breaks other tests. BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 0, 1, 10}") TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}") END_TEST_METHOD_PROPERTIES() int dx, dy; VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer"); VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer"); CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); cursor.SetYPosition(0); auto initialViewHeight = si.GetViewport().Height(); auto initialViewWidth = si.GetViewport().Width(); auto expectedViewWidth = initialViewWidth + dx; auto expectedViewHeight = initialViewHeight + dy; std::wstringstream ss; ss << L"\x1b[8;" << expectedViewHeight << L";" << expectedViewWidth << L"t"; Log::Comment(NoThrowString().Format( L"Write '\\x1b[8;%d;%dt'" L" The viewport should be w,h=%d,%d", expectedViewHeight, expectedViewWidth, expectedViewWidth, expectedViewHeight)); stateMachine.ProcessString(ss.str()); auto newViewHeight = si.GetViewport().Height(); auto newViewWidth = si.GetViewport().Width(); VERIFY_ARE_EQUAL(expectedViewWidth, newViewWidth); VERIFY_ARE_EQUAL(expectedViewHeight, newViewHeight); } void ScreenBufferTests::VtResizeDECCOLM() { // Run this test in isolation - for one reason or another, this breaks other tests. BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") END_TEST_METHOD_PROPERTIES() auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& stateMachine = si.GetStateMachine(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const auto setInitialMargins = L"\x1b[5;15r"; const auto setInitialCursor = L"\x1b[10;40HABCDEF"; const auto allowDECCOLM = L"\x1b[?40h"; const auto disallowDECCOLM = L"\x1b[?40l"; const auto setDECCOLM = L"\x1b[?3h"; const auto resetDECCOLM = L"\x1b[?3l"; auto getRelativeCursorPosition = [&]() { return si.GetTextBuffer().GetCursor().GetPosition() - si.GetViewport().Origin(); }; stateMachine.ProcessString(setInitialMargins); stateMachine.ProcessString(setInitialCursor); auto initialMargins = si.GetRelativeScrollMargins(); auto initialCursorPosition = getRelativeCursorPosition(); auto initialSbHeight = si.GetBufferSize().Height(); auto initialSbWidth = si.GetBufferSize().Width(); auto initialViewHeight = si.GetViewport().Height(); auto initialViewWidth = si.GetViewport().Width(); Log::Comment(L"By default, setting DECCOLM should have no effect"); stateMachine.ProcessString(setDECCOLM); auto newSbHeight = si.GetBufferSize().Height(); auto newSbWidth = si.GetBufferSize().Width(); auto newViewHeight = si.GetViewport().Height(); auto newViewWidth = si.GetViewport().Width(); VERIFY_IS_TRUE(si.AreMarginsSet()); VERIFY_ARE_EQUAL(initialMargins, si.GetRelativeScrollMargins()); VERIFY_ARE_EQUAL(initialCursorPosition, getRelativeCursorPosition()); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); VERIFY_ARE_EQUAL(initialSbWidth, newSbWidth); VERIFY_ARE_EQUAL(initialViewWidth, newViewWidth); stateMachine.ProcessString(setInitialMargins); stateMachine.ProcessString(setInitialCursor); initialSbHeight = newSbHeight; initialSbWidth = newSbWidth; initialViewHeight = newViewHeight; initialViewWidth = newViewWidth; Log::Comment( L"Once DECCOLM is allowed, setting it " L"should change the width to 132 columns " L"and reset the margins and cursor position"); stateMachine.ProcessString(allowDECCOLM); stateMachine.ProcessString(setDECCOLM); newSbHeight = si.GetBufferSize().Height(); newSbWidth = si.GetBufferSize().Width(); newViewHeight = si.GetViewport().Height(); newViewWidth = si.GetViewport().Width(); VERIFY_IS_FALSE(si.AreMarginsSet()); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), getRelativeCursorPosition()); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); VERIFY_ARE_EQUAL(132, newSbWidth); VERIFY_ARE_EQUAL(132, newViewWidth); stateMachine.ProcessString(setInitialMargins); stateMachine.ProcessString(setInitialCursor); initialMargins = si.GetRelativeScrollMargins(); initialCursorPosition = getRelativeCursorPosition(); initialSbHeight = newSbHeight; initialSbWidth = newSbWidth; initialViewHeight = newViewHeight; initialViewWidth = newViewWidth; Log::Comment(L"If DECCOLM is disallowed, resetting it should have no effect"); stateMachine.ProcessString(disallowDECCOLM); stateMachine.ProcessString(resetDECCOLM); newSbHeight = si.GetBufferSize().Height(); newSbWidth = si.GetBufferSize().Width(); newViewHeight = si.GetViewport().Height(); newViewWidth = si.GetViewport().Width(); VERIFY_IS_TRUE(si.AreMarginsSet()); VERIFY_ARE_EQUAL(initialMargins, si.GetRelativeScrollMargins()); VERIFY_ARE_EQUAL(initialCursorPosition, getRelativeCursorPosition()); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); VERIFY_ARE_EQUAL(initialSbWidth, newSbWidth); VERIFY_ARE_EQUAL(initialViewWidth, newViewWidth); stateMachine.ProcessString(setInitialMargins); stateMachine.ProcessString(setInitialCursor); initialSbHeight = newSbHeight; initialSbWidth = newSbWidth; initialViewHeight = newViewHeight; initialViewWidth = newViewWidth; Log::Comment( L"Once DECCOLM is allowed again, resetting it " L"should change the width to 80 columns " L"and reset the margins and cursor position"); stateMachine.ProcessString(allowDECCOLM); stateMachine.ProcessString(resetDECCOLM); newSbHeight = si.GetBufferSize().Height(); newSbWidth = si.GetBufferSize().Width(); newViewHeight = si.GetViewport().Height(); newViewWidth = si.GetViewport().Width(); VERIFY_IS_FALSE(si.AreMarginsSet()); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), getRelativeCursorPosition()); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); VERIFY_ARE_EQUAL(80, newSbWidth); VERIFY_ARE_EQUAL(80, newViewWidth); } void ScreenBufferTests::VtSoftResetCursorPosition() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); Log::Comment(NoThrowString().Format( L"Move the cursor to 2,2, then execute a soft reset.\n" L"The cursor should not move.")); stateMachine.ProcessString(L"\x1b[2;2H"); VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition()); stateMachine.ProcessString(L"\x1b[!p"); VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition()); Log::Comment(NoThrowString().Format( L"Set some margins. The cursor should move home.")); stateMachine.ProcessString(L"\x1b[2;10r"); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); Log::Comment(NoThrowString().Format( L"Move the cursor to 2,2, then execute a soft reset.\n" L"The cursor should not move, even though there are margins.")); stateMachine.ProcessString(L"\x1b[2;2H"); VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition()); stateMachine.ProcessString(L"\x1b[!p"); VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition()); Log::Comment( L"Set the origin mode, some margins, and move the cursor to 2,2.\n" L"The position should be relative to the top-left of the margin area."); stateMachine.ProcessString(L"\x1b[?6h"); stateMachine.ProcessString(L"\x1b[5;10r"); stateMachine.ProcessString(L"\x1b[2;2H"); VERIFY_ARE_EQUAL(COORD({ 1, 5 }), cursor.GetPosition()); Log::Comment( L"Execute a soft reset, reapply the margins, and move the cursor to 2,2.\n" L"The position should now be relative to the top-left of the screen."); stateMachine.ProcessString(L"\x1b[!p"); stateMachine.ProcessString(L"\x1b[5;10r"); stateMachine.ProcessString(L"\x1b[2;2H"); VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition()); } void ScreenBufferTests::VtScrollMarginsNewlineColor() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); cursor.SetPosition(COORD({ 0, 0 })); const COLORREF yellow = RGB(255, 255, 0); const COLORREF magenta = RGB(255, 0, 255); gci.SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, yellow); gci.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, magenta); gci.CalculateDefaultColorIndices(); const TextAttribute defaultAttrs = {}; si.SetAttributes(defaultAttrs); Log::Comment(NoThrowString().Format(L"Begin by clearing the screen.")); stateMachine.ProcessString(L"\x1b[2J"); stateMachine.ProcessString(L"\x1b[m"); Log::Comment(NoThrowString().Format( L"Set the margins to 2, 5, then emit 10 'X\\n' strings. " L"Each time, check that rows 0-10 have default attributes in their entire row.")); stateMachine.ProcessString(L"\x1b[2;5r"); // Make sure we clear the margins to not screw up another test. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); for (int iteration = 0; iteration < 10; iteration++) { Log::Comment(NoThrowString().Format( L"Iteration:%d", iteration)); stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\n"); const COORD cursorPos = cursor.GetPosition(); Log::Comment(NoThrowString().Format( L"Cursor=%s", VerifyOutputTraits::ToString(cursorPos).GetBuffer())); const auto viewport = si.GetViewport(); Log::Comment(NoThrowString().Format( L"Viewport=%s", VerifyOutputTraits::ToString(viewport.ToInclusive()).GetBuffer())); const auto viewTop = viewport.Top(); for (int y = viewTop; y < viewTop + 10; y++) { SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); const ROW& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; for (int x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; VERIFY_ARE_EQUAL(false, attr.IsLegacy()); VERIFY_ARE_EQUAL(defaultAttrs, attr); VERIFY_ARE_EQUAL(std::make_pair(yellow, magenta), gci.LookupAttributeColors(attr)); } } } } void ScreenBufferTests::VtNewlinePastViewport() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = si.GetTextBuffer().GetCursor(); // Make sure we're in VT mode WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); cursor.SetPosition(COORD({ 0, 0 })); stateMachine.ProcessString(L"\x1b[m"); stateMachine.ProcessString(L"\x1b[2J"); const TextAttribute defaultAttrs{}; Log::Comment(NoThrowString().Format( L"Move the cursor to the bottom of the viewport")); const auto initialViewport = si.GetViewport(); Log::Comment(NoThrowString().Format( L"initialViewport=%s", VerifyOutputTraits::ToString(initialViewport.ToInclusive()).GetBuffer())); cursor.SetPosition(COORD({ 0, initialViewport.BottomInclusive() })); // Set the attributes that will be used to initialize new rows. auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; expectedFillAttr.SetStandardErase(); stateMachine.ProcessString(L"\n"); const auto viewport = si.GetViewport(); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(viewport.ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().Y); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); for (int y = viewport.Top(); y < viewport.BottomInclusive(); y++) { SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); const ROW& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; for (int x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; VERIFY_ARE_EQUAL(defaultAttrs, attr); } } const ROW& row = tbi.GetRowByOffset(viewport.BottomInclusive()); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; for (int x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; VERIFY_ARE_EQUAL(expectedFillAttr, attr); } } void ScreenBufferTests::VtNewlinePastEndOfBuffer() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = si.GetTextBuffer().GetCursor(); // Make sure we're in VT mode WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); cursor.SetPosition(COORD({ 0, 0 })); stateMachine.ProcessString(L"\x1b[m"); stateMachine.ProcessString(L"\x1b[2J"); const TextAttribute defaultAttrs{}; Log::Comment(L"Move the cursor to the bottom of the buffer"); for (auto i = 0; i < si.GetBufferSize().Height(); i++) { stateMachine.ProcessString(L"\n"); } const auto initialViewport = si.GetViewport(); Log::Comment(NoThrowString().Format( L"initialViewport=%s", VerifyOutputTraits::ToString(initialViewport.ToInclusive()).GetBuffer())); cursor.SetPosition(COORD({ 0, initialViewport.BottomInclusive() })); // Set the attributes that will be used to initialize new rows. auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; expectedFillAttr.SetStandardErase(); stateMachine.ProcessString(L"\n"); const auto viewport = si.GetViewport(); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(viewport.ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().Y); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); for (int y = viewport.Top(); y < viewport.BottomInclusive(); y++) { SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); const ROW& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; for (int x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; VERIFY_ARE_EQUAL(defaultAttrs, attr); } } const ROW& row = tbi.GetRowByOffset(viewport.BottomInclusive()); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; for (int x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; VERIFY_ARE_EQUAL(expectedFillAttr, attr); } } void ScreenBufferTests::VtNewlineOutsideMargins() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); const auto viewportTop = si.GetViewport().Top(); const auto viewportBottom = si.GetViewport().BottomInclusive(); // Make sure the bottom margin will fit inside the viewport. VERIFY_IS_TRUE(si.GetViewport().Height() > 5); Log::Comment(L"LF at bottom of viewport scrolls the viewport"); cursor.SetPosition({ 0, viewportBottom }); stateMachine.ProcessString(L"\n"); VERIFY_ARE_EQUAL(COORD({ 0, viewportBottom + 1 }), cursor.GetPosition()); VERIFY_ARE_EQUAL(COORD({ 0, viewportTop + 1 }), si.GetViewport().Origin()); Log::Comment(L"Reset viewport and apply DECSTBM margins"); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, viewportTop }), true)); stateMachine.ProcessString(L"\x1b[1;5r"); // Make sure we clear the margins on exit so they can't break other tests. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); Log::Comment(L"LF no longer scrolls the viewport when below bottom margin"); cursor.SetPosition({ 0, viewportBottom }); stateMachine.ProcessString(L"\n"); VERIFY_ARE_EQUAL(COORD({ 0, viewportBottom }), cursor.GetPosition()); VERIFY_ARE_EQUAL(COORD({ 0, viewportTop }), si.GetViewport().Origin()); } void ScreenBufferTests::VtSetColorTable() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); StateMachine& stateMachine = si.GetStateMachine(); // Start with a known value gci.SetColorTableEntry(0, RGB(0, 0, 0)); Log::Comment(NoThrowString().Format( L"Process some valid sequences for setting the table")); stateMachine.ProcessString(L"\x1b]4;0;rgb:1/1/1\x7"); VERIFY_ARE_EQUAL(RGB(0x11, 0x11, 0x11), gci.GetColorTableEntry(0)); stateMachine.ProcessString(L"\x1b]4;1;rgb:1/23/1\x7"); VERIFY_ARE_EQUAL(RGB(0x11, 0x23, 0x11), gci.GetColorTableEntry(1)); stateMachine.ProcessString(L"\x1b]4;2;rgb:1/23/12\x7"); VERIFY_ARE_EQUAL(RGB(0x11, 0x23, 0x12), gci.GetColorTableEntry(2)); stateMachine.ProcessString(L"\x1b]4;3;rgb:12/23/12\x7"); VERIFY_ARE_EQUAL(RGB(0x12, 0x23, 0x12), gci.GetColorTableEntry(3)); stateMachine.ProcessString(L"\x1b]4;4;rgb:ff/a1/1b\x7"); VERIFY_ARE_EQUAL(RGB(0xff, 0xa1, 0x1b), gci.GetColorTableEntry(4)); stateMachine.ProcessString(L"\x1b]4;5;rgb:ff/a1/1b\x1b\\"); VERIFY_ARE_EQUAL(RGB(0xff, 0xa1, 0x1b), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"Try a bunch of invalid sequences.")); Log::Comment(NoThrowString().Format( L"First start by setting an entry to a known value to compare to.")); stateMachine.ProcessString(L"\x1b]4;5;rgb:09/09/09\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"invalid: Missing the first component")); stateMachine.ProcessString(L"\x1b]4;5;rgb:/1/1\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"invalid: too many components")); stateMachine.ProcessString(L"\x1b]4;5;rgb:1/1/1/1\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"invalid: no second component")); stateMachine.ProcessString(L"\x1b]4;5;rgb:1//1\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"invalid: no components")); stateMachine.ProcessString(L"\x1b]4;5;rgb://\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"invalid: no third component")); stateMachine.ProcessString(L"\x1b]4;5;rgb:1/11/\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"invalid: rgbi is not a supported color space")); stateMachine.ProcessString(L"\x1b]4;5;rgbi:1/1/1\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"invalid: cmyk is not a supported color space")); stateMachine.ProcessString(L"\x1b]4;5;cmyk:1/1/1\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"invalid: no table index should do nothing")); stateMachine.ProcessString(L"\x1b]4;;rgb:1/1/1\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); Log::Comment(NoThrowString().Format( L"invalid: need to specify a color space")); stateMachine.ProcessString(L"\x1b]4;5;1/1/1\x1b\\"); VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5)); } void ScreenBufferTests::ResizeTraditionalDoesNotDoubleFreeAttrRows() { // there is not much to verify here, this test passes if the console doesn't crash. CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); gci.SetWrapText(false); COORD newBufferSize = si.GetBufferSize().Dimensions(); newBufferSize.Y--; VERIFY_SUCCEEDED(si.ResizeTraditional(newBufferSize)); } void ScreenBufferTests::ResizeCursorUnchanged() { // Created for MSFT:19863799. Make sure when we resize the buffer, the // cursor looks the same as it did before. BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:useResizeWithReflow", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 0, 1, 10}") TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}") END_TEST_METHOD_PROPERTIES(); bool useResizeWithReflow; VERIFY_SUCCEEDED(TestData::TryGetValue(L"useResizeWithReflow", useResizeWithReflow), L"Use ResizeWithReflow or not"); int dx, dy; VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer"); VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer"); CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const auto& initialCursor = si.GetTextBuffer().GetCursor(); // Get initial cursor values const CursorType initialType = initialCursor.GetType(); const auto initialSize = initialCursor.GetSize(); // set our wrap mode accordingly - ResizeScreenBuffer will be smart enough // to call the appropriate implementation gci.SetWrapText(useResizeWithReflow); COORD newBufferSize = si.GetBufferSize().Dimensions(); newBufferSize.X += static_cast(dx); newBufferSize.Y += static_cast(dy); VERIFY_SUCCEEDED(si.ResizeScreenBuffer(newBufferSize, false)); const auto& finalCursor = si.GetTextBuffer().GetCursor(); const CursorType finalType = finalCursor.GetType(); const auto finalSize = finalCursor.GetSize(); VERIFY_ARE_EQUAL(initialType, finalType); VERIFY_ARE_EQUAL(initialSize, finalSize); } void ScreenBufferTests::ResizeAltBuffer() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Log::Comment(NoThrowString().Format( L"Try resizing the alt buffer. Make sure the call doesn't stack overflow.")); VERIFY_IS_FALSE(si._IsAltBuffer()); const Viewport originalMainSize = Viewport(si._viewport); Log::Comment(NoThrowString().Format( L"Switch to alt buffer")); stateMachine.ProcessString(L"\x1b[?1049h"); VERIFY_IS_FALSE(si._IsAltBuffer()); VERIFY_IS_NOT_NULL(si._psiAlternateBuffer); SCREEN_INFORMATION* const psiAlt = si._psiAlternateBuffer; COORD newSize = originalMainSize.Dimensions(); newSize.X += 2; newSize.Y += 2; Log::Comment(NoThrowString().Format( L"MSFT:15917333 This call shouldn't stack overflow")); psiAlt->SetViewportSize(&newSize); VERIFY_IS_TRUE(true); Log::Comment(NoThrowString().Format( L"Switch back from buffer")); stateMachine.ProcessString(L"\x1b[?1049l"); VERIFY_IS_FALSE(si._IsAltBuffer()); VERIFY_IS_NULL(si._psiAlternateBuffer); } void ScreenBufferTests::ResizeAltBufferGetScreenBufferInfo() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 1, 10}") TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 1, 10}") END_TEST_METHOD_PROPERTIES(); int dx, dy; VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer"); VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer"); // Tests MSFT:19918103 Log::Comment(NoThrowString().Format( L"Switch to the alt buffer, then resize the buffer. " L"GetConsoleScreenBufferInfoEx(mainBuffer) should return the alt " L"buffer's size, not the main buffer's size.")); auto& g = ServiceLocator::LocateGlobals(); CONSOLE_INFORMATION& gci = g.getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer().GetActiveBuffer(); StateMachine& stateMachine = mainBuffer.GetStateMachine(); VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); const Viewport originalMainSize = Viewport(mainBuffer._viewport); Log::Comment(NoThrowString().Format( L"Switch to alt buffer")); stateMachine.ProcessString(L"\x1b[?1049h"); VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); VERIFY_IS_NOT_NULL(mainBuffer._psiAlternateBuffer); auto& altBuffer = *(mainBuffer._psiAlternateBuffer); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); COORD newBufferSize = originalMainSize.Dimensions(); newBufferSize.X += static_cast(dx); newBufferSize.Y += static_cast(dy); const Viewport originalAltSize = Viewport(altBuffer._viewport); VERIFY_ARE_EQUAL(originalMainSize.Width(), originalAltSize.Width()); VERIFY_ARE_EQUAL(originalMainSize.Height(), originalAltSize.Height()); altBuffer.SetViewportSize(&newBufferSize); CONSOLE_SCREEN_BUFFER_INFOEX csbiex{ 0 }; g.api.GetConsoleScreenBufferInfoExImpl(mainBuffer, csbiex); const auto newActualMainView = mainBuffer.GetViewport(); const auto newActualAltView = altBuffer.GetViewport(); const auto newApiViewport = Viewport::FromExclusive(csbiex.srWindow); VERIFY_ARE_NOT_EQUAL(originalAltSize.Width(), newActualAltView.Width()); VERIFY_ARE_NOT_EQUAL(originalAltSize.Height(), newActualAltView.Height()); VERIFY_ARE_NOT_EQUAL(originalMainSize.Width(), newActualAltView.Width()); VERIFY_ARE_NOT_EQUAL(originalMainSize.Height(), newActualAltView.Height()); VERIFY_ARE_EQUAL(newActualAltView.Width(), newApiViewport.Width()); VERIFY_ARE_EQUAL(newActualAltView.Height(), newApiViewport.Height()); } void ScreenBufferTests::VtEraseAllPersistCursor() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); Log::Comment(NoThrowString().Format( L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); Log::Comment(NoThrowString().Format( L"Move the cursor to 2,2, then execute a Erase All.\n" L"The cursor should not move relative to the viewport.")); stateMachine.ProcessString(L"\x1b[2;2H"); VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition()); stateMachine.ProcessString(L"\x1b[2J"); auto newViewport = si._viewport; COORD expectedCursor = { 1, 1 }; newViewport.ConvertFromOrigin(&expectedCursor); VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); } void ScreenBufferTests::VtEraseAllPersistCursorFillColor() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Log::Comment(NoThrowString().Format( L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); Log::Comment(NoThrowString().Format( L"Change the colors to dark_red on bright_blue, then execute a Erase All.\n" L"The viewport should be full of dark_red on bright_blue")); auto expectedAttr = TextAttribute{}; expectedAttr.SetIndexedForeground(TextColor::DARK_RED); expectedAttr.SetIndexedBackground(TextColor::BRIGHT_BLUE); stateMachine.ProcessString(L"\x1b[31;104m"); VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes()); stateMachine.ProcessString(L"\x1b[2J"); VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes()); auto newViewport = si._viewport; Log::Comment(NoThrowString().Format( L"new Viewport: %s", VerifyOutputTraits::ToString(newViewport.ToInclusive()).GetBuffer())); Log::Comment(NoThrowString().Format( L"Buffer Size: %s", VerifyOutputTraits::ToString(si.GetBufferSize().ToInclusive()).GetBuffer())); auto iter = tbi.GetCellDataAt(newViewport.Origin()); auto height = newViewport.Height(); auto width = newViewport.Width(); for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { VERIFY_ARE_EQUAL(expectedAttr, iter->TextAttr()); iter++; } } } void ScreenBufferTests::GetWordBoundary() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const auto text = L"This is some test text for word boundaries."; const auto length = wcslen(text); // Make the buffer as big as our test text. const COORD newBufferSize = { gsl::narrow(length), 10 }; VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); const OutputCellIterator it(text, si.GetAttributes()); si.Write(it, { 0, 0 }); // Now find some words in it. Log::Comment(L"Find first word from its front."); COORD expectedFirst = { 0, 0 }; COORD expectedSecond = { 4, 0 }; auto boundary = si.GetWordBoundary({ 0, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find first word from its middle."); boundary = si.GetWordBoundary({ 1, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find first word from its end."); boundary = si.GetWordBoundary({ 3, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find middle word from its front."); expectedFirst = { 13, 0 }; expectedSecond = { 17, 0 }; boundary = si.GetWordBoundary({ 13, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find middle word from its middle."); boundary = si.GetWordBoundary({ 15, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find middle word from its end."); boundary = si.GetWordBoundary({ 16, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find end word from its front."); expectedFirst = { 32, 0 }; expectedSecond = { 43, 0 }; boundary = si.GetWordBoundary({ 32, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find end word from its middle."); boundary = si.GetWordBoundary({ 39, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find end word from its end."); boundary = si.GetWordBoundary({ 43, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find a word starting from a boundary character."); expectedFirst = { 8, 0 }; expectedSecond = { 12, 0 }; boundary = si.GetWordBoundary({ 12, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); } void ScreenBufferTests::GetWordBoundaryTrimZeros(const bool on) { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const auto text = L"000fe12 0xfe12 0Xfe12 0nfe12 0Nfe12"; const auto length = wcslen(text); // Make the buffer as big as our test text. const COORD newBufferSize = { gsl::narrow(length), 10 }; VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); const OutputCellIterator it(text, si.GetAttributes()); si.Write(it, { 0, 0 }); gci.SetTrimLeadingZeros(on); COORD expectedFirst; COORD expectedSecond; std::pair boundary; Log::Comment(L"Find lead with 000"); expectedFirst = on ? COORD{ 3, 0 } : COORD{ 0, 0 }; expectedSecond = COORD{ 7, 0 }; boundary = si.GetWordBoundary({ 0, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find lead with 0x"); expectedFirst = COORD{ 8, 0 }; expectedSecond = COORD{ 14, 0 }; boundary = si.GetWordBoundary({ 8, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find lead with 0X"); expectedFirst = COORD{ 15, 0 }; expectedSecond = COORD{ 21, 0 }; boundary = si.GetWordBoundary({ 15, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find lead with 0n"); expectedFirst = COORD{ 22, 0 }; expectedSecond = COORD{ 28, 0 }; boundary = si.GetWordBoundary({ 22, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); Log::Comment(L"Find lead with 0N"); expectedFirst = on ? COORD{ 30, 0 } : COORD{ 29, 0 }; expectedSecond = COORD{ 35, 0 }; boundary = si.GetWordBoundary({ 29, 0 }); VERIFY_ARE_EQUAL(expectedFirst, boundary.first); VERIFY_ARE_EQUAL(expectedSecond, boundary.second); } void ScreenBufferTests::GetWordBoundaryTrimZerosOn() { GetWordBoundaryTrimZeros(true); } void ScreenBufferTests::GetWordBoundaryTrimZerosOff() { GetWordBoundaryTrimZeros(false); } void ScreenBufferTests::TestAltBufferCursorState() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); Log::Comment(L"Creating one alternate buffer"); auto& original = gci.GetActiveOutputBuffer(); VERIFY_IS_NULL(original._psiAlternateBuffer); VERIFY_IS_NULL(original._psiMainBuffer); NTSTATUS Status = original.UseAlternateScreenBuffer(); if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) { Log::Comment(L"Alternate buffer successfully created"); auto& alternate = gci.GetActiveOutputBuffer(); // Make sure that when the test is done, we switch back to the main buffer. // Otherwise, one test could pollute another. auto useMain = wil::scope_exit([&] { alternate.UseMainScreenBuffer(); }); const auto* pMain = &original; const auto* pAlt = &alternate; // Validate that the pointers were mapped appropriately to link // alternate and main buffers VERIFY_ARE_NOT_EQUAL(pMain, pAlt); VERIFY_ARE_EQUAL(pAlt, original._psiAlternateBuffer); VERIFY_ARE_EQUAL(pMain, alternate._psiMainBuffer); VERIFY_IS_NULL(original._psiMainBuffer); VERIFY_IS_NULL(alternate._psiAlternateBuffer); auto& mainCursor = original.GetTextBuffer().GetCursor(); auto& altCursor = alternate.GetTextBuffer().GetCursor(); // Validate that the cursor state was copied appropriately into the // alternate buffer VERIFY_ARE_EQUAL(mainCursor.GetSize(), altCursor.GetSize()); VERIFY_ARE_EQUAL(mainCursor.GetType(), altCursor.GetType()); } } void ScreenBufferTests::TestAltBufferVtDispatching() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); Log::Comment(L"Creating one alternate buffer"); auto& mainBuffer = gci.GetActiveOutputBuffer(); // Make sure we're in VT mode WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); // Make sure we're suing the default attributes at the start of the test, // Otherwise they could be polluted from a previous test. mainBuffer.SetAttributes({}); VERIFY_IS_NULL(mainBuffer._psiAlternateBuffer); VERIFY_IS_NULL(mainBuffer._psiMainBuffer); NTSTATUS Status = mainBuffer.UseAlternateScreenBuffer(); if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) { Log::Comment(L"Alternate buffer successfully created"); auto& alternate = gci.GetActiveOutputBuffer(); // Make sure that when the test is done, we switch back to the main buffer. // Otherwise, one test could pollute another. auto useMain = wil::scope_exit([&] { alternate.UseMainScreenBuffer(); }); // Manually turn on VT mode - usually gci enables this for you. WI_SetFlag(alternate.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const auto* pMain = &mainBuffer; const auto* pAlt = &alternate; // Validate that the pointers were mapped appropriately to link // alternate and main buffers VERIFY_ARE_NOT_EQUAL(pMain, pAlt); VERIFY_ARE_EQUAL(pAlt, mainBuffer._psiAlternateBuffer); VERIFY_ARE_EQUAL(pMain, alternate._psiMainBuffer); VERIFY_IS_NULL(mainBuffer._psiMainBuffer); VERIFY_IS_NULL(alternate._psiAlternateBuffer); auto& mainCursor = mainBuffer.GetTextBuffer().GetCursor(); auto& altCursor = alternate.GetTextBuffer().GetCursor(); const COORD origin = { 0, 0 }; mainCursor.SetPosition(origin); altCursor.SetPosition(origin); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(mainBuffer.SetViewportOrigin(true, origin, true)); VERIFY_SUCCEEDED(alternate.SetViewportOrigin(true, origin, true)); VERIFY_ARE_EQUAL(origin, mainCursor.GetPosition()); VERIFY_ARE_EQUAL(origin, altCursor.GetPosition()); // We're going to write some data to either the main buffer or the alt // buffer, as if we were using the API. std::unique_ptr waiter; std::wstring seq = L"\x1b[5;6H"; size_t seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, false, waiter)); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition()); // recall: vt coordinates are (row, column), 1-indexed VERIFY_ARE_EQUAL(COORD({ 5, 4 }), altCursor.GetPosition()); const TextAttribute expectedDefaults = {}; TextAttribute expectedRgb = expectedDefaults; expectedRgb.SetBackground(RGB(255, 0, 255)); VERIFY_ARE_EQUAL(expectedDefaults, mainBuffer.GetAttributes()); VERIFY_ARE_EQUAL(expectedDefaults, alternate.GetAttributes()); seq = L"\x1b[48;2;255;0;255m"; seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, false, waiter)); VERIFY_ARE_EQUAL(expectedDefaults, mainBuffer.GetAttributes()); VERIFY_ARE_EQUAL(expectedRgb, alternate.GetAttributes()); seq = L"X"; seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, false, waiter)); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition()); VERIFY_ARE_EQUAL(COORD({ 6, 4 }), altCursor.GetPosition()); // Recall we didn't print an 'X' to the main buffer, so there's no // char to inspect the attributes of. const ROW& altRow = alternate.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); const auto altAttrRow = &altRow.GetAttrRow(); const std::vector altAttrs{ altAttrRow->begin(), altAttrRow->end() }; const auto altAttrA = altAttrs[altCursor.GetPosition().X - 1]; VERIFY_ARE_EQUAL(expectedRgb, altAttrA); } } void ScreenBufferTests::TestAltBufferRIS() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Log::Comment(L"Initially in main buffer"); VERIFY_IS_FALSE(gci.GetActiveOutputBuffer()._IsAltBuffer()); Log::Comment(L"Switch to alt buffer"); stateMachine.ProcessString(L"\x1b[?1049h"); VERIFY_IS_TRUE(gci.GetActiveOutputBuffer()._IsAltBuffer()); Log::Comment(L"RIS returns to main buffer"); stateMachine.ProcessString(L"\033c"); VERIFY_IS_FALSE(gci.GetActiveOutputBuffer()._IsAltBuffer()); } void ScreenBufferTests::SetDefaultsIndividuallyBothDefault() { // Tests MSFT:19828103 CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); cursor.SetPosition({ 0, 0 }); COLORREF magenta = RGB(255, 0, 255); COLORREF yellow = RGB(255, 255, 0); COLORREF brightGreen = gci.GetColorTableEntry(TextColor::BRIGHT_GREEN); COLORREF darkBlue = gci.GetColorTableEntry(TextColor::DARK_BLUE); gci.SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, yellow); gci.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, magenta); gci.CalculateDefaultColorIndices(); si.SetDefaultAttributes({}, TextAttribute{ gci.GetPopupFillAttribute() }); Log::Comment(NoThrowString().Format(L"Write 6 X's:")); Log::Comment(NoThrowString().Format(L" The first in default-fg on default-bg (yellow on magenta)")); Log::Comment(NoThrowString().Format(L" The second with bright-green on dark-blue")); Log::Comment(NoThrowString().Format(L" The third with default-fg on dark-blue")); Log::Comment(NoThrowString().Format(L" The fourth in default-fg on default-bg (yellow on magenta)")); Log::Comment(NoThrowString().Format(L" The fifth with bright-green on dark-blue")); Log::Comment(NoThrowString().Format(L" The sixth with bright-green on default-bg")); stateMachine.ProcessString(L"\x1b[m"); // Reset to defaults stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\x1b[92;44m"); // bright-green on dark-blue stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\x1b[39m"); // reset fg stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\x1b[49m"); // reset bg stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\x1b[92;44m"); // bright-green on dark-blue stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\x1b[49m"); // reset bg stateMachine.ProcessString(L"X"); // See the log comment above for description of these values. TextAttribute expectedDefaults{}; TextAttribute expectedTwo; expectedTwo.SetIndexedForeground(TextColor::BRIGHT_GREEN); expectedTwo.SetIndexedBackground(TextColor::DARK_BLUE); TextAttribute expectedThree = expectedTwo; expectedThree.SetDefaultForeground(); // Four is the same as Defaults // Five is the same as two TextAttribute expectedSix = expectedTwo; expectedSix.SetDefaultBackground(); COORD expectedCursor{ 6, 0 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const ROW& row = tbi.GetRowByOffset(0); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; const auto attrC = attrs[2]; const auto attrD = attrs[3]; const auto attrE = attrs[4]; const auto attrF = attrs[5]; LOG_ATTR(attrA); LOG_ATTR(attrB); LOG_ATTR(attrC); LOG_ATTR(attrD); LOG_ATTR(attrE); LOG_ATTR(attrF); VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); VERIFY_ARE_EQUAL(true, attrB.IsLegacy()); VERIFY_ARE_EQUAL(false, attrC.IsLegacy()); VERIFY_ARE_EQUAL(false, attrD.IsLegacy()); VERIFY_ARE_EQUAL(true, attrE.IsLegacy()); VERIFY_ARE_EQUAL(false, attrF.IsLegacy()); VERIFY_ARE_EQUAL(expectedDefaults, attrA); VERIFY_ARE_EQUAL(expectedTwo, attrB); VERIFY_ARE_EQUAL(expectedThree, attrC); VERIFY_ARE_EQUAL(expectedDefaults, attrD); VERIFY_ARE_EQUAL(expectedTwo, attrE); VERIFY_ARE_EQUAL(expectedSix, attrF); VERIFY_ARE_EQUAL(std::make_pair(yellow, magenta), gci.LookupAttributeColors(attrA)); VERIFY_ARE_EQUAL(std::make_pair(brightGreen, darkBlue), gci.LookupAttributeColors(attrB)); VERIFY_ARE_EQUAL(std::make_pair(yellow, darkBlue), gci.LookupAttributeColors(attrC)); VERIFY_ARE_EQUAL(std::make_pair(yellow, magenta), gci.LookupAttributeColors(attrD)); VERIFY_ARE_EQUAL(std::make_pair(brightGreen, darkBlue), gci.LookupAttributeColors(attrE)); VERIFY_ARE_EQUAL(std::make_pair(brightGreen, magenta), gci.LookupAttributeColors(attrF)); } void ScreenBufferTests::SetDefaultsTogether() { // Tests MSFT:19828103 CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); cursor.SetPosition({ 0, 0 }); COLORREF magenta = RGB(255, 0, 255); COLORREF yellow = RGB(255, 255, 0); COLORREF color250 = gci.GetColorTableEntry(250); gci.SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, yellow); gci.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, magenta); gci.CalculateDefaultColorIndices(); si.SetDefaultAttributes({}, TextAttribute{ gci.GetPopupFillAttribute() }); Log::Comment(NoThrowString().Format(L"Write 6 X's:")); Log::Comment(NoThrowString().Format(L" The first in default-fg on default-bg (yellow on magenta)")); Log::Comment(NoThrowString().Format(L" The second with default-fg on xterm(250)")); Log::Comment(NoThrowString().Format(L" The third with defaults again")); std::wstring seq = L"\x1b[m"; // Reset to defaults stateMachine.ProcessString(seq); seq = L"X"; stateMachine.ProcessString(seq); seq = L"\x1b[48;5;250m"; // bright-green on dark-blue stateMachine.ProcessString(seq); seq = L"X"; stateMachine.ProcessString(seq); seq = L"\x1b[39;49m"; // reset fg stateMachine.ProcessString(seq); seq = L"X"; stateMachine.ProcessString(seq); // See the log comment above for description of these values. TextAttribute expectedDefaults{}; TextAttribute expectedTwo{}; expectedTwo.SetIndexedBackground256(250); COORD expectedCursor{ 3, 0 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const ROW& row = tbi.GetRowByOffset(0); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; const auto attrC = attrs[2]; LOG_ATTR(attrA); LOG_ATTR(attrB); LOG_ATTR(attrC); VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); VERIFY_ARE_EQUAL(false, attrB.IsLegacy()); VERIFY_ARE_EQUAL(false, attrC.IsLegacy()); VERIFY_ARE_EQUAL(expectedDefaults, attrA); VERIFY_ARE_EQUAL(expectedTwo, attrB); VERIFY_ARE_EQUAL(expectedDefaults, attrC); VERIFY_ARE_EQUAL(std::make_pair(yellow, magenta), gci.LookupAttributeColors(attrA)); VERIFY_ARE_EQUAL(std::make_pair(yellow, color250), gci.LookupAttributeColors(attrB)); VERIFY_ARE_EQUAL(std::make_pair(yellow, magenta), gci.LookupAttributeColors(attrC)); } void ScreenBufferTests::ReverseResetWithDefaultBackground() { // Tests MSFT:19694089 CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); cursor.SetPosition({ 0, 0 }); COLORREF magenta = RGB(255, 0, 255); gci.SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, INVALID_COLOR); gci.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, magenta); gci.CalculateDefaultColorIndices(); si.SetDefaultAttributes({}, TextAttribute{ gci.GetPopupFillAttribute() }); Log::Comment(NoThrowString().Format(L"Write 3 X's:")); Log::Comment(NoThrowString().Format(L" The first in default-attr on default color (magenta)")); Log::Comment(NoThrowString().Format(L" The second with reversed attrs")); Log::Comment(NoThrowString().Format(L" The third after resetting the attrs back")); stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\x1b[7m"); stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\x1b[27m"); stateMachine.ProcessString(L"X"); TextAttribute expectedDefaults{ gci.GetFillAttribute() }; expectedDefaults.SetDefaultBackground(); TextAttribute expectedReversed = expectedDefaults; expectedReversed.Invert(); COORD expectedCursor{ 3, 0 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const ROW& row = tbi.GetRowByOffset(0); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; const auto attrC = attrs[2]; LOG_ATTR(attrA); LOG_ATTR(attrB); LOG_ATTR(attrC); VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); VERIFY_ARE_EQUAL(false, attrB.IsLegacy()); VERIFY_ARE_EQUAL(false, attrC.IsLegacy()); VERIFY_ARE_EQUAL(false, attrA.IsReverseVideo()); VERIFY_ARE_EQUAL(true, attrB.IsReverseVideo()); VERIFY_ARE_EQUAL(false, attrC.IsReverseVideo()); VERIFY_ARE_EQUAL(expectedDefaults, attrA); VERIFY_ARE_EQUAL(expectedReversed, attrB); VERIFY_ARE_EQUAL(expectedDefaults, attrC); VERIFY_ARE_EQUAL(magenta, gci.LookupAttributeColors(attrA).second); VERIFY_ARE_EQUAL(magenta, gci.LookupAttributeColors(attrB).first); VERIFY_ARE_EQUAL(magenta, gci.LookupAttributeColors(attrC).second); } void ScreenBufferTests::BackspaceDefaultAttrs() { // Created for MSFT:19735050, but doesn't actually test that. // That bug actually involves the input line, and that needs to use // TextAttributes instead of WORDs CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); cursor.SetPosition({ 0, 0 }); COLORREF magenta = RGB(255, 0, 255); gci.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, magenta); gci.CalculateDefaultColorIndices(); si.SetDefaultAttributes({}, TextAttribute{ gci.GetPopupFillAttribute() }); Log::Comment(NoThrowString().Format(L"Write 2 X's, then backspace one.")); stateMachine.ProcessString(L"\x1b[m"); stateMachine.ProcessString(L"XX"); stateMachine.ProcessString({ &UNICODE_BACKSPACE, 1 }); TextAttribute expectedDefaults{}; expectedDefaults.SetDefaultBackground(); COORD expectedCursor{ 1, 0 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const ROW& row = tbi.GetRowByOffset(0); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); VERIFY_ARE_EQUAL(false, attrB.IsLegacy()); VERIFY_ARE_EQUAL(expectedDefaults, attrA); VERIFY_ARE_EQUAL(expectedDefaults, attrB); VERIFY_ARE_EQUAL(magenta, gci.LookupAttributeColors(attrA).second); VERIFY_ARE_EQUAL(magenta, gci.LookupAttributeColors(attrB).second); } void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:writeSingly", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:writeCharsLegacyMode", L"{0, 1, 2, 3, 4, 5, 6, 7}") END_TEST_METHOD_PROPERTIES(); bool writeSingly; VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeSingly", writeSingly), L"Write one at a time = true, all at the same time = false"); DWORD writeCharsLegacyMode; VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeCharsLegacyMode", writeCharsLegacyMode), L""); // Created for MSFT:19735050. // Kinda the same as above, but with WriteCharsLegacy instead. // The variable that really breaks this scenario CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); cursor.SetPosition({ 0, 0 }); COLORREF magenta = RGB(255, 0, 255); gci.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, magenta); gci.CalculateDefaultColorIndices(); si.SetDefaultAttributes({}, TextAttribute{ gci.GetPopupFillAttribute() }); Log::Comment(NoThrowString().Format(L"Write 2 X's, then backspace one.")); stateMachine.ProcessString(L"\x1b[m"); if (writeSingly) { wchar_t* str = L"X"; size_t seqCb = 2; VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); str = L"\x08"; VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); } else { wchar_t* str = L"XX\x08"; size_t seqCb = 6; VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); } TextAttribute expectedDefaults{}; expectedDefaults.SetDefaultBackground(); COORD expectedCursor{ 1, 0 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const ROW& row = tbi.GetRowByOffset(0); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); VERIFY_ARE_EQUAL(false, attrB.IsLegacy()); VERIFY_ARE_EQUAL(expectedDefaults, attrA); VERIFY_ARE_EQUAL(expectedDefaults, attrB); VERIFY_ARE_EQUAL(magenta, gci.LookupAttributeColors(attrA).second); VERIFY_ARE_EQUAL(magenta, gci.LookupAttributeColors(attrB).second); } void ScreenBufferTests::BackspaceDefaultAttrsInPrompt() { // Tests MSFT:19853701 - when you edit the prompt line at a bash prompt, // make sure that the end of the line isn't filled with default/garbage attributes. CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = si.GetTextBuffer().GetCursor(); // Make sure we're in VT mode WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); cursor.SetPosition({ 0, 0 }); COLORREF magenta = RGB(255, 0, 255); gci.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, magenta); gci.CalculateDefaultColorIndices(); si.SetDefaultAttributes({}, TextAttribute{ gci.GetPopupFillAttribute() }); TextAttribute expectedDefaults{}; Log::Comment(NoThrowString().Format(L"Write 3 X's, move to the left, then delete-char the second.")); Log::Comment(NoThrowString().Format(L"This emulates editing the prompt line on bash")); stateMachine.ProcessString(L"\x1b[m"); Log::Comment(NoThrowString().Format( L"Clear the screen - make sure the line is filled with the current attributes.")); stateMachine.ProcessString(L"\x1b[2J"); const auto viewport = si.GetViewport(); const ROW& row = tbi.GetRowByOffset(cursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); { SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); Log::Comment(NoThrowString().Format( L"Make sure the row contains what we're expecting before we start." L"It should entirely be filled with defaults")); const std::vector initialAttrs{ attrRow->begin(), attrRow->end() }; for (int x = 0; x <= viewport.RightInclusive(); x++) { const auto& attr = initialAttrs[x]; VERIFY_ARE_EQUAL(expectedDefaults, attr); } } Log::Comment(NoThrowString().Format( L"Print 'XXX', move the cursor left 2, delete a character.")); stateMachine.ProcessString(L"XXX"); stateMachine.ProcessString(L"\x1b[2D"); stateMachine.ProcessString(L"\x1b[P"); COORD expectedCursor{ 1, 1 }; // We're expecting y=1, because the 2J above // should have moved the viewport down a line. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const std::vector attrs{ attrRow->begin(), attrRow->end() }; for (int x = 0; x <= viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; VERIFY_ARE_EQUAL(expectedDefaults, attr); } } void ScreenBufferTests::SetGlobalColorTable() { // Created for MSFT:19723934. // Changing the value of the color table should apply to the attributes in // both the alt AND main buffer. While many other properties should be // reset upon returning to the main buffer, the color table is a // global property. This behavior is consistent with other terminals // tested. CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to swap buffers. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer(); VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); StateMachine& stateMachine = mainBuffer.GetStateMachine(); Cursor& mainCursor = mainBuffer.GetTextBuffer().GetCursor(); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(mainBuffer.SetViewportOrigin(true, COORD({ 0, 0 }), true)); mainCursor.SetPosition({ 0, 0 }); const COLORREF originalRed = gci.GetColorTableEntry(TextColor::DARK_RED); const COLORREF testColor = RGB(0x11, 0x22, 0x33); VERIFY_ARE_NOT_EQUAL(originalRed, testColor); stateMachine.ProcessString(L"\x1b[41m"); stateMachine.ProcessString(L"X"); COORD expectedCursor{ 1, 0 }; VERIFY_ARE_EQUAL(expectedCursor, mainCursor.GetPosition()); { const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; LOG_ATTR(attrA); VERIFY_ARE_EQUAL(originalRed, gci.LookupAttributeColors(attrA).second); } Log::Comment(NoThrowString().Format(L"Create an alt buffer")); VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); SCREEN_INFORMATION& altBuffer = gci.GetActiveOutputBuffer(); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); WI_SetFlag(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); Cursor& altCursor = altBuffer.GetTextBuffer().GetCursor(); altCursor.SetPosition({ 0, 0 }); Log::Comment(NoThrowString().Format( L"Print one X in red, should be the original red color")); stateMachine.ProcessString(L"\x1b[41m"); stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(expectedCursor, altCursor.GetPosition()); { const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; LOG_ATTR(attrA); VERIFY_ARE_EQUAL(originalRed, gci.LookupAttributeColors(attrA).second); } Log::Comment(NoThrowString().Format(L"Change the value of red to RGB(0x11, 0x22, 0x33)")); stateMachine.ProcessString(L"\x1b]4;1;rgb:11/22/33\x07"); Log::Comment(NoThrowString().Format( L"Print another X, both should be the new \"red\" color")); stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(COORD({ 2, 0 }), altCursor.GetPosition()); { const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(testColor, gci.LookupAttributeColors(attrA).second); VERIFY_ARE_EQUAL(testColor, gci.LookupAttributeColors(attrB).second); } Log::Comment(NoThrowString().Format(L"Switch back to the main buffer")); useMain.release(); altBuffer.UseMainScreenBuffer(); const auto& mainBufferPostSwitch = gci.GetActiveOutputBuffer(); VERIFY_ARE_EQUAL(&mainBufferPostSwitch, &mainBuffer); Log::Comment(NoThrowString().Format( L"Print another X, both should be the new \"red\" color")); stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(COORD({ 2, 0 }), mainCursor.GetPosition()); { const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(testColor, gci.LookupAttributeColors(attrA).second); VERIFY_ARE_EQUAL(testColor, gci.LookupAttributeColors(attrB).second); } } void ScreenBufferTests::SetColorTableThreeDigits() { // Created for MSFT:19723934. // Changing the value of the color table above index 99 should work CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to swap buffers. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer(); VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); StateMachine& stateMachine = mainBuffer.GetStateMachine(); Cursor& mainCursor = mainBuffer.GetTextBuffer().GetCursor(); Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); VERIFY_SUCCEEDED(mainBuffer.SetViewportOrigin(true, COORD({ 0, 0 }), true)); mainCursor.SetPosition({ 0, 0 }); const COLORREF originalRed = gci.GetColorTableEntry(123); const COLORREF testColor = RGB(0x11, 0x22, 0x33); VERIFY_ARE_NOT_EQUAL(originalRed, testColor); stateMachine.ProcessString(L"\x1b[48;5;123m"); stateMachine.ProcessString(L"X"); COORD expectedCursor{ 1, 0 }; VERIFY_ARE_EQUAL(expectedCursor, mainCursor.GetPosition()); { const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; LOG_ATTR(attrA); VERIFY_ARE_EQUAL(originalRed, gci.LookupAttributeColors(attrA).second); } Log::Comment(NoThrowString().Format(L"Create an alt buffer")); VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); SCREEN_INFORMATION& altBuffer = gci.GetActiveOutputBuffer(); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); WI_SetFlag(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); Cursor& altCursor = altBuffer.GetTextBuffer().GetCursor(); altCursor.SetPosition({ 0, 0 }); Log::Comment(NoThrowString().Format( L"Print one X in red, should be the original red color")); stateMachine.ProcessString(L"\x1b[48;5;123m"); stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(expectedCursor, altCursor.GetPosition()); { const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; LOG_ATTR(attrA); VERIFY_ARE_EQUAL(originalRed, gci.LookupAttributeColors(attrA).second); } Log::Comment(NoThrowString().Format(L"Change the value of red to RGB(0x11, 0x22, 0x33)")); stateMachine.ProcessString(L"\x1b]4;123;rgb:11/22/33\x07"); Log::Comment(NoThrowString().Format( L"Print another X, it should be the new \"red\" color")); // TODO MSFT:20105972 - // You shouldn't need to manually update the attributes again. stateMachine.ProcessString(L"\x1b[48;5;123m"); stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(COORD({ 2, 0 }), altCursor.GetPosition()); { const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrB = attrs[1]; // TODO MSFT:20105972 - attrA and attrB should both be the same color now LOG_ATTR(attrB); VERIFY_ARE_EQUAL(testColor, gci.LookupAttributeColors(attrB).second); } } void ScreenBufferTests::SetDefaultForegroundColor() { // Setting the default foreground color should work CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to swap buffers. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer(); VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); StateMachine& stateMachine = mainBuffer.GetStateMachine(); COLORREF originalColor = gci.GetColorTableEntry(TextColor::DEFAULT_FOREGROUND); COLORREF newColor = gci.GetColorTableEntry(TextColor::DEFAULT_FOREGROUND); COLORREF testColor = RGB(0x33, 0x66, 0x99); VERIFY_ARE_NOT_EQUAL(originalColor, testColor); Log::Comment(L"Valid Hexadecimal Notation"); stateMachine.ProcessString(L"\x1b]10;rgb:33/66/99\x1b\\"); newColor = gci.GetColorTableEntry(TextColor::DEFAULT_FOREGROUND); VERIFY_ARE_EQUAL(testColor, newColor); Log::Comment(L"Valid Hexadecimal Notation"); originalColor = newColor; testColor = RGB(0xff, 0xff, 0xff); stateMachine.ProcessString(L"\x1b]10;rgb:ff/ff/ff\x1b\\"); newColor = gci.GetColorTableEntry(TextColor::DEFAULT_FOREGROUND); VERIFY_ARE_EQUAL(testColor, newColor); Log::Comment(L"Invalid syntax"); originalColor = newColor; testColor = RGB(153, 102, 51); stateMachine.ProcessString(L"\x1b]10;99/66/33\x1b\\"); newColor = gci.GetColorTableEntry(TextColor::DEFAULT_FOREGROUND); VERIFY_ARE_NOT_EQUAL(testColor, newColor); // it will, in fact leave the color the way it was VERIFY_ARE_EQUAL(originalColor, newColor); } void ScreenBufferTests::SetDefaultBackgroundColor() { // Setting the default Background color should work CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to swap buffers. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer(); VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); StateMachine& stateMachine = mainBuffer.GetStateMachine(); COLORREF originalColor = gci.GetColorTableEntry(TextColor::DEFAULT_BACKGROUND); COLORREF newColor = gci.GetColorTableEntry(TextColor::DEFAULT_BACKGROUND); COLORREF testColor = RGB(0x33, 0x66, 0x99); VERIFY_ARE_NOT_EQUAL(originalColor, testColor); Log::Comment(L"Valid Hexadecimal Notation"); stateMachine.ProcessString(L"\x1b]11;rgb:33/66/99\x1b\\"); newColor = gci.GetColorTableEntry(TextColor::DEFAULT_BACKGROUND); VERIFY_ARE_EQUAL(testColor, newColor); Log::Comment(L"Valid Hexadecimal Notation"); originalColor = newColor; testColor = RGB(0xff, 0xff, 0xff); stateMachine.ProcessString(L"\x1b]11;rgb:ff/ff/ff\x1b\\"); newColor = gci.GetColorTableEntry(TextColor::DEFAULT_BACKGROUND); VERIFY_ARE_EQUAL(testColor, newColor); Log::Comment(L"Invalid Syntax"); originalColor = newColor; testColor = RGB(153, 102, 51); stateMachine.ProcessString(L"\x1b]11;99/66/33\x1b\\"); newColor = gci.GetColorTableEntry(TextColor::DEFAULT_BACKGROUND); VERIFY_ARE_NOT_EQUAL(testColor, newColor); // it will, in fact leave the color the way it was VERIFY_ARE_EQUAL(originalColor, newColor); } void ScreenBufferTests::DeleteCharsNearEndOfLine() { // Created for MSFT:19888564. // There are some cases when you DCH N chars, where there are artifacts left // from the previous contents of the row after the DCH finishes. // If you are deleting N chars, // and there are N+X chars left in the row after the cursor, such that X v_w - 1 - c_x - d) && (v_w - 1 - c_x - d >= 0)` // where: // - `d`: num chars to delete // - `v_w`: viewport.Width() // - `c_x`: cursor.X // // Example: (this is tested by DeleteCharsNearEndOfLineSimpleFirstCase) // start with the following buffer contents, and the cursor on the "D" // [ABCDEFG ] // ^ // When you DCH(3) here, we are trying to delete the D, E and F. // We do that by shifting the contents of the line after the deleted // characters to the left. HOWEVER, there are only 2 chars left to move. // So (before the fix) the buffer end up like this: // [ABCG F ] // ^ // The G and " " have moved, but the F did not get overwritten. BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:dx", L"{1, 2, 3, 5, 8, 13, 21, 34}") TEST_METHOD_PROPERTY(L"Data:numCharsToDelete", L"{1, 2, 3, 5, 8, 13, 21, 34}") END_TEST_METHOD_PROPERTIES(); int dx; VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"Distance to move the cursor back into the line"); int numCharsToDelete; VERIFY_SUCCEEDED(TestData::TryGetValue(L"numCharsToDelete", numCharsToDelete), L"Number of characters to delete"); // let W = viewport.Width // Print W 'X' chars // Move to (0, W-dx) // DCH(numCharsToDelete) // There should be N 'X' chars, and then numSpaces spaces // where // numSpaces = min(dx, numCharsToDelete) // N = W - numSpaces auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& mainBuffer = gci.GetActiveOutputBuffer(); auto& tbi = mainBuffer.GetTextBuffer(); auto& stateMachine = mainBuffer.GetStateMachine(); auto& mainCursor = tbi.GetCursor(); auto& mainView = mainBuffer.GetViewport(); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition()); VERIFY_ARE_EQUAL(mainBuffer.GetBufferSize().Width(), mainView.Width()); VERIFY_IS_GREATER_THAN(mainView.Width(), (dx + numCharsToDelete)); for (int x = 0; x < mainView.Width(); x++) { stateMachine.ProcessString(L"X"); } VERIFY_ARE_EQUAL(COORD({ mainView.Width() - 1, 0 }), mainCursor.GetPosition()); Log::Comment(NoThrowString().Format( L"row_i=[%s]", tbi.GetRowByOffset(0).GetText().c_str())); mainCursor.SetPosition({ mainView.Width() - static_cast(dx), 0 }); std::wstringstream ss; ss << L"\x1b[" << numCharsToDelete << L"P"; // Delete N chars stateMachine.ProcessString(ss.str()); Log::Comment(NoThrowString().Format( L"row_f=[%s]", tbi.GetRowByOffset(0).GetText().c_str())); VERIFY_ARE_EQUAL(COORD({ mainView.Width() - static_cast(dx), 0 }), mainCursor.GetPosition()); auto iter = tbi.GetCellDataAt({ 0, 0 }); auto expectedNumSpaces = std::min(dx, numCharsToDelete); for (int x = 0; x < mainView.Width() - expectedNumSpaces; x++) { SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); if (iter->Chars() != L"X") { Log::Comment(NoThrowString().Format(L"character [%d] was mismatched", x)); } VERIFY_ARE_EQUAL(L"X", iter->Chars()); iter++; } for (int x = mainView.Width() - expectedNumSpaces; x < mainView.Width(); x++) { if (iter->Chars() != L"\x20") { Log::Comment(NoThrowString().Format(L"character [%d] was mismatched", x)); } VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); iter++; } } void ScreenBufferTests::DeleteCharsNearEndOfLineSimpleFirstCase() { // Created for MSFT:19888564. // This is a single case that I'm absolutely sure will repro this bug - // DeleteCharsNearEndOfLine is the more comprehensive version of this test. // Write a string, move the cursor into it, then delete some chars. // There should be no artifacts left behind. auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); const auto newBufferWidth = 8; VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ newBufferWidth, si.GetBufferSize().Height() }, false)); auto& mainBuffer = gci.GetActiveOutputBuffer(); const COORD newViewSize{ newBufferWidth, mainBuffer.GetViewport().Height() }; mainBuffer.SetViewportSize(&newViewSize); auto& tbi = mainBuffer.GetTextBuffer(); auto& mainView = mainBuffer.GetViewport(); auto& mainCursor = tbi.GetCursor(); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition()); VERIFY_ARE_EQUAL(newBufferWidth, mainView.Width()); VERIFY_ARE_EQUAL(mainBuffer.GetBufferSize().Width(), mainView.Width()); stateMachine.ProcessString(L"ABCDEFG"); VERIFY_ARE_EQUAL(COORD({ 7, 0 }), mainCursor.GetPosition()); // Place the cursor on the 'D' mainCursor.SetPosition({ 3, 0 }); Log::Comment(NoThrowString().Format(L"before=[%s]", tbi.GetRowByOffset(0).GetText().c_str())); // Delete 3 chars - [D, E, F] std::wstringstream ss; ss << L"\x1b[" << 3 << L"P"; stateMachine.ProcessString(ss.str()); Log::Comment(NoThrowString().Format(L"after =[%s]", tbi.GetRowByOffset(0).GetText().c_str())); // Cursor shouldn't have moved VERIFY_ARE_EQUAL(COORD({ 3, 0 }), mainCursor.GetPosition()); auto iter = tbi.GetCellDataAt({ 0, 0 }); VERIFY_ARE_EQUAL(L"A", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"B", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"C", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"G", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); iter++; } void ScreenBufferTests::DeleteCharsNearEndOfLineSimpleSecondCase() { // Created for MSFT:19888564. // This is another single case that I'm absolutely sure will repro this bug // DeleteCharsNearEndOfLine is the more comprehensive version of this test. // Write a string, move the cursor into it, then delete some chars. // There should be no artifacts left behind. auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); const auto newBufferWidth = 8; VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ newBufferWidth, si.GetBufferSize().Height() }, false)); auto& mainBuffer = gci.GetActiveOutputBuffer(); const COORD newViewSize{ newBufferWidth, mainBuffer.GetViewport().Height() }; mainBuffer.SetViewportSize(&newViewSize); auto& tbi = mainBuffer.GetTextBuffer(); auto& mainView = mainBuffer.GetViewport(); auto& mainCursor = tbi.GetCursor(); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition()); VERIFY_ARE_EQUAL(newBufferWidth, mainView.Width()); VERIFY_ARE_EQUAL(mainBuffer.GetBufferSize().Width(), mainView.Width()); stateMachine.ProcessString(L"ABCDEFG"); VERIFY_ARE_EQUAL(COORD({ 7, 0 }), mainCursor.GetPosition()); // Place the cursor on the 'C' mainCursor.SetPosition({ 2, 0 }); Log::Comment(NoThrowString().Format(L"before=[%s]", tbi.GetRowByOffset(0).GetText().c_str())); // Delete 4 chars - [C, D, E, F] std::wstringstream ss; ss << L"\x1b[" << 4 << L"P"; stateMachine.ProcessString(ss.str()); Log::Comment(NoThrowString().Format(L"after =[%s]", tbi.GetRowByOffset(0).GetText().c_str())); VERIFY_ARE_EQUAL(COORD({ 2, 0 }), mainCursor.GetPosition()); auto iter = tbi.GetCellDataAt({ 0, 0 }); VERIFY_ARE_EQUAL(L"A", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"B", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"G", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); iter++; VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); iter++; } void ScreenBufferTests::DontResetColorsAboveVirtualBottom() { // Created for MSFT:19989333. // Print some colored text, then scroll the viewport up, so the colored text // is below the visible viewport. Change the colors, then write a character. // Both the old chars and the new char should have different colors, the // first character should not have been reset to the new colors. auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); VERIFY_SUCCESS_NTSTATUS(si.SetViewportOrigin(true, { 0, 1 }, true)); cursor.SetPosition({ 0, si.GetViewport().BottomInclusive() }); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); const auto darkRed = gci.GetColorTableEntry(TextColor::DARK_RED); const auto darkBlue = gci.GetColorTableEntry(TextColor::DARK_BLUE); const auto darkBlack = gci.GetColorTableEntry(TextColor::DARK_BLACK); const auto darkWhite = gci.GetColorTableEntry(TextColor::DARK_WHITE); stateMachine.ProcessString(L"\x1b[31;44m"); stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\x1b[m"); stateMachine.ProcessString(L"X"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(2, cursor.GetPosition().X); { const ROW& row = tbi.GetRowByOffset(cursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(std::make_pair(darkRed, darkBlue), gci.LookupAttributeColors(attrA)); VERIFY_ARE_EQUAL(std::make_pair(darkWhite, darkBlack), gci.LookupAttributeColors(attrB)); } Log::Comment(NoThrowString().Format(L"Emulate scrolling up with the mouse")); VERIFY_SUCCESS_NTSTATUS(si.SetViewportOrigin(true, { 0, 0 }, false)); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_IS_GREATER_THAN(cursor.GetPosition().Y, si.GetViewport().BottomInclusive()); stateMachine.ProcessString(L"X"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(3, cursor.GetPosition().X); { const ROW& row = tbi.GetRowByOffset(cursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; const auto attrC = attrs[1]; LOG_ATTR(attrA); LOG_ATTR(attrB); LOG_ATTR(attrC); VERIFY_ARE_EQUAL(std::make_pair(darkRed, darkBlue), gci.LookupAttributeColors(attrA)); VERIFY_ARE_EQUAL(std::make_pair(darkWhite, darkBlack), gci.LookupAttributeColors(attrB)); VERIFY_ARE_EQUAL(std::make_pair(darkWhite, darkBlack), gci.LookupAttributeColors(attrC)); } } template void _FillLine(COORD position, T fillContent, TextAttribute fillAttr) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& row = si.GetTextBuffer().GetRowByOffset(position.Y); row.WriteCells({ fillContent, fillAttr }, position.X, false); } template void _FillLine(int line, T fillContent, TextAttribute fillAttr) { _FillLine({ 0, gsl::narrow(line) }, fillContent, fillAttr); } template void _FillLines(int startLine, int endLine, T fillContent, TextAttribute fillAttr) { for (auto line = startLine; line < endLine; ++line) { _FillLine(line, fillContent, fillAttr); } } template bool _ValidateLineContains(COORD position, T&&... expectedContent) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto actual = si.GetCellLineDataAt(position); auto expected = OutputCellIterator{ std::forward(expectedContent)... }; while (actual && expected) { if (actual->Chars() != expected->Chars() || actual->TextAttr() != expected->TextAttr()) { return false; } ++actual; ++expected; } return true; }; template bool _ValidateLineContains(int line, T expectedContent, TextAttribute expectedAttr) { return _ValidateLineContains({ 0, gsl::narrow(line) }, expectedContent, expectedAttr); } template auto _ValidateLinesContain(int startLine, int endLine, T expectedContent, TextAttribute expectedAttr) { for (auto line = startLine; line < endLine; ++line) { if (!_ValidateLineContains(line, expectedContent, expectedAttr)) { return false; } } return true; }; void ScreenBufferTests::ScrollOperations() { enum ScrollType : int { ScrollUp, ScrollDown, InsertLine, DeleteLine, ReverseIndex }; enum ScrollDirection : int { Up, Down }; ScrollType scrollType; ScrollDirection scrollDirection; int scrollMagnitude; BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:scrollType", L"{0, 1, 2, 3, 4}") TEST_METHOD_PROPERTY(L"Data:scrollMagnitude", L"{1, 2, 5}") END_TEST_METHOD_PROPERTIES() VERIFY_SUCCEEDED(TestData::TryGetValue(L"scrollType", (int&)scrollType)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"scrollMagnitude", scrollMagnitude)); std::wstringstream escapeSequence; switch (scrollType) { case ScrollUp: Log::Comment(L"Testing scroll up (SU)."); escapeSequence << "\x1b[" << scrollMagnitude << "S"; scrollDirection = Up; break; case ScrollDown: Log::Comment(L"Testing scroll down (SD)."); escapeSequence << "\x1b[" << scrollMagnitude << "T"; scrollDirection = Down; break; case InsertLine: Log::Comment(L"Testing insert line (IL)."); escapeSequence << "\x1b[" << scrollMagnitude << "L"; scrollDirection = Down; break; case DeleteLine: Log::Comment(L"Testing delete line (DL)."); escapeSequence << "\x1b[" << scrollMagnitude << "M"; scrollDirection = Up; break; case ReverseIndex: Log::Comment(L"Testing reverse index (RI)."); for (auto i = 0; i < scrollMagnitude; ++i) { escapeSequence << "\x1bM"; } scrollDirection = Down; break; default: VERIFY_FAIL(); return; } auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& stateMachine = si.GetStateMachine(); const auto& cursor = si.GetTextBuffer().GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const auto bufferWidth = si.GetBufferSize().Width(); const auto bufferHeight = si.GetBufferSize().Height(); // Move the viewport down a few lines, and only cover part of the buffer width. si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 10 }), true); const auto viewportStart = si.GetViewport().Top(); const auto viewportEnd = si.GetViewport().BottomExclusive(); // Fill the entire buffer with Zs. Blue on Green. const auto bufferChar = L'Z'; const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; _FillLines(0, bufferHeight, bufferChar, bufferAttr); // Fill the viewport with a range of letters to see if they move. Red on Blue. const auto viewportAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; auto viewportChar = L'A'; auto viewportLine = viewportStart; while (viewportLine < viewportEnd) { _FillLine(viewportLine++, viewportChar++, viewportAttr); } // Set the attributes that will be used to fill the revealed area. auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; expectedFillAttr.SetStandardErase(); // Place the cursor in the center. auto cursorPos = COORD{ bufferWidth / 2, (viewportStart + viewportEnd) / 2 }; // Unless this is reverse index, which has to be be at the top of the viewport. if (scrollType == ReverseIndex) { cursorPos.Y = viewportStart; } Log::Comment(L"Set the cursor position and perform the operation."); VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true)); stateMachine.ProcessString(escapeSequence.str()); // The cursor shouldn't move. auto expectedCursorPos = cursorPos; // Unless this is an IL or DL control, which moves the cursor to the left margin. if (scrollType == InsertLine || scrollType == DeleteLine) { expectedCursorPos.X = 0; } Log::Comment(L"Verify expected cursor position."); VERIFY_ARE_EQUAL(expectedCursorPos, cursor.GetPosition()); Log::Comment(L"Field of Zs outside viewport should remain unchanged."); VERIFY_IS_TRUE(_ValidateLinesContain(0, viewportStart, bufferChar, bufferAttr)); VERIFY_IS_TRUE(_ValidateLinesContain(viewportEnd, bufferHeight, bufferChar, bufferAttr)); // Depending on the direction of scrolling, lines are either deleted or inserted. const auto deletedLines = scrollDirection == Up ? scrollMagnitude : 0; const auto insertedLines = scrollDirection == Down ? scrollMagnitude : 0; // Insert and delete operations only scroll the viewport below the cursor position. const auto scrollStart = (scrollType == InsertLine || scrollType == DeleteLine) ? cursorPos.Y : viewportStart; // Reset the viewport character and line number for the verification loop. viewportChar = L'A'; viewportLine = viewportStart; Log::Comment(L"Lines above the scrolled area should remain unchanged."); while (viewportLine < scrollStart) { VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr)); } Log::Comment(L"Scrolled area should have moved up/down by given magnitude."); viewportChar += gsl::narrow(deletedLines); // Characters dropped when deleting viewportLine += gsl::narrow(insertedLines); // Lines skipped when inserting while (viewportLine < viewportEnd - deletedLines) { VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr)); } Log::Comment(L"The revealed area should now be blank, with standard erase attributes."); const auto revealedStart = scrollDirection == Up ? viewportEnd - deletedLines : scrollStart; const auto revealedEnd = revealedStart + scrollMagnitude; VERIFY_IS_TRUE(_ValidateLinesContain(revealedStart, revealedEnd, L' ', expectedFillAttr)); } void ScreenBufferTests::InsertChars() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:setMargins", L"{false, true}") END_TEST_METHOD_PROPERTIES(); bool setMargins; VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins)); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& stateMachine = si.GetStateMachine(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); // Set the buffer width to 40, with a centered viewport of 20. const auto bufferWidth = 40; const auto bufferHeight = si.GetBufferSize().Height(); const auto viewportStart = 10; const auto viewportEnd = viewportStart + 20; VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false)); si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true); // Tests are run both with and without the DECSTBM margins set. This should not alter // the results, since the ICH operation is not affected by vertical margins. stateMachine.ProcessString(setMargins ? L"\x1b[15;20r" : L"\x1b[r"); // Make sure we clear the margins on exit so they can't break other tests. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); Log::Comment( L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. " L"Then insert 5 spaces at the cursor. Watch spaces get inserted, text slides right " L"out of the viewport, pushing some of the Qs out of the buffer."); const auto insertLine = SHORT{ 10 }; auto insertPos = SHORT{ 20 }; // Place the cursor in the center of the line. VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true)); // Save the cursor position. It shouldn't move for the rest of the test. const auto& cursor = si.GetTextBuffer().GetCursor(); auto expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. const auto bufferChar = L'Q'; const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; _FillLine(insertLine, bufferChar, bufferAttr); // Fill the viewport range with text. Red on Blue. const auto textChars = L"ABCDEFGHIJKLMNOPQRST"; const auto textAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; _FillLine({ viewportStart, insertLine }, textChars, textAttr); // Set the attributes that will be used to fill the revealed area. auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; expectedFillAttr.SetStandardErase(); // Insert 5 spaces at the cursor position. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ // After: QQQQQQQQQQABCDEFGHIJ KLMNOPQRSTQQQQQ Log::Comment(L"Inserting 5 spaces in the middle of the line."); auto before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); stateMachine.ProcessString(L"\x1b[5@"); auto after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); Log::Comment(before.c_str(), L"Before"); Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation."); // Verify the updated structure of the line. VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr), L"Field of Qs left of the viewport should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJ", textAttr), L"First half of the alphabet should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr), L"Spaces should be inserted with standard erase attributes at the cursor position."); VERIFY_IS_TRUE(_ValidateLineContains({ insertPos + 5, insertLine }, L"KLMNOPQRST", textAttr), L"Second half of the alphabet should have moved to the right by the number of spaces inserted."); VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd + 5, insertLine }, L"QQQQQ", bufferAttr), L"Field of Qs right of the viewport should be moved right, half pushed outside the buffer."); Log::Comment( L"Test 2: Inserting at the exact end of the line. Same line structure. " L"Move cursor to right edge of window and insert > 1 space. " L"Only 1 should be inserted, everything else unchanged."); // Move cursor to right edge. insertPos = bufferWidth - 1; VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true)); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. _FillLine(insertLine, bufferChar, bufferAttr); // Fill the viewport range with text. Red on Blue. _FillLine({ viewportStart, insertLine }, textChars, textAttr); // Insert 5 spaces at the right edge. Only 1 should be inserted. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ Log::Comment(L"Inserting 5 spaces at the right edge of the buffer."); before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); stateMachine.ProcessString(L"\x1b[5@"); after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); Log::Comment(before.c_str(), L"Before"); Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation."); // Verify the updated structure of the line. VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr), L"Field of Qs left of the viewport should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr), L"Entire viewport range should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQ", bufferAttr), L"Field of Qs right of the viewport should remain unchanged except for the last spot."); VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr), L"One space should be inserted with standard erase attributes at the cursor position."); Log::Comment( L"Test 3: Inserting at the exact beginning of the line. Same line structure. " L"Move cursor to left edge of buffer and insert > buffer width of space. " L"The whole row should be replaced with spaces."); // Move cursor to left edge. VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, insertLine }, true)); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. _FillLine(insertLine, bufferChar, bufferAttr); // Fill the viewport range with text. Red on Blue. _FillLine({ viewportStart, insertLine }, textChars, textAttr); // Insert greater than the buffer width at the left edge. The entire line should be erased. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ // After: Log::Comment(L"Inserting 100 spaces at the left edge of the buffer."); before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); stateMachine.ProcessString(L"\x1b[100@"); after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); Log::Comment(before.c_str(), L"Before"); Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation."); // Verify the updated structure of the line. VERIFY_IS_TRUE(_ValidateLineContains(insertLine, L' ', expectedFillAttr), L"A whole line of spaces was inserted at the start, erasing the line."); } void ScreenBufferTests::DeleteChars() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:setMargins", L"{false, true}") END_TEST_METHOD_PROPERTIES(); bool setMargins; VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins)); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& stateMachine = si.GetStateMachine(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); // Set the buffer width to 40, with a centered viewport of 20. const auto bufferWidth = 40; const auto bufferHeight = si.GetBufferSize().Height(); const auto viewportStart = 10; const auto viewportEnd = viewportStart + 20; VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false)); si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true); // Tests are run both with and without the DECSTBM margins set. This should not alter // the results, since the DCH operation is not affected by vertical margins. stateMachine.ProcessString(setMargins ? L"\x1b[15;20r" : L"\x1b[r"); // Make sure we clear the margins on exit so they can't break other tests. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); Log::Comment( L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. " L"Then delete 5 characters at the cursor. Watch the rest of the line slide left, " L"replacing the deleted characters, with spaces inserted at the end of the line."); const auto deleteLine = SHORT{ 10 }; auto deletePos = SHORT{ 20 }; // Place the cursor in the center of the line. VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true)); // Save the cursor position. It shouldn't move for the rest of the test. const auto& cursor = si.GetTextBuffer().GetCursor(); auto expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. const auto bufferChar = L'Q'; const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; _FillLine(deleteLine, bufferChar, bufferAttr); // Fill the viewport range with text. Red on Blue. const auto textChars = L"ABCDEFGHIJKLMNOPQRST"; const auto textAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; _FillLine({ viewportStart, deleteLine }, textChars, textAttr); // Set the attributes that will be used to fill the revealed area. auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; expectedFillAttr.SetStandardErase(); // Delete 5 characters at the cursor position. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ // After: QQQQQQQQQQABCDEFGHIJPQRSTQQQQQQQQQQ Log::Comment(L"Deleting 5 characters in the middle of the line."); auto before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); stateMachine.ProcessString(L"\x1b[5P"); auto after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); Log::Comment(before.c_str(), L"Before"); Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation."); // Verify the updated structure of the line. VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr), L"Field of Qs left of the viewport should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJ", textAttr), L"First half of the alphabet should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L"PQRST", textAttr), L"Only half of the second part of the alphabet remains."); VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd - 5, deleteLine }, L"QQQQQQQQQQ", bufferAttr), L"Field of Qs right of the viewport should be moved left."); VERIFY_IS_TRUE(_ValidateLineContains({ bufferWidth - 5, deleteLine }, L" ", expectedFillAttr), L"The rest of the line should be replaced with spaces with standard erase attributes."); Log::Comment( L"Test 2: Deleting at the exact end of the line. Same line structure. " L"Move cursor to right edge of window and delete > 1 character. " L"Only 1 should be deleted, everything else unchanged."); // Move cursor to right edge. deletePos = bufferWidth - 1; VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true)); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. _FillLine(deleteLine, bufferChar, bufferAttr); // Fill the viewport range with text. Red on Blue. _FillLine({ viewportStart, deleteLine }, textChars, textAttr); // Delete 5 characters at the right edge. Only 1 should be deleted. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ Log::Comment(L"Deleting 5 characters at the right edge of the buffer."); before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); stateMachine.ProcessString(L"\x1b[5P"); after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); Log::Comment(before.c_str(), L"Before"); Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation."); // Verify the updated structure of the line. VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr), L"Field of Qs left of the viewport should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr), L"Entire viewport range should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQ", bufferAttr), L"Field of Qs right of the viewport should remain unchanged except for the last spot."); VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L" ", expectedFillAttr), L"One character should be erased with standard erase attributes at the cursor position."); Log::Comment( L"Test 3: Deleting at the exact beginning of the line. Same line structure. " L"Move cursor to left edge of buffer and delete > buffer width of characters. " L"The whole row should be replaced with spaces."); // Move cursor to left edge. VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, deleteLine }, true)); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. _FillLine(deleteLine, bufferChar, bufferAttr); // Fill the viewport range with text. Red on Blue. _FillLine({ viewportStart, deleteLine }, textChars, textAttr); // Delete greater than the buffer width at the left edge. The entire line should be erased. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ // After: Log::Comment(L"Deleting 100 characters at the left edge of the buffer."); before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); stateMachine.ProcessString(L"\x1b[100P"); after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); Log::Comment(before.c_str(), L"Before"); Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation."); // Verify the updated structure of the line. VERIFY_IS_TRUE(_ValidateLineContains(deleteLine, L' ', expectedFillAttr), L"A whole line of spaces was inserted from the right, erasing the line."); } void ScreenBufferTests::EraseScrollbackTests() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& stateMachine = si.GetStateMachine(); const auto& cursor = si.GetTextBuffer().GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const auto bufferWidth = si.GetBufferSize().Width(); const auto bufferHeight = si.GetBufferSize().Height(); // Move the viewport down a few lines, and only cover part of the buffer width. si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 10 }), true); const auto viewport = si.GetViewport(); // Fill the entire buffer with Zs. Blue on Green. const auto bufferChar = L'Z'; const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; _FillLines(0, bufferHeight, bufferChar, bufferAttr); // Fill the viewport with a range of letters to see if they move. Red on Blue. const auto viewportAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; auto viewportChar = L'A'; auto viewportLine = viewport.Top(); while (viewportLine < viewport.BottomExclusive()) { _FillLine(viewportLine++, viewportChar++, viewportAttr); } // Set the colors to Green on Red. This should have no effect on the results. si.SetAttributes(TextAttribute{ FOREGROUND_GREEN | BACKGROUND_RED }); // Place the cursor in the center. const short centerX = bufferWidth / 2; const short centerY = (si.GetViewport().Top() + si.GetViewport().BottomExclusive()) / 2; const auto cursorPos = COORD{ centerX, centerY }; Log::Comment(L"Set the cursor position and erase the scrollback."); VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true)); stateMachine.ProcessString(L"\x1b[3J"); // The viewport should move to the top of the buffer, while the cursor // maintains the same relative position. const auto expectedOffset = COORD{ 0, -viewport.Top() }; const auto expectedViewport = Viewport::Offset(viewport, expectedOffset); const auto expectedCursorPos = COORD{ cursorPos.X, cursorPos.Y + expectedOffset.Y }; Log::Comment(L"Verify expected viewport."); VERIFY_ARE_EQUAL(expectedViewport, si.GetViewport()); Log::Comment(L"Verify expected cursor position."); VERIFY_ARE_EQUAL(expectedCursorPos, cursor.GetPosition()); Log::Comment(L"Viewport contents should have moved to the new location."); viewportChar = L'A'; viewportLine = expectedViewport.Top(); while (viewportLine < expectedViewport.BottomExclusive()) { VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr)); } Log::Comment(L"The rest of the buffer should be cleared with default attributes."); VERIFY_IS_TRUE(_ValidateLinesContain(viewportLine, bufferHeight, L' ', TextAttribute{})); } void ScreenBufferTests::EraseTests() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:eraseType", L"{0, 1, 2}") // corresponds to options in DispatchTypes::EraseType TEST_METHOD_PROPERTY(L"Data:eraseScreen", L"{false, true}") // corresponds to Line (false) or Screen (true) END_TEST_METHOD_PROPERTIES() DispatchTypes::EraseType eraseType; VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseType", (size_t&)eraseType)); bool eraseScreen; VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseScreen", eraseScreen)); std::wstringstream escapeSequence; escapeSequence << "\x1b["; switch (eraseType) { case DispatchTypes::EraseType::ToEnd: Log::Comment(L"Erasing line from cursor to end."); escapeSequence << "0"; break; case DispatchTypes::EraseType::FromBeginning: Log::Comment(L"Erasing line from beginning to cursor."); escapeSequence << "1"; break; case DispatchTypes::EraseType::All: Log::Comment(L"Erasing all."); escapeSequence << "2"; break; default: VERIFY_FAIL(L"Unsupported erase type."); } if (!eraseScreen) { Log::Comment(L"Erasing just one line (the cursor's line)."); escapeSequence << "K"; } else { Log::Comment(L"Erasing entire display (viewport). May be bounded by the cursor."); escapeSequence << "J"; } auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& stateMachine = si.GetStateMachine(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const auto bufferWidth = si.GetBufferSize().Width(); const auto bufferHeight = si.GetBufferSize().Height(); // Move the viewport down a few lines, and only cover part of the buffer width. si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 10 }), true); // Fill the entire buffer with Zs. Blue on Green. const auto bufferChar = L'Z'; const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; _FillLines(0, bufferHeight, bufferChar, bufferAttr); // Set the attributes that will be used to fill the erased area. auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; expectedFillAttr.SetStandardErase(); // Place the cursor in the center. const short centerX = bufferWidth / 2; const short centerY = (si.GetViewport().Top() + si.GetViewport().BottomExclusive()) / 2; Log::Comment(L"Set the cursor position and perform the operation."); VERIFY_SUCCEEDED(si.SetCursorPosition({ centerX, centerY }, true)); stateMachine.ProcessString(escapeSequence.str()); // Get cursor position and viewport range. const auto cursorPos = si.GetTextBuffer().GetCursor().GetPosition(); const auto viewportStart = si.GetViewport().Top(); const auto viewportEnd = si.GetViewport().BottomExclusive(); Log::Comment(L"Lines outside the viewport should remain unchanged."); VERIFY_IS_TRUE(_ValidateLinesContain(0, viewportStart, bufferChar, bufferAttr)); VERIFY_IS_TRUE(_ValidateLinesContain(viewportEnd, bufferHeight, bufferChar, bufferAttr)); // 1. Lines before cursor line if (eraseScreen && eraseType != DispatchTypes::EraseType::ToEnd) { // For eraseScreen, if we're not erasing to the end, these rows will be cleared. Log::Comment(L"Lines before the cursor line should be erased."); VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, cursorPos.Y, L' ', expectedFillAttr)); } else { // Otherwise we'll be left with the original buffer content. Log::Comment(L"Lines before the cursor line should remain unchanged."); VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, cursorPos.Y, bufferChar, bufferAttr)); } // 2. Cursor Line auto prefixPos = COORD{ 0, cursorPos.Y }; auto suffixPos = cursorPos; // When erasing from the beginning, the cursor column is included in the range. suffixPos.X += (eraseType == DispatchTypes::EraseType::FromBeginning); size_t prefixWidth = suffixPos.X; size_t suffixWidth = bufferWidth - prefixWidth; if (eraseType == DispatchTypes::EraseType::ToEnd) { Log::Comment(L"The start of the cursor line should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains(prefixPos, bufferChar, bufferAttr, prefixWidth)); Log::Comment(L"The end of the cursor line should be erased."); VERIFY_IS_TRUE(_ValidateLineContains(suffixPos, L' ', expectedFillAttr, suffixWidth)); } if (eraseType == DispatchTypes::EraseType::FromBeginning) { Log::Comment(L"The start of the cursor line should be erased."); VERIFY_IS_TRUE(_ValidateLineContains(prefixPos, L' ', expectedFillAttr, prefixWidth)); Log::Comment(L"The end of the cursor line should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains(suffixPos, bufferChar, bufferAttr, suffixWidth)); } if (eraseType == DispatchTypes::EraseType::All) { Log::Comment(L"The entire cursor line should be erased."); VERIFY_IS_TRUE(_ValidateLineContains(cursorPos.Y, L' ', expectedFillAttr)); } // 3. Lines after cursor line if (eraseScreen && eraseType != DispatchTypes::EraseType::FromBeginning) { // For eraseScreen, if we're not erasing from the beginning, these rows will be cleared. Log::Comment(L"Lines after the cursor line should be erased."); VERIFY_IS_TRUE(_ValidateLinesContain(cursorPos.Y + 1, viewportEnd, L' ', expectedFillAttr)); } else { // Otherwise we'll be left with the original buffer content. Log::Comment(L"Lines after the cursor line should remain unchanged."); VERIFY_IS_TRUE(_ValidateLinesContain(cursorPos.Y + 1, viewportEnd, bufferChar, bufferAttr)); } } void _CommonScrollingSetup() { // Used for testing MSFT:20204600 // Place an A on the first line, and a B on the 6th line (index 5). // Set the scrolling region in between those lines (so scrolling won't affect them.) // First write "1\n2\n3\n4", to put 1-4 on the lines in between the A and B. // the viewport will look like: // A // 1 // 2 // 3 // 4 // B // then write "\n5\n6\n7\n", which will cycle around the scroll region a bit. // the viewport will look like: // A // 5 // 6 // 7 // // B auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); const auto oldView = si.GetViewport(); const auto view = Viewport::FromDimensions({ 0, 0 }, { oldView.Width(), 6 }); si.SetViewport(view, true); cursor.SetPosition({ 0, 0 }); stateMachine.ProcessString(L"A"); cursor.SetPosition({ 0, 5 }); stateMachine.ProcessString(L"B"); stateMachine.ProcessString(L"\x1b[2;5r"); stateMachine.ProcessString(L"\x1b[2;1H"); stateMachine.ProcessString(L"1\n2\n3\n4"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(1, cursor.GetPosition().X); VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"A", iter0->Chars()); VERIFY_ARE_EQUAL(L"1", iter1->Chars()); VERIFY_ARE_EQUAL(L"2", iter2->Chars()); VERIFY_ARE_EQUAL(L"3", iter3->Chars()); VERIFY_ARE_EQUAL(L"4", iter4->Chars()); VERIFY_ARE_EQUAL(L"B", iter5->Chars()); } stateMachine.ProcessString(L"\n5\n6\n7\n"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"A", iter0->Chars()); VERIFY_ARE_EQUAL(L"5", iter1->Chars()); VERIFY_ARE_EQUAL(L"6", iter2->Chars()); VERIFY_ARE_EQUAL(L"7", iter3->Chars()); // Chars() will return a single space for an empty row. VERIFY_ARE_EQUAL(L"\x20", iter4->Chars()); VERIFY_ARE_EQUAL(L"B", iter5->Chars()); } } void ScreenBufferTests::ScrollUpInMargins() { // Tests MSFT:20204600 // Do the common scrolling setup, then executes a Scroll Up, and verifies // the rows have what we'd expect. _CommonScrollingSetup(); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); // Execute a Scroll Up command stateMachine.ProcessString(L"\x1b[S"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"A", iter0->Chars()); VERIFY_ARE_EQUAL(L"6", iter1->Chars()); VERIFY_ARE_EQUAL(L"7", iter2->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter3->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter4->Chars()); VERIFY_ARE_EQUAL(L"B", iter5->Chars()); } } void ScreenBufferTests::ScrollDownInMargins() { // Tests MSFT:20204600 // Do the common scrolling setup, then executes a Scroll Down, and verifies // the rows have what we'd expect. _CommonScrollingSetup(); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); // Execute a Scroll Down command stateMachine.ProcessString(L"\x1b[T"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"A", iter0->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter1->Chars()); VERIFY_ARE_EQUAL(L"5", iter2->Chars()); VERIFY_ARE_EQUAL(L"6", iter3->Chars()); VERIFY_ARE_EQUAL(L"7", iter4->Chars()); VERIFY_ARE_EQUAL(L"B", iter5->Chars()); } } void ScreenBufferTests::InsertLinesInMargins() { Log::Comment( L"Does the common scrolling setup, then inserts two lines inside the " L"margin boundaries, and verifies the rows have what we'd expect."); _CommonScrollingSetup(); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); // Move to column 5 of line 3 stateMachine.ProcessString(L"\x1b[3;5H"); // Insert 2 lines stateMachine.ProcessString(L"\x1b[2L"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); // Verify cursor moved to left margin. VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(2, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"A", iter0->Chars()); VERIFY_ARE_EQUAL(L"5", iter1->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter2->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter3->Chars()); VERIFY_ARE_EQUAL(L"6", iter4->Chars()); VERIFY_ARE_EQUAL(L"B", iter5->Chars()); } Log::Comment( L"Does the common scrolling setup, then inserts one line with no " L"margins set, and verifies the rows have what we'd expect."); _CommonScrollingSetup(); // Clear the scroll margins stateMachine.ProcessString(L"\x1b[r"); // Move to column 5 of line 2 stateMachine.ProcessString(L"\x1b[2;5H"); // Insert 1 line stateMachine.ProcessString(L"\x1b[L"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); // Verify cursor moved to left margin. VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"A", iter0->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter1->Chars()); VERIFY_ARE_EQUAL(L"5", iter2->Chars()); VERIFY_ARE_EQUAL(L"6", iter3->Chars()); VERIFY_ARE_EQUAL(L"7", iter4->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter5->Chars()); } } void ScreenBufferTests::DeleteLinesInMargins() { Log::Comment( L"Does the common scrolling setup, then deletes two lines inside the " L"margin boundaries, and verifies the rows have what we'd expect."); _CommonScrollingSetup(); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); // Move to column 5 of line 3 stateMachine.ProcessString(L"\x1b[3;5H"); // Delete 2 lines stateMachine.ProcessString(L"\x1b[2M"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); // Verify cursor moved to left margin. VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(2, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"A", iter0->Chars()); VERIFY_ARE_EQUAL(L"5", iter1->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter2->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter3->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter4->Chars()); VERIFY_ARE_EQUAL(L"B", iter5->Chars()); } Log::Comment( L"Does the common scrolling setup, then deletes one line with no " L"margins set, and verifies the rows have what we'd expect."); _CommonScrollingSetup(); // Clear the scroll margins stateMachine.ProcessString(L"\x1b[r"); // Move to column 5 of line 2 stateMachine.ProcessString(L"\x1b[2;5H"); // Delete 1 line stateMachine.ProcessString(L"\x1b[M"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); // Verify cursor moved to left margin. VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"A", iter0->Chars()); VERIFY_ARE_EQUAL(L"6", iter1->Chars()); VERIFY_ARE_EQUAL(L"7", iter2->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter3->Chars()); VERIFY_ARE_EQUAL(L"B", iter4->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter5->Chars()); } } void ScreenBufferTests::ReverseLineFeedInMargins() { Log::Comment( L"Does the common scrolling setup, then executes a reverse line feed " L"below the top margin, and verifies the rows have what we'd expect."); _CommonScrollingSetup(); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); // Move to column 5 of line 2, the top margin stateMachine.ProcessString(L"\x1b[2;5H"); // Execute a reverse line feed (RI) stateMachine.ProcessString(L"\x1bM"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(4, cursor.GetPosition().X); VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"A", iter0->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter1->Chars()); VERIFY_ARE_EQUAL(L"5", iter2->Chars()); VERIFY_ARE_EQUAL(L"6", iter3->Chars()); VERIFY_ARE_EQUAL(L"7", iter4->Chars()); VERIFY_ARE_EQUAL(L"B", iter5->Chars()); } Log::Comment( L"Does the common scrolling setup, then executes a reverse line feed " L"with the top margin at the top of the screen, and verifies the rows " L"have what we'd expect."); _CommonScrollingSetup(); // Set the top scroll margin to the top of the screen stateMachine.ProcessString(L"\x1b[1;5r"); // Make sure we clear the margins on exit so they can't break other tests. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); // Move to column 5 of line 1, the top of the screen stateMachine.ProcessString(L"\x1b[1;5H"); // Execute a reverse line feed (RI) stateMachine.ProcessString(L"\x1bM"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(4, cursor.GetPosition().X); VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); { auto iter0 = tbi.GetCellDataAt({ 0, 0 }); auto iter1 = tbi.GetCellDataAt({ 0, 1 }); auto iter2 = tbi.GetCellDataAt({ 0, 2 }); auto iter3 = tbi.GetCellDataAt({ 0, 3 }); auto iter4 = tbi.GetCellDataAt({ 0, 4 }); auto iter5 = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"\x20", iter0->Chars()); VERIFY_ARE_EQUAL(L"A", iter1->Chars()); VERIFY_ARE_EQUAL(L"5", iter2->Chars()); VERIFY_ARE_EQUAL(L"6", iter3->Chars()); VERIFY_ARE_EQUAL(L"7", iter4->Chars()); VERIFY_ARE_EQUAL(L"B", iter5->Chars()); } } void ScreenBufferTests::LineFeedEscapeSequences() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:withReturn", L"{true, false}") END_TEST_METHOD_PROPERTIES() bool withReturn; VERIFY_SUCCEEDED(TestData::TryGetValue(L"withReturn", withReturn)); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); std::wstring escapeSequence; if (withReturn) { Log::Comment(L"Testing line feed with carriage return (NEL)."); escapeSequence = L"\033E"; } else { Log::Comment(L"Testing line feed without carriage return (IND)."); escapeSequence = L"\033D"; } // Set the viewport to a reasonable size. const auto view = Viewport::FromDimensions({ 0, 0 }, { 80, 25 }); si.SetViewport(view, true); // We'll place the cursor in the center of the line. // If we are performing a line feed with carriage return, // the cursor should move to the leftmost column. const short initialX = view.Width() / 2; const short expectedX = withReturn ? 0 : initialX; { Log::Comment(L"Starting at the top of viewport"); const short initialY = 0; const short expectedY = initialY + 1; const short expectedViewportTop = si.GetViewport().Top(); cursor.SetPosition(COORD{ initialX, initialY }); stateMachine.ProcessString(escapeSequence); VERIFY_ARE_EQUAL(expectedX, cursor.GetPosition().X); VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().Y); VERIFY_ARE_EQUAL(expectedViewportTop, si.GetViewport().Top()); } { Log::Comment(L"Starting at the bottom of viewport"); const short initialY = si.GetViewport().BottomInclusive(); const short expectedY = initialY + 1; const short expectedViewportTop = si.GetViewport().Top() + 1; cursor.SetPosition(COORD{ initialX, initialY }); stateMachine.ProcessString(escapeSequence); VERIFY_ARE_EQUAL(expectedX, cursor.GetPosition().X); VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().Y); VERIFY_ARE_EQUAL(expectedViewportTop, si.GetViewport().Top()); } { Log::Comment(L"Starting at the bottom of the scroll margins"); // Set the margins to rows 5 to 10. stateMachine.ProcessString(L"\x1b[5;10r"); // Make sure we clear the margins on exit so they can't break other tests. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); const short initialY = si.GetViewport().Top() + 9; const short expectedY = initialY; const short expectedViewportTop = si.GetViewport().Top(); _FillLine(initialY, L'Q', {}); cursor.SetPosition(COORD{ initialX, initialY }); stateMachine.ProcessString(escapeSequence); VERIFY_ARE_EQUAL(expectedX, cursor.GetPosition().X); VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().Y); VERIFY_ARE_EQUAL(expectedViewportTop, si.GetViewport().Top()); // Verify the line of Qs has been scrolled up. VERIFY_IS_TRUE(_ValidateLineContains(initialY - 1, L'Q', {})); VERIFY_IS_TRUE(_ValidateLineContains(initialY, L' ', si.GetAttributes())); } } void ScreenBufferTests::ScrollLines256Colors() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:scrollType", L"{0, 1, 2}") TEST_METHOD_PROPERTY(L"Data:colorStyle", L"{0, 1, 2}") END_TEST_METHOD_PROPERTIES(); // colorStyle will be used to control whether we use a color from the 16 // color table, a color from the 256 color table, or a pure RGB color. const int Use16Color = 0; const int Use256Color = 1; const int UseRGBColor = 2; // scrollType will be used to control whether we use InsertLines, // DeleteLines, or ReverseIndex to scroll the contents of the buffer. const int InsertLines = 0; const int DeleteLines = 1; const int ReverseLineFeed = 2; int scrollType; int colorStyle; VERIFY_SUCCEEDED(TestData::TryGetValue(L"scrollType", scrollType), L"controls whether to use InsertLines, DeleteLines ot ReverseLineFeed"); VERIFY_SUCCEEDED(TestData::TryGetValue(L"colorStyle", colorStyle), L"controls whether to use the 16 color table, 256 table, or RGB colors"); // This test is largely taken from repro code from // https://github.com/microsoft/terminal/issues/832#issuecomment-507447272 Log::Comment( L"Sets the attributes to a 256/RGB color, then scrolls some lines with" L" IL/DL/RI. Verifies the rows are cleared with the attributes we'd expect."); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); TextAttribute expectedAttr{ si.GetAttributes() }; std::wstring_view sgrSeq = L"\x1b[42m"; if (colorStyle == Use16Color) { expectedAttr.SetIndexedBackground(2); } else if (colorStyle == Use256Color) { expectedAttr.SetIndexedBackground256(20); sgrSeq = L"\x1b[48;5;20m"; } else if (colorStyle == UseRGBColor) { expectedAttr.SetBackground(RGB(1, 2, 3)); sgrSeq = L"\x1b[48;2;1;2;3m"; } // Set some scrolling margins stateMachine.ProcessString(L"\x1b[1;3r"); // Set the BG color to the table index 2, as a 256-color sequence stateMachine.ProcessString(sgrSeq); VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes()); // Move to home stateMachine.ProcessString(L"\x1b[H"); // Insert/Delete/Reverse Index 10 lines std::wstring_view scrollSeq = L""; if (scrollType == InsertLines) { scrollSeq = L"\x1b[10L"; } if (scrollType == DeleteLines) { scrollSeq = L"\x1b[10M"; } if (scrollType == ReverseLineFeed) { // This is 10 "Reverse Index" commands, which don't accept a parameter. scrollSeq = L"\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM"; } stateMachine.ProcessString(scrollSeq); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); stateMachine.ProcessString(L"foo"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); VERIFY_ARE_EQUAL(3, cursor.GetPosition().X); VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); { auto iter00 = tbi.GetCellDataAt({ 0, 0 }); auto iter10 = tbi.GetCellDataAt({ 1, 0 }); auto iter20 = tbi.GetCellDataAt({ 2, 0 }); auto iter30 = tbi.GetCellDataAt({ 3, 0 }); auto iter01 = tbi.GetCellDataAt({ 0, 1 }); auto iter02 = tbi.GetCellDataAt({ 0, 2 }); VERIFY_ARE_EQUAL(L"f", iter00->Chars()); VERIFY_ARE_EQUAL(L"o", iter10->Chars()); VERIFY_ARE_EQUAL(L"o", iter20->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter30->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter01->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter02->Chars()); VERIFY_ARE_EQUAL(expectedAttr, iter00->TextAttr()); VERIFY_ARE_EQUAL(expectedAttr, iter10->TextAttr()); VERIFY_ARE_EQUAL(expectedAttr, iter20->TextAttr()); VERIFY_ARE_EQUAL(expectedAttr, iter30->TextAttr()); VERIFY_ARE_EQUAL(expectedAttr, iter01->TextAttr()); VERIFY_ARE_EQUAL(expectedAttr, iter02->TextAttr()); } } void ScreenBufferTests::SetScreenMode() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); const auto rgbForeground = RGB(12, 34, 56); const auto rgbBackground = RGB(78, 90, 12); const auto testAttr = TextAttribute{ rgbForeground, rgbBackground }; Log::Comment(L"By default the screen mode is normal."); VERIFY_IS_FALSE(gci.IsScreenReversed()); VERIFY_ARE_EQUAL(std::make_pair(rgbForeground, rgbBackground), gci.LookupAttributeColors(testAttr)); Log::Comment(L"When DECSCNM is set, background and foreground colors are switched."); stateMachine.ProcessString(L"\x1B[?5h"); VERIFY_IS_TRUE(gci.IsScreenReversed()); VERIFY_ARE_EQUAL(std::make_pair(rgbBackground, rgbForeground), gci.LookupAttributeColors(testAttr)); Log::Comment(L"When DECSCNM is reset, the colors are normal again."); stateMachine.ProcessString(L"\x1B[?5l"); VERIFY_IS_FALSE(gci.IsScreenReversed()); VERIFY_ARE_EQUAL(std::make_pair(rgbForeground, rgbBackground), gci.LookupAttributeColors(testAttr)); } void ScreenBufferTests::SetOriginMode() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); const auto view = Viewport::FromDimensions({ 0, 0 }, { 80, 25 }); si.SetViewport(view, true); // Testing the default state (absolute cursor addressing) Log::Comment(L"By default, setting a margin moves the cursor to the top-left of the screen."); cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[6;20r"); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); Log::Comment(L"Cursor addressing is relative to the top-left of the screen."); stateMachine.ProcessString(L"\x1B[13;41H"); VERIFY_ARE_EQUAL(COORD({ 40, 12 }), cursor.GetPosition()); Log::Comment(L"The cursor can be moved below the bottom margin."); stateMachine.ProcessString(L"\x1B[23;41H"); VERIFY_ARE_EQUAL(COORD({ 40, 22 }), cursor.GetPosition()); // Testing the effects of DECOM being set (relative cursor addressing) Log::Comment(L"Setting DECOM moves the cursor to the top-left of the margin area."); cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[?6h"); VERIFY_ARE_EQUAL(COORD({ 0, 5 }), cursor.GetPosition()); Log::Comment(L"Setting a margin moves the cursor to the top-left of the margin area."); cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[6;20r"); VERIFY_ARE_EQUAL(COORD({ 0, 5 }), cursor.GetPosition()); Log::Comment(L"Cursor addressing is relative to the top-left of the margin area."); stateMachine.ProcessString(L"\x1B[8;41H"); VERIFY_ARE_EQUAL(COORD({ 40, 12 }), cursor.GetPosition()); Log::Comment(L"The cursor cannot be moved below the bottom margin."); stateMachine.ProcessString(L"\x1B[100;41H"); VERIFY_ARE_EQUAL(COORD({ 40, 19 }), cursor.GetPosition()); // Testing the effects of DECOM being reset (absolute cursor addressing) Log::Comment(L"Resetting DECOM moves the cursor to the top-left of the screen."); cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[?6l"); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); Log::Comment(L"Setting a margin moves the cursor to the top-left of the screen."); cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[6;20r"); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); Log::Comment(L"Cursor addressing is relative to the top-left of the screen."); stateMachine.ProcessString(L"\x1B[13;41H"); VERIFY_ARE_EQUAL(COORD({ 40, 12 }), cursor.GetPosition()); Log::Comment(L"The cursor can be moved below the bottom margin."); stateMachine.ProcessString(L"\x1B[23;41H"); VERIFY_ARE_EQUAL(COORD({ 40, 22 }), cursor.GetPosition()); // Testing the effects of DECOM being set with no margins Log::Comment(L"With no margins, setting DECOM moves the cursor to the top-left of the screen."); stateMachine.ProcessString(L"\x1B[r"); cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[?6h"); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); Log::Comment(L"Cursor addressing is still relative to the top-left of the screen."); stateMachine.ProcessString(L"\x1B[13;41H"); VERIFY_ARE_EQUAL(COORD({ 40, 12 }), cursor.GetPosition()); // Reset DECOM so we don't affect future tests stateMachine.ProcessString(L"\x1B[?6l"); } void ScreenBufferTests::SetAutoWrapMode() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); const auto attributes = si.GetAttributes(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const auto view = Viewport::FromDimensions({ 0, 0 }, { 80, 25 }); si.SetViewport(view, true); Log::Comment(L"By default, output should wrap onto the next line."); // Output 6 characters, 3 spaces from the end of the line. short startLine = 0; cursor.SetPosition({ 80 - 3, startLine }); stateMachine.ProcessString(L"abcdef"); // Half of the the content should wrap onto the next line. VERIFY_IS_TRUE(_ValidateLineContains({ 80 - 3, startLine }, L"abc", attributes)); VERIFY_IS_TRUE(_ValidateLineContains({ 0, startLine + 1 }, L"def", attributes)); VERIFY_ARE_EQUAL(COORD({ 3, startLine + 1 }), cursor.GetPosition()); Log::Comment(L"When DECAWM is reset, output is clamped to the line width."); stateMachine.ProcessString(L"\x1b[?7l"); // Output 6 characters, 3 spaces from the end of the line. startLine = 2; cursor.SetPosition({ 80 - 3, startLine }); stateMachine.ProcessString(L"abcdef"); // Content should be clamped to the line width, overwriting the last char. VERIFY_IS_TRUE(_ValidateLineContains({ 80 - 3, startLine }, L"abf", attributes)); VERIFY_ARE_EQUAL(COORD({ 79, startLine }), cursor.GetPosition()); Log::Comment(L"When DECAWM is set, output is wrapped again."); stateMachine.ProcessString(L"\x1b[?7h"); // Output 6 characters, 3 spaces from the end of the line. startLine = 4; cursor.SetPosition({ 80 - 3, startLine }); stateMachine.ProcessString(L"abcdef"); // Half of the the content should wrap onto the next line. VERIFY_IS_TRUE(_ValidateLineContains({ 80 - 3, startLine }, L"abc", attributes)); VERIFY_IS_TRUE(_ValidateLineContains({ 0, startLine + 1 }, L"def", attributes)); VERIFY_ARE_EQUAL(COORD({ 3, startLine + 1 }), cursor.GetPosition()); } void ScreenBufferTests::HardResetBuffer() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& stateMachine = si.GetStateMachine(); const auto& viewport = si.GetViewport(); const auto& cursor = si.GetTextBuffer().GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); auto isBufferClear = [&]() { auto offset = 0; auto width = si.GetBufferSize().Width(); for (auto iter = si.GetCellDataAt({}); iter; ++iter, ++offset) { if (iter->Chars() != L" " || iter->TextAttr() != TextAttribute{}) { Log::Comment(NoThrowString().Format( L"Buffer not clear at (X:%d, Y:%d)", offset % width, offset / width)); return false; } } return true; }; const auto resetToInitialState = L"\033c"; Log::Comment(L"Start with a clear buffer, viewport and cursor at 0,0"); si.SetAttributes(TextAttribute()); si.ClearTextData(); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, true)); VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, 0 }, true)); VERIFY_IS_TRUE(isBufferClear()); Log::Comment(L"Write a single line of text to the buffer"); stateMachine.ProcessString(L"Hello World!\n"); VERIFY_IS_FALSE(isBufferClear()); VERIFY_ARE_EQUAL(COORD({ 0, 1 }), cursor.GetPosition()); Log::Comment(L"After a reset, buffer should be clear, with cursor at 0,0"); stateMachine.ProcessString(resetToInitialState); VERIFY_IS_TRUE(isBufferClear()); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); Log::Comment(L"Set the background color to red"); stateMachine.ProcessString(L"\x1b[41m"); Log::Comment(L"Write multiple pages of text to the buffer"); for (auto i = 0; i < viewport.Height() * 2; i++) { stateMachine.ProcessString(L"Hello World!\n"); } VERIFY_IS_FALSE(isBufferClear()); VERIFY_IS_GREATER_THAN(viewport.Top(), viewport.Height()); VERIFY_IS_GREATER_THAN(cursor.GetPosition().Y, viewport.Height()); Log::Comment(L"After a reset, buffer should be clear, with viewport and cursor at 0,0"); stateMachine.ProcessString(resetToInitialState); VERIFY_IS_TRUE(isBufferClear()); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), viewport.Origin()); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); VERIFY_ARE_EQUAL(TextAttribute{}, si.GetAttributes()); } void ScreenBufferTests::RestoreDownAltBufferWithTerminalScrolling() { // This is a test for microsoft/terminal#1206. Refer to that issue for more // context CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.SetTerminalScrolling(true); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); auto& siMain = gci.GetActiveOutputBuffer(); COORD const coordFontSize = siMain.GetScreenFontSize(); siMain._virtualBottom = siMain._viewport.BottomInclusive(); auto originalView = siMain._viewport; VERIFY_IS_NULL(siMain._psiMainBuffer); VERIFY_IS_NULL(siMain._psiAlternateBuffer); Log::Comment(L"Create an alternate buffer"); if (VERIFY_IS_TRUE(NT_SUCCESS(siMain.UseAlternateScreenBuffer()))) { VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer); auto& altBuffer = *siMain._psiAlternateBuffer; VERIFY_ARE_EQUAL(0, altBuffer._viewport.Top()); VERIFY_ARE_EQUAL(altBuffer._viewport.BottomInclusive(), altBuffer._virtualBottom); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); const COORD originalSize = originalView.Dimensions(); const COORD doubledSize = { originalSize.X * 2, originalSize.Y * 2 }; // Create some RECTs, which are dimensions in pixels, because // ProcessResizeWindow needs to work on rects in screen _pixel_ // dimensions, not character sizes. RECT originalClientRect{ 0 }, maximizedClientRect{ 0 }; originalClientRect.right = originalSize.X * coordFontSize.X; originalClientRect.bottom = originalSize.Y * coordFontSize.Y; maximizedClientRect.right = doubledSize.X * coordFontSize.X; maximizedClientRect.bottom = doubledSize.Y * coordFontSize.Y; Log::Comment(NoThrowString().Format( L"Emulate a maximize")); // Note that just calling _InternalSetViewportSize does not hit the // exceptional case here. There's other logic farther down the stack // that triggers it. altBuffer.ProcessResizeWindow(&maximizedClientRect, &originalClientRect); VERIFY_ARE_EQUAL(0, altBuffer._viewport.Top()); VERIFY_ARE_EQUAL(altBuffer._viewport.BottomInclusive(), altBuffer._virtualBottom); Log::Comment(NoThrowString().Format( L"Emulate a restore down")); altBuffer.ProcessResizeWindow(&originalClientRect, &maximizedClientRect); // Before the bugfix, this would fail, with the top being roughly 80, // halfway into the buffer, with the bottom being anchored to the old // size. VERIFY_ARE_EQUAL(0, altBuffer._viewport.Top()); VERIFY_ARE_EQUAL(altBuffer._viewport.BottomInclusive(), altBuffer._virtualBottom); } } void ScreenBufferTests::SnapCursorWithTerminalScrolling() { // This is a test for microsoft/terminal#1222. Refer to that issue for more // context auto& g = ServiceLocator::LocateGlobals(); CONSOLE_INFORMATION& gci = g.getConsoleInformation(); gci.SetTerminalScrolling(true); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); auto& si = gci.GetActiveOutputBuffer(); auto& cursor = si.GetTextBuffer().GetCursor(); const auto originalView = si._viewport; si._virtualBottom = originalView.BottomInclusive(); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"originalView=%s", VerifyOutputTraits::ToString(originalView.ToInclusive()).GetBuffer())); Log::Comment(NoThrowString().Format( L"First set the viewport somewhere lower in the buffer, as if the text " L"was output there. Manually move the cursor there as well, so the " L"cursor is within that viewport.")); const COORD secondWindowOrigin{ 0, 10 }; VERIFY_SUCCEEDED(si.SetViewportOrigin(true, secondWindowOrigin, true)); si.GetTextBuffer().GetCursor().SetPosition(secondWindowOrigin); const auto secondView = si._viewport; const auto secondVirtualBottom = si._virtualBottom; Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"secondView=%s", VerifyOutputTraits::ToString(secondView.ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(10, secondView.Top()); VERIFY_ARE_EQUAL(originalView.Height() + 10, secondView.BottomExclusive()); VERIFY_ARE_EQUAL(originalView.Height() + 10 - 1, secondVirtualBottom); Log::Comment(NoThrowString().Format( L"Emulate scrolling upwards with the mouse (not moving the virtual view)")); const COORD thirdWindowOrigin{ 0, 2 }; VERIFY_SUCCEEDED(si.SetViewportOrigin(true, thirdWindowOrigin, false)); const auto thirdView = si._viewport; const auto thirdVirtualBottom = si._virtualBottom; Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"thirdView=%s", VerifyOutputTraits::ToString(thirdView.ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(2, thirdView.Top()); VERIFY_ARE_EQUAL(originalView.Height() + 2, thirdView.BottomExclusive()); VERIFY_ARE_EQUAL(secondVirtualBottom, thirdVirtualBottom); Log::Comment(NoThrowString().Format( L"Call SetConsoleCursorPosition to snap to the cursor")); VERIFY_SUCCEEDED(g.api.SetConsoleCursorPositionImpl(si, secondWindowOrigin)); const auto fourthView = si._viewport; const auto fourthVirtualBottom = si._virtualBottom; Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"thirdView=%s", VerifyOutputTraits::ToString(fourthView.ToInclusive()).GetBuffer())); VERIFY_ARE_EQUAL(10, fourthView.Top()); VERIFY_ARE_EQUAL(originalView.Height() + 10, fourthView.BottomExclusive()); VERIFY_ARE_EQUAL(secondVirtualBottom, fourthVirtualBottom); } void ScreenBufferTests::ClearAlternateBuffer() { // This is a test for microsoft/terminal#1189. Refer to that issue for more // context CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& g = ServiceLocator::LocateGlobals(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); auto& siMain = gci.GetActiveOutputBuffer(); auto WriteText = [&](TextBuffer& tbi) { // Write text to buffer auto& stateMachine = siMain.GetStateMachine(); auto& cursor = tbi.GetCursor(); stateMachine.ProcessString(L"foo\nfoo"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1); }; auto VerifyText = [&](TextBuffer& tbi) { // Verify written text in buffer { auto iter00 = tbi.GetCellDataAt({ 0, 0 }); auto iter10 = tbi.GetCellDataAt({ 1, 0 }); auto iter20 = tbi.GetCellDataAt({ 2, 0 }); auto iter30 = tbi.GetCellDataAt({ 3, 0 }); auto iter01 = tbi.GetCellDataAt({ 0, 1 }); auto iter02 = tbi.GetCellDataAt({ 1, 1 }); auto iter03 = tbi.GetCellDataAt({ 2, 1 }); VERIFY_ARE_EQUAL(L"f", iter00->Chars()); VERIFY_ARE_EQUAL(L"o", iter10->Chars()); VERIFY_ARE_EQUAL(L"o", iter20->Chars()); VERIFY_ARE_EQUAL(L"\x20", iter30->Chars()); VERIFY_ARE_EQUAL(L"f", iter01->Chars()); VERIFY_ARE_EQUAL(L"o", iter02->Chars()); VERIFY_ARE_EQUAL(L"o", iter03->Chars()); } }; WriteText(siMain.GetTextBuffer()); VerifyText(siMain.GetTextBuffer()); Log::Comment(L"Create an alternate buffer"); if (VERIFY_IS_TRUE(NT_SUCCESS(siMain.UseAlternateScreenBuffer()))) { VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer); auto& altBuffer = *siMain._psiAlternateBuffer; VERIFY_ARE_EQUAL(0, altBuffer._viewport.Top()); VERIFY_ARE_EQUAL(altBuffer._viewport.BottomInclusive(), altBuffer._virtualBottom); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); // Set the position to home, otherwise it's inherited from the main buffer. VERIFY_SUCCEEDED(altBuffer.SetCursorPosition({ 0, 0 }, true)); WriteText(altBuffer.GetTextBuffer()); VerifyText(altBuffer.GetTextBuffer()); #pragma region Test ScrollConsoleScreenBufferWImpl() // Clear text of alt buffer (same params as in CMD) VERIFY_SUCCEEDED(g.api.ScrollConsoleScreenBufferWImpl(siMain, { 0, 0, 120, 9001 }, { 0, -9001 }, std::nullopt, L' ', 7)); // Verify text is now gone VERIFY_ARE_EQUAL(L" ", altBuffer.GetTextBuffer().GetCellDataAt({ 0, 0 })->Chars()); #pragma endregion #pragma region Test SetConsoleCursorPositionImpl() // Reset cursor position as we do with CLS command (same params as in CMD) VERIFY_SUCCEEDED(g.api.SetConsoleCursorPositionImpl(siMain, { 0 })); // Verify state of alt buffer auto& altBufferCursor = altBuffer.GetTextBuffer().GetCursor(); VERIFY_ARE_EQUAL(altBufferCursor.GetPosition().X, 0); VERIFY_ARE_EQUAL(altBufferCursor.GetPosition().Y, 0); #pragma endregion } // Verify state of main buffer is untouched auto& cursor = siMain.GetTextBuffer().GetCursor(); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1); VerifyText(siMain.GetTextBuffer()); } void ScreenBufferTests::TestExtendedTextAttributes() { // This is a test for microsoft/terminal#2554. Refer to that issue for more // context. // We're going to set every possible combination of extended attributes via // VT, then disable them, and make sure that they are all always represented // internally correctly. // Run this test for each and every possible combination of states. BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:bold", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:faint", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:italics", 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: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 bold, faint, italics, underlined, doublyUnderlined, blink, invisible, crossedOut; VERIFY_SUCCEEDED(TestData::TryGetValue(L"bold", bold)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"faint", faint)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"italics", italics)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"underlined", underlined)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"doublyUnderlined", doublyUnderlined)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"blink", blink)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"invisible", invisible)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"crossedOut", crossedOut)); auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = tbi.GetCursor(); ExtendedAttributes expectedAttrs{ ExtendedAttributes::Normal }; std::wstring vtSeq = L""; // Collect up a VT sequence to set the state given the method properties if (bold) { WI_SetFlag(expectedAttrs, ExtendedAttributes::Bold); vtSeq += L"\x1b[1m"; } if (faint) { WI_SetFlag(expectedAttrs, ExtendedAttributes::Faint); vtSeq += L"\x1b[2m"; } if (italics) { WI_SetFlag(expectedAttrs, ExtendedAttributes::Italics); vtSeq += L"\x1b[3m"; } if (underlined) { WI_SetFlag(expectedAttrs, ExtendedAttributes::Underlined); vtSeq += L"\x1b[4m"; } if (doublyUnderlined) { WI_SetFlag(expectedAttrs, ExtendedAttributes::DoublyUnderlined); vtSeq += L"\x1b[21m"; } if (blink) { WI_SetFlag(expectedAttrs, ExtendedAttributes::Blinking); vtSeq += L"\x1b[5m"; } if (invisible) { WI_SetFlag(expectedAttrs, ExtendedAttributes::Invisible); vtSeq += L"\x1b[8m"; } if (crossedOut) { WI_SetFlag(expectedAttrs, ExtendedAttributes::CrossedOut); vtSeq += L"\x1b[9m"; } // Helper lambda to write a VT sequence, then an "X", then check that the // attributes of the "X" match what we think they should be. auto validate = [&](const ExtendedAttributes expectedAttrs, const std::wstring& vtSequence) { auto cursorPos = cursor.GetPosition(); // Convert the vtSequence to something printable. Lets not set these // attrs on the test console std::wstring debugString = vtSequence; { size_t start_pos = 0; while ((start_pos = debugString.find(L"\x1b", start_pos)) != std::string::npos) { debugString.replace(start_pos, 1, L"\\x1b"); start_pos += 4; } } Log::Comment(NoThrowString().Format( L"Testing string:\"%s\"", debugString.c_str())); Log::Comment(NoThrowString().Format( L"Expecting attrs:0x%02x", expectedAttrs)); stateMachine.ProcessString(vtSequence); stateMachine.ProcessString(L"X"); auto iter = tbi.GetCellDataAt(cursorPos); auto currentExtendedAttrs = iter->TextAttr().GetExtendedAttributes(); VERIFY_ARE_EQUAL(expectedAttrs, currentExtendedAttrs); }; // Check setting all the states collected above validate(expectedAttrs, vtSeq); // One-by-one, turn off each of these states with VT, then check that the // state matched. if (bold || faint) { // The bold and faint attributes share the same reset sequence. WI_ClearAllFlags(expectedAttrs, ExtendedAttributes::Bold | ExtendedAttributes::Faint); vtSeq = L"\x1b[22m"; validate(expectedAttrs, vtSeq); } if (italics) { WI_ClearFlag(expectedAttrs, ExtendedAttributes::Italics); vtSeq = L"\x1b[23m"; validate(expectedAttrs, vtSeq); } if (underlined || doublyUnderlined) { // The two underlined attributes share the same reset sequence. WI_ClearAllFlags(expectedAttrs, ExtendedAttributes::Underlined | ExtendedAttributes::DoublyUnderlined); vtSeq = L"\x1b[24m"; validate(expectedAttrs, vtSeq); } if (blink) { WI_ClearFlag(expectedAttrs, ExtendedAttributes::Blinking); vtSeq = L"\x1b[25m"; validate(expectedAttrs, vtSeq); } if (invisible) { WI_ClearFlag(expectedAttrs, ExtendedAttributes::Invisible); vtSeq = L"\x1b[28m"; validate(expectedAttrs, vtSeq); } if (crossedOut) { WI_ClearFlag(expectedAttrs, ExtendedAttributes::CrossedOut); vtSeq = L"\x1b[29m"; validate(expectedAttrs, vtSeq); } stateMachine.ProcessString(L"\x1b[0m"); } void ScreenBufferTests::TestExtendedTextAttributesWithColors() { // This is a test for microsoft/terminal#2554. Refer to that issue for more // context. // We're going to set every possible combination of extended attributes via // VT, then set assorted colors, then disable extended attrs, then reset // colors, in various ways, and make sure that they are all always // represented internally correctly. // Run this test for each and every possible combination of states. BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:bold", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:faint", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:italics", 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:blink", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:invisible", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:crossedOut", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:setForegroundType", L"{0, 1, 2, 3}") TEST_METHOD_PROPERTY(L"Data:setBackgroundType", L"{0, 1, 2, 3}") END_TEST_METHOD_PROPERTIES() // colorStyle will be used to control whether we use a color from the 16 // color table, a color from the 256 color table, or a pure RGB color. const int UseDefault = 0; const int Use16Color = 1; const int Use256Color = 2; const int UseRGBColor = 3; bool bold, faint, italics, underlined, doublyUnderlined, blink, invisible, crossedOut; VERIFY_SUCCEEDED(TestData::TryGetValue(L"bold", bold)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"faint", faint)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"italics", italics)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"underlined", underlined)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"doublyUnderlined", doublyUnderlined)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"blink", blink)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"invisible", invisible)); VERIFY_SUCCEEDED(TestData::TryGetValue(L"crossedOut", crossedOut)); int setForegroundType, setBackgroundType; VERIFY_SUCCEEDED(TestData::TryGetValue(L"setForegroundType", setForegroundType), L"controls whether to use the 16 color table, 256 table, or RGB colors"); VERIFY_SUCCEEDED(TestData::TryGetValue(L"setBackgroundType", setBackgroundType), L"controls whether to use the 16 color table, 256 table, or RGB colors"); auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = tbi.GetCursor(); TextAttribute expectedAttr{ si.GetAttributes() }; std::wstring vtSeq = L""; // Collect up a VT sequence to set the state given the method properties if (bold) { expectedAttr.SetBold(true); vtSeq += L"\x1b[1m"; } if (faint) { expectedAttr.SetFaint(true); vtSeq += L"\x1b[2m"; } if (italics) { expectedAttr.SetItalic(true); vtSeq += L"\x1b[3m"; } if (underlined) { expectedAttr.SetUnderlined(true); vtSeq += L"\x1b[4m"; } if (doublyUnderlined) { expectedAttr.SetDoublyUnderlined(true); vtSeq += L"\x1b[21m"; } if (blink) { expectedAttr.SetBlinking(true); vtSeq += L"\x1b[5m"; } if (invisible) { expectedAttr.SetInvisible(true); vtSeq += L"\x1b[8m"; } if (crossedOut) { expectedAttr.SetCrossedOut(true); vtSeq += L"\x1b[9m"; } // Prepare the foreground attributes if (setForegroundType == UseDefault) { expectedAttr.SetDefaultForeground(); vtSeq += L"\x1b[39m"; } else if (setForegroundType == Use16Color) { expectedAttr.SetIndexedForeground(2); vtSeq += L"\x1b[32m"; } else if (setForegroundType == Use256Color) { expectedAttr.SetIndexedForeground256(20); vtSeq += L"\x1b[38;5;20m"; } else if (setForegroundType == UseRGBColor) { expectedAttr.SetForeground(RGB(1, 2, 3)); vtSeq += L"\x1b[38;2;1;2;3m"; } // Prepare the background attributes if (setBackgroundType == UseDefault) { expectedAttr.SetDefaultBackground(); vtSeq += L"\x1b[49m"; } else if (setBackgroundType == Use16Color) { expectedAttr.SetIndexedBackground(2); vtSeq += L"\x1b[42m"; } else if (setBackgroundType == Use256Color) { expectedAttr.SetIndexedBackground256(20); vtSeq += L"\x1b[48;5;20m"; } else if (setBackgroundType == UseRGBColor) { expectedAttr.SetBackground(RGB(1, 2, 3)); vtSeq += L"\x1b[48;2;1;2;3m"; } // Helper lambda to write a VT sequence, then an "X", then check that the // attributes of the "X" match what we think they should be. auto validate = [&](const TextAttribute attr, const std::wstring& vtSequence) { auto cursorPos = cursor.GetPosition(); // Convert the vtSequence to something printable. Lets not set these // attrs on the test console std::wstring debugString = vtSequence; { size_t start_pos = 0; while ((start_pos = debugString.find(L"\x1b", start_pos)) != std::string::npos) { debugString.replace(start_pos, 1, L"\\x1b"); start_pos += 4; } } Log::Comment(NoThrowString().Format( L"Testing string:\"%s\"", debugString.c_str())); Log::Comment(NoThrowString().Format( L"Expecting attrs:0x%02x", VerifyOutputTraits::ToString(attr).GetBuffer())); stateMachine.ProcessString(vtSequence); stateMachine.ProcessString(L"X"); auto iter = tbi.GetCellDataAt(cursorPos); const TextAttribute currentAttrs = iter->TextAttr(); VERIFY_ARE_EQUAL(attr, currentAttrs); }; // Check setting all the states collected above validate(expectedAttr, vtSeq); // One-by-one, turn off each of these states with VT, then check that the // state matched. if (bold || faint) { // The bold and faint attributes share the same reset sequence. expectedAttr.SetBold(false); expectedAttr.SetFaint(false); vtSeq = L"\x1b[22m"; validate(expectedAttr, vtSeq); } if (italics) { expectedAttr.SetItalic(false); vtSeq = L"\x1b[23m"; validate(expectedAttr, vtSeq); } if (underlined || doublyUnderlined) { // The two underlined attributes share the same reset sequence. expectedAttr.SetUnderlined(false); expectedAttr.SetDoublyUnderlined(false); vtSeq = L"\x1b[24m"; validate(expectedAttr, vtSeq); } if (blink) { expectedAttr.SetBlinking(false); vtSeq = L"\x1b[25m"; validate(expectedAttr, vtSeq); } if (invisible) { expectedAttr.SetInvisible(false); vtSeq = L"\x1b[28m"; validate(expectedAttr, vtSeq); } if (crossedOut) { expectedAttr.SetCrossedOut(false); vtSeq = L"\x1b[29m"; validate(expectedAttr, vtSeq); } stateMachine.ProcessString(L"\x1b[0m"); } void ScreenBufferTests::CursorUpDownAcrossMargins() { // Test inspired by: https://github.com/microsoft/terminal/issues/2929 // echo -e "\e[6;19r\e[24H\e[99AX\e[1H\e[99BY\e[r" // This does the following: // * sets the top and bottom DECSTBM margins to 6 and 19 // * moves to line 24 (i.e. below the bottom margin) // * executes the CUU sequence with a count of 99, to move up 99 lines // * writes out X // * moves to line 1 (i.e. above the top margin) // * executes the CUD sequence with a count of 99, to move down 99 lines // * writes out Y auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24); // Set some scrolling margins stateMachine.ProcessString(L"\x1b[6;19r"); stateMachine.ProcessString(L"\x1b[24H"); VERIFY_ARE_EQUAL(23, cursor.GetPosition().Y); stateMachine.ProcessString(L"\x1b[99A"); VERIFY_ARE_EQUAL(5, cursor.GetPosition().Y); stateMachine.ProcessString(L"X"); { auto iter = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"X", iter->Chars()); } stateMachine.ProcessString(L"\x1b[1H"); VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); stateMachine.ProcessString(L"\x1b[99B"); VERIFY_ARE_EQUAL(18, cursor.GetPosition().Y); stateMachine.ProcessString(L"Y"); { auto iter = tbi.GetCellDataAt({ 0, 18 }); VERIFY_ARE_EQUAL(L"Y", iter->Chars()); } stateMachine.ProcessString(L"\x1b[r"); } void ScreenBufferTests::CursorUpDownOutsideMargins() { // Test inspired by the CursorUpDownAcrossMargins test. // echo -e "\e[6;19r\e[24H\e[1AX\e[1H\e[1BY\e[r" // This does the following: // * sets the top and bottom DECSTBM margins to 6 and 19 // * moves to line 24 (i.e. below the bottom margin) // * executes the CUU sequence with a count of 1, to move up 1 lines (still below margins) // * writes out X // * moves to line 1 (i.e. above the top margin) // * executes the CUD sequence with a count of 1, to move down 1 lines (still above margins) // * writes out Y // This test is different because the end location of the vertical movement // should not be within the margins at all. We should not clamp this // movement to be within the margins. auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24); // Set some scrolling margins stateMachine.ProcessString(L"\x1b[6;19r"); stateMachine.ProcessString(L"\x1b[24H"); VERIFY_ARE_EQUAL(23, cursor.GetPosition().Y); stateMachine.ProcessString(L"\x1b[1A"); VERIFY_ARE_EQUAL(22, cursor.GetPosition().Y); stateMachine.ProcessString(L"X"); { auto iter = tbi.GetCellDataAt({ 0, 22 }); VERIFY_ARE_EQUAL(L"X", iter->Chars()); } stateMachine.ProcessString(L"\x1b[1H"); VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); stateMachine.ProcessString(L"\x1b[1B"); VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); stateMachine.ProcessString(L"Y"); { auto iter = tbi.GetCellDataAt({ 0, 1 }); VERIFY_ARE_EQUAL(L"Y", iter->Chars()); } stateMachine.ProcessString(L"\x1b[r"); } void ScreenBufferTests::CursorUpDownExactlyAtMargins() { // Test inspired by the CursorUpDownAcrossMargins test. // echo -e "\e[6;19r\e[19H\e[1B1\e[1A2\e[6H\e[1A3\e[1B4\e[r" // This does the following: // * sets the top and bottom DECSTBM margins to 6 and 19 // * moves to line 19 (i.e. on the bottom margin) // * executes the CUD sequence with a count of 1, to move down 1 lines (still on the margin) // * writes out 1 // * executes the CUU sequence with a count of 1, to move up 1 lines (now inside margins) // * writes out 2 // * moves to line 6 (i.e. on the top margin) // * executes the CUU sequence with a count of 1, to move up 1 lines (still on the margin) // * writes out 3 // * executes the CUD sequence with a count of 1, to move down 1 lines (still above margins) // * writes out 4 // This test is different because the starting location for these scroll // operations is _exactly_ on the margins auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24); // Set some scrolling margins stateMachine.ProcessString(L"\x1b[6;19r"); stateMachine.ProcessString(L"\x1b[19;1H"); VERIFY_ARE_EQUAL(18, cursor.GetPosition().Y); stateMachine.ProcessString(L"\x1b[1B"); VERIFY_ARE_EQUAL(18, cursor.GetPosition().Y); stateMachine.ProcessString(L"1"); { auto iter = tbi.GetCellDataAt({ 0, 18 }); VERIFY_ARE_EQUAL(L"1", iter->Chars()); } stateMachine.ProcessString(L"\x1b[1A"); VERIFY_ARE_EQUAL(17, cursor.GetPosition().Y); stateMachine.ProcessString(L"2"); { auto iter = tbi.GetCellDataAt({ 1, 17 }); VERIFY_ARE_EQUAL(L"2", iter->Chars()); } stateMachine.ProcessString(L"\x1b[6;1H"); VERIFY_ARE_EQUAL(5, cursor.GetPosition().Y); stateMachine.ProcessString(L"\x1b[1A"); VERIFY_ARE_EQUAL(5, cursor.GetPosition().Y); stateMachine.ProcessString(L"3"); { auto iter = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"3", iter->Chars()); } stateMachine.ProcessString(L"\x1b[1B"); VERIFY_ARE_EQUAL(6, cursor.GetPosition().Y); stateMachine.ProcessString(L"4"); { auto iter = tbi.GetCellDataAt({ 1, 6 }); VERIFY_ARE_EQUAL(L"4", iter->Chars()); } stateMachine.ProcessString(L"\x1b[r"); } void ScreenBufferTests::CursorNextPreviousLine() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(L"Make sure the viewport is at 0,0"); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); Log::Comment(L"CNL without margins"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move down 5 lines (CNL). stateMachine.ProcessString(L"\x1b[5E"); // We should end up in column 0 of line 15. VERIFY_ARE_EQUAL(COORD({ 0, 15 }), cursor.GetPosition()); Log::Comment(L"CPL without margins"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move up 5 lines (CPL). stateMachine.ProcessString(L"\x1b[5F"); // We should end up in column 0 of line 5. VERIFY_ARE_EQUAL(COORD({ 0, 5 }), cursor.GetPosition()); // Set the margins to 8:12 (9:13 in VT coordinates). stateMachine.ProcessString(L"\x1b[9;13r"); // Make sure we clear the margins on exit so they can't break other tests. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); Log::Comment(L"CNL inside margins"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move down 5 lines (CNL). stateMachine.ProcessString(L"\x1b[5E"); // We should stop on line 12, the bottom margin. VERIFY_ARE_EQUAL(COORD({ 0, 12 }), cursor.GetPosition()); Log::Comment(L"CPL inside margins"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move up 5 lines (CPL). stateMachine.ProcessString(L"\x1b[5F"); // We should stop on line 8, the top margin. VERIFY_ARE_EQUAL(COORD({ 0, 8 }), cursor.GetPosition()); Log::Comment(L"CNL below bottom"); // Starting from column 20 of line 13 (1 below bottom margin). cursor.SetPosition(COORD{ 20, 13 }); // Move down 5 lines (CNL). stateMachine.ProcessString(L"\x1b[5E"); // We should end up in column 0 of line 18. VERIFY_ARE_EQUAL(COORD({ 0, 18 }), cursor.GetPosition()); Log::Comment(L"CPL above top margin"); // Starting from column 20 of line 7 (1 above top margin). cursor.SetPosition(COORD{ 20, 7 }); // Move up 5 lines (CPL). stateMachine.ProcessString(L"\x1b[5F"); // We should end up in column 0 of line 2. VERIFY_ARE_EQUAL(COORD({ 0, 2 }), cursor.GetPosition()); } void ScreenBufferTests::CursorPositionRelative() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(L"Make sure the viewport is at 0,0"); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); Log::Comment(L"HPR without margins"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move forward 5 columns (HPR). stateMachine.ProcessString(L"\x1b[5a"); // We should end up in column 25. VERIFY_ARE_EQUAL(COORD({ 25, 10 }), cursor.GetPosition()); Log::Comment(L"VPR without margins"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move down 5 lines (VPR). stateMachine.ProcessString(L"\x1b[5e"); // We should end up on line 15. VERIFY_ARE_EQUAL(COORD({ 20, 15 }), cursor.GetPosition()); // Enable DECLRMM margin mode (future proofing for when we support it) stateMachine.ProcessString(L"\x1b[?69h"); // Set horizontal margins to 18:22 (19:23 in VT coordinates). stateMachine.ProcessString(L"\x1b[19;23s"); // Set vertical margins to 8:12 (9:13 in VT coordinates). stateMachine.ProcessString(L"\x1b[9;13r"); // Make sure we clear the margins on exit so they can't break other tests. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); stateMachine.ProcessString(L"\x1b[s"); stateMachine.ProcessString(L"\x1b[?69l"); }); Log::Comment(L"HPR inside margins"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move forward 5 columns (HPR). stateMachine.ProcessString(L"\x1b[5a"); // We should end up in column 25 (outside the right margin). VERIFY_ARE_EQUAL(COORD({ 25, 10 }), cursor.GetPosition()); Log::Comment(L"VPR inside margins"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move down 5 lines (VPR). stateMachine.ProcessString(L"\x1b[5e"); // We should end up on line 15 (outside the bottom margin). VERIFY_ARE_EQUAL(COORD({ 20, 15 }), cursor.GetPosition()); Log::Comment(L"HPR to end of line"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move forward 9999 columns (HPR). stateMachine.ProcessString(L"\x1b[9999a"); // We should end up in the rightmost column. const auto screenWidth = si.GetBufferSize().Width(); VERIFY_ARE_EQUAL(COORD({ screenWidth - 1, 10 }), cursor.GetPosition()); Log::Comment(L"VPR to bottom of screen"); // Starting from column 20 of line 10. cursor.SetPosition(COORD{ 20, 10 }); // Move down 9999 lines (VPR). stateMachine.ProcessString(L"\x1b[9999e"); // We should end up on the last line. const auto screenHeight = si.GetViewport().Height(); VERIFY_ARE_EQUAL(COORD({ 20, screenHeight - 1 }), cursor.GetPosition()); } void ScreenBufferTests::CursorSaveRestore() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si.GetTextBuffer().GetCursor(); const auto defaultAttrs = TextAttribute{}; const auto colorAttrs = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; const auto asciiText = L"lwkmvj"; const auto graphicText = L"┌┬┐└┴┘"; const auto selectAsciiChars = L"\x1b(B"; const auto selectGraphicsChars = L"\x1b(0"; const auto saveCursor = L"\x1b[s"; const auto restoreCursor = L"\x1b[u"; const auto setDECOM = L"\x1b[?6h"; const auto resetDECOM = L"\x1b[?6l"; Log::Comment(L"Make sure the viewport is at 0,0"); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); Log::Comment(L"Restore after save."); // Set the cursor position, attributes, and character set. cursor.SetPosition(COORD{ 20, 10 }); si.SetAttributes(colorAttrs); stateMachine.ProcessString(selectGraphicsChars); // Save state. stateMachine.ProcessString(saveCursor); // Reset the cursor position, attributes, and character set. cursor.SetPosition(COORD{ 0, 0 }); si.SetAttributes(defaultAttrs); stateMachine.ProcessString(selectAsciiChars); // Restore state. stateMachine.ProcessString(restoreCursor); // Verify initial position, colors, and graphic character set. VERIFY_ARE_EQUAL(COORD({ 20, 10 }), cursor.GetPosition()); VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes()); stateMachine.ProcessString(asciiText); VERIFY_IS_TRUE(_ValidateLineContains(COORD({ 20, 10 }), graphicText, colorAttrs)); Log::Comment(L"Restore again without save."); // Reset the cursor position, attributes, and character set. cursor.SetPosition(COORD{ 0, 0 }); si.SetAttributes(defaultAttrs); stateMachine.ProcessString(selectAsciiChars); // Restore state. stateMachine.ProcessString(restoreCursor); // Verify initial saved position, colors, and graphic character set. VERIFY_ARE_EQUAL(COORD({ 20, 10 }), cursor.GetPosition()); VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes()); stateMachine.ProcessString(asciiText); VERIFY_IS_TRUE(_ValidateLineContains(COORD({ 20, 10 }), graphicText, colorAttrs)); Log::Comment(L"Restore after reset."); // Soft reset. stateMachine.ProcessString(L"\x1b[!p"); // Set the cursor position, attributes, and character set. cursor.SetPosition(COORD{ 20, 10 }); si.SetAttributes(colorAttrs); stateMachine.ProcessString(selectGraphicsChars); // Restore state. stateMachine.ProcessString(restoreCursor); // Verify home position, default attributes, and ascii character set. VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); VERIFY_ARE_EQUAL(defaultAttrs, si.GetAttributes()); stateMachine.ProcessString(asciiText); VERIFY_IS_TRUE(_ValidateLineContains(COORD({ 0, 0 }), asciiText, defaultAttrs)); Log::Comment(L"Restore origin mode."); // Set margins and origin mode to relative. stateMachine.ProcessString(L"\x1b[10;20r"); stateMachine.ProcessString(setDECOM); // Verify home position inside margins. VERIFY_ARE_EQUAL(COORD({ 0, 9 }), cursor.GetPosition()); // Save state and reset origin mode to absolute. stateMachine.ProcessString(saveCursor); stateMachine.ProcessString(resetDECOM); // Verify home position at origin. VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); // Restore state and move to home position. stateMachine.ProcessString(restoreCursor); stateMachine.ProcessString(L"\x1b[H"); // Verify home position inside margins, i.e. relative origin mode restored. VERIFY_ARE_EQUAL(COORD({ 0, 9 }), cursor.GetPosition()); Log::Comment(L"Clamp inside top margin."); // Reset margins, with absolute origin, and set cursor position. stateMachine.ProcessString(L"\x1b[r"); stateMachine.ProcessString(setDECOM); cursor.SetPosition(COORD{ 5, 15 }); // Save state. stateMachine.ProcessString(saveCursor); // Set margins and restore state. stateMachine.ProcessString(L"\x1b[20;25r"); stateMachine.ProcessString(restoreCursor); // Verify Y position is clamped inside the top margin VERIFY_ARE_EQUAL(COORD({ 5, 19 }), cursor.GetPosition()); Log::Comment(L"Clamp inside bottom margin."); // Reset margins, with absolute origin, and set cursor position. stateMachine.ProcessString(L"\x1b[r"); stateMachine.ProcessString(setDECOM); cursor.SetPosition(COORD{ 5, 15 }); // Save state. stateMachine.ProcessString(saveCursor); // Set margins and restore state. stateMachine.ProcessString(L"\x1b[1;10r"); stateMachine.ProcessString(restoreCursor); // Verify Y position is clamped inside the top margin VERIFY_ARE_EQUAL(COORD({ 5, 9 }), cursor.GetPosition()); // Reset origin mode and margins. stateMachine.ProcessString(resetDECOM); stateMachine.ProcessString(L"\x1b[r"); } void ScreenBufferTests::ScreenAlignmentPattern() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto& stateMachine = si.GetStateMachine(); const auto& cursor = si.GetTextBuffer().GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); Log::Comment(L"Set the initial buffer state."); const auto bufferWidth = si.GetBufferSize().Width(); const auto bufferHeight = si.GetBufferSize().Height(); // Move the viewport down a few lines, and only cover part of the buffer width. si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 30 }), true); const auto viewportStart = si.GetViewport().Top(); const auto viewportEnd = si.GetViewport().BottomExclusive(); // Fill the entire buffer with Zs. Blue on Green. const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; _FillLines(0, bufferHeight, L'Z', bufferAttr); // Set the initial attributes. auto initialAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; initialAttr.SetReverseVideo(true); initialAttr.SetUnderlined(true); si.SetAttributes(initialAttr); // Set some margins. stateMachine.ProcessString(L"\x1b[10;20r"); VERIFY_IS_TRUE(si.AreMarginsSet()); // Place the cursor in the center. auto cursorPos = COORD{ bufferWidth / 2, (viewportStart + viewportEnd) / 2 }; VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true)); Log::Comment(L"Execute the DECALN escape sequence."); stateMachine.ProcessString(L"\x1b#8"); Log::Comment(L"Lines within view should be filled with Es, with default attributes."); auto defaultAttr = TextAttribute{}; VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, viewportEnd, L'E', defaultAttr)); Log::Comment(L"Field of Zs outside viewport should remain unchanged."); VERIFY_IS_TRUE(_ValidateLinesContain(0, viewportStart, L'Z', bufferAttr)); VERIFY_IS_TRUE(_ValidateLinesContain(viewportEnd, bufferHeight, L'Z', bufferAttr)); Log::Comment(L"Margins should not be set."); VERIFY_IS_FALSE(si.AreMarginsSet()); Log::Comment(L"Cursor position should be moved to home."); auto homePosition = COORD{ 0, viewportStart }; VERIFY_ARE_EQUAL(homePosition, cursor.GetPosition()); Log::Comment(L"Meta/rendition attributes should be reset."); auto expectedAttr = initialAttr; expectedAttr.SetStandardErase(); VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes()); } void ScreenBufferTests::TestCursorIsOn() { auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = tbi.GetCursor(); stateMachine.ProcessString(L"Hello World"); VERIFY_IS_TRUE(cursor.IsOn()); VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12l"); VERIFY_IS_TRUE(cursor.IsOn()); VERIFY_IS_FALSE(cursor.IsBlinkingAllowed()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12h"); VERIFY_IS_TRUE(cursor.IsOn()); VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); VERIFY_IS_TRUE(cursor.IsVisible()); cursor.SetIsOn(false); stateMachine.ProcessString(L"\x1b[?12l"); VERIFY_IS_TRUE(cursor.IsOn()); VERIFY_IS_FALSE(cursor.IsBlinkingAllowed()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12h"); VERIFY_IS_TRUE(cursor.IsOn()); VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?25l"); VERIFY_IS_TRUE(cursor.IsOn()); VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); VERIFY_IS_FALSE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?25h"); VERIFY_IS_TRUE(cursor.IsOn()); VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12;25l"); VERIFY_IS_TRUE(cursor.IsOn()); VERIFY_IS_FALSE(cursor.IsBlinkingAllowed()); VERIFY_IS_FALSE(cursor.IsVisible()); } void ScreenBufferTests::TestAddHyperlink() { auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); // Process the opening osc 8 sequence with no custom id stateMachine.ProcessString(L"\x1b]8;;test.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); // Send any other text stateMachine.ProcessString(L"Hello World"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); // Process the closing osc 8 sequences stateMachine.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(tbi.GetCurrentAttributes().IsHyperlink()); } void ScreenBufferTests::TestAddHyperlinkCustomId() { auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); // Process the opening osc 8 sequence with a custom id stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); // Send any other text stateMachine.ProcessString(L"Hello World"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); // Process the closing osc 8 sequences stateMachine.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(tbi.GetCurrentAttributes().IsHyperlink()); } void ScreenBufferTests::TestAddHyperlinkCustomIdDifferentUri() { auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); // Process the opening osc 8 sequence with a custom id stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); const auto oldAttributes{ tbi.GetCurrentAttributes() }; // Send any other text stateMachine.ProcessString(L"\x1b]8;id=myId;other.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"other.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"other.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); // This second URL should not change the URL of the original ID! VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(oldAttributes.GetHyperlinkId()), L"test.url"); VERIFY_ARE_NOT_EQUAL(oldAttributes.GetHyperlinkId(), tbi.GetCurrentAttributes().GetHyperlinkId()); } void ScreenBufferTests::UpdateVirtualBottomWhenCursorMovesBelowIt() { auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(L"Make sure the initial virtual bottom is at the bottom of the viewport"); si.UpdateBottom(); const auto initialVirtualBottom = si.GetViewport().BottomInclusive(); VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); Log::Comment(L"Set the initial cursor position on that virtual bottom line"); const auto initialCursorPos = COORD{ 0, initialVirtualBottom }; cursor.SetPosition(initialCursorPos); VERIFY_ARE_EQUAL(initialCursorPos, cursor.GetPosition()); Log::Comment(L"Pan down so the initial viewport has the cursor in the middle"); const auto initialOrigin = COORD{ 0, si.GetViewport().Top() + si.GetViewport().Height() / 2 }; gci.SetTerminalScrolling(false); VERIFY_SUCCEEDED(si.SetViewportOrigin(false, initialOrigin, false)); VERIFY_ARE_EQUAL(initialOrigin, si.GetViewport().Origin()); Log::Comment(L"Confirm that the virtual bottom has not changed"); VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); Log::Comment(L"Now write several lines of content using WriteCharsLegacy"); const auto content = L"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"; auto numBytes = wcslen(content) * sizeof(wchar_t); VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, content, content, content, &numBytes, nullptr, 0, 0, nullptr)); Log::Comment(L"Confirm that the cursor position has moved down 10 lines"); const auto newCursorPos = COORD{ initialCursorPos.X, initialCursorPos.Y + 10 }; VERIFY_ARE_EQUAL(newCursorPos, cursor.GetPosition()); Log::Comment(L"Confirm that the virtual bottom matches that new cursor position"); const auto newVirtualBottom = newCursorPos.Y; VERIFY_ARE_EQUAL(newVirtualBottom, si._virtualBottom); Log::Comment(L"The viewport itself should not have changed at this point"); VERIFY_ARE_EQUAL(initialOrigin, si.GetViewport().Origin()); Log::Comment(L"But after MoveToBottom, the viewport should align with the new virtual bottom"); si.MoveToBottom(); VERIFY_ARE_EQUAL(newVirtualBottom, si.GetViewport().BottomInclusive()); } void ScreenBufferTests::RetainHorizontalOffsetWhenMovingToBottom() { auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& cursor = si.GetTextBuffer().GetCursor(); Log::Comment(L"Make the viewport half the default width"); auto initialSize = COORD{ CommonState::s_csWindowWidth / 2, CommonState::s_csWindowHeight }; si.SetViewportSize(&initialSize); Log::Comment(L"Offset the viewport both vertically and horizontally"); auto initialOrigin = COORD{ 10, 20 }; VERIFY_SUCCEEDED(si.SetViewportOrigin(true, initialOrigin, true)); Log::Comment(L"Verify that the virtual viewport is where it's expected to be"); VERIFY_ARE_EQUAL(initialSize, si.GetVirtualViewport().Dimensions()); VERIFY_ARE_EQUAL(initialOrigin, si.GetVirtualViewport().Origin()); Log::Comment(L"Set the cursor position at the viewport origin"); cursor.SetPosition(initialOrigin); VERIFY_ARE_EQUAL(initialOrigin, cursor.GetPosition()); Log::Comment(L"Pan the viewport up by 10 lines"); VERIFY_SUCCEEDED(si.SetViewportOrigin(false, { 0, -10 }, false)); Log::Comment(L"Verify Y offset has moved up and X is unchanged"); VERIFY_ARE_EQUAL(initialOrigin.Y - 10, si.GetViewport().Top()); VERIFY_ARE_EQUAL(initialOrigin.X, si.GetViewport().Left()); Log::Comment(L"Move the viewport back to the virtual bottom"); si.MoveToBottom(); Log::Comment(L"Verify Y offset has moved back and X is unchanged"); VERIFY_ARE_EQUAL(initialOrigin.Y, si.GetViewport().Top()); VERIFY_ARE_EQUAL(initialOrigin.X, si.GetViewport().Left()); } void ScreenBufferTests::TestWriteConsoleVTQuirkMode() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:useQuirk", L"{false, true}") END_TEST_METHOD_PROPERTIES() bool useQuirk; VERIFY_SUCCEEDED(TestData::TryGetValue(L"useQuirk", useQuirk), L"whether to enable the quirk"); CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.LockConsole(); // Lock must be taken to manipulate buffer. auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); auto& mainBuffer = gci.GetActiveOutputBuffer(); auto& cursor = mainBuffer.GetTextBuffer().GetCursor(); // Make sure we're in VT mode WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const TextAttribute defaultAttribute{}; // Make sure we're using the default attributes at the start of the test, // Otherwise they could be polluted from a previous test. mainBuffer.SetAttributes(defaultAttribute); const auto verifyLastAttribute = [&](const TextAttribute& expected) { const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(cursor.GetPosition().Y); const auto attrRow = &row.GetAttrRow(); auto iter{ attrRow->begin() }; iter += cursor.GetPosition().X - 1; VERIFY_ARE_EQUAL(expected, *iter); }; std::unique_ptr waiter; std::wstring seq{}; size_t seqCb{ 0 }; /* Write red on blue, verify that it comes through */ { TextAttribute vtRedOnBlueAttribute{}; vtRedOnBlueAttribute.SetForeground(TextColor{ TextColor::DARK_RED, false }); vtRedOnBlueAttribute.SetBackground(TextColor{ TextColor::DARK_BLUE, false }); seq = L"\x1b[31;44m"; seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); VERIFY_ARE_EQUAL(vtRedOnBlueAttribute, mainBuffer.GetAttributes()); seq = L"X"; seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); verifyLastAttribute(vtRedOnBlueAttribute); } /* Write white on black, verify that it acts as expected for the quirk mode */ { TextAttribute vtWhiteOnBlackAttribute{}; vtWhiteOnBlackAttribute.SetForeground(TextColor{ TextColor::DARK_WHITE, false }); vtWhiteOnBlackAttribute.SetBackground(TextColor{ TextColor::DARK_BLACK, false }); const TextAttribute quirkExpectedAttribute{ useQuirk ? defaultAttribute : vtWhiteOnBlackAttribute }; seq = L"\x1b[37;40m"; // the quirk should suppress this, turning it into "defaults" seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); VERIFY_ARE_EQUAL(quirkExpectedAttribute, mainBuffer.GetAttributes()); seq = L"X"; seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); verifyLastAttribute(quirkExpectedAttribute); } /* Write bright white on black, verify that it acts as expected for the quirk mode */ { TextAttribute vtBrightWhiteOnBlackAttribute{}; vtBrightWhiteOnBlackAttribute.SetForeground(TextColor{ TextColor::DARK_WHITE, false }); vtBrightWhiteOnBlackAttribute.SetBackground(TextColor{ TextColor::DARK_BLACK, false }); vtBrightWhiteOnBlackAttribute.SetBold(true); TextAttribute vtBrightWhiteOnDefaultAttribute{ vtBrightWhiteOnBlackAttribute }; // copy the above attribute vtBrightWhiteOnDefaultAttribute.SetDefaultBackground(); const TextAttribute quirkExpectedAttribute{ useQuirk ? vtBrightWhiteOnDefaultAttribute : vtBrightWhiteOnBlackAttribute }; seq = L"\x1b[1;37;40m"; // the quirk should suppress black only, turning it into "default background" seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); VERIFY_ARE_EQUAL(quirkExpectedAttribute, mainBuffer.GetAttributes()); seq = L"X"; seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); verifyLastAttribute(quirkExpectedAttribute); } /* Write a 256-color white on a 256-color black, make sure the quirk does not suppress it */ { TextAttribute vtWhiteOnBlack256Attribute{}; vtWhiteOnBlack256Attribute.SetForeground(TextColor{ TextColor::DARK_WHITE, true }); vtWhiteOnBlack256Attribute.SetBackground(TextColor{ TextColor::DARK_BLACK, true }); // reset (disable bold from the last test) before setting both colors seq = L"\x1b[m\x1b[38;5;7;48;5;0m"; // the quirk should *not* suppress this (!) seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); VERIFY_ARE_EQUAL(vtWhiteOnBlack256Attribute, mainBuffer.GetAttributes()); seq = L"X"; seqCb = 2 * seq.size(); VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); verifyLastAttribute(vtWhiteOnBlack256Attribute); } }