terminal/src/host/ut_host/ScreenBufferTests.cpp
James Holderness b604117421
Standardize the color table order (#11602)
## Summary of the Pull Request

In the original implementation, we used two different orderings for the color tables. The WT color table used ANSI order, while the conhost color table used a Windows-specific order. This PR standardizes on the ANSI color order everywhere, so the usage of indexed colors is consistent across both parts of the code base, which will hopefully allow more of the code to be shared one day.

## References

This is another small step towards de-duplicating `AdaptDispatch` and `TerminalDispatch` for issue #3849, and is essentially a followup to the SGR dispatch refactoring in PR #6728.

## PR Checklist
* [x] Closes #11461
* [x] CLA signed.
* [x] Tests added/passed
* [ ] Documentation updated.
* [ ] Schema updated.
* [x] I've discussed this with core contributors already. Issue number where discussion took place: #11461

## Detailed Description of the Pull Request / Additional comments

Conhost still needs to deal with legacy attributes using Windows color order, so those values now need to be transposed to ANSI colors order when creating a `TextAttribute` object. This is done with a simple mapping table, which also handles the translation of the default color entries, so it's actually slightly faster than the original code.

And when converting `TextAttribute` values back to legacy console attributes, we were already using a mapping table to handle the narrowing of 256-color values down to 16 colors, so we just needed to adjust that table to account for the translation from ANSI to Windows, and then could make use of the same table for both 256-color and 16-color values.

There are also a few places in conhost that read from or write to the color tables, and those now need to transpose the index values. I've addressed this by creating separate `SetLegacyColorTableEntry` and `GetLegacyColorTableEntry` methods in the `Settings` class which take care of the mapping, so it's now clearer in which cases the code is dealing with legacy values, and which are ANSI values.

These methods are used in the `SetConsoleScreenBufferInfoEx` and `GetConsoleScreenBufferInfoEx` APIs, as well as a few place where color preferences are handled (the registry, shortcut links, and the properties dialog), none of which are particularly sensitive to performance. However, we also use the legacy table when looking up the default colors for rendering (which happens a lot), so I've refactored that code so the default color calculations now only occur once per frame.

The plus side of all of this is that the VT code doesn't need to do the index translation anymore, so we can finally get rid of all the calls to `XTermToWindowsIndex`, and we no longer need a separate color table initialization method for conhost, so I was able to merge a number of color initialization methods into one. We also no longer need to translate from legacy values to ANSI when generating VT sequences for conpty.

The one exception to that is the 16-color VT renderer, which uses the `TextColor::GetLegacyIndex` method to approximate 16-color equivalents for RGB and 256-color values. Since that method returns a legacy index, it still needs to be translated to ANSI before it can be used in a VT sequence. But this should be no worse than it was before.

One more special case is conhost's secret _Color Selection_ feature. That uses `Ctrl`+Number and `Alt`+Number key sequences to highlight parts of the buffer, and the mapping from number to color is based on the Windows color order. So that mapping now needs to be transposed, but that's also not performance sensitive.

The only thing that I haven't bothered to update is the trace logging code in the `Telemetry` class, which logs the first 16 entries in the color table. Those entries are now going to be in a different order, but I didn't think that would be of great concern to anyone.

## Validation Steps Performed

A lot of unit tests needed to be updated to use ANSI color constants when setting indexed colors, where before they might have been expecting values in Windows order. But this replaced a wild mix of different constants, sometimes having to use bit shifting, as well as values mapped with `XTermToWindowsIndex`, so I think the tests are a whole lot clearer now. Only a few cases have been left with literal numbers where that seemed more appropriate.

In addition to getting the unit tests working, I've also manually tested the behaviour of all the console APIs which I thought could be affected by these changes, and confirmed that they produced the same results in the new code as they did in the original implementation.

This includes:
- `WriteConsoleOutput`
- `ReadConsoleOutput`
- `SetConsoleTextAttribute` with `WriteConsoleOutputCharacter`
- `FillConsoleOutputAttribute` and `FillConsoleOutputCharacter` 
- `ScrollConsoleScreenBuffer`
- `GetConsoleScreenBufferInfo`
- `GetConsoleScreenBufferInfoEx`
- `SetConsoleScreenBufferInfoEx`

I've also manually tested changing colors via the console properties menu, the registry, and shortcut links, including setting default colors and popup colors. And I've tested that the "Quirks Mode" is still working as expected in PowerShell.

In terms of performance, I wrote a little test app that filled a 80x9999 buffer with random color combinations using `WriteConsoleOutput`, which I figured was likely to be the most performance sensitive call, and I think it now actually performs slightly better than the original implementation.

I've also tested similar code - just filling the visible window - with SGR VT sequences of various types, and the performance seems about the same as it was before.
2021-11-04 22:13:22 +00:00

6253 lines
254 KiB
C++

// 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 <sstream>
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.SetDefaultForegroundColor(INVALID_COLOR);
gci.SetDefaultBackgroundColor(INVALID_COLOR);
gci.SetFillAttribute(0x07); // DARK_WHITE on DARK_BLACK
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 mainCursorColor = RGB(1, 2, 3);
auto mainCursorType = CursorType::DoubleUnderscore;
auto mainCursorBlinking = false;
mainCursor.SetPosition(mainCursorPos);
mainCursor.SetIsVisible(mainCursorVisible);
mainCursor.SetStyle(mainCursorSize, mainCursorColor, 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(mainCursorColor, altCursor.GetColor());
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 altCursorColor = RGB(3, 2, 1);
auto altCursorType = CursorType::EmptyBox;
auto altCursorBlinking = true;
altCursor.SetPosition(altCursorPos);
altCursor.SetIsVisible(altCursorVisible);
altCursor.SetStyle(altCursorSize, altCursorColor, 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(altCursorColor, mainCursor.GetColor());
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<short> 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<short> _GetTabStops(SCREEN_INFORMATION& screenInfo)
{
std::list<short> 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<short> 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<short> 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<short> 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<short> 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<short> 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<short> 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<short> 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<short> 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<short> 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.SetDefaultForegroundColor(yellow);
gci.SetDefaultBackgroundColor(magenta);
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<COORD>::ToString(cursorPos).GetBuffer()));
const auto viewport = si.GetViewport();
Log::Comment(NoThrowString().Format(
L"Viewport=%s",
VerifyOutputTraits<SMALL_RECT>::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<TextAttribute> 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<SMALL_RECT>::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<SMALL_RECT>::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<TextAttribute> 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<TextAttribute> 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<SMALL_RECT>::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<SMALL_RECT>::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<TextAttribute> 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<TextAttribute> 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();
const COLORREF initialColor = initialCursor.GetColor();
// 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<short>(dx);
newBufferSize.Y += static_cast<short>(dy);
VERIFY_SUCCEEDED(si.ResizeScreenBuffer(newBufferSize, false));
const auto& finalCursor = si.GetTextBuffer().GetCursor();
const CursorType finalType = finalCursor.GetType();
const auto finalSize = finalCursor.GetSize();
const COLORREF finalColor = finalCursor.GetColor();
VERIFY_ARE_EQUAL(initialType, finalType);
VERIFY_ARE_EQUAL(initialColor, finalColor);
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<short>(dx);
newBufferSize.Y += static_cast<short>(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<SMALL_RECT>::ToString(newViewport.ToInclusive()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"Buffer Size: %s",
VerifyOutputTraits<SMALL_RECT>::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<SHORT>(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<SHORT>(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<COORD, COORD> 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.GetColor(), altCursor.GetColor());
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<WriteData> 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<TextAttribute> 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.SetDefaultForegroundColor(yellow);
gci.SetDefaultBackgroundColor(magenta);
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<TextAttribute> 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.SetDefaultForegroundColor(yellow);
gci.SetDefaultBackgroundColor(magenta);
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<TextAttribute> 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.SetDefaultForegroundColor(INVALID_COLOR);
gci.SetDefaultBackgroundColor(magenta);
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<TextAttribute> 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.SetDefaultBackgroundColor(magenta);
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<TextAttribute> 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.SetDefaultBackgroundColor(magenta);
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<TextAttribute> 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.SetDefaultBackgroundColor(magenta);
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<TextAttribute> 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<TextAttribute> 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<TextAttribute> 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<TextAttribute> 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<TextAttribute> 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<TextAttribute> 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<TextAttribute> 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<TextAttribute> 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<TextAttribute> 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.GetDefaultForegroundColor();
COLORREF newColor = gci.GetDefaultForegroundColor();
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.GetDefaultForegroundColor();
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.GetDefaultForegroundColor();
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.GetDefaultForegroundColor();
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.GetDefaultBackgroundColor();
COLORREF newColor = gci.GetDefaultBackgroundColor();
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.GetDefaultBackgroundColor();
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.GetDefaultBackgroundColor();
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.GetDefaultBackgroundColor();
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<N,
// We'll move the X chars to the left, and delete X chars both at the cursor
// pos and at cursor.X+N, but the region of characters at
// [cursor.X+X, cursor.X+N] is left untouched.
//
// Which is the case:
// `(d - 1 > 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<short>(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<short>(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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<TextAttribute> 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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<TextAttribute> 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<class T>
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<class T>
void _FillLine(int line, T fillContent, TextAttribute fillAttr)
{
_FillLine({ 0, gsl::narrow<SHORT>(line) }, fillContent, fillAttr);
}
template<class T>
void _FillLines(int startLine, int endLine, T fillContent, TextAttribute fillAttr)
{
for (auto line = startLine; line < endLine; ++line)
{
_FillLine(line, fillContent, fillAttr);
}
}
template<class... T>
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<T>(expectedContent)... };
while (actual && expected)
{
if (actual->Chars() != expected->Chars() || actual->TextAttr() != expected->TextAttr())
{
return false;
}
++actual;
++expected;
}
return true;
};
template<class T>
bool _ValidateLineContains(int line, T expectedContent, TextAttribute expectedAttr)
{
return _ValidateLineContains({ 0, gsl::narrow<SHORT>(line) }, expectedContent, expectedAttr);
}
template<class T>
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<wchar_t>(deletedLines); // Characters dropped when deleting
viewportLine += gsl::narrow<SHORT>(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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"originalView=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"secondView=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"thirdView=%s", VerifyOutputTraits<SMALL_RECT>::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<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"thirdView=%s", VerifyOutputTraits<SMALL_RECT>::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<TextAttribute>::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\x9c");
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;;\x9c");
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\x9c");
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;;\x9c");
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\x9c");
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\x9c");
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<WriteData> 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);
}
}