From 0b86e8c19f28047b0887904d00ce950f1157a0de Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Mon, 18 Jan 2021 13:51:29 -0800 Subject: [PATCH] Add some tests for TextBuffer::Reflow (#8715) This is by no means comprehensive. It will be unmarked as draft when it is more comprehensive. This pull request adds some tests for resizing a TextBuffer and reflowing its contents. Each test takes the form of an initial state and a number of buffers of different sizes. The initial state is used to seed the first TextBuffer, and the subsequent buffers are only used to compare. I manually reimplemented some of the DBCS logic to ensure that the buffers contain _exactly_ what they're supposed to. I know this is non-ideal. After some of the CharRow changes in #8446 land, this will need to be updated. There's a cool bit of TAEF gore in here: the IDataSource. An IDataSource allows us to programmatically return test cases. It's a code-only version of its support for parameterized tests of the form `Data:x = {0, 1, 2}` . The only downsides are... 1. It looks like COM (it is not using COM under the hood, just the COM ABI) 2. Property values must be returned as strings. To best support rich test types, I used IDataSource to produce _a lit of array indices_ and nothing more. The test is run once for array member, and it is the test's responsibility to look up the object to which that index refers. Works great though! Each reflow test is its own unit, and a failure in an earlier reflow test will not tank a later one. --- .github/actions/spell-check/expect/expect.txt | 32 +- src/buffer/out/CharRow.hpp | 2 + src/buffer/out/ut_textbuffer/ReflowTests.cpp | 856 ++++++++++++++++++ .../TextBuffer.Unit.Tests.vcxproj | 4 + src/buffer/out/ut_textbuffer/sources | 1 + 5 files changed, 888 insertions(+), 7 deletions(-) create mode 100644 src/buffer/out/ut_textbuffer/ReflowTests.cpp diff --git a/.github/actions/spell-check/expect/expect.txt b/.github/actions/spell-check/expect/expect.txt index 5f2703f73666..9ed2042bdd97 100644 --- a/.github/actions/spell-check/expect/expect.txt +++ b/.github/actions/spell-check/expect/expect.txt @@ -1,8 +1,11 @@ +AAAAA AAAAAABBBBBBCCC AAAAABBBBBBBCCC +AAAAABBBBBBCCC AAAAABCCCCCCCCC AAAAADCCCCCCCCC ABANDONFONT +abcd ABCDEFGHIJKLMNO ABCG abf @@ -129,6 +132,7 @@ backstory Batang baz Bazz +BBBBBCCC BBBBCCCCC BBDM bbwe @@ -290,6 +294,7 @@ CNTRL codebase Codeflow codepage +codepath codepoint codeproject COINIT @@ -755,6 +760,7 @@ FFrom FGCOLOR fgetc fgetwc +FGHIJ fgidx FILEDESCRIPTION fileno @@ -892,6 +898,9 @@ GFEh Gfun gfx gh +GHIJK +GHIJKL +GHIJKLM gitfilters github gitlab @@ -1198,6 +1207,7 @@ KILLFOCUS kinda KJ KLF +KLMNO KLMNOPQRST KLMNOPQRSTQQQQQ KU @@ -1244,6 +1254,7 @@ lld llvm llx LMENU +LMNOP lnk lnkd lnkfile @@ -1286,6 +1297,7 @@ LPFNADDPROPSHEETPAGE LPINT lpl LPMEASUREITEMSTRUCT +LPMINMAXINFO lpmsg LPNEWCPLINFO LPNEWCPLINFOA @@ -1317,6 +1329,7 @@ lsproj lss lstatus lstrcmp +lstrcmpi LTEXT LTLTLTLTL ltype @@ -1383,6 +1396,7 @@ mindbogglingly mingw minimizeall minkernel +MINMAXINFO minwin minwindef Mip @@ -1392,6 +1406,8 @@ mmcc MMCPL mmsystem MNC +MNOPQ +MNOPQR MODALFRAME modelproj MODERNCORE @@ -1487,9 +1503,9 @@ Nls NLSMODE NOACTIVATE NOAPPLYNOW -NOCOMM NOCLIP NOCOLOR +NOCOMM NOCONTEXTHELP NOCOPYBITS nodiscard @@ -1513,6 +1529,7 @@ NONPREROTATED nonspace NOOWNERZORDER NOPAINT +NOPQRST noprofile NOREDRAW NOREMOVE @@ -1866,6 +1883,7 @@ pythonw qi QJ qo +QRSTU qsort queryable QUESTIONMARK @@ -2213,6 +2231,7 @@ strrev strsafe strtok structs +STUVWX STX stylecop SUA @@ -2506,6 +2525,8 @@ utr uuid uuidof uuidv +UVWX +UVWXY UWA uwp uxtheme @@ -2573,6 +2594,7 @@ VTRGBTo vtseq vtterm vttest +VWX waaay waitable waivable @@ -2812,6 +2834,8 @@ YVIRTUALSCREEN Yw YWalk yx +YZ +yzx Zc ZCmd ZCtrl @@ -2822,9 +2846,3 @@ zsh zu zxcvbnm zy -AAAAABBBBBBCCC -BBBBBCCC -abcd -LPMINMAXINFO -MINMAXINFO -lstrcmpi diff --git a/src/buffer/out/CharRow.hpp b/src/buffer/out/CharRow.hpp index b2f7ab41c1cf..ce797b0f17c7 100644 --- a/src/buffer/out/CharRow.hpp +++ b/src/buffer/out/CharRow.hpp @@ -81,9 +81,11 @@ class CharRow final // iterators iterator begin() noexcept; const_iterator cbegin() const noexcept; + const_iterator begin() const noexcept { return cbegin(); } iterator end() noexcept; const_iterator cend() const noexcept; + const_iterator end() const noexcept { return cend(); } UnicodeStorage& GetUnicodeStorage() noexcept; const UnicodeStorage& GetUnicodeStorage() const noexcept; diff --git a/src/buffer/out/ut_textbuffer/ReflowTests.cpp b/src/buffer/out/ut_textbuffer/ReflowTests.cpp new file mode 100644 index 000000000000..2f69b6c6631c --- /dev/null +++ b/src/buffer/out/ut_textbuffer/ReflowTests.cpp @@ -0,0 +1,856 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "../../inc/consoletaeftemplates.hpp" + +#include "../textBuffer.hpp" +#include "../../renderer/inc/DummyRenderTarget.hpp" +#include "../../types/inc/Utf16Parser.hpp" +#include "../../types/inc/GlyphWidth.hpp" + +#include + +template<> +class WEX::TestExecution::VerifyOutputTraits +{ +public: + static WEX::Common::NoThrowString ToString(const wchar_t& wch) + { + return WEX::Common::NoThrowString().Format(L"'%c'", wch); + } +}; + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +namespace +{ + struct TestRow + { + std::wstring_view text; + bool wrap; + }; + + struct TestBuffer + { + COORD size; + std::vector rows; + COORD cursor; + }; + + struct TestCase + { + std::wstring_view name; + std::vector buffers; + }; + + static constexpr auto true_due_to_exact_wrap_bug{ true }; + + static const TestCase testCases[] = { + TestCase{ + L"No reflow required", + { + TestBuffer{ + { 6, 5 }, + { + { L"AB ", false }, + { L"$ ", false }, + { L"CD ", false }, + { L"EFG ", false }, + { L" ", false }, + }, + { 0, 1 } // cursor on $ + }, + TestBuffer{ + { 5, 5 }, // reduce width by 1 + { + { L"AB ", false }, + { L"$ ", false }, + { L"CD ", false }, + { L"EFG ", false }, + { L" ", false }, + }, + { 0, 1 } // cursor on $ + }, + TestBuffer{ + { 4, 5 }, + { + { L"AB ", false }, + { L"$ ", false }, + { L"CD ", false }, + { L"EFG ", false }, + { L" ", false }, + }, + { 0, 1 } // cursor on $ + }, + }, + }, + TestCase{ + L"SBCS, cursor remains in buffer, no circling, no original wrap", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", false }, + { L"$ ", false }, + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 0, 1 } // cursor on $ + }, + TestBuffer{ + { 5, 5 }, // reduce width by 1 + { + { L"ABCDE", true }, + { L"F$ ", false }, // [BUG] EXACT WRAP. $ should be alone on next line. + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 1, 1 } // cursor on $ + }, + TestBuffer{ + { 6, 5 }, // grow width back to original + { + { L"ABCDEF", true_due_to_exact_wrap_bug }, + { L"$ ", false }, + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 0, 1 } // cursor on $ + }, + TestBuffer{ + { 7, 5 }, // grow width wider than original + { + { L"ABCDEF$", true_due_to_exact_wrap_bug }, // EXACT WRAP BUG: $ should be alone on next line + { L" ", false }, + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 6, 0 } // cursor on $ + }, + }, + }, + TestCase{ + L"SBCS, cursor remains in buffer, no circling, with original wrap", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", true }, + { L"G$ ", false }, + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 1, 1 } // cursor on $ + }, + TestBuffer{ + { 5, 5 }, // reduce width by 1 + { + { L"ABCDE", true }, + { L"FG$ ", false }, + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 2, 1 } // cursor on $ + }, + TestBuffer{ + { 6, 5 }, // grow width back to original + { + { L"ABCDEF", true }, + { L"G$ ", false }, + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 1, 1 } // cursor on $ + }, + TestBuffer{ + { 7, 5 }, // grow width wider than original + { + { L"ABCDEFG", true }, + { L"$ ", false }, + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 0, 1 } // cursor on $ + }, + }, + }, + TestCase{ + L"SBCS line padded with spaces (to wrap)", + { + TestBuffer{ + { 6, 5 }, + { + { L"AB ", true }, // AB $ CD is one long wrapped line + { L"$ ", true }, + { L"CD ", false }, + { L"EFG ", false }, + { L" ", false }, + }, + { 0, 1 } // cursor on $ + }, + TestBuffer{ + { 7, 5 }, // reduce width by 1 + { + { L"AB $", true }, + { L" CD", true_due_to_exact_wrap_bug }, + { L" ", false }, + { L"EFG ", false }, + { L" ", false }, + }, + { 6, 0 } // cursor on $ + }, + TestBuffer{ + { 8, 5 }, + { + { L"AB $ ", true }, + { L" CD ", false }, // Goes to false because we hit the end of ..CD + { L"EFG ", false }, // [BUG] EFG moves up due to exact wrap bug above + { L" ", false }, + { L" ", false }, + }, + { 6, 0 } // cursor on $ + }, + }, + }, + TestCase{ + L"DBCS, cursor remains in buffer, no circling, with original wrap", + { + TestBuffer{ + { 6, 5 }, + { + //--0123456-- + { L"カタカ", true }, // KA TA KA + { L"ナ$ ", false }, // NA + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 2, 1 } // cursor on $ + }, + TestBuffer{ + { 5, 5 }, // reduce width by 1 + { + //--012345-- + { L"カタ ", true }, // KA TA [FORCED SPACER] + { L"カナ$", true_due_to_exact_wrap_bug }, // KA NA + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 4, 1 } // cursor on $ + }, + TestBuffer{ + { 6, 5 }, // grow width back to original + { + //--0123456-- + { L"カタカ", true }, // KA TA KA + { L"ナ$ ", false }, // NA + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 2, 1 } // cursor on $ + }, + TestBuffer{ + { 7, 5 }, // grow width wider than original (by one; no visible change!) + { + //--0123456-- + { L"カタカ ", true }, // KA TA KA [FORCED SPACER] + { L"ナ$ ", false }, // NA + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 2, 1 } // cursor on $ + }, + TestBuffer{ + { 8, 5 }, // grow width enough to fit second DBCS + { + //--01234567-- + { L"カタカナ", true }, // KA TA KA NA + { L"$ ", false }, + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 0, 1 } // cursor on $ + }, + }, + }, + TestCase{ + L"SBCS, cursor remains in buffer, with circling, no original wrap", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", false }, + { L"$ ", false }, + { L"GHIJKL", false }, + { L"MNOPQR", false }, + { L"STUVWX", false }, + }, + { 0, 1 } // cursor on $ + }, + TestBuffer{ + { 5, 5 }, // reduce width by 1 + { + { L"F$ ", false }, + { L"GHIJK", true }, // [BUG] We should see GHIJK\n L\n MNOPQ\n R\n + { L"LMNOP", true }, // The wrapping here is irregular + { L"QRSTU", true }, + { L"VWX ", false }, + }, + { 1, 1 } // [BUG] cursor moves to 1,1 instead of sticking with the $ + }, + TestBuffer{ + { 6, 5 }, // going back to 6,5, the data lost has been destroyed + { + //{ L"F$ ", false }, // [BUG] this line is erroneously destroyed too! + { L"GHIJKL", true }, + { L"MNOPQR", true }, + { L"STUVWX", true }, + { L" ", false }, + { L" ", false }, // [BUG] this line is added + }, + { 1, 1 }, // [BUG] cursor does not follow [H], it sticks at 1,1 + }, + TestBuffer{ + { 7, 5 }, // a number of errors are carried forward from the previous buffer + { + { L"GHIJKLM", true }, + { L"NOPQRST", true }, + { L"UVWX ", false }, // [BUG] This line loses wrap for some reason + { L" ", false }, + { L" ", false }, + }, + { 0, 1 }, // [BUG] The cursor moves to 0, 1 now, sticking with the [N] from before + }, + }, + }, + TestCase{ + // The cursor is not found during character insertion. + // Instead, it is found off the right edge of the text. This triggers + // a separate cursor found codepath in the original algorithm. + L"SBCS, cursor off rightmost char in non-wrapped line", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", false }, + { L"$ ", false }, + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 1, 1 } // cursor *after* $ + }, + TestBuffer{ + { 5, 5 }, // reduce width by 1 + { + { L"ABCDE", true }, + { L"F$ ", false }, // [BUG] EXACT WRAP. $ should be alone on next line. + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 2, 1 } // cursor follows space after $ to next line + }, + }, + }, + TestCase{ + L"SBCS, cursor off rightmost char in wrapped line, which is then pushed off bottom", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", true }, + { L"GHIJKL", true }, + { L"MNOPQR", true }, + { L"STUVWX", true }, + { L"YZ0 $ ", false }, + }, + { 5, 4 } // cursor *after* $ + }, + TestBuffer{ + { 5, 5 }, // reduce width by 1 + { + { L"FGHIJ", true }, + { L"KLMNO", true }, + { L"PQRST", true }, + { L"UVWXY", true }, + { L"Z0 $ ", false }, + }, + { 4, 4 } // cursor follows space after $ to newly introduced bottom line + }, + }, + }, + TestCase{ + L"SBCS, cursor off in space to far right of text (end of buffer content)", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", false }, + // v cursor + { L"$ ", false }, + // ^ cursor + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 5, 1 } // cursor in space far after $ + }, + TestBuffer{ + { 5, 5 }, // reduce width by 1 + { + { L"ABCDE", true }, + { L"F$ ", true }, // [BUG] This line is marked wrapped, and I do not know why + // v cursor + { L" ", false }, + // ^ cursor + { L" ", false }, + { L" ", false }, + }, + { 1, 2 } // cursor stays same linear distance from $ + }, + TestBuffer{ + { 6, 5 }, // grow back to original size + { + { L"ABCDEF", true_due_to_exact_wrap_bug }, + // v cursor [BUG] cursor does not retain linear distance from $ + { L"$ ", false }, + // ^ cursor + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 4, 1 } // cursor stays same linear distance from $ + }, + }, + }, + TestCase{ + L"SBCS, cursor off in space to far right of text (middle of buffer content)", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", false }, + // v cursor + { L"$ ", false }, + // ^ cursor + { L"BLAH ", false }, + { L"BLAH ", false }, + { L" ", false }, + }, + { 5, 1 } // cursor in space far after $ + }, + TestBuffer{ + { 5, 5 }, // reduce width by 1 + { + { L"ABCDE", true }, + { L"F$ ", false }, + { L"BLAH ", false }, + { L"BLAH ", true }, // [BUG] this line wraps, no idea why + // v cursor [BUG] cursor erroneously moved to end of all content + { L" ", false }, + // ^ cursor + }, + { 0, 4 } }, + TestBuffer{ + { 6, 5 }, // grow back to original size + { + { L"ABCDEF", true }, + { L"$ ", false }, + { L"BLAH ", false }, + // v cursor [BUG] cursor is pulled up to previous line because it was marked wrapped + { L"BLAH ", false }, + // ^ cursor + { L" ", false }, + }, + { 5, 3 } }, + }, + }, + TestCase{ + // Shrinking the buffer this much forces a multi-line wrap before the cursor + L"SBCS, cursor off in space to far right of text (end of buffer content), aggressive shrink", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", false }, + // v cursor + { L"$ ", false }, + // ^ cursor + { L" ", false }, + { L" ", false }, + { L" ", false }, + }, + { 5, 1 } // cursor in space far after $ + }, + TestBuffer{ + { 2, 5 }, // reduce width aggressively + { + { L"CD", true }, + { L"EF", true }, + { L"$ ", true }, + { L" ", true }, + // v cursor + { L" ", false }, + // ^ cursor + }, + { 1, 4 } }, + }, + }, + TestCase{ + L"SBCS, cursor off in space to far right of text (end of buffer content), fully wrapped, aggressive shrink", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", true }, + // v cursor + { L"$ ", true }, + // ^ cursor + { L" ", true }, + { L" ", true }, + { L" ", true }, + }, + { 5, 1 } // cursor in space far after $ + }, + TestBuffer{ + { 2, 5 }, // reduce width aggressively + { + { L"EF", true }, + { L"$ ", true }, + { L" ", true }, + { L" ", true }, + // v cursor [BUG] cursor does not maintain linear distance from $ + { L" ", false }, + // ^ cursor + }, + { 1, 4 } }, + }, + }, + TestCase{ + L"SBCS, cursor off in space to far right of text (middle of buffer content), fully wrapped, aggressive shrink", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", true }, + // v cursor + { L"$ ", true }, + // ^ cursor + { L" ", true }, + { L" ", true }, + { L" Q", true }, + }, + { 5, 1 } // cursor in space far after $ + }, + TestBuffer{ + { 2, 5 }, // reduce width aggressively + { + { L" ", true }, + { L" ", true }, + { L" ", true }, + { L" Q", true }, + // v cursor [BUG] cursor jumps to end of world + { L" ", false }, // POTENTIAL [BUG] a whole new blank line is added for the cursor + // ^ cursor + }, + { 1, 4 } }, + }, + }, + TestCase{ + L"SBCS, cursor off in space to far right of text (middle of buffer content), partially wrapped, aggressive shrink", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", false }, + // v cursor + { L"$ ", false }, + // ^ cursor + { L" ", false }, + { L" ", true }, + { L" Q", true }, + }, + { 5, 1 } // cursor in space far after $ + }, + TestBuffer{ + { 2, 5 }, // reduce width aggressively + { + { L" ", true }, + { L" ", true }, + { L" ", true }, + { L" Q", true }, + // v cursor [BUG] cursor jumps to different place than fully wrapped case + { L" ", false }, + // ^ cursor + }, + { 0, 4 } }, + }, + }, + TestCase{ + // This triggers the cursor being walked forward w/ newlines to maintain + // distance from the last char in the buffer + L"SBCS, cursor at end of buffer, otherwise same as previous test", + { + TestBuffer{ + { 6, 5 }, + { + { L"ABCDEF", false }, + { L"$ ", false }, + { L" Q", true }, + { L" ", true }, + // v cursor + { L" ", true }, + // ^ cursor + }, + { 5, 4 } // cursor at end of buffer + }, + TestBuffer{ + { 2, 5 }, // reduce width aggressively + { + { L" ", true }, + { L" ", true }, + { L" Q", true }, + { L" ", false }, + // v cursor [BUG] cursor loses linear distance from Q; is this important? + { L" ", false }, + // ^ cursor + }, + { 0, 4 } }, + }, + }, + }; + +#pragma region TAEF hookup for the test case array above + struct ArrayIndexTaefAdapterRow : public Microsoft::WRL::RuntimeClass, IDataRow> + { + HRESULT RuntimeClassInitialize(const size_t index) + { + _index = index; + return S_OK; + } + + STDMETHODIMP GetTestData(BSTR /*pszName*/, SAFEARRAY** ppData) override + { + const auto indexString{ wil::str_printf(L"%zu", _index) }; + auto safeArray{ SafeArrayCreateVector(VT_BSTR, 0, 1) }; + LONG index{ 0 }; + auto indexBstr{ wil::make_bstr(indexString.c_str()) }; + (void)SafeArrayPutElement(safeArray, &index, indexBstr.release()); + *ppData = safeArray; + return S_OK; + } + + STDMETHODIMP GetMetadataNames(SAFEARRAY** ppMetadataNames) override + { + *ppMetadataNames = nullptr; + return S_FALSE; + } + + STDMETHODIMP GetMetadata(BSTR /*pszName*/, SAFEARRAY** ppData) override + { + *ppData = nullptr; + return S_FALSE; + } + + STDMETHODIMP GetName(BSTR* ppszRowName) override + { + *ppszRowName = nullptr; + return S_FALSE; + } + + private: + size_t _index; + }; + + struct ArrayIndexTaefAdapterSource : public Microsoft::WRL::RuntimeClass, IDataSource> + { + STDMETHODIMP Advance(IDataRow** ppDataRow) override + { + if (_index < std::extent::value) + { + Microsoft::WRL::MakeAndInitialize(ppDataRow, _index++); + } + else + { + *ppDataRow = nullptr; + } + return S_OK; + } + + STDMETHODIMP Reset() override + { + _index = 0; + return S_OK; + } + + STDMETHODIMP GetTestDataNames(SAFEARRAY** names) override + { + auto safeArray{ SafeArrayCreateVector(VT_BSTR, 0, 1) }; + LONG index{ 0 }; + auto dataNameBstr{ wil::make_bstr(L"index") }; + (void)SafeArrayPutElement(safeArray, &index, dataNameBstr.release()); + *names = safeArray; + return S_OK; + } + + STDMETHODIMP GetTestDataType(BSTR /*name*/, BSTR* type) override + { + *type = nullptr; + return S_OK; + } + + private: + size_t _index{ 0 }; + }; +#pragma endregion +} + +extern "C" HRESULT __declspec(dllexport) __cdecl ReflowTestDataSource(IDataSource** ppDataSource, void*) +{ + auto source{ Microsoft::WRL::Make() }; + return source.CopyTo(ppDataSource); +} + +class ReflowTests +{ + TEST_CLASS(ReflowTests); + + static DummyRenderTarget target; + static std::unique_ptr _textBufferFromTestBuffer(const TestBuffer& testBuffer) + { + auto buffer = std::make_unique(testBuffer.size, TextAttribute{ 0x7 }, 0, target); + + size_t i{}; + for (const auto& testRow : testBuffer.rows) + { + auto& row{ buffer->GetRowByOffset(i) }; + + auto& charRow{ row.GetCharRow() }; + charRow.SetWrapForced(testRow.wrap); + + size_t j{}; + for (auto it{ charRow.begin() }; it != charRow.end(); ++it) + { + // Yes, we're about to manually create a buffer. It is unpleasant. + const auto ch{ til::at(testRow.text, j) }; + it->Char() = ch; + if (IsGlyphFullWidth(ch)) + { + it->DbcsAttr().SetLeading(); + it++; + it->Char() = ch; + it->DbcsAttr().SetTrailing(); + } + else + { + it->DbcsAttr().SetSingle(); + } + j++; + } + i++; + } + + buffer->GetCursor().SetPosition(testBuffer.cursor); + return buffer; + } + + static std::unique_ptr _textBufferByReflowingTextBuffer(TextBuffer& originalBuffer, const COORD newSize) + { + auto buffer = std::make_unique(newSize, TextAttribute{ 0x7 }, 0, target); + TextBuffer::Reflow(originalBuffer, *buffer, std::nullopt, std::nullopt); + return buffer; + } + + static void _compareTextBufferAgainstTestBuffer(const TextBuffer& buffer, const TestBuffer& testBuffer) + { + VERIFY_ARE_EQUAL(testBuffer.cursor, buffer.GetCursor().GetPosition()); + VERIFY_ARE_EQUAL(testBuffer.size, buffer.GetSize().Dimensions()); + + size_t i{}; + for (const auto& testRow : testBuffer.rows) + { + NoThrowString indexString; + const auto& row{ buffer.GetRowByOffset(i) }; + + const auto& charRow{ row.GetCharRow() }; + + indexString.Format(L"[Row %d]", i); + VERIFY_ARE_EQUAL(testRow.wrap, charRow.WasWrapForced(), indexString); + + size_t j{}; + for (auto it{ charRow.begin() }; it != charRow.end(); ++it) + { + indexString.Format(L"[Cell %d, %d; Text line index %d]", it - charRow.begin(), i, j); + // Yes, we're about to manually create a buffer. It is unpleasant. + const auto ch{ til::at(testRow.text, j) }; + if (IsGlyphFullWidth(ch)) + { + // Char is full width in test buffer, so + // ensure that real buffer is LEAD, TRAIL (ch) + VERIFY_IS_TRUE(it->DbcsAttr().IsLeading(), indexString); + VERIFY_ARE_EQUAL(ch, it->Char(), indexString); + + it++; + VERIFY_IS_TRUE(it->DbcsAttr().IsTrailing(), indexString); + } + else + { + VERIFY_IS_TRUE(it->DbcsAttr().IsSingle(), indexString); + } + + VERIFY_ARE_EQUAL(ch, it->Char(), indexString); + j++; + } + i++; + } + } + + TEST_METHOD(TestReflowCases) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"DataSource", L"Export:ReflowTestDataSource") + END_TEST_METHOD_PROPERTIES() + + WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{}; + WEX::TestExecution::SetVerifyOutput verifyOutputScope{ WEX::TestExecution::VerifyOutputSettings::LogOnlyFailures }; + + unsigned int i{}; + TestData::TryGetValue(L"index", i); // index is produced by the ArrayIndexTaefAdapterSource above + const auto& testCase{ testCases[i] }; + Log::Comment(NoThrowString().Format(L"[%zu.0] Test case \"%.*s\"", i, testCase.name.size(), testCase.name.data())); + + // Create initial text buffer from Buffer 0 + auto textBuffer{ _textBufferFromTestBuffer(testCase.buffers.front()) }; + for (size_t bufferIndex{ 1 }; bufferIndex < testCase.buffers.size(); ++bufferIndex) + { + const auto& testBuffer{ til::at(testCase.buffers, bufferIndex) }; + Log::Comment(NoThrowString().Format(L"[%zu.%zu] Resizing to %dx%d", i, bufferIndex, testBuffer.size.X, testBuffer.size.Y)); + + auto newBuffer{ _textBufferByReflowingTextBuffer(*textBuffer, testBuffer.size) }; + + // All future operations are based on the new buffer + std::swap(textBuffer, newBuffer); + + _compareTextBufferAgainstTestBuffer(*textBuffer, testBuffer); + } + } +}; + +DummyRenderTarget ReflowTests::target{}; diff --git a/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj b/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj index 0b2492922f36..b8821689c279 100644 --- a/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj +++ b/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj @@ -10,6 +10,7 @@ + @@ -18,6 +19,9 @@ + + {18d09a24-8240-42d6-8cb6-236eee820263} + {0cf235bd-2da0-407e-90ee-c467e8bbc714} diff --git a/src/buffer/out/ut_textbuffer/sources b/src/buffer/out/ut_textbuffer/sources index ba50be8e342e..53fd1ee63414 100644 --- a/src/buffer/out/ut_textbuffer/sources +++ b/src/buffer/out/ut_textbuffer/sources @@ -14,6 +14,7 @@ DLLDEF = SOURCES = \ $(SOURCES) \ + ReflowTests.cpp \ TextColorTests.cpp \ TextAttributeTests.cpp \ DefaultResource.rc \