terminal/src/host/ut_host/VtRendererTests.cpp
James Holderness b604117421
Standardize the color table order (#11602)
## Summary of the Pull Request

In the original implementation, we used two different orderings for the color tables. The WT color table used ANSI order, while the conhost color table used a Windows-specific order. This PR standardizes on the ANSI color order everywhere, so the usage of indexed colors is consistent across both parts of the code base, which will hopefully allow more of the code to be shared one day.

## References

This is another small step towards de-duplicating `AdaptDispatch` and `TerminalDispatch` for issue #3849, and is essentially a followup to the SGR dispatch refactoring in PR #6728.

## PR Checklist
* [x] Closes #11461
* [x] CLA signed.
* [x] Tests added/passed
* [ ] Documentation updated.
* [ ] Schema updated.
* [x] I've discussed this with core contributors already. Issue number where discussion took place: #11461

## Detailed Description of the Pull Request / Additional comments

Conhost still needs to deal with legacy attributes using Windows color order, so those values now need to be transposed to ANSI colors order when creating a `TextAttribute` object. This is done with a simple mapping table, which also handles the translation of the default color entries, so it's actually slightly faster than the original code.

And when converting `TextAttribute` values back to legacy console attributes, we were already using a mapping table to handle the narrowing of 256-color values down to 16 colors, so we just needed to adjust that table to account for the translation from ANSI to Windows, and then could make use of the same table for both 256-color and 16-color values.

There are also a few places in conhost that read from or write to the color tables, and those now need to transpose the index values. I've addressed this by creating separate `SetLegacyColorTableEntry` and `GetLegacyColorTableEntry` methods in the `Settings` class which take care of the mapping, so it's now clearer in which cases the code is dealing with legacy values, and which are ANSI values.

These methods are used in the `SetConsoleScreenBufferInfoEx` and `GetConsoleScreenBufferInfoEx` APIs, as well as a few place where color preferences are handled (the registry, shortcut links, and the properties dialog), none of which are particularly sensitive to performance. However, we also use the legacy table when looking up the default colors for rendering (which happens a lot), so I've refactored that code so the default color calculations now only occur once per frame.

The plus side of all of this is that the VT code doesn't need to do the index translation anymore, so we can finally get rid of all the calls to `XTermToWindowsIndex`, and we no longer need a separate color table initialization method for conhost, so I was able to merge a number of color initialization methods into one. We also no longer need to translate from legacy values to ANSI when generating VT sequences for conpty.

The one exception to that is the 16-color VT renderer, which uses the `TextColor::GetLegacyIndex` method to approximate 16-color equivalents for RGB and 256-color values. Since that method returns a legacy index, it still needs to be translated to ANSI before it can be used in a VT sequence. But this should be no worse than it was before.

One more special case is conhost's secret _Color Selection_ feature. That uses `Ctrl`+Number and `Alt`+Number key sequences to highlight parts of the buffer, and the mapping from number to color is based on the Windows color order. So that mapping now needs to be transposed, but that's also not performance sensitive.

The only thing that I haven't bothered to update is the trace logging code in the `Telemetry` class, which logs the first 16 entries in the color table. Those entries are now going to be in a different order, but I didn't think that would be of great concern to anyone.

## Validation Steps Performed

A lot of unit tests needed to be updated to use ANSI color constants when setting indexed colors, where before they might have been expecting values in Windows order. But this replaced a wild mix of different constants, sometimes having to use bit shifting, as well as values mapped with `XTermToWindowsIndex`, so I think the tests are a whole lot clearer now. Only a few cases have been left with literal numbers where that seemed more appropriate.

In addition to getting the unit tests working, I've also manually tested the behaviour of all the console APIs which I thought could be affected by these changes, and confirmed that they produced the same results in the new code as they did in the original implementation.

This includes:
- `WriteConsoleOutput`
- `ReadConsoleOutput`
- `SetConsoleTextAttribute` with `WriteConsoleOutputCharacter`
- `FillConsoleOutputAttribute` and `FillConsoleOutputCharacter` 
- `ScrollConsoleScreenBuffer`
- `GetConsoleScreenBufferInfo`
- `GetConsoleScreenBufferInfoEx`
- `SetConsoleScreenBufferInfoEx`

I've also manually tested changing colors via the console properties menu, the registry, and shortcut links, including setting default colors and popup colors. And I've tested that the "Quirks Mode" is still working as expected in PowerShell.

In terms of performance, I wrote a little test app that filled a 80x9999 buffer with random color combinations using `WriteConsoleOutput`, which I figured was likely to be the most performance sensitive call, and I think it now actually performs slightly better than the original implementation.

I've also tested similar code - just filling the visible window - with SGR VT sequences of various types, and the performance seems about the same as it was before.
2021-11-04 22:13:22 +00:00

1614 lines
66 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 "../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;
using namespace Microsoft::Console::VirtualTerminal::DispatchTypes;
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 Microsoft::Console::Render::VtRendererTest
{
TEST_CLASS(VtRendererTest);
TEST_CLASS_SETUP(ClassSetup)
{
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(Xterm256TestAttributesAcrossReset);
TEST_METHOD(XtermTestInvalidate);
TEST_METHOD(XtermTestColors);
TEST_METHOD(XtermTestCursor);
TEST_METHOD(XtermTestAttributesAcrossReset);
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), SetUpViewport());
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), SetUpViewport());
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), SetUpViewport());
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
RenderData renderData;
// 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 },
&renderData,
false,
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 },
&renderData,
false,
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 },
&renderData,
false,
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 },
&renderData,
false,
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.
// However, instead of using a closest match ANSI color, we can reproduce
// the exact RGB or 256-color index value stored in the TextAttribute.
Log::Comment(NoThrowString().Format(
L"Begin by setting the default colors"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({},
&renderData,
false,
false));
TestPaint(*engine, [&]() {
TextAttribute textAttributes;
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
textAttributes.SetIndexedBackground(TextColor::DARK_RED);
qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
textAttributes.SetIndexedForeground(TextColor::DARK_WHITE);
qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to an RGB value----"));
textAttributes.SetBackground(RGB(19, 161, 14));
qExpectedInput.push_back("\x1b[48;2;19;161;14m"); // Background RGB(19,161,14)
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG to an RGB value----"));
textAttributes.SetForeground(RGB(193, 156, 0));
qExpectedInput.push_back("\x1b[38;2;193;156;0m"); // Foreground RGB(193,156,0)
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to the 'Default' background----"));
textAttributes.SetDefaultBackground();
qExpectedInput.push_back("\x1b[49m"); // Background default
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG to a 256-color index----"));
textAttributes.SetIndexedForeground256(TextColor::DARK_WHITE);
qExpectedInput.push_back("\x1b[38;5;7m"); // Foreground DARK_WHITE (256-Color Index)
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to a 256-color index----"));
textAttributes.SetIndexedBackground256(TextColor::DARK_RED);
qExpectedInput.push_back("\x1b[48;5;1m"); // Background DARK_RED (256-Color Index)
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG to the 'Default' foreground----"));
textAttributes.SetDefaultForeground();
qExpectedInput.push_back("\x1b[39m"); // Background default
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Back to defaults----"));
textAttributes = {};
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
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({},
&renderData,
false,
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), SetUpViewport());
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:faint", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:underlined", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:doublyUnderlined", L"{false, true}")
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 faint, underlined, doublyUnderlined, italics, blink, invisible, crossedOut;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"faint", faint));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"underlined", underlined));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"doublyUnderlined", doublyUnderlined));
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));
TextAttribute desiredAttrs;
std::vector<std::string> onSequences, offSequences;
// Collect up a VT sequence to set the state given the method properties
if (faint)
{
desiredAttrs.SetFaint(true);
onSequences.push_back("\x1b[2m");
offSequences.push_back("\x1b[22m");
}
if (underlined)
{
desiredAttrs.SetUnderlined(true);
onSequences.push_back("\x1b[4m");
offSequences.push_back("\x1b[24m");
}
if (doublyUnderlined)
{
desiredAttrs.SetDoublyUnderlined(true);
onSequences.push_back("\x1b[21m");
// The two underlines share the same off sequence, so we
// only add it here if that hasn't already been done.
if (!underlined)
{
offSequences.push_back("\x1b[24m");
}
}
if (italics)
{
desiredAttrs.SetItalic(true);
onSequences.push_back("\x1b[3m");
offSequences.push_back("\x1b[23m");
}
if (blink)
{
desiredAttrs.SetBlinking(true);
onSequences.push_back("\x1b[5m");
offSequences.push_back("\x1b[25m");
}
if (invisible)
{
desiredAttrs.SetInvisible(true);
onSequences.push_back("\x1b[8m");
offSequences.push_back("\x1b[28m");
}
if (crossedOut)
{
desiredAttrs.SetCrossedOut(true);
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), SetUpViewport());
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({}));
});
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::Xterm256TestAttributesAcrossReset()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:renditionAttribute", L"{1, 2, 3, 4, 5, 7, 8, 9, 21, 53}")
END_TEST_METHOD_PROPERTIES()
int renditionAttribute;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"renditionAttribute", renditionAttribute));
std::stringstream renditionSequence;
renditionSequence << "\x1b[" << renditionAttribute << "m";
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<Xterm256Engine> engine = std::make_unique<Xterm256Engine>(std::move(hFile), SetUpViewport());
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
RenderData renderData;
Log::Comment(L"Make sure rendition attributes are retained when colors are reset");
Log::Comment(L"----Start With All Attributes Reset----");
TextAttribute textAttributes = {};
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
switch (renditionAttribute)
{
case GraphicsOptions::BoldBright:
Log::Comment(L"----Set Bold Attribute----");
textAttributes.SetBold(true);
break;
case GraphicsOptions::RGBColorOrFaint:
Log::Comment(L"----Set Faint Attribute----");
textAttributes.SetFaint(true);
break;
case GraphicsOptions::Italics:
Log::Comment(L"----Set Italics Attribute----");
textAttributes.SetItalic(true);
break;
case GraphicsOptions::Underline:
Log::Comment(L"----Set Underline Attribute----");
textAttributes.SetUnderlined(true);
break;
case GraphicsOptions::DoublyUnderlined:
Log::Comment(L"----Set Doubly Underlined Attribute----");
textAttributes.SetDoublyUnderlined(true);
break;
case GraphicsOptions::Overline:
Log::Comment(L"----Set Overline Attribute----");
textAttributes.SetOverlined(true);
break;
case GraphicsOptions::BlinkOrXterm256Index:
Log::Comment(L"----Set Blink Attribute----");
textAttributes.SetBlinking(true);
break;
case GraphicsOptions::Negative:
Log::Comment(L"----Set Negative Attribute----");
textAttributes.SetReverseVideo(true);
break;
case GraphicsOptions::Invisible:
Log::Comment(L"----Set Invisible Attribute----");
textAttributes.SetInvisible(true);
break;
case GraphicsOptions::CrossedOut:
Log::Comment(L"----Set Crossed Out Attribute----");
textAttributes.SetCrossedOut(true);
break;
}
qExpectedInput.push_back(renditionSequence.str());
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
Log::Comment(L"----Set Green Foreground----");
textAttributes.SetIndexedForeground(TextColor::DARK_GREEN);
qExpectedInput.push_back("\x1b[32m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
Log::Comment(L"----Reset Default Foreground and Retain Rendition----");
textAttributes.SetDefaultForeground();
qExpectedInput.push_back("\x1b[m");
qExpectedInput.push_back(renditionSequence.str());
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
Log::Comment(L"----Set Green Background----");
textAttributes.SetIndexedBackground(TextColor::DARK_GREEN);
qExpectedInput.push_back("\x1b[42m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
Log::Comment(L"----Reset Default Background and Retain Rendition----");
textAttributes.SetDefaultBackground();
qExpectedInput.push_back("\x1b[m");
qExpectedInput.push_back(renditionSequence.str());
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
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), SetUpViewport(), 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), SetUpViewport(), false);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
RenderData renderData;
// 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"));
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({},
&renderData,
false,
false));
TestPaint(*engine, [&]() {
TextAttribute textAttributes;
Log::Comment(NoThrowString().Format(
L"----Change only the BG----"));
textAttributes.SetIndexedBackground(TextColor::DARK_RED);
qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG----"));
textAttributes.SetIndexedForeground(TextColor::DARK_WHITE);
qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to an RGB value----"));
textAttributes.SetBackground(RGB(19, 161, 14));
qExpectedInput.push_back("\x1b[42m"); // Background DARK_GREEN
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG to an RGB value----"));
textAttributes.SetForeground(RGB(193, 156, 0));
qExpectedInput.push_back("\x1b[33m"); // Foreground DARK_YELLOW
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to the 'Default' background----"));
textAttributes.SetDefaultBackground();
qExpectedInput.push_back("\x1b[m"); // Both foreground and background default
qExpectedInput.push_back("\x1b[33m"); // Reapply foreground DARK_YELLOW
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG to a 256-color index----"));
textAttributes.SetIndexedForeground256(TextColor::DARK_WHITE);
qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the BG to a 256-color index----"));
textAttributes.SetIndexedBackground256(TextColor::DARK_RED);
qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Change only the FG to the 'Default' foreground----"));
textAttributes.SetDefaultForeground();
qExpectedInput.push_back("\x1b[m"); // Both foreground and background default
qExpectedInput.push_back("\x1b[41m"); // Reapply background DARK_RED
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
false));
Log::Comment(NoThrowString().Format(
L"----Back to defaults----"));
textAttributes = {};
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes,
&renderData,
false,
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({},
&renderData,
false,
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), SetUpViewport(), 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::XtermTestAttributesAcrossReset()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:renditionAttribute", L"{1, 4, 7}")
END_TEST_METHOD_PROPERTIES()
int renditionAttribute;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"renditionAttribute", renditionAttribute));
std::stringstream renditionSequence;
renditionSequence << "\x1b[" << renditionAttribute << "m";
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
std::unique_ptr<XtermEngine> engine = std::make_unique<XtermEngine>(std::move(hFile), SetUpViewport(), false);
auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2);
engine->SetTestCallback(pfn);
RenderData renderData;
Log::Comment(L"Make sure rendition attributes are retained when colors are reset");
Log::Comment(L"----Start With All Attributes Reset----");
TextAttribute textAttributes = {};
qExpectedInput.push_back("\x1b[m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
switch (renditionAttribute)
{
case GraphicsOptions::BoldBright:
Log::Comment(L"----Set Bold Attribute----");
textAttributes.SetBold(true);
break;
case GraphicsOptions::Underline:
Log::Comment(L"----Set Underline Attribute----");
textAttributes.SetUnderlined(true);
break;
case GraphicsOptions::Negative:
Log::Comment(L"----Set Negative Attribute----");
textAttributes.SetReverseVideo(true);
break;
}
qExpectedInput.push_back(renditionSequence.str());
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
Log::Comment(L"----Set Green Foreground----");
textAttributes.SetIndexedForeground(TextColor::DARK_GREEN);
qExpectedInput.push_back("\x1b[32m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
Log::Comment(L"----Reset Default Foreground and Retain Rendition----");
textAttributes.SetDefaultForeground();
qExpectedInput.push_back("\x1b[m");
qExpectedInput.push_back(renditionSequence.str());
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
Log::Comment(L"----Set Green Background----");
textAttributes.SetIndexedBackground(TextColor::DARK_GREEN);
qExpectedInput.push_back("\x1b[42m");
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
Log::Comment(L"----Reset Default Background and Retain Rendition----");
textAttributes.SetDefaultBackground();
qExpectedInput.push_back("\x1b[m");
qExpectedInput.push_back(renditionSequence.str());
VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false));
VerifyExpectedInputsDrained();
}
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), SetUpViewport());
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), view);
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), view);
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);
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 auto format = FMT_COMPILE("\x1b[{}m");
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), view);
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->_WriteFormatted(format, value));
Log::Comment(L"2.) Write the same thing again, should be fine.");
qExpectedInput.push_back("\x1b[12m");
VERIFY_SUCCEEDED(engine->_WriteFormatted(format, value));
Log::Comment(L"3.) Now write something huge. Should resize itself and still be fine.");
static const auto bigFormat = FMT_COMPILE("\x1b[28;3;{};{};{}m");
const auto bigValue = 500;
qExpectedInput.push_back("\x1b[28;3;500;500;500m");
VERIFY_SUCCEEDED(engine->_WriteFormatted(bigFormat, bigValue, bigValue, bigValue));
}