420 lines
15 KiB
C++
420 lines
15 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
//
|
|
// This test class creates an in-proc conpty host as well as a Terminal, to
|
|
// validate that strings written to the conpty create the same response on the
|
|
// terminal end. Tests can be written that validate both the contents of the
|
|
// host buffer as well as the terminal buffer. Everytime that
|
|
// `renderer.PaintFrame()` is called, the tests will validate the expected
|
|
// output, and then flush the output of the VtEngine straight to the Terminal.
|
|
|
|
#include "precomp.h"
|
|
#include <wextestclass.h>
|
|
#include "../../inc/consoletaeftemplates.hpp"
|
|
#include "../../types/inc/Viewport.hpp"
|
|
#include "../../types/inc/convert.hpp"
|
|
|
|
#include "../renderer/inc/DummyRenderTarget.hpp"
|
|
#include "../../renderer/base/Renderer.hpp"
|
|
#include "../../renderer/vt/Xterm256Engine.hpp"
|
|
#include "../../renderer/vt/XtermEngine.hpp"
|
|
#include "../../renderer/vt/WinTelnetEngine.hpp"
|
|
|
|
class InputBuffer; // This for some reason needs to be fwd-decl'd
|
|
#include "../host/inputBuffer.hpp"
|
|
#include "../host/readDataCooked.hpp"
|
|
#include "test/CommonState.hpp"
|
|
|
|
#include "../cascadia/TerminalCore/Terminal.hpp"
|
|
|
|
#include "TestUtils.h"
|
|
|
|
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;
|
|
|
|
using namespace Microsoft::Terminal::Core;
|
|
|
|
namespace TerminalCoreUnitTests
|
|
{
|
|
class TerminalBufferTests;
|
|
};
|
|
using namespace TerminalCoreUnitTests;
|
|
|
|
class TerminalCoreUnitTests::ConptyRoundtripTests final
|
|
{
|
|
static const SHORT TerminalViewWidth = 80;
|
|
static const SHORT TerminalViewHeight = 32;
|
|
|
|
TEST_CLASS(ConptyRoundtripTests);
|
|
|
|
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)
|
|
{
|
|
// STEP 1: Set up the Terminal
|
|
term = std::make_unique<Terminal>();
|
|
term->Create({ TerminalViewWidth, TerminalViewHeight }, 100, emptyRT);
|
|
|
|
// STEP 2: Set up the Conpty
|
|
|
|
// 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();
|
|
|
|
_pVtRenderEngine = std::make_unique<Xterm256Engine>(std::move(hFile),
|
|
gci,
|
|
initialViewport,
|
|
gci.GetColorTable(),
|
|
static_cast<WORD>(gci.GetColorTableSize()));
|
|
auto pfn = std::bind(&ConptyRoundtripTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2);
|
|
_pVtRenderEngine->SetTestCallback(pfn);
|
|
|
|
// Configure the OutputStateMachine's _pfnFlushToTerminal
|
|
// Use OutputStateMachineEngine::SetTerminalConnection
|
|
g.pRender->AddRenderEngine(_pVtRenderEngine.get());
|
|
gci.GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get());
|
|
|
|
_pConApi = std::make_unique<ConhostInternalGetSet>(gci);
|
|
|
|
// 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();
|
|
|
|
expectedOutput.clear();
|
|
|
|
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.");
|
|
|
|
term = nullptr;
|
|
|
|
return true;
|
|
}
|
|
|
|
TEST_METHOD(ConptyOutputTestCanary);
|
|
TEST_METHOD(SimpleWriteOutputTest);
|
|
TEST_METHOD(WriteTwoLinesUsesNewline);
|
|
TEST_METHOD(WriteAFewSimpleLines);
|
|
TEST_METHOD(PassthroughClearScrollback);
|
|
|
|
private:
|
|
bool _writeCallback(const char* const pch, size_t const cch);
|
|
void _flushFirstFrame();
|
|
void _resizeConpty(const unsigned short sx, const unsigned short sy);
|
|
std::deque<std::string> expectedOutput;
|
|
std::unique_ptr<Microsoft::Console::Render::VtEngine> _pVtRenderEngine;
|
|
std::unique_ptr<CommonState> m_state;
|
|
std::unique_ptr<Microsoft::Console::VirtualTerminal::ConGetSet> _pConApi;
|
|
|
|
// Tests can set these variables how they link to configure the behavior of the test harness.
|
|
bool _checkConptyOutput{ true }; // If true, the test class will check that the output from conpty was expected
|
|
bool _logConpty{ false }; // If true, the test class will log all the output from conpty. Helpful for debugging.
|
|
|
|
DummyRenderTarget emptyRT;
|
|
std::unique_ptr<Terminal> term;
|
|
};
|
|
|
|
bool ConptyRoundtripTests::_writeCallback(const char* const pch, size_t const cch)
|
|
{
|
|
std::string actualString = std::string(pch, cch);
|
|
|
|
if (_checkConptyOutput)
|
|
{
|
|
VERIFY_IS_GREATER_THAN(expectedOutput.size(),
|
|
static_cast<size_t>(0),
|
|
NoThrowString().Format(L"writing=\"%hs\", expecting %u strings", TestUtils::ReplaceEscapes(actualString).c_str(), expectedOutput.size()));
|
|
|
|
std::string first = expectedOutput.front();
|
|
expectedOutput.pop_front();
|
|
|
|
Log::Comment(NoThrowString().Format(L"Expected =\t\"%hs\"", TestUtils::ReplaceEscapes(first).c_str()));
|
|
Log::Comment(NoThrowString().Format(L"Actual =\t\"%hs\"", TestUtils::ReplaceEscapes(actualString).c_str()));
|
|
|
|
VERIFY_ARE_EQUAL(first.length(), cch);
|
|
VERIFY_ARE_EQUAL(first, actualString);
|
|
}
|
|
else if (_logConpty)
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Writing \"%hs\" to Terminal", TestUtils::ReplaceEscapes(actualString).c_str()));
|
|
}
|
|
|
|
// Write the string back to our Terminal
|
|
const auto converted = ConvertToW(CP_UTF8, actualString);
|
|
term->Write(converted);
|
|
|
|
return true;
|
|
}
|
|
|
|
void ConptyRoundtripTests::_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());
|
|
}
|
|
|
|
void ConptyRoundtripTests::_resizeConpty(const unsigned short sx,
|
|
const unsigned short sy)
|
|
{
|
|
// Largely taken from implementation in PtySignalInputThread::_InputThread
|
|
if (DispatchCommon::s_ResizeWindow(*_pConApi, sx, sy))
|
|
{
|
|
// Instead of going through the VtIo to suppress the resize repaint,
|
|
// just call the method directly on the renderer. This is implemented in
|
|
// VtIo::SuppressResizeRepaint
|
|
VERIFY_SUCCEEDED(_pVtRenderEngine->SuppressResizeRepaint());
|
|
}
|
|
}
|
|
|
|
void ConptyRoundtripTests::ConptyOutputTestCanary()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"This is a simple test to make sure that everything is working as expected."));
|
|
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
|
|
|
_flushFirstFrame();
|
|
}
|
|
|
|
void ConptyRoundtripTests::SimpleWriteOutputTest()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Write some simple output, and make sure it gets rendered largely "
|
|
L"unmodified to the terminal"));
|
|
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
|
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& renderer = *g.pRender;
|
|
auto& gci = g.getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
auto& hostSm = si.GetStateMachine();
|
|
auto& termTb = *term->_buffer;
|
|
|
|
_flushFirstFrame();
|
|
|
|
expectedOutput.push_back("Hello World");
|
|
hostSm.ProcessString(L"Hello World");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
|
|
TestUtils::VerifyExpectedString(termTb, L"Hello World ", { 0, 0 });
|
|
}
|
|
|
|
void ConptyRoundtripTests::WriteTwoLinesUsesNewline()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Write two lines of output. We should use \r\n to move the cursor"));
|
|
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
|
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& renderer = *g.pRender;
|
|
auto& gci = g.getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
auto& hostSm = si.GetStateMachine();
|
|
auto& hostTb = si.GetTextBuffer();
|
|
auto& termTb = *term->_buffer;
|
|
|
|
_flushFirstFrame();
|
|
|
|
hostSm.ProcessString(L"AAA");
|
|
hostSm.ProcessString(L"\x1b[2;1H");
|
|
hostSm.ProcessString(L"BBB");
|
|
|
|
auto verifyData = [](TextBuffer& tb) {
|
|
TestUtils::VerifyExpectedString(tb, L"AAA", { 0, 0 });
|
|
TestUtils::VerifyExpectedString(tb, L"BBB", { 0, 1 });
|
|
};
|
|
|
|
verifyData(hostTb);
|
|
|
|
expectedOutput.push_back("AAA");
|
|
expectedOutput.push_back("\r\n");
|
|
expectedOutput.push_back("BBB");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
|
|
verifyData(termTb);
|
|
}
|
|
|
|
void ConptyRoundtripTests::WriteAFewSimpleLines()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Write more lines of outout. We should use \r\n to move the cursor"));
|
|
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
|
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& renderer = *g.pRender;
|
|
auto& gci = g.getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
auto& hostSm = si.GetStateMachine();
|
|
auto& hostTb = si.GetTextBuffer();
|
|
auto& termTb = *term->_buffer;
|
|
|
|
_flushFirstFrame();
|
|
|
|
hostSm.ProcessString(L"AAA\n");
|
|
hostSm.ProcessString(L"BBB\n");
|
|
hostSm.ProcessString(L"\n");
|
|
hostSm.ProcessString(L"CCC");
|
|
auto verifyData = [](TextBuffer& tb) {
|
|
TestUtils::VerifyExpectedString(tb, L"AAA", { 0, 0 });
|
|
TestUtils::VerifyExpectedString(tb, L"BBB", { 0, 1 });
|
|
TestUtils::VerifyExpectedString(tb, L" ", { 0, 2 });
|
|
TestUtils::VerifyExpectedString(tb, L"CCC", { 0, 3 });
|
|
};
|
|
|
|
verifyData(hostTb);
|
|
|
|
expectedOutput.push_back("AAA");
|
|
expectedOutput.push_back("\r\n");
|
|
expectedOutput.push_back("BBB");
|
|
expectedOutput.push_back("\r\n");
|
|
// Here, we're going to emit 3 spaces. The region that got invalidated was a
|
|
// rectangle from 0,0 to 3,3, so the vt renderer will try to render the
|
|
// region in between BBB and CCC as well, because it got included in the
|
|
// rectangle Or() operation.
|
|
// This behavior should not be seen as binding - if a future optimization
|
|
// breaks this test, it wouldn't be the worst.
|
|
expectedOutput.push_back(" ");
|
|
expectedOutput.push_back("\r\n");
|
|
expectedOutput.push_back("CCC");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
|
|
verifyData(termTb);
|
|
}
|
|
|
|
void ConptyRoundtripTests::PassthroughClearScrollback()
|
|
{
|
|
Log::Comment(NoThrowString().Format(
|
|
L"Write more lines of outout. We should use \r\n to move the cursor"));
|
|
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
|
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
auto& renderer = *g.pRender;
|
|
auto& gci = g.getConsoleInformation();
|
|
auto& si = gci.GetActiveOutputBuffer();
|
|
auto& hostSm = si.GetStateMachine();
|
|
auto& termTb = *term->_buffer;
|
|
|
|
_flushFirstFrame();
|
|
|
|
_logConpty = true;
|
|
|
|
const auto hostView = si.GetViewport();
|
|
const auto end = 2 * hostView.Height();
|
|
for (auto i = 0; i < end; i++)
|
|
{
|
|
Log::Comment(NoThrowString().Format(L"Writing line %d/%d", i, end));
|
|
expectedOutput.push_back("X");
|
|
if (i < hostView.BottomInclusive())
|
|
{
|
|
expectedOutput.push_back("\r\n");
|
|
}
|
|
else
|
|
{
|
|
// After we hit the bottom of the viewport, the newlines come in
|
|
// seperated for whatever reason.
|
|
expectedOutput.push_back("\r");
|
|
expectedOutput.push_back("\n");
|
|
expectedOutput.push_back("");
|
|
}
|
|
|
|
hostSm.ProcessString(L"X\n");
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
|
|
// Verify that we've printed height*2 lines of X's to the Terminal
|
|
const auto termFirstView = term->GetViewport();
|
|
for (short y = 0; y < 2 * termFirstView.Height(); y++)
|
|
{
|
|
TestUtils::VerifyExpectedString(termTb, L"X ", { 0, y });
|
|
}
|
|
|
|
// Write a Erase Scrollback VT sequence to the host, it should come through to the Terminal
|
|
expectedOutput.push_back("\x1b[3J");
|
|
hostSm.ProcessString(L"\x1b[3J");
|
|
|
|
_checkConptyOutput = false;
|
|
|
|
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
|
|
|
const auto termSecondView = term->GetViewport();
|
|
VERIFY_ARE_EQUAL(0, termSecondView.Top());
|
|
|
|
// Verify the top of the Terminal veiwoprt contains the contents of the old viewport
|
|
for (short y = 0; y < termSecondView.BottomInclusive(); y++)
|
|
{
|
|
TestUtils::VerifyExpectedString(termTb, L"X ", { 0, y });
|
|
}
|
|
|
|
// Verify below the new viewport (the old viewport) has been cleared out
|
|
for (short y = termSecondView.BottomInclusive(); y < termFirstView.BottomInclusive(); y++)
|
|
{
|
|
TestUtils::VerifyExpectedString(termTb, std::wstring(TerminalViewWidth, L' '), { 0, y });
|
|
}
|
|
}
|