From 62765f152e1fc9732a2ff212c9b98c1d0dbb9665 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 17 Jan 2020 10:40:12 -0600 Subject: [PATCH] Create tests that roundtrip output through a conpty to a Terminal (#4213) ## Summary of the Pull Request This PR adds two tests: * First, I started by writing a test where I could write output to the console host and inspect what output came out of conpty. This is the `ConptyOutputTests` in the host unit tests. * Then I got crazy and thought _"what if I could take that output and dump it straight into the `Terminal`"_? Hence, the `ConptyRoundtripTests` were born, into the TerminalCore unit tests. ## References Done in pursuit of #4200, but I felt this warranted it's own atomic PR ## PR Checklist * [x] Doesn't close anything on it's own. * [x] I work here * [x] you better believe this adds tests * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments From the comment in `ConptyRoundtripTests`: > 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 resopnse 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 th Also, some other bits had to be updated: * The renderer needed to be able to survive without a thread, so I hadded a simple check that it actually had a thread before calling `pThread->NotifyPaint` * Bits in `CommonState` used `NTSTATUS_FROM_HRESULT` which did _not_ work outside the host project. Since the `NTSTATUS` didn't seem that important, I replaced that with a `HRESULT` * `CommonState` likes to initialize the console to some _weird_ defaults. I added an optional param to let us just use the defaults. --- src/cascadia/TerminalCore/Terminal.hpp | 9 + .../ConptyRoundtripTests.cpp | 366 ++++++++++++++++++ .../UnitTests_TerminalCore/UnitTests.vcxproj | 41 +- src/cascadia/UnitTests_TerminalCore/precomp.h | 20 +- src/host/screenInfo.hpp | 2 + src/host/ut_host/ConptyOutputTests.cpp | 309 +++++++++++++++ src/host/ut_host/Host.UnitTests.vcxproj | 1 + src/host/ut_host/TextBufferTests.cpp | 2 +- src/host/ut_host/sources | 1 + src/inc/test/CommonState.hpp | 21 +- src/renderer/base/renderer.cpp | 8 +- src/renderer/base/renderer.hpp | 4 + src/renderer/vt/Xterm256Engine.hpp | 1 + src/renderer/vt/XtermEngine.hpp | 1 + src/renderer/vt/state.cpp | 14 +- src/renderer/vt/vtrenderer.hpp | 7 + 16 files changed, 788 insertions(+), 19 deletions(-) create mode 100644 src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp create mode 100644 src/host/ut_host/ConptyOutputTests.cpp diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 8981951588..0980b51393 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -28,6 +28,11 @@ namespace Microsoft::Terminal::Core class Terminal; } +// fwdecl unittest classes +#ifdef UNIT_TESTING +class ConptyRoundtripTests; +#endif + class Microsoft::Terminal::Core::Terminal final : public Microsoft::Terminal::Core::ITerminalApi, public Microsoft::Terminal::Core::ITerminalInput, @@ -245,4 +250,8 @@ private: SMALL_RECT _GetSelectionRow(const SHORT row, const COORD higherCoord, const COORD lowerCoord) const; void _ExpandSelectionRow(SMALL_RECT& selectionRow) const; #pragma endregion + +#ifdef UNIT_TESTING + friend class ::ConptyRoundtripTests; +#endif }; diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp new file mode 100644 index 0000000000..badc0548e9 --- /dev/null +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -0,0 +1,366 @@ +// 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 resopnse 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 +#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" + +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; + +class ConptyRoundtripTests +{ + TEST_CLASS(ConptyRoundtripTests); + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = std::make_unique(); + + m_state->InitEvents(); + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + 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(); + term->Create({ CommonState::s_csBufferWidth, CommonState::s_csBufferHeight }, 0, 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); + 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(std::move(hFile), + gci, + initialViewport, + gci.GetColorTable(), + static_cast(gci.GetColorTableSize())); + auto pfn = std::bind(&ConptyRoundtripTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2); + _pVtRenderEngine->SetTestCallback(pfn); + + g.pRender->AddRenderEngine(_pVtRenderEngine.get()); + gci.GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get()); + + 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); + +private: + bool _writeCallback(const char* const pch, size_t const cch); + void _flushFirstFrame(); + std::deque expectedOutput; + std::unique_ptr _pVtRenderEngine; + std::unique_ptr m_state; + + DummyRenderTarget emptyRT; + std::unique_ptr term; +}; + +bool ConptyRoundtripTests::_writeCallback(const char* const pch, size_t const cch) +{ + std::string actualString = std::string(pch, cch); + VERIFY_IS_GREATER_THAN(expectedOutput.size(), + static_cast(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())); + + VERIFY_ARE_EQUAL(first.length(), cch); + VERIFY_ARE_EQUAL(first, actualString); + + // 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()); +} + +// 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: +// - +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)); +} + +// Function Description: +// - Helper function to validate that the next characters pointed to by `iter` +// are the provided string. Will increment iter as it walks the provided +// string of characters. It will leave `iter` on the first character after the +// expectedString. +// Arguments: +// - expectedString: The characters we're expecting +// - iter: a iterator pointing to the cell we'd like to start validating at. +// Return Value: +// - +void _verifyExpectedString(std::wstring_view expectedString, + TextBufferCellIterator& iter) +{ + for (const auto wch : expectedString) + { + wchar_t buffer[]{ wch, L'\0' }; + std::wstring_view view{ buffer, 1 }; + VERIFY_IS_TRUE(iter, L"Ensure iterator is still valid"); + VERIFY_ARE_EQUAL(view, (iter++)->Chars(), NoThrowString().Format(L"%s", view.data())); + } +} + +// Function Description: +// - Helper function to validate that the next characters in the buffer at the +// given location are the provided string. Will return an iterator on the +// first character after the expectedString. +// Arguments: +// - tb: the buffer who's content we should check +// - expectedString: The characters we're expecting +// - pos: the starting position in the buffer to check the contents of +// Return Value: +// - an iterator on the first character after the expectedString. +TextBufferCellIterator _verifyExpectedString(const TextBuffer& tb, + std::wstring_view expectedString, + const COORD pos) +{ + auto iter = tb.GetCellDataAt(pos); + _verifyExpectedString(expectedString, iter); + return iter; +} + +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()); + + _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) { + _verifyExpectedString(tb, L"AAA", { 0, 0 }); + _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) { + _verifyExpectedString(tb, L"AAA", { 0, 0 }); + _verifyExpectedString(tb, L"BBB", { 0, 1 }); + _verifyExpectedString(tb, L" ", { 0, 2 }); + _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); +} diff --git a/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj b/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj index feec2c0c73..44bb2970d2 100644 --- a/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj +++ b/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj @@ -18,6 +18,7 @@ Create + @@ -38,6 +39,42 @@ {ca5cad1a-abcd-429c-b551-8562ec954746} + + + + + {990F2657-8580-4828-943F-5DD657D11843} + + + {af0a096a-8b3a-4949-81ef-7df8f0fee91f} + + + {06ec74cb-9a12-429c-b551-8562ec954746} + + + {345fd5a4-b32b-4f29-bd1c-b033bd2c35cc} + + + {06ec74cb-9a12-429c-b551-8562ec964846} + + + {06ec74cb-9a12-429c-b551-8532ec964726} + + + {2fd12fbb-1ddb-46d8-b818-1023c624caca} + + + {18d09a24-8240-42d6-8cb6-236eee820262} + + + {dcf55140-ef6a-4736-a403-957e4f7430bb} + + + {ef3e32a7-5ff6-42b4-b6e2-96cd7d033f00} + @@ -45,7 +82,7 @@ - ..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalSettings\Generated Files";%(AdditionalIncludeDirectories) + ..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalSettings\Generated Files";$(OpenConsoleDir)\src\host;%(AdditionalIncludeDirectories) precomp.h @@ -55,4 +92,4 @@ - \ No newline at end of file + diff --git a/src/cascadia/UnitTests_TerminalCore/precomp.h b/src/cascadia/UnitTests_TerminalCore/precomp.h index ddc9a41164..53dbb75242 100644 --- a/src/cascadia/UnitTests_TerminalCore/precomp.h +++ b/src/cascadia/UnitTests_TerminalCore/precomp.h @@ -6,8 +6,10 @@ Module Name: - precomp.h Abstract: -- Contains external headers to include in the precompile phase of console build process. -- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +- Contains external headers to include in the precompile phase of console build + process. +- Avoid including internal project headers. Instead include them only in the + classes that need them (helps with test project building). Author(s): - Carlos Zamora (cazamor) April 2019 @@ -15,6 +17,18 @@ Author(s): #pragma once +// +// This header and define are needed so that the console host code can build in +// this test binary. + +// Block minwindef.h min/max macros to prevent conflict +#define NOMINMAX + +// This includes a lot of common headers needed by both the host and the propsheet +// including: windows.h, winuser, ntstatus, assert, and the DDK +#include "HostAndPropsheetIncludes.h" +// + // This includes support libraries from the CRT, STL, WIL, and GSL #include "LibraryIncludes.h" @@ -30,4 +44,4 @@ Author(s): #ifdef CON_BUILD_PUBLIC #define CON_USERPRIVAPI_INDIRECT #define CON_DPIAPI_INDIRECT -#endif \ No newline at end of file +#endif diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index 4fe8d21689..eb25708122 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -307,5 +307,7 @@ private: friend class TextBufferIteratorTests; friend class ScreenBufferTests; friend class CommonState; + friend class ConptyOutputTests; + friend class ConptyRoundtripTests; #endif }; diff --git a/src/host/ut_host/ConptyOutputTests.cpp b/src/host/ut_host/ConptyOutputTests.cpp new file mode 100644 index 0000000000..1bc6a18990 --- /dev/null +++ b/src/host/ut_host/ConptyOutputTests.cpp @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#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 "../../renderer/vt/WinTelnetEngine.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 +{ + TEST_CLASS(ConptyOutputTests); + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = std::make_unique(); + + m_state->InitEvents(); + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + 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); + 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(std::move(hFile), + gci, + initialViewport, + gci.GetColorTable(), + static_cast(gci.GetColorTableSize())); + auto pfn = std::bind(&ConptyOutputTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2); + _pVtRenderEngine->SetTestCallback(pfn); + + g.pRender->AddRenderEngine(_pVtRenderEngine.get()); + gci.GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get()); + + 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."); + + return true; + } + + TEST_METHOD(ConptyOutputTestCanary); + TEST_METHOD(SimpleWriteOutputTest); + TEST_METHOD(WriteTwoLinesUsesNewline); + TEST_METHOD(WriteAFewSimpleLines); + +private: + bool _writeCallback(const char* const pch, size_t const cch); + void _flushFirstFrame(); + std::deque expectedOutput; + std::unique_ptr _pVtRenderEngine; + std::unique_ptr m_state; +}; + +bool ConptyOutputTests::_writeCallback(const char* const pch, size_t const cch) +{ + std::string actualString = std::string(pch, cch); + VERIFY_IS_GREATER_THAN(expectedOutput.size(), + static_cast(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())); + + VERIFY_ARE_EQUAL(first.length(), cch); + 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: +// - +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.")); + VERIFY_IS_NOT_NULL(_pVtRenderEngine.get()); + + _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")); + VERIFY_IS_NOT_NULL(_pVtRenderEngine.get()); + + 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")); + VERIFY_IS_NOT_NULL(_pVtRenderEngine.get()); + + 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")); + VERIFY_IS_NOT_NULL(_pVtRenderEngine.get()); + + 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"); + 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()); +} diff --git a/src/host/ut_host/Host.UnitTests.vcxproj b/src/host/ut_host/Host.UnitTests.vcxproj index 5c8f0e996a..5834e16a82 100644 --- a/src/host/ut_host/Host.UnitTests.vcxproj +++ b/src/host/ut_host/Host.UnitTests.vcxproj @@ -40,6 +40,7 @@ + Create diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 7ad1fa874e..e4af5b5e1a 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -149,7 +149,7 @@ class TextBufferTests void TextBufferTests::TestBufferCreate() { - VERIFY_SUCCESS_NTSTATUS(m_state->GetTextBufferInfoInitResult()); + VERIFY_SUCCEEDED(m_state->GetTextBufferInfoInitResult()); } TextBuffer& TextBufferTests::GetTbi() diff --git a/src/host/ut_host/sources b/src/host/ut_host/sources index 07f2e95e6e..7138c4db11 100644 --- a/src/host/ut_host/sources +++ b/src/host/ut_host/sources @@ -36,6 +36,7 @@ SOURCES = \ InputBufferTests.cpp \ VtIoTests.cpp \ VtRendererTests.cpp \ + ConptyOutputTests.cpp \ ViewportTests.cpp \ ConsoleArgumentsTests.cpp \ CommandLineTests.cpp \ diff --git a/src/inc/test/CommonState.hpp b/src/inc/test/CommonState.hpp index cd10f24f52..85dfeb960c 100644 --- a/src/inc/test/CommonState.hpp +++ b/src/inc/test/CommonState.hpp @@ -38,7 +38,7 @@ public: CommonState() : m_heap(GetProcessHeap()), - m_ntstatusTextBufferInfo(STATUS_FAIL_CHECK), + m_hrTextBufferInfo(E_FAIL), m_pFontInfo(nullptr), m_backupTextBufferInfo(), m_readHandle(nullptr) @@ -143,7 +143,7 @@ public: gci.SetCookedReadData(nullptr); } - void PrepareNewTextBufferInfo() + void PrepareNewTextBufferInfo(const bool useDefaultAttributes = false) { CONSOLE_INFORMATION& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation(); COORD coordScreenBufferSize; @@ -152,26 +152,29 @@ public: UINT uiCursorSize = 12; + auto initialAttributes = useDefaultAttributes ? gci.GetDefaultAttributes() : + TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY }; + m_backupTextBufferInfo.swap(gci.pCurrentScreenBuffer->_textBuffer); try { std::unique_ptr textBuffer = std::make_unique(coordScreenBufferSize, - TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY }, + initialAttributes, uiCursorSize, gci.pCurrentScreenBuffer->GetRenderTarget()); if (textBuffer.get() == nullptr) { - m_ntstatusTextBufferInfo = STATUS_NO_MEMORY; + m_hrTextBufferInfo = E_OUTOFMEMORY; } else { - m_ntstatusTextBufferInfo = STATUS_SUCCESS; + m_hrTextBufferInfo = S_OK; } gci.pCurrentScreenBuffer->_textBuffer.swap(textBuffer); } catch (...) { - m_ntstatusTextBufferInfo = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + m_hrTextBufferInfo = wil::ResultFromCaughtException(); } } @@ -221,14 +224,14 @@ public: textBuffer.GetCursor().SetYPosition(cRowsToFill); } - [[nodiscard]] NTSTATUS GetTextBufferInfoInitResult() + [[nodiscard]] HRESULT GetTextBufferInfoInitResult() { - return m_ntstatusTextBufferInfo; + return m_hrTextBufferInfo; } private: HANDLE m_heap; - NTSTATUS m_ntstatusTextBufferInfo; + HRESULT m_hrTextBufferInfo; FontInfo* m_pFontInfo; std::unique_ptr m_backupTextBufferInfo; std::unique_ptr m_readHandle; diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index d53837d810..0b573c796a 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -151,8 +151,12 @@ Renderer::~Renderer() void Renderer::_NotifyPaintFrame() { - // The thread will provide throttling for us. - _pThread->NotifyPaint(); + // If we're running in the unittests, we might not have a render thread. + if (_pThread) + { + // The thread will provide throttling for us. + _pThread->NotifyPaint(); + } } // Routine Description: diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 52ecd295a2..d16fe7fa13 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -125,5 +125,9 @@ namespace Microsoft::Console::Render // Helper functions to diagnose issues with painting and layout. // These are only actually effective/on in Debug builds when the flag is set using an attached debugger. bool _fDebug = false; + +#ifdef UNIT_TESTING + friend class ConptyOutputTests; +#endif }; } diff --git a/src/renderer/vt/Xterm256Engine.hpp b/src/renderer/vt/Xterm256Engine.hpp index 03817fa772..48fe1fc397 100644 --- a/src/renderer/vt/Xterm256Engine.hpp +++ b/src/renderer/vt/Xterm256Engine.hpp @@ -46,6 +46,7 @@ namespace Microsoft::Console::Render #ifdef UNIT_TESTING friend class VtRendererTest; + friend class ConptyOutputTests; #endif }; } diff --git a/src/renderer/vt/XtermEngine.hpp b/src/renderer/vt/XtermEngine.hpp index dc64a8c1a1..28bdfc4408 100644 --- a/src/renderer/vt/XtermEngine.hpp +++ b/src/renderer/vt/XtermEngine.hpp @@ -73,6 +73,7 @@ namespace Microsoft::Console::Render #ifdef UNIT_TESTING friend class VtRendererTest; + friend class ConptyOutputTests; #endif }; } diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index 11131d3921..acb014f12e 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -80,8 +80,18 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, #ifdef UNIT_TESTING if (_usingTestCallback) { - RETURN_LAST_ERROR_IF(!_pfnTestCallback(str.data(), str.size())); - return S_OK; + // Try to get the last error. If that wasn't set, then the test probably + // doesn't set last error. No matter. We'll just return with E_FAIL + // then. This is a unit test, we don't particularily care. + const auto succeeded = _pfnTestCallback(str.data(), str.size()); + auto hr = E_FAIL; + if (!succeeded) + { + const auto err = ::GetLastError(); + // If there wasn't an error in GLE, just use E_FAIL + hr = SUCCEEDED_WIN32(err) ? hr : HRESULT_FROM_WIN32(err); + } + return succeeded ? S_OK : hr; } #endif diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 5adf1cd1d9..cc6fed764c 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -24,6 +24,11 @@ Author(s): #include #include +// fwdecl unittest classes +#ifdef UNIT_TESTING +class ConptyRoundtripTests; +#endif + namespace Microsoft::Console::Render { class VtEngine : public RenderEngineBase, public Microsoft::Console::ITerminalOutputConnection @@ -220,6 +225,8 @@ namespace Microsoft::Console::Render bool _usingTestCallback; friend class VtRendererTest; + friend class ConptyOutputTests; + friend class ConptyRoundtripTests; #endif void SetTestCallback(_In_ std::function pfn);