terminal/src/host/ut_host/ApiRoutinesTests.cpp
Dustin L. Howett 1bf4c082b4
Reintroduce a color compatibility hack, but only for PowerShells (#6810)
There is going to be a very long tail of applications that will
explicitly request VT SGR 40/37 when what they really want is to
SetConsoleTextAttribute() with a black background/white foreground.
Instead of making those applications look bad (and therefore making us
look bad, because we're releasing this as an update to something that
"looks good" already), we're introducing this compatibility quirk.
Before the color reckoning in #6698 + #6506, *every* color was subject
to being spontaneously and erroneously turned into the default color.
Now, only the 16-color palette value that matches the active console
background/foreground color will be destroyed, and only when received
from specific applications.

Removal will be tracked by #6807.

Michael and I discussed what layer this quirk really belonged in. I
originally believed it would be sufficient to detect a background color
that matched the legacy default background, but @j4james provided an
example of where that wouldn't work out (powershell setting the
foreground color to white/gray). In addition, it was too heavyhanded: it
re-broke black backgrounds for every application.

Michael thought that it should live in the server, as a small VT parser
that righted the wrongs coming directly out of the application. On
further investigation, however, I realized that we'd need to push more
information up into the server (so that it could make the decision about
which VT was wrong and which was right) than should be strictly
necessary.

The host knows which colors are right and wrong, and it gets final say
in what ends up in the buffer.

Because of that, I chose to push the quirk state down through
WriteConsole to DoWriteConsole and toggle state on the
SCREEN_INFORMATION that indicates whether the colors coming out of the
application are to be distrusted. This quirk _only applies to pwsh.exe
and powershell.exe._

NOTE: This doesn't work for PowerShell the .NET Global tool, because it
is run as an assembly through dotnet.exe. I have no opinion on how to
fix this, or whether it is worth fixing.

VALIDATION
----------
I configured my terminals to have an incredibly garish color scheme to
show exactly what's going to happen as a result of this. The _default
terminal background_ is purple or red, and the foreground green. I've
printed out a heap of test colors to see how black interacts with them.

Pull request #6810 contains the images generated from this test.

The only color lines that change are the ones where black as a
background or white as a foreground is selected out of the 16-color
palette explicitly. Reverse video still works fine (because black is in
the foreground!), and it's even possible to represent "black on default"
and reverse it into "default on black", despite the black in question
having been `40`.

Fixes #6767.
2020-07-10 15:25:39 -07:00

869 lines
40 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 "ApiRoutines.h"
#include "getset.h"
#include "dbcs.h"
#include "misc.h"
#include "..\interactivity\inc\ServiceLocator.hpp"
using namespace Microsoft::Console::Types;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using Microsoft::Console::Interactivity::ServiceLocator;
class ApiRoutinesTests
{
TEST_CLASS(ApiRoutinesTests);
std::unique_ptr<CommonState> m_state;
ApiRoutines _Routines;
IApiRoutines* _pApiRoutines = &_Routines;
TEST_METHOD_SETUP(MethodSetup)
{
m_state = std::make_unique<CommonState>();
m_state->PrepareGlobalFont();
m_state->PrepareGlobalScreenBuffer();
m_state->PrepareGlobalInputBuffer();
return true;
}
TEST_METHOD_CLEANUP(MethodCleanup)
{
m_state->CleanupGlobalInputBuffer();
m_state->CleanupGlobalScreenBuffer();
m_state->CleanupGlobalFont();
m_state.reset(nullptr);
return true;
}
BOOL _fPrevInsertMode;
void PrepVerifySetConsoleInputModeImpl(const ULONG ulOriginalInputMode)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.Flags = 0;
gci.pInputBuffer->InputMode = ulOriginalInputMode & ~(ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION | ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS);
gci.SetInsertMode(WI_IsFlagSet(ulOriginalInputMode, ENABLE_INSERT_MODE));
WI_UpdateFlag(gci.Flags, CONSOLE_QUICK_EDIT_MODE, WI_IsFlagSet(ulOriginalInputMode, ENABLE_QUICK_EDIT_MODE));
WI_UpdateFlag(gci.Flags, CONSOLE_AUTO_POSITION, WI_IsFlagSet(ulOriginalInputMode, ENABLE_AUTO_POSITION));
// Set cursor DB to on so we can verify that it turned off when the Insert Mode changes.
gci.GetActiveOutputBuffer().SetCursorDBMode(true);
// Record the insert mode at this time to see if it changed.
_fPrevInsertMode = gci.GetInsertMode();
}
void VerifySetConsoleInputModeImpl(const HRESULT hrExpected,
const ULONG ulNewMode)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
InputBuffer* const pii = gci.pInputBuffer;
// The expected mode set in the buffer is the mode given minus the flags that are stored in different fields.
ULONG ulModeExpected = ulNewMode;
WI_ClearAllFlags(ulModeExpected, (ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION | ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS));
bool const fQuickEditExpected = WI_IsFlagSet(ulNewMode, ENABLE_QUICK_EDIT_MODE);
bool const fAutoPositionExpected = WI_IsFlagSet(ulNewMode, ENABLE_AUTO_POSITION);
bool const fInsertModeExpected = WI_IsFlagSet(ulNewMode, ENABLE_INSERT_MODE);
// If the insert mode changed, we expect the cursor to have turned off.
bool const fCursorDBModeExpected = ((!!_fPrevInsertMode) == fInsertModeExpected);
// Call the API
HRESULT const hrActual = _pApiRoutines->SetConsoleInputModeImpl(*pii, ulNewMode);
// Now do verifications of final state.
VERIFY_ARE_EQUAL(hrExpected, hrActual);
VERIFY_ARE_EQUAL(ulModeExpected, pii->InputMode);
VERIFY_ARE_EQUAL(fQuickEditExpected, WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE));
VERIFY_ARE_EQUAL(fAutoPositionExpected, WI_IsFlagSet(gci.Flags, CONSOLE_AUTO_POSITION));
VERIFY_ARE_EQUAL(!!fInsertModeExpected, !!gci.GetInsertMode());
VERIFY_ARE_EQUAL(fCursorDBModeExpected, gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().IsDouble());
}
TEST_METHOD(ApiSetConsoleInputModeImplValidNonExtended)
{
Log::Comment(L"Set some perfectly valid, non-extended flags.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);
}
TEST_METHOD(ApiSetConsoleInputModeImplValidExtended)
{
Log::Comment(L"Set some perfectly valid, extended flags.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION);
}
TEST_METHOD(ApiSetConsoleInputModeImplExtendedTurnOff)
{
Log::Comment(L"Try to turn off extended flags.");
PrepVerifySetConsoleInputModeImpl(ENABLE_EXTENDED_FLAGS | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS);
}
TEST_METHOD(ApiSetConsoleInputModeImplInvalid)
{
Log::Comment(L"Set some invalid flags.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Should get invalid argument code because we set invalid flags.");
Log::Comment(L"Flags should be set anyway despite invalid code.");
VerifySetConsoleInputModeImpl(E_INVALIDARG, 0x8000000);
}
TEST_METHOD(ApiSetConsoleInputModeImplInsertNoCookedRead)
{
Log::Comment(L"Turn on insert mode without cooked read data.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE);
Log::Comment(L"Turn back off and verify.");
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS);
}
TEST_METHOD(ApiSetConsoleInputModeImplInsertCookedRead)
{
Log::Comment(L"Turn on insert mode with cooked read data.");
m_state->PrepareReadHandle();
auto cleanupReadHandle = wil::scope_exit([&]() { m_state->CleanupReadHandle(); });
m_state->PrepareCookedReadData();
auto cleanupCookedRead = wil::scope_exit([&]() { m_state->CleanupCookedReadData(); });
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE);
Log::Comment(L"Turn back off and verify.");
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS);
}
TEST_METHOD(ApiSetConsoleInputModeImplEchoOnLineOff)
{
Log::Comment(L"Set ECHO on with LINE off. It's invalid, but it should get set anyway and return an error code.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Setting ECHO without LINE should return an invalid argument code.");
Log::Comment(L"Input mode should be set anyway despite FAILED return code.");
VerifySetConsoleInputModeImpl(E_INVALIDARG, ENABLE_ECHO_INPUT);
}
TEST_METHOD(ApiSetConsoleInputModeExtendedFlagBehaviors)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
Log::Comment(L"Verify that we can set various extended flags even without the ENABLE_EXTENDED_FLAGS flag.");
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_INSERT_MODE);
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_QUICK_EDIT_MODE);
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_AUTO_POSITION);
Log::Comment(L"Verify that we cannot unset various extended flags without the ENABLE_EXTENDED_FLAGS flag.");
PrepVerifySetConsoleInputModeImpl(ENABLE_INSERT_MODE | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION);
InputBuffer* const pii = gci.pInputBuffer;
HRESULT const hr = _pApiRoutines->SetConsoleInputModeImpl(*pii, 0);
VERIFY_ARE_EQUAL(S_OK, hr);
VERIFY_ARE_EQUAL(true, !!gci.GetInsertMode());
VERIFY_ARE_EQUAL(true, WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE));
VERIFY_ARE_EQUAL(true, WI_IsFlagSet(gci.Flags, CONSOLE_AUTO_POSITION));
}
TEST_METHOD(ApiSetConsoleInputModeImplPSReadlineScenario)
{
Log::Comment(L"Set Powershell PSReadline expected modes.");
PrepVerifySetConsoleInputModeImpl(0x1F7);
Log::Comment(L"Should return an invalid argument code because ECHO is set without LINE.");
Log::Comment(L"Input mode should be set anyway despite FAILED return code.");
VerifySetConsoleInputModeImpl(E_INVALIDARG, 0x1E4);
}
TEST_METHOD(ApiGetConsoleTitleA)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetTitle(L"Test window title.");
int const iBytesNeeded = WideCharToMultiByte(gci.OutputCP,
0,
gci.GetTitle().c_str(),
-1,
nullptr,
0,
nullptr,
nullptr);
wistd::unique_ptr<char[]> pszExpected = wil::make_unique_nothrow<char[]>(iBytesNeeded);
VERIFY_IS_NOT_NULL(pszExpected);
VERIFY_WIN32_BOOL_SUCCEEDED(WideCharToMultiByte(gci.OutputCP,
0,
gci.GetTitle().c_str(),
-1,
pszExpected.get(),
iBytesNeeded,
nullptr,
nullptr));
char pszTitle[MAX_PATH]; // most applications use MAX_PATH
size_t cchWritten = 0;
size_t cchNeeded = 0;
VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleAImpl(gsl::span<char>(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded));
VERIFY_ARE_NOT_EQUAL(0u, cchWritten);
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
VERIFY_ARE_EQUAL(gci.GetTitle().length() + 1, cchWritten);
VERIFY_ARE_EQUAL(gci.GetTitle().length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(pszExpected.get()), WEX::Common::String(pszTitle));
}
TEST_METHOD(ApiGetConsoleTitleW)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetTitle(L"Test window title.");
wchar_t pwszTitle[MAX_PATH]; // most applications use MAX_PATH
size_t cchWritten = 0;
size_t cchNeeded = 0;
VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleWImpl(gsl::span<wchar_t>(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded));
VERIFY_ARE_NOT_EQUAL(0u, cchWritten);
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
VERIFY_ARE_EQUAL(gci.GetTitle().length(), cchWritten);
VERIFY_ARE_EQUAL(gci.GetTitle().length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(gci.GetTitle().c_str()), WEX::Common::String(pwszTitle));
}
TEST_METHOD(ApiGetConsoleOriginalTitleA)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetOriginalTitle(L"Test original window title.");
int const iBytesNeeded = WideCharToMultiByte(gci.OutputCP,
0,
gci.GetOriginalTitle().c_str(),
-1,
nullptr,
0,
nullptr,
nullptr);
wistd::unique_ptr<char[]> pszExpected = wil::make_unique_nothrow<char[]>(iBytesNeeded);
VERIFY_IS_NOT_NULL(pszExpected);
VERIFY_WIN32_BOOL_SUCCEEDED(WideCharToMultiByte(gci.OutputCP,
0,
gci.GetOriginalTitle().c_str(),
-1,
pszExpected.get(),
iBytesNeeded,
nullptr,
nullptr));
char pszTitle[MAX_PATH]; // most applications use MAX_PATH
size_t cchWritten = 0;
size_t cchNeeded = 0;
VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleAImpl(gsl::span<char>(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded));
VERIFY_ARE_NOT_EQUAL(0u, cchWritten);
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length() + 1, cchWritten);
VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(pszExpected.get()), WEX::Common::String(pszTitle));
}
TEST_METHOD(ApiGetConsoleOriginalTitleW)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetOriginalTitle(L"Test original window title.");
wchar_t pwszTitle[MAX_PATH]; // most applications use MAX_PATH
size_t cchWritten = 0;
size_t cchNeeded = 0;
VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleWImpl(gsl::span<wchar_t>(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded));
VERIFY_ARE_NOT_EQUAL(0u, cchWritten);
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length(), cchWritten);
VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(gci.GetOriginalTitle().c_str()), WEX::Common::String(pwszTitle));
}
static void s_AdjustOutputWait(const bool fShouldBlock)
{
WI_SetFlagIf(ServiceLocator::LocateGlobals().getConsoleInformation().Flags, CONSOLE_SELECTING, fShouldBlock);
WI_ClearFlagIf(ServiceLocator::LocateGlobals().getConsoleInformation().Flags, CONSOLE_SELECTING, !fShouldBlock);
}
TEST_METHOD(ApiWriteConsoleA)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:fInduceWait", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:dwCodePage", L"{437, 932, 65001}")
TEST_METHOD_PROPERTY(L"Data:dwIncrement", L"{0, 1, 2}")
END_TEST_METHOD_PROPERTIES();
bool fInduceWait;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"fInduceWait", fInduceWait), L"Get whether or not we should exercise this function off a wait state.");
DWORD dwCodePage;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dwCodePage", dwCodePage), L"Get the codepage for the test. Check a single byte, a double byte, and UTF-8.");
DWORD dwIncrement;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dwIncrement", dwIncrement),
L"Get how many chars we should feed in at a time. This validates lead bytes and bytes held across calls.");
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
gci.LockConsole();
auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
// Ensure global state is updated for our codepage.
gci.OutputCP = dwCodePage;
SetConsoleCPInfo(TRUE);
PCSTR pszTestText;
switch (dwCodePage)
{
case CP_USA: // US English ANSI
pszTestText = "Test Text";
break;
case CP_JAPANESE: // Japanese Shift-JIS
pszTestText = "J\x82\xa0\x82\xa2";
break;
case CP_UTF8:
pszTestText = "Test \xe3\x82\xab Text";
break;
default:
VERIFY_FAIL(L"Test is not ready for this codepage.");
return;
}
size_t cchTestText = strlen(pszTestText);
// Set our increment value for the loop.
// 0 represents the special case of feeding the whole string in at at time.
// Otherwise, we try different segment sizes to ensure preservation across calls
// for appropriate handling of DBCS and UTF-8 sequences.
const size_t cchIncrement = dwIncrement == 0 ? cchTestText : dwIncrement;
for (size_t i = 0; i < cchTestText; i += cchIncrement)
{
Log::Comment(WEX::Common::String().Format(L"Iteration %d of loop with increment %d", i, cchIncrement));
if (fInduceWait)
{
Log::Comment(L"Blocking global output state to induce waits.");
s_AdjustOutputWait(true);
}
size_t cchRead = 0;
std::unique_ptr<IWaitRoutine> waiter;
// The increment is either the specified length or the remaining text in the string (if that is smaller).
const size_t cchWriteLength = std::min(cchIncrement, cchTestText - i);
// Run the test method
const HRESULT hr = _pApiRoutines->WriteConsoleAImpl(si, { pszTestText + i, cchWriteLength }, cchRead, false, waiter);
VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing.");
if (!fInduceWait)
{
VERIFY_IS_NULL(waiter.get(), L"We should have no waiter for this case.");
VERIFY_ARE_EQUAL(cchWriteLength, cchRead, L"We should have the same character count back as 'written' that we gave in.");
}
else
{
VERIFY_IS_NOT_NULL(waiter.get(), L"We should have a waiter for this case.");
// The cchRead is irrelevant at this point as it's not going to be returned until we're off the wait.
Log::Comment(L"Unblocking global output state so the wait can be serviced.");
s_AdjustOutputWait(false);
Log::Comment(L"Dispatching the wait.");
NTSTATUS Status = STATUS_SUCCESS;
size_t dwNumBytes = 0;
DWORD dwControlKeyState = 0; // unused but matches the pattern for read.
void* pOutputData = nullptr; // unused for writes but used for read.
const BOOL bNotifyResult = waiter->Notify(WaitTerminationReason::NoReason, FALSE, &Status, &dwNumBytes, &dwControlKeyState, &pOutputData);
VERIFY_IS_TRUE(!!bNotifyResult, L"Wait completion on notify should be successful.");
VERIFY_ARE_EQUAL(STATUS_SUCCESS, Status, L"We should have a successful return code to pass to the caller.");
const size_t dwBytesExpected = cchWriteLength;
VERIFY_ARE_EQUAL(dwBytesExpected, dwNumBytes, L"We should have the byte length of the string we put in as the returned value.");
}
}
}
TEST_METHOD(ApiWriteConsoleW)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:fInduceWait", L"{false, true}")
END_TEST_METHOD_PROPERTIES();
bool fInduceWait;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"fInduceWait", fInduceWait), L"Get whether or not we should exercise this function off a wait state.");
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
gci.LockConsole();
auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
const std::wstring testText(L"Test text");
if (fInduceWait)
{
Log::Comment(L"Blocking global output state to induce waits.");
s_AdjustOutputWait(true);
}
size_t cchRead = 0;
std::unique_ptr<IWaitRoutine> waiter;
const HRESULT hr = _pApiRoutines->WriteConsoleWImpl(si, testText, cchRead, false, waiter);
VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing.");
if (!fInduceWait)
{
VERIFY_IS_NULL(waiter.get(), L"We should have no waiter for this case.");
VERIFY_ARE_EQUAL(testText.size(), cchRead, L"We should have the same character count back as 'written' that we gave in.");
}
else
{
VERIFY_IS_NOT_NULL(waiter.get(), L"We should have a waiter for this case.");
// The cchRead is irrelevant at this point as it's not going to be returned until we're off the wait.
Log::Comment(L"Unblocking global output state so the wait can be serviced.");
s_AdjustOutputWait(false);
Log::Comment(L"Dispatching the wait.");
NTSTATUS Status = STATUS_SUCCESS;
size_t dwNumBytes = 0;
DWORD dwControlKeyState = 0; // unused but matches the pattern for read.
void* pOutputData = nullptr; // unused for writes but used for read.
const BOOL bNotifyResult = waiter->Notify(WaitTerminationReason::NoReason, TRUE, &Status, &dwNumBytes, &dwControlKeyState, &pOutputData);
VERIFY_IS_TRUE(!!bNotifyResult, L"Wait completion on notify should be successful.");
VERIFY_ARE_EQUAL(STATUS_SUCCESS, Status, L"We should have a successful return code to pass to the caller.");
const size_t dwBytesExpected = testText.size() * sizeof(wchar_t);
VERIFY_ARE_EQUAL(dwBytesExpected, dwNumBytes, L"We should have the byte length of the string we put in as the returned value.");
}
}
void ValidateScreen(SCREEN_INFORMATION& si,
const CHAR_INFO background,
const CHAR_INFO fill,
const COORD delta,
const std::optional<Viewport> clip)
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& activeSi = si.GetActiveBuffer();
auto bufferSize = activeSi.GetBufferSize();
// Find the background area viewport by taking the size, translating it by the delta, then cropping it back to the buffer size.
Viewport backgroundArea = Viewport::Offset(bufferSize, delta);
bufferSize.Clamp(backgroundArea);
auto it = activeSi.GetCellDataAt({ 0, 0 }); // We're going to walk the whole thing. Start in the top left corner.
while (it)
{
if (backgroundArea.IsInBounds(it._pos) ||
(clip.has_value() && !clip.value().IsInBounds(it._pos)))
{
auto cellInfo = gci.AsCharInfo(*it);
VERIFY_ARE_EQUAL(background, cellInfo);
}
else
{
VERIFY_ARE_EQUAL(fill, gci.AsCharInfo(*it));
}
it++;
}
}
void ValidateComplexScreen(SCREEN_INFORMATION& si,
const CHAR_INFO background,
const CHAR_INFO fill,
const CHAR_INFO scroll,
const Viewport scrollArea,
const COORD destPoint,
const std::optional<Viewport> clip)
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& activeSi = si.GetActiveBuffer();
auto bufferSize = activeSi.GetBufferSize();
// Find the delta by comparing the scroll area to the destination point
COORD delta;
delta.X = destPoint.X - scrollArea.Left();
delta.Y = destPoint.Y - scrollArea.Top();
// Find the area where the scroll text should have gone by taking the scrolled area by the delta
Viewport scrolledDestination = Viewport::Offset(scrollArea, delta);
bufferSize.Clamp(scrolledDestination);
auto it = activeSi.GetCellDataAt({ 0, 0 }); // We're going to walk the whole thing. Start in the top left corner.
while (it)
{
// If there's no clip rectangle...
if (!clip.has_value())
{
// Three states.
// 1. We filled the background with something (background CHAR_INFO)
// 2. We filled another smaller area with a different something (scroll CHAR_INFO)
// 3. We moved #2 by delta and the uncovered area was filled with a third something (fill CHAR_INFO)
// If it's in the scrolled destination, it's the value that just got moved.
if (scrolledDestination.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(scroll, gci.AsCharInfo(*it));
}
// Otherwise, if it's not in the destination but it was in the source, assume it got filled in.
else if (scrollArea.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(fill, gci.AsCharInfo(*it));
}
// Lastly if it's not in either spot, it should have our background CHAR_INFO
else
{
VERIFY_ARE_EQUAL(background, gci.AsCharInfo(*it));
}
}
// If there is a clip rectangle...
else
{
const auto unboxedClip = clip.value();
if (unboxedClip.IsInBounds(it._pos))
{
if (scrolledDestination.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(scroll, gci.AsCharInfo(*it));
}
else if (scrollArea.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(fill, gci.AsCharInfo(*it));
}
else
{
VERIFY_ARE_EQUAL(background, gci.AsCharInfo(*it));
}
}
else
{
if (scrollArea.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(scroll, gci.AsCharInfo(*it));
}
else
{
VERIFY_ARE_EQUAL(background, gci.AsCharInfo(*it));
}
}
}
// Move to next iterator position and check.
it++;
}
}
TEST_METHOD(ApiScrollConsoleScreenBufferW)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"data:setMargins", L"{false, true}")
TEST_METHOD_PROPERTY(L"data:checkClipped", L"{false, true}")
END_TEST_METHOD_PROPERTIES();
bool setMargins;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins), L"Get whether or not we should set the DECSTBM margins.");
bool checkClipped;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"checkClipped", checkClipped), L"Get whether or not we should check all the options using a clipping rectangle.");
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional({ 5, 5 }), L"Make the buffer small so this doesn't take forever.");
// Tests are run both with and without the DECSTBM margins set. This should not alter
// the results, since ScrollConsoleScreenBuffer should not be affected by VT margins.
auto& stateMachine = si.GetStateMachine();
stateMachine.ProcessString(setMargins ? L"\x1b[2;4r" : 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"); });
gci.LockConsole();
auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
CHAR_INFO fill;
fill.Char.UnicodeChar = L'A';
fill.Attributes = FOREGROUND_RED;
// By default, we're going to use a nullopt clip rectangle.
// If this instance of the test is checking clipping, we'll assign a clip value
// prior to each call variation.
std::optional<SMALL_RECT> clipRectangle = std::nullopt;
std::optional<Viewport> clipViewport = std::nullopt;
const auto bufferSize = si.GetBufferSize();
SMALL_RECT scroll = bufferSize.ToInclusive();
COORD destination{ 0, -2 }; // scroll up.
Log::Comment(L"Fill screen with green Zs. Scroll all up by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
CHAR_INFO background;
background.Char.UnicodeChar = L'Z';
background.Attributes = FOREGROUND_GREEN;
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
if (checkClipped)
{
// for scrolling up and down, we're going to clip to only modify the left half of the buffer
COORD clipRectDimensions = bufferSize.Dimensions();
clipRectDimensions.X /= 2;
clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions);
clipRectangle = clipViewport.value().ToInclusive();
}
// Scroll everything up and backfill with red As.
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Fill screen with green Zs. Scroll all down by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything down and backfill with red As.
destination = { 0, 2 };
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
if (checkClipped)
{
// for scrolling left and right, we're going to clip to only modify the top half of the buffer
COORD clipRectDimensions = bufferSize.Dimensions();
clipRectDimensions.Y /= 2;
clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions);
clipRectangle = clipViewport.value().ToInclusive();
}
Log::Comment(L"Fill screen with green Zs. Scroll all left by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything left and backfill with red As.
destination = { -2, 0 };
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Fill screen with green Zs. Scroll all right by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything right and backfill with red As.
destination = { 2, 0 };
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Fill screen with green Zs. Move everything down and right by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything down and right and backfill with red As.
destination = { 2, 2 };
if (checkClipped)
{
// Clip out the left most and top most column.
clipViewport = Viewport::FromDimensions({ 1, 1 }, { 4, 4 });
clipRectangle = clipViewport.value().ToInclusive();
}
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Fill screen with green Zs. Move everything up and left by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything up and left and backfill with red As.
destination = { -2, -2 };
if (checkClipped)
{
// Clip out the bottom most and right most column
clipViewport = Viewport::FromDimensions({ 0, 0 }, { 4, 4 });
clipRectangle = clipViewport.value().ToInclusive();
}
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Scroll everything completely off the screen.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything way off the screen.
destination = { 0, -10 };
if (checkClipped)
{
// for scrolling up and down, we're going to clip to only modify the left half of the buffer
COORD clipRectDimensions = bufferSize.Dimensions();
clipRectDimensions.X /= 2;
clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions);
clipRectangle = clipViewport.value().ToInclusive();
}
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Scroll everything completely off the screen but use a null fill and confirm it is replaced with default attribute spaces.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything way off the screen.
destination = { -10, -10 };
CHAR_INFO nullFill = { 0 };
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, nullFill.Char.UnicodeChar, nullFill.Attributes));
CHAR_INFO fillExpected;
fillExpected.Char.UnicodeChar = UNICODE_SPACE;
fillExpected.Attributes = si.GetAttributes().GetLegacyAttributes();
ValidateScreen(si, background, fillExpected, destination, clipViewport);
if (checkClipped)
{
// If we're doing clipping here, we're going to clip the scrolled area (after Bs are filled onto field of Zs)
// to only the 3rd and 4th columns of the pattern.
clipViewport = Viewport::FromDimensions({ 2, 0 }, { 2, 5 });
clipRectangle = clipViewport.value().ToInclusive();
}
Log::Comment(L"Scroll a small portion of the screen in an overlapping fashion.");
scroll.Top = 1;
scroll.Bottom = 2;
scroll.Left = 1;
scroll.Right = 2;
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Screen now looks like:
// ZZZZZ
// ZZZZZ
// ZZZZZ
// ZZZZZ
// ZZZZZ
// Fill the scroll rectangle with Blue Bs.
CHAR_INFO scrollRect;
scrollRect.Char.UnicodeChar = L'B';
scrollRect.Attributes = FOREGROUND_BLUE;
si.GetActiveBuffer().WriteRect(OutputCellIterator(scrollRect), Viewport::FromInclusive(scroll));
// Screen now looks like:
// ZZZZZ
// ZBBZZ
// ZBBZZ
// ZZZZZ
// ZZZZZ
// We're going to move our little embedded rectangle of Blue Bs inside the field of Green Zs down and to the right just one.
destination = { scroll.Left + 1, scroll.Top + 1 };
// Move rectangle and backfill with red As.
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
// Screen should now look like either:
// (with no clip rectangle):
// ZZZZZ
// ZAAZZ
// ZABBZ
// ZZBBZ
// ZZZZZ
// or with clip rectangle (of 3rd and 4th columns only, defined above)
// ZZZZZ
// ZBAZZ
// ZBBBZ
// ZZBBZ
// ZZZZZ
ValidateComplexScreen(si, background, fill, scrollRect, Viewport::FromInclusive(scroll), destination, clipViewport);
Log::Comment(L"Scroll a small portion of the screen in a non-overlapping fashion.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Screen now looks like:
// ZZZZZ
// ZZZZZ
// ZZZZZ
// ZZZZZ
// ZZZZZ
// Fill the scroll rectangle with Blue Bs.
si.GetActiveBuffer().WriteRect(OutputCellIterator(scrollRect), Viewport::FromInclusive(scroll));
// Screen now looks like:
// ZZZZZ
// ZBBZZ
// ZBBZZ
// ZZZZZ
// ZZZZZ
// We're going to move our little embedded rectangle of Blue Bs inside the field of Green Zs down and to the right by two.
destination = { scroll.Left + 2, scroll.Top + 2 };
// Move rectangle and backfill with red As.
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
// Screen should now look like either:
// (with no clip rectangle):
// ZZZZZ
// ZAAZZ
// ZAAZZ
// ZZZBB
// ZZZBB
// or with clip rectangle (of 3rd and 4th columns only, defined above)
// ZZZZZ
// ZBAZZ
// ZBAZZ
// ZZZBZ
// ZZZBZ
ValidateComplexScreen(si, background, fill, scrollRect, Viewport::FromInclusive(scroll), destination, clipViewport);
}
};