terminal/src/host/ut_host/ApiRoutinesTests.cpp

881 lines
41 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.");
const auto title = gci.GetTitle();
int const iBytesNeeded = WideCharToMultiByte(gci.OutputCP,
0,
title.data(),
gsl::narrow_cast<int>(title.size()),
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,
title.data(),
gsl::narrow_cast<int>(title.size()),
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);
const auto title = gci.GetTitle();
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
VERIFY_ARE_EQUAL(title.length(), cchWritten);
VERIFY_ARE_EQUAL(title.length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(title.data(), gsl::narrow_cast<int>(title.size())), WEX::Common::String(pwszTitle));
}
TEST_METHOD(ApiGetConsoleOriginalTitleA)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetOriginalTitle(L"Test original window title.");
const auto originalTitle = gci.GetOriginalTitle();
int const iBytesNeeded = WideCharToMultiByte(gci.OutputCP,
0,
originalTitle.data(),
gsl::narrow_cast<int>(originalTitle.size()),
nullptr,
0,
nullptr,
nullptr);
wistd::unique_ptr<char[]> pszExpected = wil::make_unique_nothrow<char[]>(iBytesNeeded + 1);
VERIFY_IS_NOT_NULL(pszExpected);
VERIFY_WIN32_BOOL_SUCCEEDED(WideCharToMultiByte(gci.OutputCP,
0,
originalTitle.data(),
gsl::narrow_cast<int>(originalTitle.size()),
pszExpected.get(),
iBytesNeeded,
nullptr,
nullptr));
// Make sure we terminate the expected title -- WC2MB does not add the \0 if we use the size variant
pszExpected[iBytesNeeded] = '\0';
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);
const auto originalTitle = gci.GetOriginalTitle();
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
VERIFY_ARE_EQUAL(originalTitle.length(), cchWritten);
VERIFY_ARE_EQUAL(originalTitle.length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(originalTitle.data(), gsl::narrow_cast<int>(originalTitle.size())), 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);
}
};