mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 00:48:23 -06:00
The conhost v2 rewrite from a decade ago introduced a race condition: Previously, we would acquire and hold the global console lock while servicing a console API call. If the call cannot be completed a wait task is enqueued, while the lock is held. The v2 rewrite then split the project up into a "server" and "host" component (which remain to this day). The "host" would hold the console lock, while the "server" was responsible for enqueueing wait tasks _outside of the console lock_. Without any form of synchronization, any operations on the waiter list would then of course introduce a race condition. In conhost this primarily meant keyboard/mouse input, because that runs on the separate Win32 window thread. For Windows Terminal it primarily meant the VT input thread. I do not know why this issue is so extremely noticeable specifically when we respond to DSC CPR requests, but I'm also not surprised: I suspect that the overall performance issues that conhost had for a long time, meant that most things it did were slower than allocating the wait task. Now that both conhost and Windows Terminal became orders of magnitudes faster over the last few years, it probably just so happens that the DSC CPR request takes almost exactly as many cycles to complete as allocating the wait task does, hence perfectly reproducing the race condition. There's also a slight chance that this is actually a regression from my ConPTY rewrite #17510, but I fail to see what that would be. Regardless of that, I'm 100% certain though, that this is a bug that has existed in v0.1. Closes #18117 Closes #18800 ## Validation Steps Performed * See repro in #18800. In other words: * Continuously emit DSC CPR sequences * ...read the response from stdin * ...and print the response to stdout * Doesn't deadlock randomly anymore ✅ * Feature & Unit tests ✅
824 lines
36 KiB
C++
824 lines
36 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;
|
|
CommandHistory* m_pHistory;
|
|
|
|
TEST_METHOD_SETUP(MethodSetup)
|
|
{
|
|
m_state = std::make_unique<CommonState>();
|
|
|
|
m_state->PrepareGlobalInputBuffer();
|
|
m_state->PrepareGlobalScreenBuffer();
|
|
|
|
m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr);
|
|
if (!m_pHistory)
|
|
{
|
|
return false;
|
|
}
|
|
// History must be prepared before COOKED_READ
|
|
return true;
|
|
}
|
|
|
|
TEST_METHOD_CLEANUP(MethodCleanup)
|
|
{
|
|
CommandHistory::s_Free(nullptr);
|
|
m_pHistory = nullptr;
|
|
|
|
m_state->CleanupGlobalInputBuffer();
|
|
|
|
m_state->CleanupGlobalScreenBuffer();
|
|
|
|
m_state.reset(nullptr);
|
|
|
|
return true;
|
|
}
|
|
|
|
BOOL _fPrevInsertMode;
|
|
void PrepVerifySetConsoleInputModeImpl(const ULONG ulOriginalInputMode)
|
|
{
|
|
auto& 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)
|
|
{
|
|
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
const auto pii = gci.pInputBuffer;
|
|
|
|
// The expected mode set in the buffer is the mode given minus the flags that are stored in different fields.
|
|
auto ulModeExpected = ulNewMode;
|
|
WI_ClearAllFlags(ulModeExpected, (ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION | ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS));
|
|
const auto fQuickEditExpected = WI_IsFlagSet(ulNewMode, ENABLE_QUICK_EDIT_MODE);
|
|
const auto fAutoPositionExpected = WI_IsFlagSet(ulNewMode, ENABLE_AUTO_POSITION);
|
|
const auto fInsertModeExpected = WI_IsFlagSet(ulNewMode, ENABLE_INSERT_MODE);
|
|
|
|
// If the insert mode changed, we expect the cursor to have turned off.
|
|
const auto fCursorDBModeExpected = ((!!_fPrevInsertMode) == fInsertModeExpected);
|
|
|
|
// Call the API
|
|
const auto 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)
|
|
{
|
|
auto& 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);
|
|
const auto pii = gci.pInputBuffer;
|
|
const auto 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)
|
|
{
|
|
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
|
|
// SetTitle() runs some extra code. Let's not skip it since this is a test.
|
|
gci.SetTitle(L"Test window title.");
|
|
const auto pszExpected = til::u16u8(gci.GetTitle());
|
|
|
|
char pszTitle[MAX_PATH]; // most applications use MAX_PATH
|
|
size_t cchWritten = 0;
|
|
size_t cchNeeded = 0;
|
|
VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleAImpl(std::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(std::string_view{ pszExpected }, std::string_view{ pszTitle });
|
|
}
|
|
|
|
TEST_METHOD(ApiGetConsoleTitleW)
|
|
{
|
|
auto& 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(std::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)
|
|
{
|
|
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
gci.SetOriginalTitle(L"Test original window title.");
|
|
|
|
const auto originalTitle = gci.GetOriginalTitle();
|
|
|
|
const auto iBytesNeeded = WideCharToMultiByte(gci.OutputCP,
|
|
0,
|
|
originalTitle.data(),
|
|
gsl::narrow_cast<int>(originalTitle.size()),
|
|
nullptr,
|
|
0,
|
|
nullptr,
|
|
nullptr);
|
|
|
|
auto 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(std::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)
|
|
{
|
|
auto& 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(std::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.");
|
|
|
|
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
auto& 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;
|
|
}
|
|
auto 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));
|
|
s_AdjustOutputWait(fInduceWait);
|
|
|
|
size_t cchRead = 0;
|
|
|
|
// The increment is either the specified length or the remaining text in the string (if that is smaller).
|
|
const auto cchWriteLength = std::min(cchIncrement, cchTestText - i);
|
|
|
|
// Run the test method
|
|
const auto hr = _pApiRoutines->WriteConsoleAImpl(si, { pszTestText + i, cchWriteLength }, cchRead, nullptr);
|
|
|
|
if (!fInduceWait)
|
|
{
|
|
VERIFY_ARE_EQUAL(S_OK, hr);
|
|
VERIFY_ARE_EQUAL(cchWriteLength, cchRead, L"We should have the same character count back as 'written' that we gave in.");
|
|
}
|
|
else
|
|
{
|
|
VERIFY_ARE_EQUAL(CONSOLE_STATUS_WAIT, hr);
|
|
}
|
|
}
|
|
}
|
|
|
|
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.");
|
|
|
|
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
|
|
gci.LockConsole();
|
|
auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
|
|
|
|
const std::wstring_view testText(L"Test text");
|
|
|
|
s_AdjustOutputWait(fInduceWait);
|
|
|
|
size_t cchRead = 0;
|
|
const auto hr = _pApiRoutines->WriteConsoleWImpl(si, testText, cchRead, nullptr);
|
|
|
|
if (!fInduceWait)
|
|
{
|
|
VERIFY_ARE_EQUAL(S_OK, hr);
|
|
VERIFY_ARE_EQUAL(testText.size(), cchRead, L"We should have the same character count back as 'written' that we gave in.");
|
|
}
|
|
else
|
|
{
|
|
VERIFY_ARE_EQUAL(CONSOLE_STATUS_WAIT, hr);
|
|
}
|
|
}
|
|
|
|
void ValidateScreen(SCREEN_INFORMATION& si,
|
|
const CHAR_INFO background,
|
|
const CHAR_INFO fill,
|
|
const til::point 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.
|
|
auto 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 til::point 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
|
|
til::point 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
|
|
auto 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.");
|
|
|
|
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
|
|
si.GetTextBuffer().ResizeTraditional({ 5, 5 });
|
|
|
|
// 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<til::inclusive_rect> clipRectangle = std::nullopt;
|
|
std::optional<Viewport> clipViewport = std::nullopt;
|
|
const auto bufferSize = si.GetBufferSize();
|
|
|
|
auto scroll = bufferSize.ToInclusive();
|
|
til::point 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
|
|
auto clipRectDimensions = bufferSize.Dimensions();
|
|
clipRectDimensions.width /= 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
|
|
auto clipRectDimensions = bufferSize.Dimensions();
|
|
clipRectDimensions.height /= 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
|
|
auto clipRectDimensions = bufferSize.Dimensions();
|
|
clipRectDimensions.width /= 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);
|
|
}
|
|
};
|