Files
terminal/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp
Leonard Hecker ed27737233 Use 32-bit coordinates throughout the project (#13025)
Previously this project used a great variety of types to present text buffer
coordinates: `short`, `unsigned short`, `int`, `unsigned int`, `size_t`,
`ptrdiff_t`, `COORD`/`SMALL_RECT` (aka `short`), and more.
This massive commit migrates almost all use of those types over to the
centralized types `til::point`/`size`/`rect`/`inclusive_rect` and their
underlying type `til::CoordType` (aka `int32_t`).

Due to the size of the changeset and statistics I expect it to contain bugs.
The biggest risk I see is that some code potentially, maybe implicitly, expected
arithmetic to be mod 2^16 and that this code now allows it to be mod 2^32.
Any narrowing into `short` later on would then throw exceptions.

## PR Checklist
* [x] Closes #4015
* [x] I work here
* [x] Tests added/passed

## Validation Steps Performed
Casual usage of OpenConsole and Windows Terminal. 
2022-06-03 23:02:46 +00:00

238 lines
10 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include <WexTestClass.h>
#include <DefaultSettings.h>
#include "../renderer/inc/DummyRenderer.hpp"
#include "../renderer/base/Renderer.hpp"
#include "../renderer/dx/DxRenderer.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "MockTermSettings.h"
#include "consoletaeftemplates.hpp"
#include "../../inc/TestUtils.h"
using namespace winrt::Microsoft::Terminal::Core;
using namespace Microsoft::Terminal::Core;
using namespace Microsoft::Console::Render;
using namespace ::Microsoft::Console::Types;
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
namespace
{
class MockScrollRenderEngine final : public RenderEngineBase
{
public:
std::optional<til::point> TriggerScrollDelta() const
{
return _triggerScrollDelta;
}
void Reset()
{
_triggerScrollDelta.reset();
}
HRESULT StartPaint() noexcept { return S_OK; }
HRESULT EndPaint() noexcept { return S_OK; }
HRESULT Present() noexcept { return S_OK; }
HRESULT PrepareForTeardown(_Out_ bool* /*pForcePaint*/) noexcept { return S_OK; }
HRESULT ScrollFrame() noexcept { return S_OK; }
HRESULT Invalidate(const til::rect* /*psrRegion*/) noexcept { return S_OK; }
HRESULT InvalidateCursor(const til::rect* /*psrRegion*/) noexcept { return S_OK; }
HRESULT InvalidateSystem(const til::rect* /*prcDirtyClient*/) noexcept { return S_OK; }
HRESULT InvalidateSelection(const std::vector<til::rect>& /*rectangles*/) noexcept { return S_OK; }
HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept
{
_triggerScrollDelta = *pcoordDelta;
return S_OK;
}
HRESULT InvalidateAll() noexcept { return S_OK; }
HRESULT InvalidateCircling(_Out_ bool* /*pForcePaint*/) noexcept { return S_OK; }
HRESULT PaintBackground() noexcept { return S_OK; }
HRESULT PaintBufferLine(gsl::span<const Cluster> /*clusters*/, til::point /*coord*/, bool /*fTrimLeft*/, bool /*lineWrapped*/) noexcept { return S_OK; }
HRESULT PaintBufferGridLines(GridLineSet /*lines*/, COLORREF /*color*/, size_t /*cchLine*/, til::point /*coordTarget*/) noexcept { return S_OK; }
HRESULT PaintSelection(const til::rect& /*rect*/) noexcept { return S_OK; }
HRESULT PaintCursor(const CursorOptions& /*options*/) noexcept { return S_OK; }
HRESULT UpdateDrawingBrushes(const TextAttribute& /*textAttributes*/, const RenderSettings& /*renderSettings*/, gsl::not_null<IRenderData*> /*pData*/, bool /*usingSoftFont*/, bool /*isSettingDefaultBrushes*/) noexcept { return S_OK; }
HRESULT UpdateFont(const FontInfoDesired& /*FontInfoDesired*/, _Out_ FontInfo& /*FontInfo*/) noexcept { return S_OK; }
HRESULT UpdateDpi(int /*iDpi*/) noexcept { return S_OK; }
HRESULT UpdateViewport(const til::inclusive_rect& /*srNewViewport*/) noexcept { return S_OK; }
HRESULT GetProposedFont(const FontInfoDesired& /*FontInfoDesired*/, _Out_ FontInfo& /*FontInfo*/, int /*iDpi*/) noexcept { return S_OK; }
HRESULT GetDirtyArea(gsl::span<const til::rect>& /*area*/) noexcept { return S_OK; }
HRESULT GetFontSize(_Out_ til::size* /*pFontSize*/) noexcept { return S_OK; }
HRESULT IsGlyphWideByFont(std::wstring_view /*glyph*/, _Out_ bool* /*pResult*/) noexcept { return S_OK; }
protected:
HRESULT _DoUpdateTitle(const std::wstring_view /*newTitle*/) noexcept { return S_OK; }
private:
std::optional<til::point> _triggerScrollDelta;
};
struct ScrollBarNotification
{
int ViewportTop;
int ViewportHeight;
int BufferHeight;
};
}
namespace TerminalCoreUnitTests
{
class ScrollTest;
};
using namespace TerminalCoreUnitTests;
class TerminalCoreUnitTests::ScrollTest final
{
// !!! DANGER: Many tests in this class expect the Terminal buffer
// to be 80x32. If you change these, you'll probably inadvertently break a
// bunch of tests !!!
static const til::CoordType TerminalViewWidth = 80;
static const til::CoordType TerminalViewHeight = 32;
// For TestNotifyScrolling, it's important that this value is ~=9000.
// Something smaller like 100 won't cause the test to fail.
static const til::CoordType TerminalHistoryLength = 9001;
TEST_CLASS(ScrollTest);
TEST_METHOD(TestNotifyScrolling);
TEST_METHOD_SETUP(MethodSetup)
{
_term = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
_scrollBarNotification = std::make_shared<std::optional<ScrollBarNotification>>();
_term->SetScrollPositionChangedCallback([scrollBarNotification = _scrollBarNotification](const int top, const int height, const int bottom) {
ScrollBarNotification tmp;
tmp.ViewportTop = top;
tmp.ViewportHeight = height;
tmp.BufferHeight = bottom;
*scrollBarNotification = { tmp };
});
_renderEngine = std::make_unique<MockScrollRenderEngine>();
_renderer = std::make_unique<DummyRenderer>(_term.get());
_renderer->AddRenderEngine(_renderEngine.get());
_term->Create({ TerminalViewWidth, TerminalViewHeight }, TerminalHistoryLength, *_renderer);
return true;
}
TEST_METHOD_CLEANUP(MethodCleanup)
{
_term = nullptr;
return true;
}
private:
std::unique_ptr<Terminal> _term;
std::unique_ptr<MockScrollRenderEngine> _renderEngine;
std::unique_ptr<DummyRenderer> _renderer;
std::shared_ptr<std::optional<ScrollBarNotification>> _scrollBarNotification;
};
void ScrollTest::TestNotifyScrolling()
{
// See https://github.com/microsoft/terminal/pull/5630
//
// This is a test for GH#5540, in the most bizarre way. The origin of that
// bug was that as newlines were emitted, we'd accumulate an enormous scroll
// delta into a selection region, to the point of overflowing a SHORT. When
// the overflow occurred, the Terminal would fail to send a NotifyScroll() to
// the TermControl hosting it.
//
// For this bug to repro, we need to:
// - Have a sufficiently large buffer, because each newline we'll accumulate
// a delta of (0, ~bufferHeight), so (bufferHeight^2 + bufferHeight) >
// SHRT_MAX
// - Have a selection
Log::Comment(L"Watch out - this test takes a while to run, and won't "
L"output anything unless in encounters an error. This is expected.");
auto& termTb = *_term->_mainBuffer;
auto& termSm = *_term->_stateMachine;
const auto totalBufferSize = termTb.GetSize().Height();
auto currentRow = 0;
// We're outputting like 18000 lines of text here, so emitting 18000*4 lines
// of output to the console is actually quite unnecessary
WEX::TestExecution::SetVerifyOutput settings(WEX::TestExecution::VerifyOutputSettings::LogOnlyFailures);
// Emit a bunch of newlines to test scrolling.
for (; currentRow < totalBufferSize * 2; currentRow++)
{
*_scrollBarNotification = std::nullopt;
_renderEngine->Reset();
termSm.ProcessString(L"X\r\n");
// When we're on TerminalViewHeight-1, we'll emit the newline that
// causes the first scroll event
auto scrolled = currentRow >= TerminalViewHeight - 1;
// When we circle the buffer, the scroll bar's position does not
// change.
auto circledBuffer = currentRow >= totalBufferSize - 1;
auto expectScrollBarNotification = scrolled && !circledBuffer;
if (expectScrollBarNotification)
{
VERIFY_IS_TRUE(_scrollBarNotification->has_value(),
fmt::format(L"Expected a 'scroll bar position changed' notification for row {}", currentRow).c_str());
}
else
{
VERIFY_IS_FALSE(_scrollBarNotification->has_value(),
fmt::format(L"Expected to not see a 'scroll bar position changed' notification for row {}", currentRow).c_str());
}
// If we scrolled but it circled the buffer, then the terminal will
// call `TriggerScroll` with a delta to tell the renderer about it.
if (scrolled && circledBuffer)
{
VERIFY_IS_TRUE(_renderEngine->TriggerScrollDelta().has_value(),
fmt::format(L"Expected a 'trigger scroll' notification in Render Engine for row {}", currentRow).c_str());
til::point expectedDelta;
expectedDelta.X = 0;
expectedDelta.Y = -1;
VERIFY_ARE_EQUAL(expectedDelta, _renderEngine->TriggerScrollDelta().value(), fmt::format(L"Wrong value in 'trigger scroll' notification in Render Engine for row {}", currentRow).c_str());
}
else
{
VERIFY_IS_FALSE(_renderEngine->TriggerScrollDelta().has_value(),
fmt::format(L"Expected to not see a 'trigger scroll' notification in Render Engine for row {}", currentRow).c_str());
}
if (_scrollBarNotification->has_value())
{
const auto tmp = _scrollBarNotification->value();
const auto expectedTop = std::clamp<int>(currentRow - TerminalViewHeight + 2,
0,
TerminalHistoryLength);
const int expectedHeight = TerminalViewHeight;
const auto expectedBottom = expectedTop + TerminalViewHeight;
if ((tmp.ViewportTop != expectedTop) ||
(tmp.ViewportHeight != expectedHeight) ||
(tmp.BufferHeight != expectedBottom))
{
Log::Comment(NoThrowString().Format(L"Expected viewport values did not match on line %d", currentRow));
}
VERIFY_ARE_EQUAL(tmp.ViewportTop, expectedTop);
VERIFY_ARE_EQUAL(tmp.ViewportHeight, expectedHeight);
VERIFY_ARE_EQUAL(tmp.BufferHeight, expectedBottom);
}
}
}