Compare commits

...

8 Commits

Author SHA1 Message Date
Mike Griese
06bc6a199d Verify both the host and Terminal buffers the same way 2020-01-14 10:24:36 -06:00
Mike Griese
240916d56e oh my this is horrible and _wonderful_ 2020-01-14 10:03:19 -06:00
Mike Griese
180f1b3dfd add a simple wrapped line test 2020-01-14 09:00:26 -06:00
Mike Griese
065f3610c9 start writing more involved tests 2020-01-14 08:14:25 -06:00
Mike Griese
c0249ef26e Create a test class for e2e conpty output tests
Hopefully I should be able to write to the buffer, and see what gets rendered now.
2020-01-13 17:08:21 -06:00
Mike Griese
1a2654d291 Try to wrap the line properly with conpty
This confusingly doesn't always work
2020-01-13 17:07:43 -06:00
Mike Griese
9aec69467c add a doc comment because I'm not a barbarian 2020-01-13 11:34:50 -06:00
Mike Griese
b5c8c854cc let's first move reflowing to the text buffer 2020-01-13 11:23:42 -06:00
21 changed files with 1134 additions and 226 deletions

View File

@@ -1541,3 +1541,225 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi
return {};
}
}
// Function Description:
// - Reflow the contents from the old buffer into the new buffer. The new buffer
// can have different dimensions than the old buffer. If it does, then this
// function will attempt to maintain the logical contents of the old buffer,
// by continuing wrapped lines onto the next line in the new buffer.
// Arguments:
// - oldBuffer - the text buffer to copy the contents FROM
// - newBuffer - the text buffer to copy the contents TO
// Return Value:
// - S_OK if we successfully copied the contents to the new buffer, otherwise an appropriate HRESULT.
HRESULT TextBuffer::ReflowBuffer(TextBuffer& oldBuffer, TextBuffer& newBuffer)
{
Cursor& oldCursor = oldBuffer.GetCursor();
Cursor& newCursor = newBuffer.GetCursor();
// skip any drawing updates that might occur as we manipulate the new buffer
oldCursor.StartDeferDrawing();
newCursor.StartDeferDrawing();
// We need to save the old cursor position so that we can
// place the new cursor back on the equivalent character in
// the new buffer.
COORD cOldCursorPos = oldCursor.GetPosition();
COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter();
short const cOldRowsTotal = cOldLastChar.Y + 1;
short const cOldColsTotal = oldBuffer.GetSize().Width();
COORD cNewCursorPos = { 0 };
bool fFoundCursorPos = false;
HRESULT hr = S_OK;
// Loop through all the rows of the old buffer and reprint them into the new buffer
for (short iOldRow = 0; iOldRow < cOldRowsTotal; iOldRow++)
{
// Fetch the row and its "right" which is the last printable character.
const ROW& row = oldBuffer.GetRowByOffset(iOldRow);
const CharRow& charRow = row.GetCharRow();
short iRight = static_cast<short>(charRow.MeasureRight());
// There is a special case here. If the row has a "wrap"
// flag on it, but the right isn't equal to the width (one
// index past the final valid index in the row) then there
// were a bunch trailing of spaces in the row.
// (But the measuring functions for each row Left/Right do
// not count spaces as "displayable" so they're not
// included.)
// As such, adjust the "right" to be the width of the row
// to capture all these spaces
if (charRow.WasWrapForced())
{
iRight = cOldColsTotal;
// And a combined special case.
// If we wrapped off the end of the row by adding a
// piece of padding because of a double byte LEADING
// character, then remove one from the "right" to
// leave this padding out of the copy process.
if (charRow.WasDoubleBytePadded())
{
iRight--;
}
}
// Loop through every character in the current row (up to
// the "right" boundary, which is one past the final valid
// character)
for (short iOldCol = 0; iOldCol < iRight; iOldCol++)
{
if (iOldCol == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)
{
cNewCursorPos = newCursor.GetPosition();
fFoundCursorPos = true;
}
try
{
// TODO: MSFT: 19446208 - this should just use an iterator and the inserter...
const auto glyph = row.GetCharRow().GlyphAt(iOldCol);
const auto dbcsAttr = row.GetCharRow().DbcsAttrAt(iOldCol);
const auto textAttr = row.GetAttrRow().GetAttrByColumn(iOldCol);
if (!newBuffer.InsertCharacter(glyph, dbcsAttr, textAttr))
{
hr = E_OUTOFMEMORY;
break;
}
}
CATCH_RETURN();
}
if (SUCCEEDED(hr))
{
// If we didn't have a full row to copy, insert a new
// line into the new buffer.
// Only do so if we were not forced to wrap. If we did
// force a word wrap, then the existing line break was
// only because we ran out of space.
if (iRight < cOldColsTotal && !charRow.WasWrapForced())
{
if (iRight == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)
{
cNewCursorPos = newCursor.GetPosition();
fFoundCursorPos = true;
}
// Only do this if it's not the final line in the buffer.
// On the final line, we want the cursor to sit
// where it is done printing for the cursor
// adjustment to follow.
if (iOldRow < cOldRowsTotal - 1)
{
hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY;
}
else
{
// If we are on the final line of the buffer, we have one more check.
// We got into this code path because we are at the right most column of a row in the old buffer
// that had a hard return (no wrap was forced).
// However, as we're inserting, the old row might have just barely fit into the new buffer and
// caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below.
// We need to preserve the memory of the hard return at this point by inserting one additional
// hard newline, otherwise we've lost that information.
// We only do this when the cursor has just barely poured over onto the next line so the hard return
// isn't covered by the soft one.
// e.g.
// The old line was:
// |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a.
// The cursor was here ^
// And the new line will be:
// |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end
// | |
// ^ and the cursor is now there.
// If we leave it like this, we've lost the newline information.
// So we insert one more newline so a continued reflow of this buffer by resizing larger will
// continue to look as the original output intended with the newline data.
// After this fix, it looks like this:
// |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline)
// | |
// ^ and the cursor is now here.
const COORD coordNewCursor = newCursor.GetPosition();
if (coordNewCursor.X == 0 && coordNewCursor.Y > 0)
{
if (newBuffer.GetRowByOffset(coordNewCursor.Y - 1).GetCharRow().WasWrapForced())
{
hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY;
}
}
}
}
}
}
if (SUCCEEDED(hr))
{
// Finish copying remaining parameters from the old text buffer to the new one
newBuffer.CopyProperties(oldBuffer);
// If we found where to put the cursor while placing characters into the buffer,
// just put the cursor there. Otherwise we have to advance manually.
if (fFoundCursorPos)
{
newCursor.SetPosition(cNewCursorPos);
}
else
{
// Advance the cursor to the same offset as before
// get the number of newlines and spaces between the old end of text and the old cursor,
// then advance that many newlines and chars
int iNewlines = cOldCursorPos.Y - cOldLastChar.Y;
int iIncrements = cOldCursorPos.X - cOldLastChar.X;
const COORD cNewLastChar = newBuffer.GetLastNonSpaceCharacter();
// If the last row of the new buffer wrapped, there's going to be one less newline needed,
// because the cursor is already on the next line
if (newBuffer.GetRowByOffset(cNewLastChar.Y).GetCharRow().WasWrapForced())
{
iNewlines = std::max(iNewlines - 1, 0);
}
else
{
// if this buffer didn't wrap, but the old one DID, then the d(columns) of the
// old buffer will be one more than in this buffer, so new need one LESS.
if (oldBuffer.GetRowByOffset(cOldLastChar.Y).GetCharRow().WasWrapForced())
{
iNewlines = std::max(iNewlines - 1, 0);
}
}
for (int r = 0; r < iNewlines; r++)
{
if (!newBuffer.NewlineCursor())
{
hr = E_OUTOFMEMORY;
break;
}
}
if (SUCCEEDED(hr))
{
for (int c = 0; c < iIncrements - 1; c++)
{
if (!newBuffer.IncrementCursor())
{
hr = E_OUTOFMEMORY;
break;
}
}
}
}
}
if (SUCCEEDED(hr))
{
// Save old cursor size before we delete it
ULONG const ulSize = oldCursor.GetSize();
// Set size back to real size as it will be taking over the rendering duties.
newCursor.SetSize(ulSize);
}
newCursor.EndDeferDrawing();
oldCursor.EndDeferDrawing();
return hr;
}

View File

@@ -158,6 +158,8 @@ public:
const std::wstring_view fontFaceName,
const COLORREF backgroundColor);
static HRESULT ReflowBuffer(TextBuffer& oldBuffer, TextBuffer& newBuffer);
private:
std::deque<ROW> _storage;
Cursor _cursor;

View File

@@ -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
};

View File

@@ -0,0 +1,416 @@
// 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 "../../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 = new CommonState();
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();
delete m_state;
return true;
}
TEST_METHOD_SETUP(MethodSetup)
{
// STEP 1: Set up the Terminal
term = std::make_unique<Terminal>();
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<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);
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);
TEST_METHOD(WriteWrappedLine);
private:
bool _writeCallback(const char* const pch, size_t const cch);
void _flushFirstFrame();
std::deque<std::string> expectedOutput;
std::unique_ptr<Microsoft::Console::Render::VtEngine> _pVtRenderEngine;
CommonState* m_state;
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);
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()));
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());
}
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());
auto iter = termTb.GetCellDataAt({ 0, 0 });
VERIFY_ARE_EQUAL(L"H", (iter++)->Chars());
VERIFY_ARE_EQUAL(L"e", (iter++)->Chars());
VERIFY_ARE_EQUAL(L"l", (iter++)->Chars());
VERIFY_ARE_EQUAL(L"l", (iter++)->Chars());
VERIFY_ARE_EQUAL(L"o", (iter++)->Chars());
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
VERIFY_ARE_EQUAL(L"W", (iter++)->Chars());
VERIFY_ARE_EQUAL(L"o", (iter++)->Chars());
VERIFY_ARE_EQUAL(L"r", (iter++)->Chars());
VERIFY_ARE_EQUAL(L"l", (iter++)->Chars());
VERIFY_ARE_EQUAL(L"d", (iter++)->Chars());
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
}
void ConptyRoundtripTests::WriteTwoLinesUsesNewline()
{
Log::Comment(NoThrowString().Format(
L"Write two 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");
hostSm.ProcessString(L"\x1b[2;1H");
hostSm.ProcessString(L"BBB");
auto verifyData = [](TextBuffer& tb) {
{
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());
}
};
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) {
{
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());
}
};
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 _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 ConptyRoundtripTests::WriteWrappedLine()
{
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;
const auto view = si.GetViewport();
_flushFirstFrame();
const std::wstring aWString(view.Width() - 1, L'A');
const std::wstring bWString(view.Width() + 1, L'B');
hostSm.ProcessString(aWString);
hostSm.ProcessString(L"\n");
hostSm.ProcessString(bWString);
hostSm.ProcessString(L"\n");
Log::Comment(NoThrowString().Format(
L"Ensure the buffer contains what we'd expect"));
auto verifyData = [&view](TextBuffer& tb) {
{
auto iter = tb.GetCellDataAt({ 0, 0 });
_verifySpanOfText(L"A", iter, 0, view.Width() - 1);
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars(), L"The last char of the line should be a space");
}
{
// Every char in this line should be 'B'
auto iter = tb.GetCellDataAt({ 0, 1 });
_verifySpanOfText(L"B", iter, 0, view.Width());
}
{
// Only the first char should be 'B', the rest should be blank
auto iter = tb.GetCellDataAt({ 0, 2 });
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
_verifySpanOfText(L" ", iter, 1, view.Width());
}
};
verifyData(hostTb);
std::string aLine(view.Width() - 1, 'A');
aLine += ' ';
std::string bLine(view.Width(), 'B');
// First, the line of 'A's with a space at the end
expectedOutput.push_back(aLine);
expectedOutput.push_back("\r\n");
// Then, the line of all 'B's
expectedOutput.push_back(bLine);
// No trailing newline here. Instead, onto the next line, another 'B'
expectedOutput.push_back("B");
// Followed by us using ECH to clear the rest of the spaces in the line.
expectedOutput.push_back("\x1b[K");
// and finally a newline.
expectedOutput.push_back("\r\n");
VERIFY_SUCCEEDED(renderer.PaintFrame());
// TODO:GH#780 - this test will fail until we implement WriteCharsLegacy2ElectricBoogaloo
// verifyData(termTb);
}

View File

@@ -18,6 +18,7 @@
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="TerminalApiTest.cpp" />
<ClCompile Include="ConptyRoundtripTests.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\buffer\out\lib\bufferout.vcxproj">
@@ -38,6 +39,42 @@
<ProjectReference Include="..\TerminalCore\lib\TerminalCore-lib.vcxproj">
<Project>{ca5cad1a-abcd-429c-b551-8562ec954746}</Project>
</ProjectReference>
<!-- The following are all Console Host (host.lib) dependencies. We're
including them for the ConptyRoundtripTests, which instantiate a console
host, then user the output from conpty to dump directly into a Terminal,
and make sure the buffer contents align. -->
<ProjectReference Include="..\..\renderer\vt\ut_lib\vt.unittest.vcxproj">
<Project>{990F2657-8580-4828-943F-5DD657D11843}</Project>
</ProjectReference>
<ProjectReference Include="..\..\renderer\base\lib\base.vcxproj">
<Project>{af0a096a-8b3a-4949-81ef-7df8f0fee91f}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)\src\host\ut_lib\host.unittest.vcxproj">
<Project>{06ec74cb-9a12-429c-b551-8562ec954746}</Project>
</ProjectReference>
<ProjectReference Include="..\..\propslib\propslib.vcxproj">
<Project>{345fd5a4-b32b-4f29-bd1c-b033bd2c35cc}</Project>
</ProjectReference>
<ProjectReference Include="..\..\interactivity\base\lib\InteractivityBase.vcxproj">
<Project>{06ec74cb-9a12-429c-b551-8562ec964846}</Project>
</ProjectReference>
<ProjectReference Include="..\..\interactivity\win32\lib\win32.LIB.vcxproj">
<Project>{06ec74cb-9a12-429c-b551-8532ec964726}</Project>
</ProjectReference>
<ProjectReference Include="..\..\tsf\tsf.vcxproj">
<Project>{2fd12fbb-1ddb-46d8-b818-1023c624caca}</Project>
</ProjectReference>
<ProjectReference Include="..\..\server\lib\server.vcxproj">
<Project>{18d09a24-8240-42d6-8cb6-236eee820262}</Project>
</ProjectReference>
<ProjectReference Include="..\..\terminal\adapter\lib\adapter.vcxproj">
<Project>{dcf55140-ef6a-4736-a403-957e4f7430bb}</Project>
</ProjectReference>
<ProjectReference Include="..\..\internal\internal.vcxproj">
<Project>{ef3e32a7-5ff6-42b4-b6e2-96cd7d033f00}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ClInclude Include="MockTermSettings.h" />
@@ -45,7 +82,7 @@
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalSettings\Generated Files";%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalSettings\Generated Files";$(OpenConsoleDir)\src\host;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
@@ -55,4 +92,4 @@
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
<Import Project="$(SolutionDir)src\common.build.post.props" />
<Import Project="$(SolutionDir)src\common.build.tests.props" />
</Project>
</Project>

View File

@@ -15,6 +15,10 @@ Author(s):
#pragma once
// 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 +34,4 @@ Author(s):
#ifdef CON_BUILD_PUBLIC
#define CON_USERPRIVAPI_INDIRECT
#define CON_DPIAPI_INDIRECT
#endif
#endif

View File

@@ -1425,224 +1425,21 @@ bool SCREEN_INFORMATION::IsMaximizedY() const
// Save cursor's relative height versus the viewport
SHORT const sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().Y - _viewport.Top();
Cursor& oldCursor = _textBuffer->GetCursor();
Cursor& newCursor = newTextBuffer->GetCursor();
// skip any drawing updates that might occur as we manipulate the new buffer
oldCursor.StartDeferDrawing();
newCursor.StartDeferDrawing();
HRESULT hr = TextBuffer::ReflowBuffer(*_textBuffer.get(), *newTextBuffer.get());
// We need to save the old cursor position so that we can
// place the new cursor back on the equivalent character in
// the new buffer.
COORD cOldCursorPos = oldCursor.GetPosition();
COORD cOldLastChar = _textBuffer->GetLastNonSpaceCharacter();
short const cOldRowsTotal = cOldLastChar.Y + 1;
short const cOldColsTotal = GetBufferSize().Width();
COORD cNewCursorPos = { 0 };
bool fFoundCursorPos = false;
NTSTATUS status = STATUS_SUCCESS;
// Loop through all the rows of the old buffer and reprint them into the new buffer
for (short iOldRow = 0; iOldRow < cOldRowsTotal; iOldRow++)
{
// Fetch the row and its "right" which is the last printable character.
const ROW& Row = _textBuffer->GetRowByOffset(iOldRow);
const CharRow& charRow = Row.GetCharRow();
short iRight = static_cast<short>(charRow.MeasureRight());
// There is a special case here. If the row has a "wrap"
// flag on it, but the right isn't equal to the width (one
// index past the final valid index in the row) then there
// were a bunch trailing of spaces in the row.
// (But the measuring functions for each row Left/Right do
// not count spaces as "displayable" so they're not
// included.)
// As such, adjust the "right" to be the width of the row
// to capture all these spaces
if (charRow.WasWrapForced())
{
iRight = cOldColsTotal;
// And a combined special case.
// If we wrapped off the end of the row by adding a
// piece of padding because of a double byte LEADING
// character, then remove one from the "right" to
// leave this padding out of the copy process.
if (charRow.WasDoubleBytePadded())
{
iRight--;
}
}
// Loop through every character in the current row (up to
// the "right" boundary, which is one past the final valid
// character)
for (short iOldCol = 0; iOldCol < iRight; iOldCol++)
{
if (iOldCol == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)
{
cNewCursorPos = newCursor.GetPosition();
fFoundCursorPos = true;
}
try
{
// TODO: MSFT: 19446208 - this should just use an iterator and the inserter...
const auto glyph = Row.GetCharRow().GlyphAt(iOldCol);
const auto dbcsAttr = Row.GetCharRow().DbcsAttrAt(iOldCol);
const auto textAttr = Row.GetAttrRow().GetAttrByColumn(iOldCol);
if (!newTextBuffer->InsertCharacter(glyph, dbcsAttr, textAttr))
{
status = STATUS_NO_MEMORY;
break;
}
}
catch (...)
{
return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
}
}
if (NT_SUCCESS(status))
{
// If we didn't have a full row to copy, insert a new
// line into the new buffer.
// Only do so if we were not forced to wrap. If we did
// force a word wrap, then the existing line break was
// only because we ran out of space.
if (iRight < cOldColsTotal && !charRow.WasWrapForced())
{
if (iRight == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)
{
cNewCursorPos = newCursor.GetPosition();
fFoundCursorPos = true;
}
// Only do this if it's not the final line in the buffer.
// On the final line, we want the cursor to sit
// where it is done printing for the cursor
// adjustment to follow.
if (iOldRow < cOldRowsTotal - 1)
{
status = newTextBuffer->NewlineCursor() ? status : STATUS_NO_MEMORY;
}
else
{
// If we are on the final line of the buffer, we have one more check.
// We got into this code path because we are at the right most column of a row in the old buffer
// that had a hard return (no wrap was forced).
// However, as we're inserting, the old row might have just barely fit into the new buffer and
// caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below.
// We need to preserve the memory of the hard return at this point by inserting one additional
// hard newline, otherwise we've lost that information.
// We only do this when the cursor has just barely poured over onto the next line so the hard return
// isn't covered by the soft one.
// e.g.
// The old line was:
// |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a.
// The cursor was here ^
// And the new line will be:
// |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end
// | |
// ^ and the cursor is now there.
// If we leave it like this, we've lost the newline information.
// So we insert one more newline so a continued reflow of this buffer by resizing larger will
// continue to look as the original output intended with the newline data.
// After this fix, it looks like this:
// |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline)
// | |
// ^ and the cursor is now here.
const COORD coordNewCursor = newCursor.GetPosition();
if (coordNewCursor.X == 0 && coordNewCursor.Y > 0)
{
if (newTextBuffer->GetRowByOffset(coordNewCursor.Y - 1).GetCharRow().WasWrapForced())
{
status = newTextBuffer->NewlineCursor() ? status : STATUS_NO_MEMORY;
}
}
}
}
}
}
if (NT_SUCCESS(status))
{
// Finish copying remaining parameters from the old text buffer to the new one
newTextBuffer->CopyProperties(*_textBuffer);
// If we found where to put the cursor while placing characters into the buffer,
// just put the cursor there. Otherwise we have to advance manually.
if (fFoundCursorPos)
{
newCursor.SetPosition(cNewCursorPos);
}
else
{
// Advance the cursor to the same offset as before
// get the number of newlines and spaces between the old end of text and the old cursor,
// then advance that many newlines and chars
int iNewlines = cOldCursorPos.Y - cOldLastChar.Y;
int iIncrements = cOldCursorPos.X - cOldLastChar.X;
const COORD cNewLastChar = newTextBuffer->GetLastNonSpaceCharacter();
// If the last row of the new buffer wrapped, there's going to be one less newline needed,
// because the cursor is already on the next line
if (newTextBuffer->GetRowByOffset(cNewLastChar.Y).GetCharRow().WasWrapForced())
{
iNewlines = std::max(iNewlines - 1, 0);
}
else
{
// if this buffer didn't wrap, but the old one DID, then the d(columns) of the
// old buffer will be one more than in this buffer, so new need one LESS.
if (_textBuffer->GetRowByOffset(cOldLastChar.Y).GetCharRow().WasWrapForced())
{
iNewlines = std::max(iNewlines - 1, 0);
}
}
for (int r = 0; r < iNewlines; r++)
{
if (!newTextBuffer->NewlineCursor())
{
status = STATUS_NO_MEMORY;
break;
}
}
if (NT_SUCCESS(status))
{
for (int c = 0; c < iIncrements - 1; c++)
{
if (!newTextBuffer->IncrementCursor())
{
status = STATUS_NO_MEMORY;
break;
}
}
}
}
}
if (NT_SUCCESS(status))
if (SUCCEEDED(hr))
{
Cursor& newCursor = newTextBuffer->GetCursor();
// Adjust the viewport so the cursor doesn't wildly fly off up or down.
SHORT const sCursorHeightInViewportAfter = newCursor.GetPosition().Y - _viewport.Top();
COORD coordCursorHeightDiff = { 0 };
coordCursorHeightDiff.Y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore;
LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, true));
// Save old cursor size before we delete it
ULONG const ulSize = oldCursor.GetSize();
_textBuffer.swap(newTextBuffer);
// Set size back to real size as it will be taking over the rendering duties.
newCursor.SetSize(ulSize);
newCursor.EndDeferDrawing();
}
oldCursor.EndDeferDrawing();
return status;
return NTSTATUS_FROM_HRESULT(hr);
}
//

View File

@@ -307,5 +307,7 @@ private:
friend class TextBufferIteratorTests;
friend class ScreenBufferTests;
friend class CommonState;
friend class ConptyOutputTests;
friend class ConptyRoundtripTests;
#endif
};

View File

@@ -0,0 +1,361 @@
// 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 "../../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 = new CommonState();
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();
delete m_state;
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<Xterm256Engine>(std::move(hFile),
gci,
initialViewport,
gci.GetColorTable(),
static_cast<WORD>(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);
TEST_METHOD(WriteWrappedLine);
private:
bool _writeCallback(const char* const pch, size_t const cch);
void _flushFirstFrame();
std::deque<std::string> expectedOutput;
std::unique_ptr<Microsoft::Console::Render::VtEngine> _pVtRenderEngine;
CommonState* 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<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()));
// try
// {
VERIFY_ARE_EQUAL(first.length(), cch);
VERIFY_ARE_EQUAL(first, actualString);
// }
// catch (...)
// {
// return false;
// }
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());
}
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 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& 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 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& 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());
}
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());
}
}
void ConptyOutputTests::WriteWrappedLine()
{
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& sm = si.GetStateMachine();
auto& tb = si.GetTextBuffer();
const auto view = si.GetViewport();
_flushFirstFrame();
const std::wstring aWString(view.Width() - 1, L'A');
const std::wstring bWString(view.Width() + 1, L'B');
sm.ProcessString(aWString);
sm.ProcessString(L"\n");
sm.ProcessString(bWString);
sm.ProcessString(L"\n");
Log::Comment(NoThrowString().Format(
L"Ensure the buffer contains what we'd expect"));
{
auto iter = tb.GetCellDataAt({ 0, 0 });
_verifySpanOfText(L"A", iter, 0, view.Width() - 1);
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars(), L"The last char of the line should be a space");
}
{
// Every char in this line should be 'B'
auto iter = tb.GetCellDataAt({ 0, 1 });
_verifySpanOfText(L"B", iter, 0, view.Width());
}
{
// Only the first char should be 'B', the rest should be blank
auto iter = tb.GetCellDataAt({ 0, 2 });
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
_verifySpanOfText(L" ", iter, 1, view.Width());
}
std::string aLine(view.Width() - 1, 'A');
aLine += ' ';
std::string bLine(view.Width(), 'B');
// First, the line of 'A's with a space at the end
expectedOutput.push_back(aLine);
expectedOutput.push_back("\r\n");
// Then, the line of all 'B's
expectedOutput.push_back(bLine);
// No trailing newline here. Instead, onto the next line, another 'B'
expectedOutput.push_back("B");
// Followed by us using ECH to clear the rest of the spaces in the line.
expectedOutput.push_back("\x1b[K");
// and finally a newline.
expectedOutput.push_back("\r\n");
VERIFY_SUCCEEDED(renderer.PaintFrame());
}

View File

@@ -40,6 +40,7 @@
<ClCompile Include="ViewportTests.cpp" />
<ClCompile Include="VtIoTests.cpp" />
<ClCompile Include="VtRendererTests.cpp" />
<ClCompile Include="ConptyOutputTests.cpp" />
<Clcompile Include="..\..\types\IInputEventStreams.cpp" />
<ClCompile Include="..\precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>

View File

@@ -149,7 +149,7 @@ class TextBufferTests
void TextBufferTests::TestBufferCreate()
{
VERIFY_SUCCESS_NTSTATUS(m_state->GetTextBufferInfoInitResult());
VERIFY_SUCCEEDED(m_state->GetTextBufferInfoInitResult());
}
TextBuffer& TextBufferTests::GetTbi()

View File

@@ -36,6 +36,7 @@ SOURCES = \
InputBufferTests.cpp \
VtIoTests.cpp \
VtRendererTests.cpp \
ConptyOutputTests.cpp \
ViewportTests.cpp \
ConsoleArgumentsTests.cpp \
CommandLineTests.cpp \

View File

@@ -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> textBuffer = std::make_unique<TextBuffer>(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<TextBuffer> m_backupTextBufferInfo;
std::unique_ptr<INPUT_READ_HANDLE_DATA> m_readHandle;

View File

@@ -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:

View File

@@ -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
};
}

View File

@@ -46,6 +46,7 @@ namespace Microsoft::Console::Render
#ifdef UNIT_TESTING
friend class VtRendererTest;
friend class ConptyOutputTests;
#endif
};
}

View File

@@ -19,7 +19,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
_ColorTable(ColorTable),
_cColorTable(cColorTable),
_fUseAsciiOnly(fUseAsciiOnly),
_previousLineWrapped(false),
// _previousLineWrapped(false),
_usingUnderLine(false),
_needToDisableCursor(false),
_lastCursorIsVisible(false),
@@ -248,7 +248,13 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
// If the previous line wrapped, then the cursor is already at this
// position, we just don't know it yet. Don't emit anything.
if (_previousLineWrapped)
bool previousLineWrapped = false;
if (_wrappedRow.has_value())
{
previousLineWrapped = coord.Y == _wrappedRow.value() + 1;
}
if (previousLineWrapped)
{
hr = S_OK;
}
@@ -298,6 +304,9 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
_newBottomLine = false;
}
_deferredCursorPos = INVALID_COORDS;
_wrappedRow = std::nullopt;
return hr;
}

View File

@@ -73,6 +73,7 @@ namespace Microsoft::Console::Render
#ifdef UNIT_TESTING
friend class VtRendererTest;
friend class ConptyOutputTests;
#endif
};
}

View File

@@ -449,6 +449,21 @@ using namespace Microsoft::Console::Types;
std::wstring wstr = std::wstring(unclusteredString.data(), cchActual);
RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8(wstr));
// If we've written text to the last column of the viewport, then mark
// that we've wrapped this line. The next time we attempt to move the
// cursor, if we're trying to move it to the start of the next line,
// we'll remember that this line was wrapped, and not manually break the
// line.
// Don't do this is the last character we're writing is a space - The last
// char will always be a space, but if we see that, we shouldn't wrap.
// TODO: This seems to be off by one char. Resizing cmd.exe, the '.' at the
// end of the initial message sometimes gets cut off weirdly.
if ((_lastText.X + (totalWidth - numSpaces)) > _lastViewport.RightInclusive())
{
_wrappedRow = coord.Y;
}
// Update our internal tracker of the cursor's position.
// See MSFT:20266233
// If the cursor is at the rightmost column of the terminal, and we write a

View File

@@ -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

View File

@@ -24,6 +24,11 @@ Author(s):
#include <string>
#include <functional>
// fwdecl unittest classes
#ifdef UNIT_TESTING
class ConptyRoundtripTests;
#endif
namespace Microsoft::Console::Render
{
class VtEngine : public RenderEngineBase, public Microsoft::Console::ITerminalOutputConnection
@@ -136,6 +141,8 @@ namespace Microsoft::Console::Render
Microsoft::Console::VirtualTerminal::RenderTracing _trace;
bool _inResizeRequest{ false };
std::optional<short> _wrappedRow{ std::nullopt };
[[nodiscard]] HRESULT _Write(std::string_view const str) noexcept;
[[nodiscard]] HRESULT _WriteFormattedString(const std::string* const pFormat, ...) noexcept;
[[nodiscard]] HRESULT _Flush() noexcept;
@@ -220,6 +227,8 @@ namespace Microsoft::Console::Render
bool _usingTestCallback;
friend class VtRendererTest;
friend class ConptyOutputTests;
friend class ::ConptyRoundtripTests;
#endif
void SetTestCallback(_In_ std::function<bool(const char* const, size_t const)> pfn);