Correct fill attributes when scrolling and erasing (#3100)

## Summary of the Pull Request

Operations that erase areas of the screen are typically meant to do so using the current color attributes, but with the rendition attributes reset (what we refer to as meta attributes). This also includes scroll operations that have to clear the area of the screen that has scrolled into view. The only exception is the _Erase Scrollback_ operation, which needs to reset the buffer with the default attributes. This PR updates all of these cases to apply the correct attributes when scrolling and erasing.

## PR Checklist
* [x] Closes #2553
* [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA
* [x] Tests added/passed
* [ ] Requires documentation to be updated
* [ ] I've not really discussed this with core contributors. I'm ready to accept this work might be rejected in favor of a different grand plan. 

## Detailed Description of the Pull Request / Additional comments

My initial plan was to use a special case legacy attribute value to indicate the "standard erase attribute" which could safely be passed through the legacy APIs. But this wouldn't cover the cases that required default attributes to be used. And then with the changes in PR #2668 and #2987, it became clear that our requirements could be better achieved with a couple of new private APIs that wouldn't have to depend on legacy attribute hacks at all.

To that end, I've added the `PrivateFillRegion` and `PrivateScrollRegion` APIs to the `ConGetSet` interface. These are just thin wrappers around the existing `SCREEN_INFORMATION::Write` method and the `ScrollRegion` function respectively, but with a simple boolean parameter to choose between filling with default attributes or the standard erase attributes (i.e the current colors but with meta attributes reset).

With those new APIs in place, I could then update most scroll operations to use `PrivateScrollRegion`, and most erase operations to use `PrivateFillRegion`.

The functions affected by scrolling included:
* `DoSrvPrivateReverseLineFeed` (the RI command)
* `DoSrvPrivateModifyLinesImpl` (the IL and DL commands)
* `AdaptDispatch::_InsertDeleteHelper` (the ICH and DCH commands)
* `AdaptDispatch::_ScrollMovement` (the SU and SD commands)

The functions affected by erasing included:
* `AdaptDispatch::_EraseSingleLineHelper` (the EL command, and most ED variants)
* `AdaptDispatch::EraseCharacters` (the ECH command)

While updating these erase methods, I noticed that both of them also required boundary fixes similar to those in PR #2505 (i.e. the horizontal extent of the erase operation should apply to the full width of the buffer, and not just the current viewport width), so I've addressed that at the same time.

In addition to the changes above, there were also a few special cases, the first being the line feed handling, which required updating in a number of places to use the correct erase attributes:

* `SCREEN_INFORMATION::InitializeCursorRowAttributes` - this is used to initialise the rows that pan into view when the viewport is moved down the buffer.
* `TextBuffer::IncrementCircularBuffer` - this occurs when we scroll passed the very end of the buffer, and a recycled row now needs to be reinitialised.
* `AdjustCursorPosition` - when within margin boundaries, this relies on a couple of direct calls to `ScrollRegion` which needed to be passed the correct fill attributes.

The second special case was the full screen erase sequence (`ESC 2 J`), which is handled separately from the other ED sequences. This required updating the `SCREEN_INFORMATION::VtEraseAll` method to use the standard erase attributes, and also required changes to the horizontal extent of the filled area, since it should have been clearing the full buffer width (the same issue as the other erase operations mentioned above).

Finally, there was the `AdaptDispatch::_EraseScrollback` method, which uses both scroll and fill operations, which could now be handled by the new `PrivateScrollRegion` and `PrivateFillRegion` APIs. But in this case we needed to fill with the default attributes rather than the standard erase attributes. And again this implementation needed some changes to make sure the full width of the active area was retained after the erase, similar to the horizontal boundary issues with the other erase operations.

Once all these changes were made, there were a few areas of the code that could then be simplified quite a bit. The `FillConsoleOutputCharacterW`, `FillConsoleOutputAttribute`, and `ScrollConsoleScreenBufferW` were no longer needed in the `ConGetSet` interface, so all of that code could now be removed. The `_EraseSingleLineDistanceHelper` and `_EraseAreaHelper` methods in the `AdaptDispatch` class were also no longer required and could be removed.

Then there were the hacks to handle legacy default colors in the `FillConsoleOutputAttributeImpl` and `ScrollConsoleScreenBufferWImpl` implementations. Since those hacks were only needed for VT operations, and the VT code no longer calls those methods, there was no longer a need to retain that behaviour (in fact there are probably some edge cases where that behaviour might have been considered a bug when reached via the public console APIs). 

## Validation Steps Performed

For most of the scrolling operations there were already existing tests in place, and those could easily be extended to check that the meta attributes were correctly reset when filling the revealed lines of the scrolling region.

In the screen buffer tests, I made updates of that sort to  the `ScrollOperations` method (handling SU, SD, IL, DL, and RI), the `InsertChars` and `DeleteChars` methods (ICH and DCH), and the `VtNewlinePastViewport` method (LF). I also added a new `VtNewlinePastEndOfBuffer` test to check the case where the line feed causes the viewport to pan past the end of the buffer.

The erase operations, however, were being covered by adapter tests, and those aren't really suited for this kind of functionality (the same sort of issue came up in PR #2505). As a result I've had to reimplement those tests as screen buffer tests.

Most of the erase operations are covered by the `EraseTests` method, except the for the scrollback erase which has a dedicated `EraseScrollbackTests` method. I've also had to replace the `HardReset` adapter test, but that was already mostly covered by the `HardResetBuffer` screen buffer test, which I've now extended slightly (it could do with some more checks, but I think that can wait for a future PR when we're fixing other RIS issues).
This commit is contained in:
James Holderness 2019-12-10 23:14:40 +00:00 committed by msftbot[bot]
parent 5bbf7e2650
commit 381b11521a
17 changed files with 638 additions and 860 deletions

View file

@ -266,3 +266,12 @@ bool TextAttribute::BackgroundIsDefault() const noexcept
{
return _background.IsDefault();
}
// Routine Description:
// - Resets the meta and extended attributes, which is what the VT standard
// requires for most erasing and filling operations.
void TextAttribute::SetStandardErase() noexcept
{
SetExtendedAttributes(ExtendedAttributes::Normal);
SetMetaAttributes(0);
}

View file

@ -156,6 +156,8 @@ public:
bool ForegroundIsDefault() const noexcept;
bool BackgroundIsDefault() const noexcept;
void SetStandardErase() noexcept;
constexpr bool IsRgb() const noexcept
{
return _foreground.IsRgb() || _background.IsRgb();

View file

@ -539,17 +539,24 @@ bool TextBuffer::NewlineCursor()
//Routine Description:
// - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable.
//Arguments:
// - <none>
// - inVtMode - set to true in VT mode, so standard erase attributes are used for the new row.
//Return Value:
// - true if we successfully incremented the buffer.
bool TextBuffer::IncrementCircularBuffer()
bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
{
// FirstRow is at any given point in time the array index in the circular buffer that corresponds
// to the logical position 0 in the window (cursor coordinates and all other coordinates).
_renderTarget.TriggerCircling();
// First, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed.
const bool fSuccess = _storage.at(_firstRow).Reset(_currentAttributes);
auto fillAttributes = _currentAttributes;
if (inVtMode)
{
// The VT standard requires that the new row is initialized with
// the current background color, but with no meta attributes set.
fillAttributes.SetStandardErase();
}
const bool fSuccess = _storage.at(_firstRow).Reset(fillAttributes);
if (fSuccess)
{
// Now proceed to increment.

View file

@ -101,7 +101,7 @@ public:
bool NewlineCursor();
// Scroll needs access to this to quickly rotate around the buffer.
bool IncrementCircularBuffer();
bool IncrementCircularBuffer(const bool inVtMode = false);
COORD GetLastNonSpaceCharacter() const;
COORD GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport viewport) const;

View file

@ -219,24 +219,6 @@ void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region)
try
{
TextAttribute useThisAttr(attribute);
// Here we're being a little clever -
// Because RGB color can't roundtrip the API, certain VT sequences will forget the RGB color
// because their first call to GetScreenBufferInfo returned a legacy attr.
// If they're calling this with the default attrs, they likely wanted to use the RGB default attrs.
// This could create a scenario where someone emitted RGB with VT,
// THEN used the API to FillConsoleOutput with the default attrs, and DIDN'T want the RGB color
// they had set.
if (screenBuffer.InVTMode())
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto bufferLegacy = gci.GenerateLegacyAttributes(screenBuffer.GetAttributes());
if (bufferLegacy == attribute)
{
useThisAttr = TextAttribute(screenBuffer.GetAttributes());
}
}
const OutputCellIterator it(useThisAttr, lengthToWrite);
const auto done = screenBuffer.Write(it, startingCoordinate);

View file

@ -74,7 +74,10 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
}
}
const auto bufferAttributes = screenInfo.GetAttributes();
// The VT standard requires the lines revealed when scrolling are filled
// with the current background color, but with no meta attributes set.
auto fillAttributes = screenInfo.GetAttributes();
fillAttributes.SetStandardErase();
const auto relativeMargins = screenInfo.GetRelativeScrollMargins();
auto viewport = screenInfo.GetViewport();
@ -144,7 +147,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
try
{
ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, bufferAttributes);
ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, fillAttributes);
}
CATCH_LOG();
@ -193,7 +196,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
try
{
ScrollRegion(screenInfo, scrollRect, scrollRect, dest, UNICODE_SPACE, bufferAttributes);
ScrollRegion(screenInfo, scrollRect, scrollRect, dest, UNICODE_SPACE, fillAttributes);
}
CATCH_LOG();

View file

@ -836,32 +836,6 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
auto& buffer = context.GetActiveBuffer();
TextAttribute useThisAttr(fillAttribute);
// Here we're being a little clever - similar to FillConsoleOutputAttributeImpl
// Because RGB/default color can't roundtrip the API, certain VT
// sequences will forget the RGB color because their first call to
// GetScreenBufferInfo returned a legacy attr.
// If they're calling this with the legacy attrs version of our current
// attributes, they likely wanted to use the full version of
// our current attributes, whether that be RGB or _default_ colored.
// This could create a scenario where someone emitted RGB with VT,
// THEN used the API to ScrollConsoleOutput with the legacy attrs,
// and DIDN'T want the RGB color. As in FillConsoleOutputAttribute,
// this scenario is highly unlikely, and we can reasonably do this
// on their behalf.
// see MSFT:19853701
if (buffer.InVTMode())
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto currentAttributes = buffer.GetAttributes();
const auto bufferLegacy = gci.GenerateLegacyAttributes(currentAttributes);
if (bufferLegacy == fillAttribute)
{
useThisAttr = currentAttributes;
}
}
ScrollRegion(buffer, source, clip, target, fillCharacter, useThisAttr);
return S_OK;
@ -1422,25 +1396,12 @@ void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool
coordDestination.X = 0;
coordDestination.Y = viewport.Top + 1;
// Here we previously called to ScrollConsoleScreenBufferWImpl to
// perform the scrolling operation. However, that function only
// accepts a WORD for the fill attributes. That means we'd lose
// 256/RGB fidelity for fill attributes. So instead, we'll just call
// ScrollRegion ourselves, with the same params that
// ScrollConsoleScreenBufferWImpl would have.
// See microsoft/terminal#832, #2702 for more context.
try
{
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
ScrollRegion(screenInfo,
srScroll,
srScroll,
coordDestination,
UNICODE_SPACE,
screenInfo.GetAttributes());
}
CATCH_LOG();
// Note the revealed lines are filled with the standard erase attributes.
Status = NTSTATUS_FROM_HRESULT(DoSrvPrivateScrollRegion(screenInfo,
srScroll,
srScroll,
coordDestination,
true));
}
}
return Status;
@ -2145,25 +2106,12 @@ void DoSrvPrivateModifyLinesImpl(const unsigned int count, const bool insert)
coordDestination.Y = (cursorPosition.Y) - gsl::narrow<short>(count);
}
// Here we previously called to ScrollConsoleScreenBufferWImpl to
// perform the scrolling operation. However, that function only accepts
// a WORD for the fill attributes. That means we'd lose 256/RGB fidelity
// for fill attributes. So instead, we'll just call ScrollRegion
// ourselves, with the same params that ScrollConsoleScreenBufferWImpl
// would have.
// See microsoft/terminal#832 for more context.
try
{
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
ScrollRegion(screenInfo,
srScroll,
srScroll,
coordDestination,
UNICODE_SPACE,
screenInfo.GetAttributes());
}
CATCH_LOG();
// Note the revealed lines are filled with the standard erase attributes.
LOG_IF_FAILED(DoSrvPrivateScrollRegion(screenInfo,
srScroll,
srScroll,
coordDestination,
true));
// The IL and DL controls are also expected to move the cursor to the left margin.
// For now this is just column 0, since we don't yet support DECSLRM.
@ -2287,3 +2235,99 @@ void DoSrvPrivateMoveToBottom(SCREEN_INFORMATION& screenInfo)
}
CATCH_RETURN();
}
// Routine Description:
// - A private API call for filling a region of the screen buffer.
// Arguments:
// - screenInfo - Reference to screen buffer info.
// - startPosition - The position to begin filling at.
// - fillLength - The number of characters to fill.
// - fillChar - Character to fill the target region with.
// - standardFillAttrs - If true, fill with the standard erase attributes.
// If false, fill with the default attributes.
// Return value:
// - S_OK or failure code from thrown exception
[[nodiscard]] HRESULT DoSrvPrivateFillRegion(SCREEN_INFORMATION& screenInfo,
const COORD startPosition,
const size_t fillLength,
const wchar_t fillChar,
const bool standardFillAttrs) noexcept
{
try
{
if (fillLength == 0)
{
return S_OK;
}
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
// For most VT erasing operations, the standard requires that the
// erased area be filled with the current background color, but with
// no additional meta attributes set. For all other cases, we just
// fill with the default attributes.
auto fillAttrs = TextAttribute{};
if (standardFillAttrs)
{
fillAttrs = screenInfo.GetAttributes();
fillAttrs.SetStandardErase();
}
const auto fillData = OutputCellIterator{ fillChar, fillAttrs, fillLength };
screenInfo.Write(fillData, startPosition, false);
// Notify accessibility
auto endPosition = startPosition;
const auto bufferSize = screenInfo.GetBufferSize();
bufferSize.MoveInBounds(fillLength - 1, endPosition);
screenInfo.NotifyAccessibilityEventing(startPosition.X, startPosition.Y, endPosition.X, endPosition.Y);
return S_OK;
}
CATCH_RETURN();
}
// Routine Description:
// - A private API call for moving a block of data in the screen buffer,
// optionally limiting the effects of the move to a clipping rectangle.
// Arguments:
// - screenInfo - Reference to screen buffer info.
// - scrollRect - Region to copy/move (source and size).
// - clipRect - Optional clip region to contain buffer change effects.
// - destinationOrigin - Upper left corner of target region.
// - standardFillAttrs - If true, fill with the standard erase attributes.
// If false, fill with the default attributes.
// Return value:
// - S_OK or failure code from thrown exception
[[nodiscard]] HRESULT DoSrvPrivateScrollRegion(SCREEN_INFORMATION& screenInfo,
const SMALL_RECT scrollRect,
const std::optional<SMALL_RECT> clipRect,
const COORD destinationOrigin,
const bool standardFillAttrs) noexcept
{
try
{
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
// For most VT scrolling operations, the standard requires that the
// erased area be filled with the current background color, but with
// no additional meta attributes set. For all other cases, we just
// fill with the default attributes.
auto fillAttrs = TextAttribute{};
if (standardFillAttrs)
{
fillAttrs = screenInfo.GetAttributes();
fillAttrs.SetStandardErase();
}
ScrollRegion(screenInfo,
scrollRect,
clipRect,
destinationOrigin,
UNICODE_SPACE,
fillAttrs);
return S_OK;
}
CATCH_RETURN();
}

View file

@ -92,3 +92,15 @@ void DoSrvPrivateMoveToBottom(SCREEN_INFORMATION& screenInfo);
[[nodiscard]] HRESULT DoSrvPrivateSetDefaultForegroundColor(const COLORREF value) noexcept;
[[nodiscard]] HRESULT DoSrvPrivateSetDefaultBackgroundColor(const COLORREF value) noexcept;
[[nodiscard]] HRESULT DoSrvPrivateFillRegion(SCREEN_INFORMATION& screenInfo,
const COORD startPosition,
const size_t fillLength,
const wchar_t fillChar,
const bool standardFillAttrs) noexcept;
[[nodiscard]] HRESULT DoSrvPrivateScrollRegion(SCREEN_INFORMATION& screenInfo,
const SMALL_RECT scrollRect,
const std::optional<SMALL_RECT> clipRect,
const COORD destinationOrigin,
const bool standardFillAttrs) noexcept;

View file

@ -296,7 +296,8 @@ static void _ScrollScreen(SCREEN_INFORMATION& screenInfo, const Viewport& source
bool StreamScrollRegion(SCREEN_INFORMATION& screenInfo)
{
// Rotate the circular buffer around and wipe out the previous final line.
bool fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer();
const bool inVtMode = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
bool fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer(inVtMode);
if (fSuccess)
{
// Trigger a graphical update if we're active.

View file

@ -157,42 +157,6 @@ BOOL ConhostInternalGetSet::SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* cons
return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleCursorInfoImpl(_io.GetActiveOutputBuffer(), pConsoleCursorInfo->dwSize, visible));
}
// Routine Description:
// - Connects the FillConsoleOutputCharacter API call directly into our Driver Message servicing call inside Conhost.exe
// Arguments:
// - wch - Character to use for filling the buffer
// - nLength - The length of the fill run in characters (depending on mode, will wrap at the window edge so multiple lines are the sum of the total length)
// - dwWriteCoord - The first fill character's coordinate position in the buffer (writes continue rightward and possibly down from there)
// - numberOfCharsWritten - Pointer to memory location to hold the total number of characters written into the buffer
// Return Value:
// - TRUE if successful (see FillConsoleOutputCharacterWImpl). FALSE otherwise.
BOOL ConhostInternalGetSet::FillConsoleOutputCharacterW(const WCHAR wch, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfCharsWritten) noexcept
{
return SUCCEEDED(ServiceLocator::LocateGlobals().api.FillConsoleOutputCharacterWImpl(_io.GetActiveOutputBuffer(),
wch,
nLength,
dwWriteCoord,
numberOfCharsWritten));
}
// Routine Description:
// - Connects the FillConsoleOutputAttribute API call directly into our Driver Message servicing call inside Conhost.exe
// Arguments:
// - wAttribute - Text attribute (colors/font style) for filling the buffer
// - nLength - The length of the fill run in characters (depending on mode, will wrap at the window edge so multiple lines are the sum of the total length)
// - dwWriteCoord - The first fill character's coordinate position in the buffer (writes continue rightward and possibly down from there)
// - numberOfCharsWritten - Pointer to memory location to hold the total number of text attributes written into the buffer
// Return Value:
// - TRUE if successful (see FillConsoleOutputAttributeImpl). FALSE otherwise.
BOOL ConhostInternalGetSet::FillConsoleOutputAttribute(const WORD wAttribute, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfAttrsWritten) noexcept
{
return SUCCEEDED(ServiceLocator::LocateGlobals().api.FillConsoleOutputAttributeImpl(_io.GetActiveOutputBuffer(),
wAttribute,
nLength,
dwWriteCoord,
numberOfAttrsWritten));
}
// Routine Description:
// - Connects the SetConsoleTextAttribute API call directly into our Driver Message servicing call inside Conhost.exe
// Sets BOTH the FG and the BG component of the attributes.
@ -343,28 +307,6 @@ BOOL ConhostInternalGetSet::PrivateWriteConsoleInputW(_Inout_ std::deque<std::un
true)); // append
}
// Routine Description:
// - Connects the ScrollConsoleScreenBuffer API call directly into our Driver Message servicing call inside Conhost.exe
// Arguments:
// - pScrollRectangle - The region to "cut" from the existing buffer text
// - pClipRectangle - The bounding rectangle within which all modifications should happen. Any modification outside this RECT should be clipped.
// - coordDestinationOrigin - The top left corner of the "paste" from pScrollREctangle
// - pFill - The text/attribute pair to fill all remaining space behind after the "cut" operation (bounded by clip, of course.)
// Return Value:
// - TRUE if successful (see DoSrvScrollConsoleScreenBuffer). FALSE otherwise.
BOOL ConhostInternalGetSet::ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle,
_In_opt_ const SMALL_RECT* pClipRectangle,
_In_ COORD coordDestinationOrigin,
const CHAR_INFO* pFill)
{
return SUCCEEDED(ServiceLocator::LocateGlobals().api.ScrollConsoleScreenBufferWImpl(_io.GetActiveOutputBuffer(),
*pScrollRectangle,
coordDestinationOrigin,
pClipRectangle != nullptr ? std::optional<SMALL_RECT>(*pClipRectangle) : std::nullopt,
pFill->Char.UnicodeChar,
pFill->Attributes));
}
// Routine Description:
// - Connects the SetConsoleWindowInfo API call directly into our Driver Message servicing call inside Conhost.exe
// Arguments:
@ -837,3 +779,54 @@ BOOL ConhostInternalGetSet::PrivateSetDefaultBackground(const COLORREF value) co
{
return SUCCEEDED(DoSrvPrivateSetDefaultBackgroundColor(value));
}
// Routine Description:
// - Connects the PrivateFillRegion call directly into our Driver Message servicing
// call inside Conhost.exe
// PrivateFillRegion is an internal-only "API" call that the vt commands can execute,
// but it is not represented as a function call on our public API surface.
// Arguments:
// - screenInfo - Reference to screen buffer info.
// - startPosition - The position to begin filling at.
// - fillLength - The number of characters to fill.
// - fillChar - Character to fill the target region with.
// - standardFillAttrs - If true, fill with the standard erase attributes.
// If false, fill with the default attributes.
// Return value:
// - TRUE if successful (see DoSrvPrivateScrollRegion). FALSE otherwise.
BOOL ConhostInternalGetSet::PrivateFillRegion(const COORD startPosition,
const size_t fillLength,
const wchar_t fillChar,
const bool standardFillAttrs) noexcept
{
return SUCCEEDED(DoSrvPrivateFillRegion(_io.GetActiveOutputBuffer(),
startPosition,
fillLength,
fillChar,
standardFillAttrs));
}
// Routine Description:
// - Connects the PrivateScrollRegion call directly into our Driver Message servicing
// call inside Conhost.exe
// PrivateScrollRegion is an internal-only "API" call that the vt commands can execute,
// but it is not represented as a function call on our public API surface.
// Arguments:
// - scrollRect - Region to copy/move (source and size).
// - clipRect - Optional clip region to contain buffer change effects.
// - destinationOrigin - Upper left corner of target region.
// - standardFillAttrs - If true, fill with the standard erase attributes.
// If false, fill with the default attributes.
// Return value:
// - TRUE if successful (see DoSrvPrivateScrollRegion). FALSE otherwise.
BOOL ConhostInternalGetSet::PrivateScrollRegion(const SMALL_RECT scrollRect,
const std::optional<SMALL_RECT> clipRect,
const COORD destinationOrigin,
const bool standardFillAttrs) noexcept
{
return SUCCEEDED(DoSrvPrivateScrollRegion(_io.GetActiveOutputBuffer(),
scrollRect,
clipRect,
destinationOrigin,
standardFillAttrs));
}

View file

@ -62,15 +62,6 @@ public:
BOOL GetConsoleCursorInfo(_In_ CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) const override;
BOOL SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) override;
BOOL FillConsoleOutputCharacterW(const WCHAR wch,
const DWORD nLength,
const COORD dwWriteCoord,
size_t& numberOfCharsWritten) noexcept override;
BOOL FillConsoleOutputAttribute(const WORD wAttribute,
const DWORD nLength,
const COORD dwWriteCoord,
size_t& numberOfAttrsWritten) noexcept override;
BOOL SetConsoleTextAttribute(const WORD wAttr) override;
BOOL PrivateSetLegacyAttributes(const WORD wAttr,
@ -96,11 +87,6 @@ public:
BOOL PrivateWriteConsoleInputW(_Inout_ std::deque<std::unique_ptr<IInputEvent>>& events,
_Out_ size_t& eventsWritten) override;
BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle,
_In_opt_ const SMALL_RECT* pClipRectangle,
_In_ COORD coordDestinationOrigin,
const CHAR_INFO* pFill) override;
BOOL SetConsoleWindowInfo(BOOL const bAbsolute,
const SMALL_RECT* const lpConsoleWindow) override;
@ -165,6 +151,16 @@ public:
BOOL PrivateSetDefaultBackground(const COLORREF value) const noexcept override;
BOOL PrivateFillRegion(const COORD startPosition,
const size_t fillLength,
const wchar_t fillChar,
const bool standardFillAttrs) noexcept override;
BOOL PrivateScrollRegion(const SMALL_RECT scrollRect,
const std::optional<SMALL_RECT> clipRect,
const COORD destinationOrigin,
const bool standardFillAttrs) noexcept override;
private:
Microsoft::Console::IIoProvider& _io;
};

View file

@ -2027,10 +2027,15 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const
const FontInfo& existingFont = GetCurrentFont();
// The buffer needs to be initialized with the standard erase attributes,
// i.e. the current background color, but with no meta attributes set.
auto initAttributes = GetAttributes();
initAttributes.SetStandardErase();
NTSTATUS Status = SCREEN_INFORMATION::CreateInstance(WindowSize,
existingFont,
WindowSize,
GetAttributes(),
initAttributes,
*GetPopupAttributes(),
Cursor::CURSOR_SMALL_SIZE,
ppsiNewScreenBuffer);
@ -2489,9 +2494,14 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport,
_viewport.ConvertFromOrigin(&relativeCursor);
RETURN_IF_FAILED(SetCursorPosition(relativeCursor, false));
// Update all the rows in the current viewport with the currently active attributes.
OutputCellIterator it(GetAttributes());
WriteRect(it, _viewport);
// Update all the rows in the current viewport with the standard erase attributes,
// i.e. the current background color, but with no meta attributes set.
auto fillAttributes = GetAttributes();
fillAttributes.SetStandardErase();
auto fillPosition = COORD{ 0, _viewport.Top() };
auto fillLength = gsl::narrow_cast<size_t>(_viewport.Height() * GetBufferSize().Width());
auto fillData = OutputCellIterator{ fillAttributes, fillLength };
Write(fillData, fillPosition, false);
return S_OK;
}
@ -2789,7 +2799,7 @@ void SCREEN_INFORMATION::UpdateBottom()
}
// Method Description:
// - Initialize the row with the cursor on it to the current text attributes.
// - Initialize the row with the cursor on it to the standard erase attributes.
// This is executed when we move the cursor below the current viewport in
// VT mode. When that happens in a real terminal, the line is brand new,
// so it gets initialized for the first time with the current attributes.
@ -2806,7 +2816,11 @@ void SCREEN_INFORMATION::InitializeCursorRowAttributes()
{
const auto& cursor = _textBuffer->GetCursor();
ROW& row = _textBuffer->GetRowByOffset(cursor.GetPosition().Y);
row.GetAttrRow().SetAttrToEnd(0, GetAttributes());
// The VT standard requires that the new row is initialized with
// the current background color, but with no meta attributes set.
auto fillAttributes = GetAttributes();
fillAttributes.SetStandardErase();
row.GetAttrRow().SetAttrToEnd(0, fillAttributes);
}
}

View file

@ -116,6 +116,7 @@ class ScreenBufferTests
TEST_METHOD(VtScrollMarginsNewlineColor);
TEST_METHOD(VtNewlinePastViewport);
TEST_METHOD(VtNewlinePastEndOfBuffer);
TEST_METHOD(VtSetColorTable);
@ -167,6 +168,9 @@ class ScreenBufferTests
TEST_METHOD(InsertChars);
TEST_METHOD(DeleteChars);
TEST_METHOD(EraseScrollbackTests);
TEST_METHOD(EraseTests);
TEST_METHOD(ScrollUpInMargins);
TEST_METHOD(ScrollDownInMargins);
TEST_METHOD(InsertLinesInMargins);
@ -1338,7 +1342,6 @@ void ScreenBufferTests::VtNewlinePastViewport()
stateMachine.ProcessString(seq);
const TextAttribute defaultAttrs{};
const TextAttribute expectedTwo{ FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE };
Log::Comment(NoThrowString().Format(
L"Move the cursor to the bottom of the viewport"));
@ -1350,8 +1353,15 @@ void ScreenBufferTests::VtNewlinePastViewport()
cursor.SetPosition(COORD({ 0, initialViewport.BottomInclusive() }));
seq = L"\x1b[92;44m"; // bright-green on dark-blue
stateMachine.ProcessString(seq);
// Set the attributes that will be used to initialize new rows.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
si.SetAttributes(fillAttr);
// But note that the meta attributes are expected to be cleared.
auto expectedFillAttr = fillAttr;
expectedFillAttr.SetStandardErase();
seq = L"\n";
stateMachine.ProcessString(seq);
@ -1382,7 +1392,86 @@ void ScreenBufferTests::VtNewlinePastViewport()
for (int x = 0; x < viewport.RightInclusive(); x++)
{
const auto& attr = attrs[x];
VERIFY_ARE_EQUAL(expectedTwo, attr);
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 }));
std::wstring seq = L"\x1b[m";
stateMachine.ProcessString(seq);
seq = L"\x1b[2J";
stateMachine.ProcessString(seq);
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.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
si.SetAttributes(fillAttr);
// But note that the meta attributes are expected to be cleared.
auto expectedFillAttr = fillAttr;
expectedFillAttr.SetStandardErase();
seq = L"\n";
stateMachine.ProcessString(seq);
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);
}
}
@ -3154,13 +3243,13 @@ void _FillLines(int startLine, int endLine, T fillContent, TextAttribute fillAtt
}
}
template<class T>
bool _ValidateLineContains(COORD position, T expectedContent, TextAttribute expectedAttr)
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{ expectedContent, expectedAttr };
auto expected = OutputCellIterator{ std::forward<T>(expectedContent)... };
while (actual && expected)
{
if (actual->Chars() != expected->Chars() || actual->TextAttr() != expected->TextAttr())
@ -3284,8 +3373,14 @@ void ScreenBufferTests::ScrollOperations()
_FillLine(viewportLine++, viewportChar++, viewportAttr);
}
// Set the background color so that it will be used to fill the revealed area.
si.SetAttributes({ BACKGROUND_RED });
// Set the attributes that will be used to fill the revealed area.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
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 };
@ -3339,10 +3434,10 @@ void ScreenBufferTests::ScrollOperations()
VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr));
}
Log::Comment(L"The revealed area should now be blank, with default buffer attributes.");
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' ', si.GetAttributes()));
VERIFY_IS_TRUE(_ValidateLinesContain(revealedStart, revealedEnd, L' ', expectedFillAttr));
}
void ScreenBufferTests::InsertChars()
@ -3398,8 +3493,14 @@ void ScreenBufferTests::InsertChars()
const auto textAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
_FillLine({ viewportStart, insertLine }, textChars, textAttr);
// Set the background color so that it will be used to fill the revealed area.
si.SetAttributes({ BACKGROUND_RED });
// Set the attributes that will be used to fill the revealed area.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
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
@ -3419,8 +3520,8 @@ void ScreenBufferTests::InsertChars()
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" ", si.GetAttributes()),
L"Spaces should be inserted with the current attributes at the cursor position.");
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),
@ -3442,9 +3543,6 @@ void ScreenBufferTests::InsertChars()
// Fill the viewport range with text. Red on Blue.
_FillLine({ viewportStart, insertLine }, textChars, textAttr);
// Set the background color so that it will be used to fill the revealed area.
si.SetAttributes({ BACKGROUND_RED });
// Insert 5 spaces at the right edge. Only 1 should be inserted.
// Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
// After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ
@ -3465,8 +3563,8 @@ void ScreenBufferTests::InsertChars()
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" ", si.GetAttributes()),
L"One space should be inserted with the current attributes at the cursor postion.");
VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr),
L"One space should be inserted with standard erase attributes at the cursor postion.");
Log::Comment(
L"Test 3: Inserting at the exact beginning of the line. Same line structure. "
@ -3497,7 +3595,7 @@ void ScreenBufferTests::InsertChars()
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' ', si.GetAttributes()),
VERIFY_IS_TRUE(_ValidateLineContains(insertLine, L' ', expectedFillAttr),
L"A whole line of spaces was inserted at the start, erasing the line.");
}
@ -3554,8 +3652,14 @@ void ScreenBufferTests::DeleteChars()
const auto textAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
_FillLine({ viewportStart, deleteLine }, textChars, textAttr);
// Set the background color so that it will be used to fill the revealed area.
si.SetAttributes({ BACKGROUND_RED });
// Set the attributes that will be used to fill the revealed area.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
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
@ -3579,8 +3683,8 @@ void ScreenBufferTests::DeleteChars()
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" ", si.GetAttributes()),
L"The rest of the line should be replaced with spaces with the current attributes.");
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. "
@ -3598,9 +3702,6 @@ void ScreenBufferTests::DeleteChars()
// Fill the viewport range with text. Red on Blue.
_FillLine({ viewportStart, deleteLine }, textChars, textAttr);
// Set the background color so that it will be used to fill the revealed area.
si.SetAttributes({ BACKGROUND_RED });
// Delete 5 characters at the right edge. Only 1 should be deleted.
// Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
// After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ
@ -3621,8 +3722,8 @@ void ScreenBufferTests::DeleteChars()
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" ", si.GetAttributes()),
L"One character should be erased with the current attributes at the cursor postion.");
VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L" ", expectedFillAttr),
L"One character should be erased with standard erase attributes at the cursor postion.");
Log::Comment(
L"Test 3: Deleting at the exact beginning of the line. Same line structure. "
@ -3653,10 +3754,217 @@ void ScreenBufferTests::DeleteChars()
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' ', si.GetAttributes()),
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({ 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", (int&)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.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
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
@ -4305,6 +4613,7 @@ void ScreenBufferTests::HardResetBuffer()
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()

View file

@ -539,11 +539,6 @@ bool AdaptDispatch::_InsertDeleteHelper(_In_ unsigned int const uiCount, const b
coordDestination.Y = cursor.Y;
coordDestination.X = cursor.X;
// Fill character for remaining space left behind by "cut" operation (or for fill if we "cut" the entire line)
CHAR_INFO ciFill;
ciFill.Attributes = csbiex.wAttributes;
ciFill.Char.UnicodeChar = L' ';
bool fSuccess = false;
if (fIsInsert)
{
@ -558,10 +553,8 @@ bool AdaptDispatch::_InsertDeleteHelper(_In_ unsigned int const uiCount, const b
if (fSuccess)
{
fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScroll,
&srScroll,
coordDestination,
&ciFill);
// Note the revealed characters are filled with the standard erase attributes.
fSuccess = !!_conApi->PrivateScrollRegion(srScroll, srScroll, coordDestination, true);
}
return fSuccess;
@ -590,54 +583,6 @@ bool AdaptDispatch::DeleteCharacter(_In_ unsigned int const uiCount)
{
return _InsertDeleteHelper(uiCount, false);
}
// Routine Description:
// - Internal helper to erase a specific number of characters in one particular line of the buffer.
// Erased positions are replaced with spaces.
// Arguments:
// - coordStartPosition - The position to begin erasing at.
// - dwLength - the number of characters to erase.
// - wFillColor - The attributes to apply to the erased positions.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_EraseSingleLineDistanceHelper(const COORD coordStartPosition, const DWORD dwLength, const WORD wFillColor) const
{
WCHAR const wchSpace = static_cast<WCHAR>(0x20); // space character. use 0x20 instead of literal space because we can't assume the compiler will always turn ' ' into 0x20.
size_t written = 0;
bool fSuccess = !!_conApi->FillConsoleOutputCharacterW(wchSpace, dwLength, coordStartPosition, written);
if (fSuccess)
{
fSuccess = !!_conApi->FillConsoleOutputAttribute(wFillColor, dwLength, coordStartPosition, written);
}
return fSuccess;
}
bool AdaptDispatch::_EraseAreaHelper(const COORD coordStartPosition, const COORD coordLastPosition, const WORD wFillColor)
{
WCHAR const wchSpace = static_cast<WCHAR>(0x20); // space character. use 0x20 instead of literal space because we can't assume the compiler will always turn ' ' into 0x20.
size_t written = 0;
FAIL_FAST_IF(!(coordStartPosition.X < coordLastPosition.X));
FAIL_FAST_IF(!(coordStartPosition.Y < coordLastPosition.Y));
bool fSuccess = false;
for (short y = coordStartPosition.Y; y < coordLastPosition.Y; y++)
{
const COORD coordLine = { coordStartPosition.X, y };
fSuccess = !!_conApi->FillConsoleOutputCharacterW(wchSpace, coordLastPosition.X - coordStartPosition.X, coordLine, written);
if (fSuccess)
{
fSuccess = !!_conApi->FillConsoleOutputAttribute(wFillColor, coordLastPosition.X - coordStartPosition.X, coordLine, written);
}
if (!fSuccess)
{
break;
}
}
return fSuccess;
}
// Routine Description:
// - Internal helper to erase one particular line of the buffer. Either from beginning to the cursor, from the cursor to the end, or the entire line.
@ -649,7 +594,7 @@ bool AdaptDispatch::_EraseAreaHelper(const COORD coordStartPosition, const COORD
// - This is not aware of circular buffer. Line 0 is always the top visible line if you scrolled the whole way up the window.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId, const WORD wFillColor) const
bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId) const
{
COORD coordStartPosition = { 0 };
coordStartPosition.Y = sLineId;
@ -660,7 +605,7 @@ bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* c
{
case DispatchTypes::EraseType::FromBeginning:
case DispatchTypes::EraseType::All:
coordStartPosition.X = pcsbiex->srWindow.Left; // from beginning and the whole line start from the left viewport edge.
coordStartPosition.X = 0; // from beginning and the whole line start from the left most edge of the buffer.
break;
case DispatchTypes::EraseType::ToEnd:
coordStartPosition.X = pcsbiex->dwCursorPosition.X; // from the current cursor position (including it)
@ -674,16 +619,17 @@ bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* c
{
case DispatchTypes::EraseType::FromBeginning:
// +1 because if cursor were at the left edge, the length would be 0 and we want to paint at least the 1 character the cursor is on.
nLength = (pcsbiex->dwCursorPosition.X - pcsbiex->srWindow.Left) + 1;
nLength = pcsbiex->dwCursorPosition.X + 1;
break;
case DispatchTypes::EraseType::ToEnd:
case DispatchTypes::EraseType::All:
// Remember the .Right value is 1 farther than the right most displayed character in the viewport. Therefore no +1.
nLength = pcsbiex->srWindow.Right - coordStartPosition.X;
// Remember the .X value is 1 farther than the right most column in the buffer. Therefore no +1.
nLength = pcsbiex->dwSize.X - coordStartPosition.X;
break;
}
return _EraseSingleLineDistanceHelper(coordStartPosition, nLength, wFillColor);
// Note that the region is filled with the standard erase attributes.
return !!_conApi->PrivateFillRegion(coordStartPosition, nLength, L' ', true);
}
// Routine Description:
@ -705,12 +651,13 @@ bool AdaptDispatch::EraseCharacters(_In_ unsigned int const uiNumChars)
{
const COORD coordStartPosition = csbiex.dwCursorPosition;
const SHORT sRemainingSpaces = csbiex.srWindow.Right - coordStartPosition.X;
const SHORT sRemainingSpaces = csbiex.dwSize.X - coordStartPosition.X;
const unsigned short usActualRemaining = (sRemainingSpaces < 0) ? 0 : sRemainingSpaces;
// erase at max the number of characters remaining in the line from the current position.
const DWORD dwEraseLength = (uiNumChars <= usActualRemaining) ? uiNumChars : usActualRemaining;
fSuccess = _EraseSingleLineDistanceHelper(coordStartPosition, dwEraseLength, csbiex.wAttributes);
// Note that the region is filled with the standard erase attributes.
fSuccess = !!_conApi->PrivateFillRegion(coordStartPosition, dwEraseLength, L' ', true);
}
return fSuccess;
}
@ -764,7 +711,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType)
// For beginning and all, erase all complete lines before (above vertically) from the cursor position.
for (SHORT sStartLine = csbiex.srWindow.Top; sStartLine < csbiex.dwCursorPosition.Y; sStartLine++)
{
fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine, csbiex.wAttributes);
fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine);
if (!fSuccess)
{
@ -776,7 +723,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType)
if (fSuccess)
{
// 2. Cursor Line
fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y, csbiex.wAttributes);
fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y);
}
if (fSuccess)
@ -788,7 +735,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType)
// Remember that the viewport bottom value is 1 beyond the viewable area of the viewport.
for (SHORT sStartLine = csbiex.dwCursorPosition.Y + 1; sStartLine < csbiex.srWindow.Bottom; sStartLine++)
{
fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine, csbiex.wAttributes);
fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine);
if (!fSuccess)
{
@ -816,7 +763,7 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType)
if (fSuccess)
{
fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y, csbiex.wAttributes);
fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y);
}
return fSuccess;
@ -986,11 +933,8 @@ bool AdaptDispatch::_ScrollMovement(const ScrollDirection sdDirection, _In_ unsi
coordDestination.X = srScreen.Left;
coordDestination.Y = srScreen.Top + sDistance * (sdDirection == ScrollDirection::Up ? -1 : 1);
// Fill character for remaining space left behind by "cut" operation (or for fill if we "cut" the entire line)
CHAR_INFO ciFill;
ciFill.Attributes = csbiex.wAttributes;
ciFill.Char.UnicodeChar = L' ';
fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScreen, &srScreen, coordDestination, &ciFill);
// Note the revealed lines are filled with the standard erase attributes.
fSuccess = !!_conApi->PrivateScrollRegion(srScreen, srScreen, coordDestination, true);
}
}
@ -1621,64 +1565,47 @@ bool AdaptDispatch::_EraseScrollback()
if (fSuccess)
{
const SMALL_RECT Screen = csbiex.srWindow;
const short sWidth = Screen.Right - Screen.Left;
const short sHeight = Screen.Bottom - Screen.Top;
FAIL_FAST_IF(!(sWidth > 0 && sHeight > 0));
FAIL_FAST_IF(!(sHeight > 0));
const COORD Cursor = csbiex.dwCursorPosition;
// Rectangle to cut out of the existing buffer
// It will be clipped to the buffer boundaries so SHORT_MAX gives us the full buffer width.
SMALL_RECT srScroll = Screen;
srScroll.Left = 0;
srScroll.Right = SHORT_MAX;
// Paste coordinate for cut text above
COORD coordDestination;
coordDestination.X = 0;
coordDestination.Y = 0;
// Fill character for remaining space left behind by "cut" operation (or for fill if we "cut" the entire line)
CHAR_INFO ciFill;
ciFill.Attributes = csbiex.wAttributes;
ciFill.Char.UnicodeChar = static_cast<WCHAR>(0x20); // space character. use 0x20 instead of literal space because we can't assume the compiler will always turn ' ' into 0x20.
fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScroll, nullptr, coordDestination, &ciFill);
// Typically a scroll operation should fill with standard erase attributes, but in
// this case we need to use the default attributes, hence standardFillAttrs is false.
fSuccess = !!_conApi->PrivateScrollRegion(srScroll, std::nullopt, coordDestination, false);
if (fSuccess)
{
// Clear everything after the viewport. This is two regions:
// A. below the viewport
// B. to the right of the viewport.
// First clear section A
// Clear everything after the viewport.
const DWORD dwTotalAreaBelow = csbiex.dwSize.X * (csbiex.dwSize.Y - sHeight);
const COORD coordBelowStartPosition = { 0, sHeight };
// We don't use the _EraseAreaHelper here because _EraseSingleLineDistanceHelper does it all in one operation
fSuccess = _EraseSingleLineDistanceHelper(coordBelowStartPosition, dwTotalAreaBelow, csbiex.wAttributes);
// Again we need to use the default attributes, hence standardFillAttrs is false.
fSuccess = _conApi->PrivateFillRegion(coordBelowStartPosition, dwTotalAreaBelow, L' ', false);
if (fSuccess)
{
// If there is a section B, clear it.
const COORD coordBottomRight = { csbiex.dwSize.X, coordBelowStartPosition.Y };
const COORD coordRightStartPosition = { sWidth, 0 };
if (coordBottomRight.X > coordRightStartPosition.X)
{
// We use the Area helper here because the Line helper would
// erase the parts of the screen we want to keep too
fSuccess = _EraseAreaHelper(coordRightStartPosition, coordBottomRight, csbiex.wAttributes);
}
// Move the viewport (CAN'T be done in one call with SetConsoleScreenBufferInfoEx, because legacy)
SMALL_RECT srNewViewport;
srNewViewport.Left = Screen.Left;
srNewViewport.Top = 0;
// SetConsoleWindowInfo uses an inclusive rect, while GetConsoleScreenBufferInfo is exclusive
srNewViewport.Right = Screen.Right - 1;
srNewViewport.Bottom = sHeight - 1;
fSuccess = !!_conApi->SetConsoleWindowInfo(true, &srNewViewport);
if (fSuccess)
{
// Move the viewport (CAN'T be done in one call with SetConsoleScreenBufferInfoEx, because legacy)
SMALL_RECT srNewViewport;
srNewViewport.Left = 0;
srNewViewport.Top = 0;
// SetConsoleWindowInfo uses an inclusive rect, while GetConsoleScreenBufferInfo is exclusive
srNewViewport.Right = sWidth - 1;
srNewViewport.Bottom = sHeight - 1;
fSuccess = !!_conApi->SetConsoleWindowInfo(true, &srNewViewport);
if (fSuccess)
{
// Move the cursor to the same relative location.
const COORD newCursor = { Cursor.X - Screen.Left, Cursor.Y - Screen.Top };
fSuccess = !!_conApi->SetConsoleCursorPosition(newCursor);
}
// Move the cursor to the same relative location.
const COORD newCursor = { Cursor.X, Cursor.Y - Screen.Top };
fSuccess = !!_conApi->SetConsoleCursorPosition(newCursor);
}
}
}

View file

@ -129,10 +129,8 @@ namespace Microsoft::Console::VirtualTerminal
bool _CursorMovement(const CursorDirection dir, _In_ unsigned int const uiDistance) const;
bool _CursorMovePosition(_In_opt_ const unsigned int* const puiRow, _In_opt_ const unsigned int* const puiCol) const;
bool _EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId, const WORD wFillColor) const;
bool _EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId) const;
void _SetGraphicsOptionHelper(const DispatchTypes::GraphicsOptions opt, _Inout_ WORD* const pAttr);
bool _EraseAreaHelper(const COORD coordStartPosition, const COORD coordLastPosition, const WORD wFillColor);
bool _EraseSingleLineDistanceHelper(const COORD coordStartPosition, const DWORD dwLength, const WORD wFillColor) const;
bool _EraseScrollback();
bool _EraseAll();
void _SetGraphicsOptionHelper(const DispatchTypes::GraphicsOptions opt, _Inout_ WORD* const pAttr) const;

View file

@ -32,14 +32,6 @@ namespace Microsoft::Console::VirtualTerminal
virtual BOOL SetConsoleScreenBufferInfoEx(const CONSOLE_SCREEN_BUFFER_INFOEX* const pConsoleScreenBufferInfoEx) = 0;
virtual BOOL SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) = 0;
virtual BOOL SetConsoleCursorPosition(const COORD coordCursorPosition) = 0;
virtual BOOL FillConsoleOutputCharacterW(const WCHAR wch,
const DWORD nLength,
const COORD dwWriteCoord,
size_t& numberOfCharsWritten) noexcept = 0;
virtual BOOL FillConsoleOutputAttribute(const WORD wAttribute,
const DWORD nLength,
const COORD dwWriteCoord,
size_t& numberOfAttrsWritten) noexcept = 0;
virtual BOOL SetConsoleTextAttribute(const WORD wAttr) = 0;
virtual BOOL PrivateSetLegacyAttributes(const WORD wAttr,
@ -60,10 +52,6 @@ namespace Microsoft::Console::VirtualTerminal
virtual BOOL PrivateWriteConsoleInputW(_Inout_ std::deque<std::unique_ptr<IInputEvent>>& events,
_Out_ size_t& eventsWritten) = 0;
virtual BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle,
_In_opt_ const SMALL_RECT* pClipRectangle,
_In_ COORD dwDestinationOrigin,
const CHAR_INFO* pFill) = 0;
virtual BOOL SetConsoleWindowInfo(const BOOL bAbsolute,
const SMALL_RECT* const lpConsoleWindow) = 0;
virtual BOOL PrivateSetCursorKeysMode(const bool fApplicationMode) = 0;
@ -113,5 +101,15 @@ namespace Microsoft::Console::VirtualTerminal
virtual BOOL PrivateSetColorTableEntry(const short index, const COLORREF value) const = 0;
virtual BOOL PrivateSetDefaultForeground(const COLORREF value) const = 0;
virtual BOOL PrivateSetDefaultBackground(const COLORREF value) const = 0;
virtual BOOL PrivateFillRegion(const COORD startPosition,
const size_t fillLength,
const wchar_t fillChar,
const bool standardFillAttrs) = 0;
virtual BOOL PrivateScrollRegion(const SMALL_RECT scrollRect,
const std::optional<SMALL_RECT> clipRect,
const COORD destinationOrigin,
const bool standardFillAttrs) = 0;
};
}

View file

@ -186,62 +186,6 @@ public:
return _fPrivateAllowCursorBlinkingResult;
}
BOOL FillConsoleOutputCharacterW(const WCHAR wch, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfCharsWritten) noexcept override
{
Log::Comment(L"FillConsoleOutputCharacterW MOCK called...");
DWORD dwCharsWritten = 0;
if (_fFillConsoleOutputCharacterWResult)
{
Log::Comment(NoThrowString().Format(L"Filling (X: %d, Y:%d) for %d characters with '%c'...", dwWriteCoord.X, dwWriteCoord.Y, nLength, wch));
COORD dwCurrentPos = dwWriteCoord;
while (dwCharsWritten < nLength)
{
CHAR_INFO* pchar = _GetCharAt(dwCurrentPos.Y, dwCurrentPos.X);
pchar->Char.UnicodeChar = wch;
dwCharsWritten++;
_IncrementCoordPos(&dwCurrentPos);
}
}
numberOfCharsWritten = dwCharsWritten;
Log::Comment(NoThrowString().Format(L"Fill wrote %d characters.", dwCharsWritten));
return _fFillConsoleOutputCharacterWResult;
}
BOOL FillConsoleOutputAttribute(const WORD wAttribute, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfAttrsWritten) noexcept override
{
Log::Comment(L"FillConsoleOutputAttribute MOCK called...");
DWORD dwCharsWritten = 0;
if (_fFillConsoleOutputAttributeResult)
{
Log::Comment(NoThrowString().Format(L"Filling (X: %d, Y:%d) for %d characters with 0x%x attribute...", dwWriteCoord.X, dwWriteCoord.Y, nLength, wAttribute));
COORD dwCurrentPos = dwWriteCoord;
while (dwCharsWritten < nLength)
{
CHAR_INFO* pchar = _GetCharAt(dwCurrentPos.Y, dwCurrentPos.X);
pchar->Attributes = wAttribute;
dwCharsWritten++;
_IncrementCoordPos(&dwCurrentPos);
}
}
numberOfAttrsWritten = dwCharsWritten;
Log::Comment(NoThrowString().Format(L"Fill modified %d characters.", dwCharsWritten));
return _fFillConsoleOutputAttributeResult;
}
BOOL SetConsoleTextAttribute(const WORD wAttr) override
{
Log::Comment(L"SetConsoleTextAttribute MOCK called...");
@ -413,13 +357,6 @@ public:
return _fPrivateWriteConsoleControlInputResult;
}
BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* /*pScrollRectangle*/, _In_opt_ const SMALL_RECT* /*pClipRectangle*/, _In_ COORD /*dwDestinationOrigin*/, const CHAR_INFO* /*pFill*/) override
{
Log::Comment(L"ScrollConsoleScreenBufferW MOCK called...");
return _fScrollConsoleScreenBufferWResult;
}
BOOL PrivateSetScrollingRegion(const SMALL_RECT* const psrScrollMargins) override
{
Log::Comment(L"PrivateSetScrollingRegion MOCK called...");
@ -723,20 +660,24 @@ public:
return _fPrivateSetDefaultBackgroundResult;
}
void _IncrementCoordPos(_Inout_ COORD* pcoord)
BOOL PrivateFillRegion(const COORD /*startPosition*/,
const size_t /*fillLength*/,
const wchar_t /*fillChar*/,
const bool /*standardFillAttrs*/) noexcept override
{
pcoord->X++;
Log::Comment(L"PrivateFillRegion MOCK called...");
if (pcoord->X >= _coordBufferSize.X)
{
pcoord->X = 0;
pcoord->Y++;
return TRUE;
}
if (pcoord->Y >= _coordBufferSize.Y)
{
pcoord->Y = _coordBufferSize.Y - 1;
}
}
BOOL PrivateScrollRegion(const SMALL_RECT /*scrollRect*/,
const std::optional<SMALL_RECT> /*clipRect*/,
const COORD /*destinationOrigin*/,
const bool /*standardFillAttrs*/) noexcept override
{
Log::Comment(L"PrivateScrollRegion MOCK called...");
return TRUE;
}
void PrepData()
@ -764,11 +705,6 @@ public:
}
void PrepData(CursorX xact, CursorY yact)
{
PrepData(xact, yact, s_wchDefault, s_wDefaultAttribute);
}
void PrepData(CursorX xact, CursorY yact, WCHAR wch, WORD wAttr)
{
Log::Comment(L"Resetting mock data state.");
@ -777,18 +713,16 @@ public:
_fGetConsoleScreenBufferInfoExResult = TRUE;
_fGetConsoleCursorInfoResult = TRUE;
_fSetConsoleCursorInfoResult = TRUE;
_fFillConsoleOutputCharacterWResult = TRUE;
_fFillConsoleOutputAttributeResult = TRUE;
_fSetConsoleTextAttributeResult = TRUE;
_fPrivateWriteConsoleInputWResult = TRUE;
_fPrivatePrependConsoleInputResult = TRUE;
_fPrivateWriteConsoleControlInputResult = TRUE;
_fScrollConsoleScreenBufferWResult = TRUE;
_fSetConsoleWindowInfoResult = TRUE;
_fPrivateGetConsoleScreenBufferAttributesResult = TRUE;
_fMoveToBottomResult = true;
_PrepCharsBuffer(wch, wAttr);
_coordBufferSize.X = 100;
_coordBufferSize.Y = 600;
// Viewport sitting in the "middle" of the buffer somewhere (so all sides have excess buffer around them)
_srViewport.Top = 20;
@ -851,47 +785,6 @@ public:
_coordExpectedCursorPos = _coordCursorPos;
}
void _PrepCharsBuffer()
{
_PrepCharsBuffer(s_wchDefault, s_wDefaultAttribute);
}
void _PrepCharsBuffer(WCHAR const wch, WORD const wAttr)
{
// Buffer large
_coordBufferSize.X = 100;
_coordBufferSize.Y = 600;
// Buffer data
_FreeCharsBuffer();
DWORD const cchTotalBufferSize = _coordBufferSize.Y * _coordBufferSize.X;
_rgchars = new CHAR_INFO[cchTotalBufferSize];
COORD coordStart = { 0 };
size_t written = 0;
// Fill buffer with Zs.
Log::Comment(L"Filling buffer with characters so we can tell what's deleted.");
FillConsoleOutputCharacterW(wch, cchTotalBufferSize, coordStart, written);
// Fill attributes with 0s
Log::Comment(L"Filling buffer with attributes so we can tell what happened.");
FillConsoleOutputAttribute(wAttr, cchTotalBufferSize, coordStart, written);
VERIFY_ARE_EQUAL(((DWORD)cchTotalBufferSize), ((DWORD)written), L"Ensure the writer says all characters in the buffer were filled.");
}
void _FreeCharsBuffer()
{
if (_rgchars != nullptr)
{
delete[] _rgchars;
_rgchars = nullptr;
}
}
void ValidateInputEvent(_In_ PCWSTR pwszExpectedResponse)
{
size_t const cchResponse = wcslen(pwszExpectedResponse);
@ -918,101 +811,6 @@ public:
}
}
bool ValidateEraseBufferState(SMALL_RECT* rgsrRegions, size_t cRegions, wchar_t wchExpectedInRegions, WORD wAttrExpectedInRegions)
{
bool fStateValid = true;
Log::Comment(NoThrowString().Format(L"The following %zu regions are used as in-bounds for this test:", cRegions));
for (size_t iRegion = 0; iRegion < cRegions; iRegion++)
{
SMALL_RECT srRegion = rgsrRegions[iRegion];
Log::Comment(NoThrowString().Format(L"#%zu - (T: %d, B: %d, L: %d, R:%d)", iRegion, srRegion.Top, srRegion.Bottom, srRegion.Left, srRegion.Right));
}
Log::Comment(L"Now checking every character within the buffer...");
for (short iRow = 0; iRow < _coordBufferSize.Y; iRow++)
{
for (short iCol = 0; iCol < _coordBufferSize.X; iCol++)
{
CHAR_INFO* pchar = _GetCharAt(iRow, iCol);
bool const fIsInclusive = _IsAnyRegionInclusive(rgsrRegions, cRegions, iRow, iCol);
WCHAR const wchExpected = fIsInclusive ? wchExpectedInRegions : TestGetSet::s_wchDefault;
WORD const wAttrExpected = fIsInclusive ? wAttrExpectedInRegions : TestGetSet::s_wDefaultAttribute;
if (pchar->Char.UnicodeChar != wchExpected)
{
fStateValid = false;
Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected: '%c'. Actual: '%c'", iCol, iRow, wchExpected, pchar->Char.UnicodeChar));
break;
}
if (pchar->Attributes != wAttrExpected)
{
fStateValid = false;
Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected Attr: 0x%x. Actual Attr: 0x%x", iCol, iRow, wAttrExpected, pchar->Attributes));
break;
}
}
if (!fStateValid)
{
break;
}
}
return fStateValid;
}
bool _IsAnyRegionInclusive(SMALL_RECT* rgsrRegions, size_t cRegions, short sRow, short sCol)
{
bool fIncludesChar = false;
for (size_t iRegion = 0; iRegion < cRegions; iRegion++)
{
fIncludesChar = _IsInRegionInclusive(rgsrRegions[iRegion], sRow, sCol);
if (fIncludesChar)
{
break;
}
}
return fIncludesChar;
}
bool _IsInRegionInclusive(SMALL_RECT srRegion, short sRow, short sCol)
{
return srRegion.Left <= sCol &&
srRegion.Right >= sCol &&
srRegion.Top <= sRow &&
srRegion.Bottom >= sRow;
}
CHAR_INFO* _GetCharAt(size_t const iRow, size_t const iCol)
{
CHAR_INFO* pchar = nullptr;
if (_rgchars != nullptr)
{
pchar = &(_rgchars[(iRow * _coordBufferSize.X) + iCol]);
}
if (pchar == nullptr)
{
VERIFY_FAIL(L"Failed to retrieve character position from buffer.");
}
return pchar;
}
void _SetMarginsHelper(SMALL_RECT* rect, SHORT top, SHORT bottom)
{
rect->Top = top;
@ -1024,7 +822,6 @@ public:
~TestGetSet()
{
_FreeCharsBuffer();
}
static const WCHAR s_wchErase = (WCHAR)0x20;
@ -1033,7 +830,6 @@ public:
static const WORD s_wDefaultAttribute = 0;
static const WORD s_wDefaultFill = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; // dark gray on black.
CHAR_INFO* _rgchars = nullptr;
std::deque<std::unique_ptr<IInputEvent>> _events;
COORD _coordBufferSize = { 0, 0 };
@ -1075,13 +871,10 @@ public:
BOOL _fSetConsoleCursorPositionResult = false;
BOOL _fGetConsoleCursorInfoResult = false;
BOOL _fSetConsoleCursorInfoResult = false;
BOOL _fFillConsoleOutputCharacterWResult = false;
BOOL _fFillConsoleOutputAttributeResult = false;
BOOL _fSetConsoleTextAttributeResult = false;
BOOL _fPrivateWriteConsoleInputWResult = false;
BOOL _fPrivatePrependConsoleInputResult = false;
BOOL _fPrivateWriteConsoleControlInputResult = false;
BOOL _fScrollConsoleScreenBufferWResult = false;
BOOL _fSetConsoleWindowInfoResult = false;
BOOL _fExpectedWindowAbsolute = false;
@ -1664,247 +1457,6 @@ public:
VERIFY_IS_FALSE(_pDispatch->CursorVisibility(fEnd));
}
// Ensures that EraseScrollback (^[[3J) deletes any content from the buffer
// above the viewport, and moves the contents of the buffer in the
// viewport to 0,0. This emulates the xterm behavior of clearing any
// scrollback content.
TEST_METHOD(EraseScrollbackTests)
{
_testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER);
_testGetSet->_wAttribute = _testGetSet->s_wAttrErase;
Log::Comment(L"Starting Test");
_testGetSet->_fSetConsoleWindowInfoResult = true;
_testGetSet->_fExpectedWindowAbsolute = true;
SMALL_RECT srRegion = { 0 };
srRegion.Bottom = _testGetSet->_srViewport.Bottom - _testGetSet->_srViewport.Top - 1;
srRegion.Right = _testGetSet->_srViewport.Right - _testGetSet->_srViewport.Left - 1;
_testGetSet->_srExpectedConsoleWindow = srRegion;
// The cursor will be moved to the same relative location in the new viewport with origin @ 0, 0
const COORD coordRelativeCursor = { _testGetSet->_coordCursorPos.X - _testGetSet->_srViewport.Left,
_testGetSet->_coordCursorPos.Y - _testGetSet->_srViewport.Top };
_testGetSet->_coordExpectedCursorPos = coordRelativeCursor;
VERIFY_IS_TRUE(_pDispatch->EraseInDisplay(DispatchTypes::EraseType::Scrollback));
// There are two portions of the screen that are cleared -
// below the viewport and to the right of the viewport.
size_t cRegionsToCheck = 2;
SMALL_RECT rgsrRegionsModified[2];
// Region 0 - Below the viewport
srRegion.Top = _testGetSet->_srViewport.Bottom + 1;
srRegion.Left = 0;
srRegion.Bottom = _testGetSet->_coordBufferSize.Y;
srRegion.Right = _testGetSet->_coordBufferSize.X;
rgsrRegionsModified[0] = srRegion;
// Region 1 - To the right of the viewport
srRegion.Top = 0;
srRegion.Left = _testGetSet->_srViewport.Right + 1;
srRegion.Bottom = _testGetSet->_coordBufferSize.Y;
srRegion.Right = _testGetSet->_coordBufferSize.X;
rgsrRegionsModified[1] = srRegion;
// Scan entire buffer and ensure only the necessary region has changed.
bool fRegionSuccess = _testGetSet->ValidateEraseBufferState(rgsrRegionsModified, cRegionsToCheck, TestGetSet::s_wchErase, TestGetSet::s_wAttrErase);
VERIFY_IS_TRUE(fRegionSuccess);
Log::Comment(L"Test 2: Gracefully fail when getting console information fails.");
_testGetSet->PrepData();
_testGetSet->_fGetConsoleScreenBufferInfoExResult = false;
VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(DispatchTypes::EraseType::Scrollback));
Log::Comment(L"Test 3: Gracefully fail when filling the rectangle fails.");
_testGetSet->PrepData();
_testGetSet->_fFillConsoleOutputCharacterWResult = false;
VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(DispatchTypes::EraseType::Scrollback));
}
TEST_METHOD(EraseTests)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:uiEraseType", L"{0, 1, 2}") // corresponds to options in DispatchTypes::EraseType
TEST_METHOD_PROPERTY(L"Data:fEraseScreen", L"{FALSE, TRUE}") // corresponds to Line (FALSE) or Screen (TRUE)
END_TEST_METHOD_PROPERTIES()
// Modify variables based on type of this test
DispatchTypes::EraseType eraseType;
unsigned int uiEraseType;
VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiEraseType", uiEraseType));
eraseType = (DispatchTypes::EraseType)uiEraseType;
bool fEraseScreen;
VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"fEraseScreen", fEraseScreen));
Log::Comment(L"Starting test...");
// This combiniation is a simple VT api call
// Verify that the adapter calls that function, and do nothing else.
// This functionality is covered by ScreenBufferTests::EraseAllTests
if (eraseType == DispatchTypes::EraseType::All && fEraseScreen)
{
Log::Comment(L"Testing Erase in Display - All");
VERIFY_IS_TRUE(_pDispatch->EraseInDisplay(eraseType));
return;
}
Log::Comment(L"Test 1: Perform standard erase operation.");
switch (eraseType)
{
case DispatchTypes::EraseType::FromBeginning:
Log::Comment(L"Erasing line from beginning to cursor.");
break;
case DispatchTypes::EraseType::ToEnd:
Log::Comment(L"Erasing line from cursor to end.");
break;
case DispatchTypes::EraseType::All:
Log::Comment(L"Erasing all.");
break;
default:
VERIFY_FAIL(L"Unsupported erase type.");
}
if (!fEraseScreen)
{
Log::Comment(L"Erasing just one line (the cursor's line).");
}
else
{
Log::Comment(L"Erasing entire display (viewport). May be bounded by the cursor.");
}
_testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER);
_testGetSet->_wAttribute = _testGetSet->s_wAttrErase;
if (!fEraseScreen)
{
VERIFY_IS_TRUE(_pDispatch->EraseInLine(eraseType));
}
else
{
VERIFY_IS_TRUE(_pDispatch->EraseInDisplay(eraseType));
}
// Will be always the region of the cursor line (minimum 1)
// and 2 more if it's the display (for the regions before and after the cursor line, total 3)
SMALL_RECT rgsrRegionsModified[3]; // max of 3 regions.
// Determine selection rectangle for line containing the cursor.
// All sides are inclusive of modified data. (unlike viewport normally)
SMALL_RECT srRegion = { 0 };
srRegion.Top = _testGetSet->_coordCursorPos.Y;
srRegion.Bottom = srRegion.Top;
switch (eraseType)
{
case DispatchTypes::EraseType::FromBeginning:
case DispatchTypes::EraseType::All:
srRegion.Left = _testGetSet->_srViewport.Left;
break;
case DispatchTypes::EraseType::ToEnd:
srRegion.Left = _testGetSet->_coordCursorPos.X;
break;
default:
VERIFY_FAIL(L"Unsupported erase type.");
break;
}
switch (eraseType)
{
case DispatchTypes::EraseType::FromBeginning:
srRegion.Right = _testGetSet->_coordCursorPos.X;
break;
case DispatchTypes::EraseType::All:
case DispatchTypes::EraseType::ToEnd:
srRegion.Right = _testGetSet->_srViewport.Right - 1;
break;
default:
VERIFY_FAIL(L"Unsupported erase type.");
break;
}
rgsrRegionsModified[0] = srRegion;
size_t cRegionsToCheck = 1; // start with 1 region to check from the line above. We may add up to 2 more.
// Need to calculate up to two more regions if this is a screen erase.
if (fEraseScreen)
{
// If from beginning or all, add the region *before* the cursor line.
if (eraseType == DispatchTypes::EraseType::FromBeginning ||
eraseType == DispatchTypes::EraseType::All)
{
srRegion.Left = _testGetSet->_srViewport.Left;
srRegion.Right = _testGetSet->_srViewport.Right - 1; // viewport is exclusive on the right. this test is inclusive so -1.
srRegion.Top = _testGetSet->_srViewport.Top;
srRegion.Bottom = _testGetSet->_coordCursorPos.Y - 1; // this might end up being above top. This will be checked below.
// Only add it if this is still valid.
if (srRegion.Bottom >= srRegion.Top)
{
rgsrRegionsModified[cRegionsToCheck] = srRegion;
cRegionsToCheck++;
}
}
// If from end or all, add the region *after* the cursor line.
if (eraseType == DispatchTypes::EraseType::ToEnd ||
eraseType == DispatchTypes::EraseType::All)
{
srRegion.Left = _testGetSet->_srViewport.Left;
srRegion.Right = _testGetSet->_srViewport.Right - 1; // viewport is exclusive rectangle on the right. this test uses inclusive rectangles so -1.
srRegion.Bottom = _testGetSet->_srViewport.Bottom - 1; // viewport is exclusive rectangle on the bottom. this test uses inclusive rectangles so -1;
srRegion.Top = _testGetSet->_coordCursorPos.Y + 1; // this might end up being below bottom. This will be checked below.
// Only add it if this is still valid.
if (srRegion.Bottom >= srRegion.Top)
{
rgsrRegionsModified[cRegionsToCheck] = srRegion;
cRegionsToCheck++;
}
}
}
// Scan entire buffer and ensure only the necessary region has changed.
bool fRegionSuccess = _testGetSet->ValidateEraseBufferState(rgsrRegionsModified, cRegionsToCheck, TestGetSet::s_wchErase, TestGetSet::s_wAttrErase);
VERIFY_IS_TRUE(fRegionSuccess);
Log::Comment(L"Test 2: Gracefully fail when getting console information fails.");
_testGetSet->PrepData();
_testGetSet->_fGetConsoleScreenBufferInfoExResult = false;
if (!fEraseScreen)
{
VERIFY_IS_FALSE(_pDispatch->EraseInLine(eraseType));
}
else
{
VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(eraseType));
}
Log::Comment(L"Test 3: Gracefully fail when filling the rectangle fails.");
_testGetSet->PrepData();
_testGetSet->_fFillConsoleOutputCharacterWResult = false;
if (!fEraseScreen)
{
VERIFY_IS_FALSE(_pDispatch->EraseInLine(eraseType));
}
else
{
VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(eraseType));
}
}
TEST_METHOD(GraphicsBaseTests)
{
Log::Comment(L"Starting test...");
@ -2704,75 +2256,6 @@ public:
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions));
}
TEST_METHOD(HardReset)
{
Log::Comment(L"Starting test...");
_testGetSet->PrepData();
///////////////// Components of a EraseScrollback //////////////////////
_testGetSet->_fExpectedWindowAbsolute = true;
SMALL_RECT srRegion = { 0 };
srRegion.Bottom = _testGetSet->_srViewport.Bottom - _testGetSet->_srViewport.Top - 1;
srRegion.Right = _testGetSet->_srViewport.Right - _testGetSet->_srViewport.Left - 1;
_testGetSet->_srExpectedConsoleWindow = srRegion;
// The cursor will be moved to the same relative location in the new viewport with origin @ 0, 0
const COORD coordRelativeCursor = { _testGetSet->_coordCursorPos.X - _testGetSet->_srViewport.Left,
_testGetSet->_coordCursorPos.Y - _testGetSet->_srViewport.Top };
const COORD coordExpectedCursorPos = { 0, 0 };
auto prepExpectedParameters = [&]() {
// Cursor to 1,1
_testGetSet->_coordExpectedCursorPos = { 0, 0 };
_testGetSet->_fSetConsoleCursorPositionResult = true;
_testGetSet->_fPrivateSetLegacyAttributesResult = true;
_testGetSet->_fPrivateSetDefaultAttributesResult = true;
_testGetSet->_fPrivateBoldTextResult = true;
_testGetSet->_fExpectedForeground = true;
_testGetSet->_fExpectedBackground = true;
_testGetSet->_fExpectedMeta = true;
_testGetSet->_fExpectedIsBold = false;
_testGetSet->_expectedShowCursor = true;
_testGetSet->_privateShowCursorResult = true;
// We're expecting _SetDefaultColorHelper to call
// PrivateSetLegacyAttributes with 0 as the wAttr param.
_testGetSet->_wExpectedAttribute = 0;
// Prepare the results of SoftReset api calls
_testGetSet->_fPrivateSetCursorKeysModeResult = true;
_testGetSet->_fPrivateSetKeypadModeResult = true;
_testGetSet->_fGetConsoleScreenBufferInfoExResult = true;
_testGetSet->_fPrivateSetScrollingRegionResult = true;
};
prepExpectedParameters();
VERIFY_IS_TRUE(_pDispatch->HardReset());
VERIFY_ARE_EQUAL(_testGetSet->_coordCursorPos, coordExpectedCursorPos);
VERIFY_ARE_EQUAL(_testGetSet->_fUsingRgbColor, false);
Log::Comment(L"Test 2: Gracefully fail when getting console information fails.");
_testGetSet->PrepData();
prepExpectedParameters();
_testGetSet->_fGetConsoleScreenBufferInfoExResult = false;
VERIFY_IS_FALSE(_pDispatch->HardReset());
Log::Comment(L"Test 3: Gracefully fail when filling the rectangle fails.");
_testGetSet->PrepData();
prepExpectedParameters();
_testGetSet->_fFillConsoleOutputCharacterWResult = false;
VERIFY_IS_FALSE(_pDispatch->HardReset());
Log::Comment(L"Test 4: Gracefully fail when setting the window fails.");
_testGetSet->PrepData();
prepExpectedParameters();
_testGetSet->_fSetConsoleWindowInfoResult = false;
VERIFY_IS_FALSE(_pDispatch->HardReset());
}
TEST_METHOD(SetColorTableValue)
{
_testGetSet->PrepData();