ddbe370d22
This PR reimplements the VT rendering engines to do a better job of preserving the original color types when propagating attributes over ConPTY. For the 16-color renderers it provides better support for default colors and improves the efficiency of the color narrowing conversions. It also fixes problems with the ordering of character renditions that could result in attributes being dropped. Originally the base renderer would calculate the RGB color values and legacy/extended attributes up front, passing that data on to the active engine's `UpdateDrawingBrushes` method. With this new implementation, the renderer now just passes through the original `TextAttribute` along with an `IRenderData` interface, and leaves it to the engines to extract the information they need. The GDI and DirectX engines now have to lookup the RGB colors themselves (via simple `IRenderData` calls), but have no need for the other attributes. The VT engines extract the information that they need from the `TextAttribute`, instead of having to reverse engineer it from `COLORREF`s. The process for the 256-color Xterm engine starts with a check for default colors. If both foreground and background are default, it outputs a SGR 0 reset, and clears the `_lastTextAttribute` completely to make sure any reset state is reapplied. With that out the way, the foreground and background are updated (if changed) in one of 4 ways. They can either be a default value (SGR 39 and 49), a 16-color index (using ANSI or AIX sequences), a 256-color index, or a 24-bit RGB value (both using SGR 38 and 48 sequences). Then once the colors are accounted for, there is a separate step that handles the character rendition attributes (bold, italics, underline, etc.) This step must come _after_ the color sequences, in case a SGR reset is required, which would otherwise have cleared any character rendition attributes if it came last (which is what happened in the original implementation). The process for the 16-color engines is a little different. The target client in this case (Windows telnet) is incapable of setting default colors individually, so we need to output an SGR 0 reset if _either_ color has changed to default. With that out the way, we use the `TextColor::GetLegacyIndex` method to obtain an approximate 16-color index for each color, and apply the bold attribute by brightening the foreground index (setting bit 8) if the color type permits that. However, since Windows telnet only supports the 8 basic ANSI colors, the best we can do for bright colors is to output an SGR 1 attribute to get a bright foreground. There is nothing we can do about a bright background, so after that we just have to drop the high bit from the colors. If the resulting index values have changed from what they were before, we then output ANSI 8-color SGR sequences to update them. As with the 256-color engine, there is also a final step to handle the character rendition attributes. But in this case, the only supported attributes are underline and reversed video. Since the VT engines no longer depend on the active color table and default color values, there was quite a lot of code that could now be removed. This included the `IDefaultColorProvider` interface and implementations, the `Find(Nearest)TableIndex` functions, and also the associated HLS conversion and difference calculations. VALIDATION Other than simple API parameter changes, the majority of updates required in the unit tests were to correct assumptions about the way the colors should be rendered, which were the source of the narrowing bugs this PR was trying to fix. Like passing white on black to the `UpdateDrawingBrushes` API, and expecting it to output the default `SGR 0` sequence, or passing an RGB color and expecting an indexed SGR sequence. In addition to that, I've added some VT renderer tests to make sure the rendition attributes (bold, underline, etc) are correctly retained when a default color update causes an `SGR 0` sequence to be generated (the source of bug #3076). And I've extended the VT renderer color tests (both 256-color and 16-color) to make sure we're covering all of the different color types (default, RGB, and both forms of indexed colors). I've also tried to manually verify that all of the test cases in the linked bug reports (and their associated duplicates) are now fixed when this PR is applied. Closes #2661 Closes #3076 Closes #3717 Closes #5384 Closes #5864 This is only a partial fix for #293, but I suspect the remaining cases are unfixable.
367 lines
13 KiB
C++
367 lines
13 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/base/Renderer.hpp"
|
|
#include "../../renderer/vt/Xterm256Engine.hpp"
|
|
#include "../../renderer/vt/XtermEngine.hpp"
|
|
#include "../Settings.hpp"
|
|
|
|
#include "CommonState.hpp"
|
|
|
|
using namespace WEX::Common;
|
|
using namespace WEX::Logging;
|
|
using namespace WEX::TestExecution;
|
|
using namespace Microsoft::Console::Types;
|
|
using namespace Microsoft::Console::Interactivity;
|
|
using namespace Microsoft::Console::VirtualTerminal;
|
|
|
|
using namespace Microsoft::Console;
|
|
using namespace Microsoft::Console::Render;
|
|
using namespace Microsoft::Console::Types;
|
|
|
|
class ConptyOutputTests
|
|
{
|
|
// !!! DANGER: Many tests in this class expect the Terminal and Host buffers
|
|
// to be 80x32. If you change these, you'll probably inadvertently break a
|
|
// bunch of tests !!!
|
|
static const SHORT TerminalViewWidth = 80;
|
|
static const SHORT TerminalViewHeight = 32;
|
|
|
|
// This test class is to write some things into the PTY and then check that
|
|
// the rendering that is coming out of the VT-sequence generator is exactly
|
|
// as we expect it to be.
|
|
BEGIN_TEST_CLASS(ConptyOutputTests)
|
|
TEST_CLASS_PROPERTY(L"IsolationLevel", L"Class")
|
|
END_TEST_CLASS()
|
|
|
|
TEST_CLASS_SETUP(ClassSetup)
|
|
{
|
|
m_state = std::make_unique<CommonState>();
|
|
|
|
m_state->InitEvents();
|
|
m_state->PrepareGlobalFont();
|
|
m_state->PrepareGlobalScreenBuffer(TerminalViewWidth, TerminalViewHeight, TerminalViewWidth, TerminalViewHeight);
|
|
m_state->PrepareGlobalInputBuffer();
|
|
|
|
return true;
|
|
}
|
|
|
|
TEST_CLASS_CLEANUP(ClassCleanup)
|
|
{
|
|
m_state->CleanupGlobalScreenBuffer();
|
|
m_state->CleanupGlobalFont();
|
|
m_state->CleanupGlobalInputBuffer();
|
|
|
|
m_state.release();
|
|
|
|
return true;
|
|
}
|
|
|
|
TEST_METHOD_SETUP(MethodSetup)
|
|
{
|
|
// Set up some sane defaults
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& gci = g.getConsoleInformation();
|
|
gci.SetDefaultForegroundColor(INVALID_COLOR);
|
|
gci.SetDefaultBackgroundColor(INVALID_COLOR);
|
|
gci.SetFillAttribute(0x07); // DARK_WHITE on DARK_BLACK
|
|
|
|
m_state->PrepareNewTextBufferInfo(true, TerminalViewWidth, TerminalViewHeight);
|
|
auto& currentBuffer = gci.GetActiveOutputBuffer();
|
|
// Make sure a test hasn't left us in the alt buffer on accident
|
|
VERIFY_IS_FALSE(currentBuffer._IsAltBuffer());
|
|
VERIFY_SUCCEEDED(currentBuffer.SetViewportOrigin(true, { 0, 0 }, true));
|
|
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), currentBuffer.GetTextBuffer().GetCursor().GetPosition());
|
|
|
|
g.pRender = new Renderer(&gci.renderData, nullptr, 0, nullptr);
|
|
|
|
// Set up an xterm-256 renderer for conpty
|
|
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
|
|
Viewport initialViewport = currentBuffer.GetViewport();
|
|
|
|
auto vtRenderEngine = std::make_unique<Xterm256Engine>(std::move(hFile),
|
|
initialViewport);
|
|
auto pfn = std::bind(&ConptyOutputTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2);
|
|
vtRenderEngine->SetTestCallback(pfn);
|
|
|
|
g.pRender->AddRenderEngine(vtRenderEngine.get());
|
|
gci.GetActiveOutputBuffer().SetTerminalConnection(vtRenderEngine.get());
|
|
|
|
expectedOutput.clear();
|
|
|
|
// Manually set the console into conpty mode. We're not actually going
|
|
// to set up the pipes for conpty, but we want the console to behave
|
|
// like it would in conpty mode.
|
|
g.EnableConptyModeForTests(std::move(vtRenderEngine));
|
|
|
|
return true;
|
|
}
|
|
|
|
TEST_METHOD_CLEANUP(MethodCleanup)
|
|
{
|
|
m_state->CleanupNewTextBufferInfo();
|
|
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
delete g.pRender;
|
|
|
|
VERIFY_ARE_EQUAL(0u, expectedOutput.size(), L"Tests should drain all the output they push into the expected output buffer.");
|
|
|
|
return true;
|
|
}
|
|
|
|
TEST_METHOD(ConptyOutputTestCanary);
|
|
TEST_METHOD(SimpleWriteOutputTest);
|
|
TEST_METHOD(WriteTwoLinesUsesNewline);
|
|
TEST_METHOD(WriteAFewSimpleLines);
|
|
TEST_METHOD(InvalidateUntilOneBeforeEnd);
|
|
|
|
private:
|
|
bool _writeCallback(const char* const pch, size_t const cch);
|
|
void _flushFirstFrame();
|
|
std::deque<std::string> expectedOutput;
|
|
std::unique_ptr<CommonState> m_state;
|
|
};
|
|
|
|
bool ConptyOutputTests::_writeCallback(const char* const pch, size_t const cch)
|
|
{
|
|
// Since rendering happens on a background thread that doesn't have the exception handler on it
|
|
// we need to rely on VERIFY's return codes instead of exceptions.
|
|
const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope;
|
|
|
|
std::string actualString = std::string(pch, cch);
|
|
RETURN_BOOL_IF_FALSE(VERIFY_IS_GREATER_THAN(expectedOutput.size(),
|
|
static_cast<size_t>(0),
|
|
NoThrowString().Format(L"writing=\"%hs\", expecting %u strings", actualString.c_str(), expectedOutput.size())));
|
|
|
|
std::string first = expectedOutput.front();
|
|
expectedOutput.pop_front();
|
|
|
|
Log::Comment(NoThrowString().Format(L"Expected =\t\"%hs\"", first.c_str()));
|
|
Log::Comment(NoThrowString().Format(L"Actual =\t\"%hs\"", actualString.c_str()));
|
|
|
|
RETURN_BOOL_IF_FALSE(VERIFY_ARE_EQUAL(first.length(), cch));
|
|
RETURN_BOOL_IF_FALSE(VERIFY_ARE_EQUAL(first, actualString));
|
|
|
|
return true;
|
|
}
|
|
|
|
void ConptyOutputTests::_flushFirstFrame()
|
|
{
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& renderer = *g.pRender;
|
|
|
|
expectedOutput.push_back("\x1b[2J");
|
|
expectedOutput.push_back("\x1b[m");
|
|
expectedOutput.push_back("\x1b[H"); // Go Home
|
|
expectedOutput.push_back("\x1b[?25h");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
}
|
|
|
|
// Function Description:
|
|
// - Helper function to validate that a number of characters in a row are all
|
|
// the same. Validates that the next end-start characters are all equal to the
|
|
// provided string. Will move the provided iterator as it validates. The
|
|
// caller should ensure that `iter` starts where they would like to validate.
|
|
// Arguments:
|
|
// - expectedChar: The character (or characters) we're expecting
|
|
// - iter: a iterator pointing to the cell we'd like to start validating at.
|
|
// - start: the first index in the range we'd like to validate
|
|
// - end: the last index in the range we'd like to validate
|
|
// Return Value:
|
|
// - <none>
|
|
void _verifySpanOfText(const wchar_t* const expectedChar,
|
|
TextBufferCellIterator& iter,
|
|
const int start,
|
|
const int end)
|
|
{
|
|
for (int x = start; x < end; x++)
|
|
{
|
|
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
|
|
if (iter->Chars() != expectedChar)
|
|
{
|
|
Log::Comment(NoThrowString().Format(L"character [%d] was mismatched", x));
|
|
}
|
|
VERIFY_ARE_EQUAL(expectedChar, (iter++)->Chars());
|
|
}
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Successfully validated %d characters were '%s'", end - start, expectedChar));
|
|
}
|
|
|
|
void ConptyOutputTests::ConptyOutputTestCanary()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"This is a simple test to make sure that everything is working as expected."));
|
|
|
|
_flushFirstFrame();
|
|
}
|
|
|
|
void ConptyOutputTests::SimpleWriteOutputTest()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Write some simple output, and make sure it gets rendered largely "
|
|
L"unmodified to the terminal"));
|
|
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& renderer = *g.pRender;
|
|
auto& gci = g.getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
auto& sm = si.GetStateMachine();
|
|
|
|
_flushFirstFrame();
|
|
|
|
expectedOutput.push_back("Hello World");
|
|
sm.ProcessString(L"Hello World");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
}
|
|
|
|
void ConptyOutputTests::WriteTwoLinesUsesNewline()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Write two lines of output. We should use \r\n to move the cursor"));
|
|
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& renderer = *g.pRender;
|
|
auto& gci = g.getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
auto& sm = si.GetStateMachine();
|
|
auto& tb = si.GetTextBuffer();
|
|
|
|
_flushFirstFrame();
|
|
|
|
sm.ProcessString(L"AAA");
|
|
sm.ProcessString(L"\x1b[2;1H");
|
|
sm.ProcessString(L"BBB");
|
|
|
|
{
|
|
auto iter = tb.GetCellDataAt({ 0, 0 });
|
|
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
|
}
|
|
{
|
|
auto iter = tb.GetCellDataAt({ 0, 1 });
|
|
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
|
}
|
|
|
|
expectedOutput.push_back("AAA");
|
|
expectedOutput.push_back("\r\n");
|
|
expectedOutput.push_back("BBB");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
}
|
|
|
|
void ConptyOutputTests::WriteAFewSimpleLines()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Write more lines of output. We should use \r\n to move the cursor"));
|
|
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& renderer = *g.pRender;
|
|
auto& gci = g.getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
auto& sm = si.GetStateMachine();
|
|
auto& tb = si.GetTextBuffer();
|
|
|
|
_flushFirstFrame();
|
|
|
|
sm.ProcessString(L"AAA\n");
|
|
sm.ProcessString(L"BBB\n");
|
|
sm.ProcessString(L"\n");
|
|
sm.ProcessString(L"CCC");
|
|
|
|
{
|
|
auto iter = tb.GetCellDataAt({ 0, 0 });
|
|
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
|
}
|
|
{
|
|
auto iter = tb.GetCellDataAt({ 0, 1 });
|
|
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
|
}
|
|
{
|
|
auto iter = tb.GetCellDataAt({ 0, 2 });
|
|
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
|
}
|
|
{
|
|
auto iter = tb.GetCellDataAt({ 0, 3 });
|
|
VERIFY_ARE_EQUAL(L"C", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"C", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"C", (iter++)->Chars());
|
|
}
|
|
|
|
expectedOutput.push_back("AAA");
|
|
expectedOutput.push_back("\r\n");
|
|
expectedOutput.push_back("BBB");
|
|
// Jump down to the fourth line because emitting spaces didn't do anything
|
|
// and we will skip to emitting the CCC segment.
|
|
expectedOutput.push_back("\x1b[4;1H");
|
|
expectedOutput.push_back("CCC");
|
|
|
|
// Cursor goes back on.
|
|
expectedOutput.push_back("\x1b[?25h");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
}
|
|
|
|
void ConptyOutputTests::InvalidateUntilOneBeforeEnd()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Make sure we don't use EL and wipe out the last column of text"));
|
|
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& renderer = *g.pRender;
|
|
auto& gci = g.getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
auto& sm = si.GetStateMachine();
|
|
auto& tb = si.GetTextBuffer();
|
|
|
|
_flushFirstFrame();
|
|
|
|
// Move the cursor to width-15, draw 15 characters
|
|
sm.ProcessString(L"\x1b[1;66H");
|
|
sm.ProcessString(L"ABCDEFGHIJKLMNO");
|
|
|
|
{
|
|
auto iter = tb.GetCellDataAt({ 78, 0 });
|
|
VERIFY_ARE_EQUAL(L"N", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"O", (iter++)->Chars());
|
|
}
|
|
|
|
expectedOutput.push_back("\x1b[65C");
|
|
expectedOutput.push_back("ABCDEFGHIJKLMNO");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
|
|
// overstrike the first with X and the middle 8 with spaces
|
|
sm.ProcessString(L"\x1b[1;66H");
|
|
// ABCDEFGHIJKLMNO
|
|
sm.ProcessString(L"X ");
|
|
|
|
{
|
|
auto iter = tb.GetCellDataAt({ 78, 0 });
|
|
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
|
VERIFY_ARE_EQUAL(L"O", (iter++)->Chars());
|
|
}
|
|
|
|
expectedOutput.push_back("\x1b[1;66H");
|
|
expectedOutput.push_back("X"); // sequence optimizer should choose ECH here
|
|
expectedOutput.push_back("\x1b[13X");
|
|
expectedOutput.push_back("\x1b[13C");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
}
|