terminal/src/host/ut_host/VtRendererTests.cpp
James Holderness fa7c1abdf8
Fix SGR indexed colors to distinguish Indexed256 color (and more) (#5834)
This PR introduces a new `ColorType` to allow us to distinguish between
`SGR` indexed colors from the 16 color table, the lower half of which
can be brightened, and the ISO/ITU indexed colors from the 256 color
table, which have a fixed brightness. Retaining the distinction between
these two types will enable us to forward the correct `SGR` sequences to
conpty when addressing issue #2661. 

The other benefit of retaining the color index (which we didn't
previously do for ISO/ITU colors) is that it ensures that the colors are
updated correctly when the color scheme is changed.

## References

* This is another step towards fixing the conpty narrowing bugs in issue
  #2661.
* This is technically a fix for issue #5384, but that won't be apparent
  until #2661 is complete.

## PR Checklist
* [x] Closes #1223
* [x] CLA signed. 
* [x] Tests added/passed
* [ ] Requires documentation to be updated
* [x] I've discussed this with core contributors already.

## Detailed Description of the Pull Request / Additional comments

The first part of this PR was the introduction of a new `ColorType` in
the `TextColor` class. Instead of just the one `IsIndex` type, there is
now an `IsIndex16` and an `IsIndex256`. `IsIndex16` covers the eight
original ANSI colors set with `SGR 3x` and `SGR 4x`, as well as the
brighter aixterm variants set with `SGR 9x` and `SGR 10x`. `IsIndex256`
covers the 256 ISO/ITU indexed colors set with `SGR 38;5` and `SGR
48;5`.

There are two reasons for this distinction. The first is that the ANSI
colors have the potential to be brightened by the `SGR 1` bold
attribute, while the ISO/ITO color do not. The second reason is that
when forwarding an attributes through conpty, we want to try and
preserve the original SGR sequence that generated each color (to the
extent that that is possible). By having the two separate types, we can
map the `IsIndex16` colors back to ANSI/aixterm values, and `IsIndex256`
to the ISO/ITU sequences.

In addition to the VT colors, we also have to deal with the legacy
colors set by the Windows console APIs, but we don't really need a
separate type for those. It seemed most appropriate to me to store them
as `IsIndex256` colors, since it doesn't make sense to have them
brightened by the `SGR 1` attribute (which is what would happen if they
were stored as `IsIndex16`). If a console app wanted a bright color it
would have selected one, so we shouldn't be messing with that choice.

The second part of the PR was the unification of the two color tables.
Originally we had a 16 color table for the legacy colors, and a separate
table for the 256 ISO/ITU colors. These have now been merged into one,
so color table lookups no longer need to decide which of the two tables
they should be referencing. I've also updated all the methods that took
a color table as a parameter to use a `basic_string_view` instead of
separate pointer and length variables, which I think makes them a lot
easier and safer to work with. 

With this new architecture in place, I could now update the
`AdaptDispatch` SGR implementation to store the ISO/ITU indexed colors
as `IsIndex256` values, where before they were mapped to RGB values
(which prevented them reflecting any color scheme changes). I could also
update the `TerminalDispatch` implementation to differentiate between
the two index types, so that the `SGR 1` brightening would only be
applied to the ANSI colors.

I've also done a bit of code refactoring to try and minimise any direct
access to the color tables, getting rid of a lot of places that were
copying tables with `memmove` operations. I'm hoping this will make it
easier for us to update the code in the future if we want to reorder the
table entries (which is likely a requirement for unifying the
`AdaptDispatch` and `TerminalDispatch` implementations). 

## Validation Steps Performed

For testing, I've just updated the existing unit tests to account for
the API changes. The `TextColorTests` required an extra parameter
specifying the index type when setting an index. And the `AdapterTest`
and `ScreenBufferTests` required the use of the new `SetIndexedXXX`
methods in order to be explicit about the index type, instead of relying
on the `TextAttribute` constructor and the old `SetForeground` and
`SetBackground` methods which didn't have a way to differentiate index
types.

I've manually tested the various console APIs
(`SetConsoleTextAttribute`, `ReadConsoleOutputAttribute`, and
`ReadConsoleOutput`), to make sure they are still setting and reading
the attributes as well as they used to. And I've tested the
`SetConsoleScreenBufferInfoEx` and `GetConsoleScreenBufferInfoEx` APIs
to make sure they can read and write the color table correctly. I've
also tested the color table in the properties dialog, made sure it was
saved and restored from the registry correctly, and similarly saved and
restored from a shortcut link.

Note that there are still a bunch of issues with the color table APIs,
but no new problems have been introduced by the changes in this PR, as
far as I could tell.

I've also done a bunch of manual tests of `OSC 4` to make sure it's
updating all the colors correctly (at least in conhost), and confirmed
that the test case in issue #1223 now works as expected.
2020-05-27 22:34:45 +00:00

1615 lines
67 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include <wextestclass.h>
#include "../../inc/consoletaeftemplates.hpp"
#include "../../types/inc/Viewport.hpp"
#include "../../renderer/vt/Xterm256Engine.hpp"
#include "../../renderer/vt/XtermEngine.hpp"
#include "../../renderer/vt/WinTelnetEngine.hpp"
#include "../Settings.hpp"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
namespace Microsoft
{
namespace Console
{
namespace Render
{
class VtRendererTest;
};
};
};
using namespace Microsoft::Console;
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Types;
COLORREF g_ColorTable[COLOR_TABLE_SIZE];
static const std::string CLEAR_SCREEN = "\x1b[2J";
static const std::string CURSOR_HOME = "\x1b[H";
// Sometimes when we're expecting the renderengine to not write anything,
// we'll add this to the expected input, and manually write this to the callback
// to make sure nothing else gets written.
// We don't use null because that will confuse the VERIFY macros re: string length.
const char* const EMPTY_CALLBACK_SENTINEL = "\xff";
class VtRenderTestColorProvider : public Microsoft::Console::IDefaultColorProvider
{
public:
virtual ~VtRenderTestColorProvider() = default;
COLORREF GetDefaultForeground() const
{
return g_ColorTable[15];
}
COLORREF GetDefaultBackground() const
{
return g_ColorTable[0];
}
};
VtRenderTestColorProvider p;
class Microsoft::Console::Render::VtRendererTest
{
TEST_CLASS(VtRendererTest);
TEST_CLASS_SETUP(ClassSetup)
{
// clang-format off
g_ColorTable[0] = RGB( 12, 12, 12); // Black
g_ColorTable[1] = RGB( 0, 55, 218); // Dark Blue
g_ColorTable[2] = RGB( 19, 161, 14); // Dark Green
g_ColorTable[3] = RGB( 58, 150, 221); // Dark Cyan
g_ColorTable[4] = RGB(197, 15, 31); // Dark Red
g_ColorTable[5] = RGB(136, 23, 152); // Dark Magenta
g_ColorTable[6] = RGB(193, 156, 0); // Dark Yellow
g_ColorTable[7] = RGB(204, 204, 204); // Dark White
g_ColorTable[8] = RGB(118, 118, 118); // Bright Black
g_ColorTable[9] = RGB( 59, 120, 255); // Bright Blue
g_ColorTable[10] = RGB( 22, 198, 12); // Bright Green
g_ColorTable[11] = RGB( 97, 214, 214); // Bright Cyan
g_ColorTable[12] = RGB(231, 72, 86); // Bright Red
g_ColorTable[13] = RGB(180, 0, 158); // Bright Magenta
g_ColorTable[14] = RGB(249, 241, 165); // Bright Yellow
g_ColorTable[15] = RGB(242, 242, 242); // White
// clang-format on
return true;
}
TEST_CLASS_CLEANUP(ClassCleanup)
{
return true;
}
TEST_METHOD_SETUP(MethodSetup)
{
qExpectedInput.clear();
return true;
}
// Defining a TEST_METHOD_CLEANUP seemed to break x86 test pass. Not sure why,
// something about the clipboard tests and
// YOU_CAN_ONLY_DESIGNATE_ONE_CLASS_METHOD_TO_BE_A_TEST_METHOD_SETUP_METHOD
// It's probably more correct to leave it out anyways.
TEST_METHOD(VtSequenceHelperTests);
TEST_METHOD(Xterm256TestInvalidate);
TEST_METHOD(Xterm256TestColors);
TEST_METHOD(Xterm256TestCursor);
TEST_METHOD(Xterm256TestExtendedAttributes);
TEST_METHOD(XtermTestInvalidate);
TEST_METHOD(XtermTestColors);
TEST_METHOD(XtermTestCursor);
TEST_METHOD(WinTelnetTestInvalidate);
TEST_METHOD(WinTelnetTestColors);
TEST_METHOD(WinTelnetTestCursor);
TEST_METHOD(FormattedString);
TEST_METHOD(TestWrapping);
TEST_METHOD(TestResize);
TEST_METHOD(TestCursorVisibility);
void Test16Colors(VtEngine* engine);
std::deque<std::string> qExpectedInput;
bool WriteCallback(const char* const pch, size_t const cch);
void TestPaint(VtEngine& engine, std::function<void()> pfn);
Viewport SetUpViewport();
void VerifyExpectedInputsDrained();
};
Viewport VtRendererTest::SetUpViewport()
{
SMALL_RECT view = {};
view.Top = view.Left = 0;
view.Bottom = 31;
view.Right = 79;
return Viewport::FromInclusive(view);
}
void VtRendererTest::VerifyExpectedInputsDrained()
{
if (!qExpectedInput.empty())
{
for (const auto& exp : qExpectedInput)
{
Log::Error(NoThrowString().Format(L"EXPECTED INPUT NEVER RECEIVED: %hs", exp.c_str()));
}
VERIFY_FAIL(L"there should be no remaining un-drained expected input");
}
}
bool VtRendererTest::WriteCallback(const char* const pch, size_t const cch)
{
std::string actualString = std::string(pch, cch);
VERIFY_IS_GREATER_THAN(qExpectedInput.size(),
static_cast<size_t>(0),
NoThrowString().Format(L"writing=\"%hs\", expecting %u strings", actualString.c_str(), qExpectedInput.size()));
std::string first = qExpectedInput.front();
qExpectedInput.pop_front();
Log::Comment(NoThrowString().Format(L"Expected =\t\"%hs\"", first.c_str()));
Log::Comment(NoThrowString().Format(L"Actual =\t\"%hs\"", actualString.c_str()));
VERIFY_ARE_EQUAL(first.length(), cch);
VERIFY_ARE_EQUAL(first, actualString);
return true;
}
// Function Description:
// - Small helper to do a series of testing wrapped by StartPaint/EndPaint calls
// Arguments:
// - engine: the engine to operate on
// - pfn: A function pointer to some test code to run.
// Return Value:
// - <none>
void VtRendererTest::TestPaint(VtEngine& engine, std::function<void()> pfn)
{
VERIFY_SUCCEEDED(engine.StartPaint());
pfn();
VERIFY_SUCCEEDED(engine.EndPaint());
}
void VtRendererTest::VtSequenceHelperTests()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), p, SetUpViewport(), g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
qExpectedInput.push_back("\x1b[?12l");
VERIFY_SUCCEEDED(engine->_StopCursorBlinking());
qExpectedInput.push_back("\x1b[?12h");
VERIFY_SUCCEEDED(engine->_StartCursorBlinking());
qExpectedInput.push_back("\x1b[?25l");
VERIFY_SUCCEEDED(engine->_HideCursor());
qExpectedInput.push_back("\x1b[?25h");
VERIFY_SUCCEEDED(engine->_ShowCursor());
qExpectedInput.push_back("\x1b[K");
VERIFY_SUCCEEDED(engine->_EraseLine());
qExpectedInput.push_back("\x1b[M");
VERIFY_SUCCEEDED(engine->_DeleteLine(1));
qExpectedInput.push_back("\x1b[2M");
VERIFY_SUCCEEDED(engine->_DeleteLine(2));
qExpectedInput.push_back("\x1b[L");
VERIFY_SUCCEEDED(engine->_InsertLine(1));
qExpectedInput.push_back("\x1b[2L");
VERIFY_SUCCEEDED(engine->_InsertLine(2));
qExpectedInput.push_back("\x1b[2X");
VERIFY_SUCCEEDED(engine->_EraseCharacter(2));
qExpectedInput.push_back("\x1b[2;3H");
VERIFY_SUCCEEDED(engine->_CursorPosition({ 2, 1 }));
qExpectedInput.push_back("\x1b[1;1H");
VERIFY_SUCCEEDED(engine->_CursorPosition({ 0, 0 }));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->_CursorHome());
qExpectedInput.push_back("\x1b[8;32;80t");
VERIFY_SUCCEEDED(engine->_ResizeWindow(80, 32));
qExpectedInput.push_back("\x1b[2J");
VERIFY_SUCCEEDED(engine->_ClearScreen());
qExpectedInput.push_back("\x1b[10C");
VERIFY_SUCCEEDED(engine->_CursorForward(10));
}
void VtRendererTest::Xterm256TestInvalidate()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), p, SetUpViewport(), g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
const Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating all invalidates the whole viewport."));
VERIFY_SUCCEEDED(engine->InvalidateAll());
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.all());
});
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating anything only invalidates that portion"));
SMALL_RECT invalid = { 1, 1, 2, 2 };
VERIFY_SUCCEEDED(engine->Invalidate(&invalid));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.one());
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, *(engine->_invalidMap.begin()));
});
Log::Comment(NoThrowString().Format(
L"Make sure that scrolling only invalidates part of the viewport, and sends the right sequences"));
COORD scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one down, only top line is invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 1;
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(1u, runs.size());
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front());
qExpectedInput.push_back("\x1b[H"); // Go Home
qExpectedInput.push_back("\x1b[L"); // insert a line
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, 3 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three down, only top 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 3;
// we should have 3 runs and build a rectangle out of them
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(3u, runs.size());
auto invalidRect = runs.front();
for (size_t i = 1; i < runs.size(); ++i)
{
invalidRect |= runs[i];
}
// verify the rect matches the invalid one.
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect);
// We would expect a CUP here, but the cursor is already at the home position
qExpectedInput.push_back("\x1b[3L"); // insert 3 lines
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one up, only bottom line is invalid. ----"));
invalid = view.ToExclusive();
invalid.Top = invalid.Bottom - 1;
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(1u, runs.size());
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front());
qExpectedInput.push_back("\x1b[32;1H"); // Bottom of buffer
qExpectedInput.push_back("\n"); // Scroll down once
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, -3 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three up, only bottom 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Top = invalid.Bottom - 3;
// we should have 3 runs and build a rectangle out of them
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(3u, runs.size());
auto invalidRect = runs.front();
for (size_t i = 1; i < runs.size(); ++i)
{
invalidRect |= runs[i];
}
// verify the rect matches the invalid one.
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect);
// We would expect a CUP here, but we're already at the bottom from the last call.
qExpectedInput.push_back("\n\n\n"); // Scroll down three times
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
Log::Comment(NoThrowString().Format(
L"Multiple scrolls are coalesced"));
scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
scrollDelta = { 0, 2 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three down, only top 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 3;
// we should have 3 runs and build a rectangle out of them
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(3u, runs.size());
auto invalidRect = runs.front();
for (size_t i = 1; i < runs.size(); ++i)
{
invalidRect |= runs[i];
}
// verify the rect matches the invalid one.
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect);
qExpectedInput.push_back("\x1b[H"); // Go to home
qExpectedInput.push_back("\x1b[3L"); // insert 3 lines
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
Log::Comment(engine->_invalidMap.to_string().c_str());
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
Log::Comment(engine->_invalidMap.to_string().c_str());
qExpectedInput.push_back("\x1b[2J");
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one down and one up, nothing should change ----"
L" But it still does for now MSFT:14169294"));
const auto runs = engine->_invalidMap.runs();
auto invalidRect = runs.front();
for (size_t i = 1; i < runs.size(); ++i)
{
invalidRect |= runs[i];
}
// only the bottom line should be dirty.
// When we scrolled down, the bitmap looked like this:
// 1111
// 0000
// 0000
// 0000
// And then we scrolled up and the top line fell off and a bottom
// line was filled in like this:
// 0000
// 0000
// 0000
// 1111
const til::rectangle expected{ til::point{ view.Left(), view.BottomInclusive() }, til::size{ view.Width(), 1 } };
VERIFY_ARE_EQUAL(expected, invalidRect);
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
}
void VtRendererTest::Xterm256TestColors()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), p, SetUpViewport(), g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test changing the text attributes"));
Log::Comment(NoThrowString().Format(
L"Begin by setting some test values - FG,BG = (1,2,3), (4,5,6) to start"
L"These values were picked for ease of formatting raw COLORREF values."));
qExpectedInput.push_back("\x1b[38;2;1;2;3m");
qExpectedInput.push_back("\x1b[48;2;5;6;7m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x00030201,
0x00070605,
0,
ExtendedAttributes::Normal,
false));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
qExpectedInput.push_back("\x1b[48;2;7;8;9m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x00030201,
0x00090807,
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
qExpectedInput.push_back("\x1b[38;2;10;11;12m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x000c0b0a,
0x00090807,
0,
ExtendedAttributes::Normal,
false));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure that color setting persists across EndPaint/StartPaint"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x000c0b0a,
0x00090807,
0,
ExtendedAttributes::Normal,
false));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
// Now also do the body of the 16color test as well.
// The only change is that the "Change only the BG to something not in the table"
// test actually uses an RGB value instead of the closest match.
Log::Comment(NoThrowString().Format(
L"Begin by setting the default colors - FG,BG = BRIGHT_WHITE,DARK_BLACK"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to something not in the table----"));
qExpectedInput.push_back("\x1b[48;2;1;1;1m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
0x010101,
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to the 'Default' background----"));
qExpectedInput.push_back("\x1b[49m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Back to defaults----"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure that color setting persists across EndPaint/StartPaint"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
}
void VtRendererTest::Xterm256TestCursor()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), p, SetUpViewport(), g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test moving the cursor around. Every sequence should have both params to CUP explicitly."));
TestPaint(*engine, [&]() {
qExpectedInput.push_back("\x1b[2;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 1 }));
Log::Comment(NoThrowString().Format(
L"----Only move Y coord----"));
qExpectedInput.push_back("\x1b[31;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 30 }));
Log::Comment(NoThrowString().Format(
L"----Only move X coord----"));
qExpectedInput.push_back("\x1b[29C");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
Log::Comment(NoThrowString().Format(
L"----Sending the same move sends nothing----"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"----moving home sends a simple sequence----"));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 0 }));
Log::Comment(NoThrowString().Format(
L"----move into the line to test some other sequences----"));
qExpectedInput.push_back("\x1b[7C");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 0 }));
Log::Comment(NoThrowString().Format(
L"----move down one line (x stays the same)----"));
qExpectedInput.push_back("\n");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 1 }));
Log::Comment(NoThrowString().Format(
L"----move to the start of the next line----"));
qExpectedInput.push_back("\r\n");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 2 }));
Log::Comment(NoThrowString().Format(
L"----move into the line to test some other sequences----"));
qExpectedInput.push_back("\x1b[2;8H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 1 }));
Log::Comment(NoThrowString().Format(
L"----move to the start of this line (y stays the same)----"));
qExpectedInput.push_back("\r");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 1 }));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."
L"The cursor's last \"real\" position was 0,0"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"Paint some text at 0,0, then try moving the cursor to where it currently is."));
qExpectedInput.push_back("\x1b[1C");
qExpectedInput.push_back("asdfghjkl");
const wchar_t* const line = L"asdfghjkl";
const unsigned char rgWidths[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
std::vector<Cluster> clusters;
for (size_t i = 0; i < wcslen(line); i++)
{
clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false, false));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
// Note that only PaintBufferLine updates the "Real" cursor position, which
// the cursor is moved back to at the end of each paint
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
}
void VtRendererTest::Xterm256TestExtendedAttributes()
{
// Run this test for each and every possible combination of states.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:italics", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:blink", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:invisible", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:crossedOut", L"{false, true}")
END_TEST_METHOD_PROPERTIES()
bool italics, blink, invisible, crossedOut;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"italics", italics));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"blink", blink));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"invisible", invisible));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"crossedOut", crossedOut));
ExtendedAttributes desiredAttrs{ ExtendedAttributes::Normal };
std::vector<std::string> onSequences, offSequences;
// Collect up a VT sequence to set the state given the method properties
if (italics)
{
WI_SetFlag(desiredAttrs, ExtendedAttributes::Italics);
onSequences.push_back("\x1b[3m");
offSequences.push_back("\x1b[23m");
}
if (blink)
{
WI_SetFlag(desiredAttrs, ExtendedAttributes::Blinking);
onSequences.push_back("\x1b[5m");
offSequences.push_back("\x1b[25m");
}
if (invisible)
{
WI_SetFlag(desiredAttrs, ExtendedAttributes::Invisible);
onSequences.push_back("\x1b[8m");
offSequences.push_back("\x1b[28m");
}
if (crossedOut)
{
WI_SetFlag(desiredAttrs, ExtendedAttributes::CrossedOut);
onSequences.push_back("\x1b[9m");
offSequences.push_back("\x1b[29m");
}
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), p, SetUpViewport(), g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test changing the text attributes"));
Log::Comment(NoThrowString().Format(
L"----Turn the extended attributes on----"));
TestPaint(*engine, [&]() {
// Merge the "on" sequences into expected input.
std::copy(onSequences.cbegin(), onSequences.cend(), std::back_inserter(qExpectedInput));
VERIFY_SUCCEEDED(engine->_UpdateExtendedAttrs(desiredAttrs));
});
Log::Comment(NoThrowString().Format(
L"----Turn the extended attributes off----"));
TestPaint(*engine, [&]() {
std::copy(offSequences.cbegin(), offSequences.cend(), std::back_inserter(qExpectedInput));
VERIFY_SUCCEEDED(engine->_UpdateExtendedAttrs(ExtendedAttributes::Normal));
});
Log::Comment(NoThrowString().Format(
L"----Turn the extended attributes back on----"));
TestPaint(*engine, [&]() {
std::copy(onSequences.cbegin(), onSequences.cend(), std::back_inserter(qExpectedInput));
VERIFY_SUCCEEDED(engine->_UpdateExtendedAttrs(desiredAttrs));
});
VerifyExpectedInputsDrained();
}
void VtRendererTest::XtermTestInvalidate()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<XtermEngine> engine = std::make_unique<XtermEngine>(std::move(hFile), p, SetUpViewport(), g_ColorTable, false);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating all invalidates the whole viewport."));
VERIFY_SUCCEEDED(engine->InvalidateAll());
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.all());
});
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating anything only invalidates that portion"));
SMALL_RECT invalid = { 1, 1, 2, 2 };
VERIFY_SUCCEEDED(engine->Invalidate(&invalid));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.one());
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, *(engine->_invalidMap.begin()));
});
Log::Comment(NoThrowString().Format(
L"Make sure that scrolling only invalidates part of the viewport, and sends the right sequences"));
COORD scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one down, only top line is invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 1;
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(1u, runs.size());
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front());
qExpectedInput.push_back("\x1b[H"); // Go Home
qExpectedInput.push_back("\x1b[L"); // insert a line
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, 3 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three down, only top 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 3;
// we should have 3 runs and build a rectangle out of them
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(3u, runs.size());
auto invalidRect = runs.front();
for (size_t i = 1; i < runs.size(); ++i)
{
invalidRect |= runs[i];
}
// verify the rect matches the invalid one.
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect);
// We would expect a CUP here, but the cursor is already at the home position
qExpectedInput.push_back("\x1b[3L"); // insert 3 lines
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one up, only bottom line is invalid. ----"));
invalid = view.ToExclusive();
invalid.Top = invalid.Bottom - 1;
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(1u, runs.size());
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front());
qExpectedInput.push_back("\x1b[32;1H"); // Bottom of buffer
qExpectedInput.push_back("\n"); // Scroll down once
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, -3 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three up, only bottom 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Top = invalid.Bottom - 3;
// we should have 3 runs and build a rectangle out of them
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(3u, runs.size());
auto invalidRect = runs.front();
for (size_t i = 1; i < runs.size(); ++i)
{
invalidRect |= runs[i];
}
// verify the rect matches the invalid one.
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect);
// We would expect a CUP here, but we're already at the bottom from the last call.
qExpectedInput.push_back("\n\n\n"); // Scroll down three times
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
Log::Comment(NoThrowString().Format(
L"Multiple scrolls are coalesced"));
scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
scrollDelta = { 0, 2 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled three down, only top 3 lines are invalid. ----"));
invalid = view.ToExclusive();
invalid.Bottom = 3;
// we should have 3 runs and build a rectangle out of them
const auto runs = engine->_invalidMap.runs();
VERIFY_ARE_EQUAL(3u, runs.size());
auto invalidRect = runs.front();
for (size_t i = 1; i < runs.size(); ++i)
{
invalidRect |= runs[i];
}
// verify the rect matches the invalid one.
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect);
qExpectedInput.push_back("\x1b[H"); // Go to home
qExpectedInput.push_back("\x1b[3L"); // insert 3 lines
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
Log::Comment(engine->_invalidMap.to_string().c_str());
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
Log::Comment(engine->_invalidMap.to_string().c_str());
qExpectedInput.push_back("\x1b[2J");
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"---- Scrolled one down and one up, nothing should change ----"
L" But it still does for now MSFT:14169294"));
const auto runs = engine->_invalidMap.runs();
auto invalidRect = runs.front();
for (size_t i = 1; i < runs.size(); ++i)
{
invalidRect |= runs[i];
}
// only the bottom line should be dirty.
// When we scrolled down, the bitmap looked like this:
// 1111
// 0000
// 0000
// 0000
// And then we scrolled up and the top line fell off and a bottom
// line was filled in like this:
// 0000
// 0000
// 0000
// 1111
const til::rectangle expected{ til::point{ view.Left(), view.BottomInclusive() }, til::size{ view.Width(), 1 } };
VERIFY_ARE_EQUAL(expected, invalidRect);
VERIFY_SUCCEEDED(engine->ScrollFrame());
});
}
void VtRendererTest::XtermTestColors()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<XtermEngine> engine = std::make_unique<XtermEngine>(std::move(hFile), p, SetUpViewport(), g_ColorTable, false);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test changing the text attributes"));
Log::Comment(NoThrowString().Format(
L"Begin by setting the default colors - FG,BG = BRIGHT_WHITE,DARK_BLACK"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to something not in the table----"));
qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], 0x010101, 0, ExtendedAttributes::Normal, false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to the 'Default' background----"));
qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Back to defaults----"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure that color setting persists across EndPaint/StartPaint"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
}
void VtRendererTest::XtermTestCursor()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<XtermEngine> engine = std::make_unique<XtermEngine>(std::move(hFile), p, SetUpViewport(), g_ColorTable, false);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test moving the cursor around. Every sequence should have both params to CUP explicitly."));
TestPaint(*engine, [&]() {
qExpectedInput.push_back("\x1b[2;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 1 }));
Log::Comment(NoThrowString().Format(
L"----Only move Y coord----"));
qExpectedInput.push_back("\x1b[31;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 30 }));
Log::Comment(NoThrowString().Format(
L"----Only move X coord----"));
qExpectedInput.push_back("\x1b[29C");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
Log::Comment(NoThrowString().Format(
L"----Sending the same move sends nothing----"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"----moving home sends a simple sequence----"));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 0 }));
Log::Comment(NoThrowString().Format(
L"----move into the line to test some other sequences----"));
qExpectedInput.push_back("\x1b[7C");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 0 }));
Log::Comment(NoThrowString().Format(
L"----move down one line (x stays the same)----"));
qExpectedInput.push_back("\n");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 1 }));
Log::Comment(NoThrowString().Format(
L"----move to the start of the next line----"));
qExpectedInput.push_back("\r\n");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 2 }));
Log::Comment(NoThrowString().Format(
L"----move into the line to test some other sequences----"));
qExpectedInput.push_back("\x1b[2;8H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 7, 1 }));
Log::Comment(NoThrowString().Format(
L"----move to the start of this line (y stays the same)----"));
qExpectedInput.push_back("\r");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 1 }));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."
L"The cursor's last \"real\" position was 0,0"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"Paint some text at 0,0, then try moving the cursor to where it currently is."));
qExpectedInput.push_back("\x1b[1C");
qExpectedInput.push_back("asdfghjkl");
const wchar_t* const line = L"asdfghjkl";
const unsigned char rgWidths[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
std::vector<Cluster> clusters;
for (size_t i = 0; i < wcslen(line); i++)
{
clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false, false));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
// Note that only PaintBufferLine updates the "Real" cursor position, which
// the cursor is moved back to at the end of each paint
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
}
void VtRendererTest::WinTelnetTestInvalidate()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<WinTelnetEngine> engine = std::make_unique<WinTelnetEngine>(std::move(hFile), p, SetUpViewport(), g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating all invalidates the whole viewport."));
VERIFY_SUCCEEDED(engine->InvalidateAll());
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.all());
});
Log::Comment(NoThrowString().Format(
L"Make sure that invalidating anything only invalidates that portion"));
SMALL_RECT invalid = { 1, 1, 2, 2 };
VERIFY_SUCCEEDED(engine->Invalidate(&invalid));
TestPaint(*engine, [&]() {
VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, *(engine->_invalidMap.begin()));
});
Log::Comment(NoThrowString().Format(
L"Make sure that scrolling invalidates the whole viewport, and sends no VT sequences"));
COORD scrollDelta = { 0, 1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.all());
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); // sentinel
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
scrollDelta = { 0, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.all());
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
scrollDelta = { 1, 0 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.all());
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
scrollDelta = { -1, 0 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.all());
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
scrollDelta = { 1, -1 };
VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.all());
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->ScrollFrame());
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
}
void VtRendererTest::WinTelnetTestColors()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<WinTelnetEngine> engine = std::make_unique<WinTelnetEngine>(std::move(hFile), p, SetUpViewport(), g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test changing the text attributes"));
Log::Comment(NoThrowString().Format(
L"Begin by setting the default colors - FG,BG = BRIGHT_WHITE,DARK_BLACK"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[4],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to something not in the table----"));
qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], 0x010101, 0, ExtendedAttributes::Normal, false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to the 'Default' background----"));
qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
Log::Comment(NoThrowString().Format(
L"----Back to defaults----"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure that color setting persists across EndPaint/StartPaint"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15],
g_ColorTable[0],
0,
ExtendedAttributes::Normal,
false));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback
});
}
void VtRendererTest::WinTelnetTestCursor()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<WinTelnetEngine> engine = std::make_unique<WinTelnetEngine>(std::move(hFile), p, SetUpViewport(), g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
Viewport view = SetUpViewport();
Log::Comment(NoThrowString().Format(
L"Test moving the cursor around. Every sequence should have both params to CUP explicitly."));
TestPaint(*engine, [&]() {
qExpectedInput.push_back("\x1b[2;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 1 }));
Log::Comment(NoThrowString().Format(
L"----Only move X coord----"));
qExpectedInput.push_back("\x1b[31;2H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 1, 30 }));
Log::Comment(NoThrowString().Format(
L"----Only move Y coord----"));
qExpectedInput.push_back("\x1b[31;31H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
Log::Comment(NoThrowString().Format(
L"----Sending the same move sends nothing----"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 30, 30 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
// The "real" location is the last place the cursor was moved to not
// during the course of VT operations - eg the last place text was written,
// or the cursor was manually painted at (MSFT 13310327)
Log::Comment(NoThrowString().Format(
L"Make sure the cursor gets moved back to the last real location it was at"));
qExpectedInput.push_back("\x1b[1;1H");
// EndPaint will send this sequence for us.
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."
L"The cursor's last \"real\" position was 0,0"));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 0 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
Log::Comment(NoThrowString().Format(
L"Paint some text at 0,0, then try moving the cursor to where it currently is."));
qExpectedInput.push_back("\x1b[2;2H");
qExpectedInput.push_back("asdfghjkl");
const wchar_t* const line = L"asdfghjkl";
const unsigned char rgWidths[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
std::vector<Cluster> clusters;
for (size_t i = 0; i < wcslen(line); i++)
{
clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false, false));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
// Note that only PaintBufferLine updates the "Real" cursor position, which
// the cursor is moved back to at the end of each paint
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Sending the same move across paint calls sends nothing."));
qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL);
VERIFY_SUCCEEDED(engine->_MoveCursor({ 10, 1 }));
WriteCallback(EMPTY_CALLBACK_SENTINEL, 1);
});
}
void VtRendererTest::TestWrapping()
{
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), p, SetUpViewport(), g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
});
Viewport view = SetUpViewport();
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Make sure the cursor is at 0,0"));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->_MoveCursor({ 0, 0 }));
});
TestPaint(*engine, [&]() {
Log::Comment(NoThrowString().Format(
L"Painting a line that wrapped, then painting another line, and "
L"making sure we don't manually move the cursor between those paints."));
qExpectedInput.push_back("asdfghjkl");
// TODO: Undoing this behavior due to 18123777. Will come back in MSFT:16485846
qExpectedInput.push_back("\r\n");
qExpectedInput.push_back("zxcvbnm,.");
const wchar_t* const line1 = L"asdfghjkl";
const wchar_t* const line2 = L"zxcvbnm,.";
const unsigned char rgWidths[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
std::vector<Cluster> clusters1;
for (size_t i = 0; i < wcslen(line1); i++)
{
clusters1.emplace_back(std::wstring_view{ &line1[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
std::vector<Cluster> clusters2;
for (size_t i = 0; i < wcslen(line2); i++)
{
clusters2.emplace_back(std::wstring_view{ &line2[i], 1 }, static_cast<size_t>(rgWidths[i]));
}
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters1.data(), clusters1.size() }, { 0, 0 }, false, false));
VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters2.data(), clusters2.size() }, { 0, 1 }, false, false));
});
}
void VtRendererTest::TestResize()
{
Viewport view = SetUpViewport();
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
auto engine = std::make_unique<Xterm256Engine>(std::move(hFile), p, view, g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear and go home
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
VERIFY_IS_TRUE(engine->_suppressResizeRepaint);
// The renderer (in Renderer@_PaintFrameForEngine..._CheckViewportAndScroll)
// will manually call UpdateViewport once before actually painting the
// first frame. Replicate that behavior here
VERIFY_SUCCEEDED(engine->UpdateViewport(view.ToInclusive()));
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_firstPaint);
VERIFY_IS_FALSE(engine->_suppressResizeRepaint);
});
// Resize the viewport to 120x30
// Everything should be invalidated, and a resize message sent.
const auto newView = Viewport::FromDimensions({ 0, 0 }, { 120, 30 });
qExpectedInput.push_back("\x1b[8;30;120t");
VERIFY_SUCCEEDED(engine->UpdateViewport(newView.ToInclusive()));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_invalidMap.all());
VERIFY_IS_FALSE(engine->_firstPaint);
VERIFY_IS_FALSE(engine->_suppressResizeRepaint);
});
}
void VtRendererTest::TestCursorVisibility()
{
Viewport view = SetUpViewport();
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
auto engine = std::make_unique<Xterm256Engine>(std::move(hFile), p, view, g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
// Verify the first paint emits a clear
qExpectedInput.push_back("\x1b[2J");
VERIFY_IS_TRUE(engine->_firstPaint);
VERIFY_IS_FALSE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
TestPaint(*engine, [&]() {
// During StartPaint, we'll mark the cursor as off. make sure that happens.
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_firstPaint);
});
// The cursor wasn't painted in the last frame.
VERIFY_IS_FALSE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
COORD origin{ 0, 0 };
VERIFY_ARE_NOT_EQUAL(origin, engine->_lastText);
IRenderEngine::CursorOptions options{};
options.coordCursor = origin;
// Frame 1: Paint the cursor at the home position. At the end of the frame,
// the cursor should be on. Because we're moving the cursor with CUP, we
// need to disable the cursor during this frame.
TestPaint(*engine, [&]() {
VERIFY_IS_FALSE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
Log::Comment(NoThrowString().Format(L"Make sure the cursor is at 0,0"));
qExpectedInput.push_back("\x1b[H");
VERIFY_SUCCEEDED(engine->PaintCursor(options));
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_TRUE(engine->_needToDisableCursor);
qExpectedInput.push_back("\x1b[?25h");
});
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
// Frame 2: Paint the cursor again at the home position. At the end of the
// frame, the cursor should be on, the same as before. We aren't moving the
// cursor during this frame, so _needToDisableCursor will stay false.
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
Log::Comment(NoThrowString().Format(L"If we just paint the cursor again at the same position, the cursor should not need to be disabled"));
VERIFY_SUCCEEDED(engine->PaintCursor(options));
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
});
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
// Frame 3: Paint the cursor at 2,2. At the end of the frame, the cursor
// should be on, the same as before. Because we're moving the cursor with
// CUP, we need to disable the cursor during this frame.
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
Log::Comment(NoThrowString().Format(L"Move the cursor to 2,2"));
qExpectedInput.push_back("\x1b[3;3H");
options.coordCursor = { 2, 2 };
VERIFY_SUCCEEDED(engine->PaintCursor(options));
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_TRUE(engine->_needToDisableCursor);
// Because _needToDisableCursor is true, we'll insert a ?25l at the
// start of the frame. Unfortunately, we can't test to make sure that
// it's there, but we can ensure that the matching ?25h is printed:
qExpectedInput.push_back("\x1b[?25h");
});
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_TRUE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
// Frame 4: Don't paint the cursor. At the end of the frame, the cursor
// should be off.
Log::Comment(NoThrowString().Format(L"Painting without calling PaintCursor will hide the cursor"));
TestPaint(*engine, [&]() {
VERIFY_IS_TRUE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
qExpectedInput.push_back("\x1b[?25l");
});
VERIFY_IS_FALSE(engine->_lastCursorIsVisible);
VERIFY_IS_FALSE(engine->_nextCursorIsVisible);
VERIFY_IS_FALSE(engine->_needToDisableCursor);
}
void VtRendererTest::FormattedString()
{
// This test works with a static cache variable that
// can be affected by other tests
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
END_TEST_METHOD_PROPERTIES();
static const std::string format("\x1b[%dm");
const auto value = 12;
Viewport view = SetUpViewport();
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
auto engine = std::make_unique<Xterm256Engine>(std::move(hFile), p, view, g_ColorTable);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
Log::Comment(L"1.) Write it once. It should resize itself.");
qExpectedInput.push_back("\x1b[12m");
VERIFY_SUCCEEDED(engine->_WriteFormattedString(&format, value));
Log::Comment(L"2.) Write the same thing again, should be fine.");
qExpectedInput.push_back("\x1b[12m");
VERIFY_SUCCEEDED(engine->_WriteFormattedString(&format, value));
Log::Comment(L"3.) Now write something huge. Should resize itself and still be fine.");
static const std::string bigFormat("\x1b[28;3;%d;%d;%dm");
const auto bigValue = 500;
qExpectedInput.push_back("\x1b[28;3;500;500;500m");
VERIFY_SUCCEEDED(engine->_WriteFormattedString(&bigFormat, bigValue, bigValue, bigValue));
}