// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "WexTestClass.h" #include "../../../inc/consoletaeftemplates.hpp" #include "../textBuffer.hpp" using namespace WEX::Common; using namespace WEX::Logging; using namespace WEX::TestExecution; namespace WEX { namespace TestExecution { template<> class VerifyOutputTraits { public: static WEX::Common::NoThrowString ToString(const TextAttributeRun& tar) { return WEX::Common::NoThrowString().Format( L"Length:%d, attr:%s", tar.GetLength(), VerifyOutputTraits::ToString(tar.GetAttributes()).GetBuffer()); } }; template<> class VerifyCompareTraits { public: static bool AreEqual(const TextAttributeRun& expected, const TextAttributeRun& actual) { return expected.GetAttributes() == actual.GetAttributes() && expected.GetLength() == actual.GetLength(); } static bool AreSame(const TextAttributeRun& expected, const TextAttributeRun& actual) { return &expected == &actual; } static bool IsLessThan(const TextAttributeRun&, const TextAttributeRun&) = delete; static bool IsGreaterThan(const TextAttributeRun&, const TextAttributeRun&) = delete; static bool IsNull(const TextAttributeRun& object) { return object.GetAttributes().IsLegacy() && object.GetAttributes().GetLegacyAttributes() == 0 && object.GetLength() == 0; } }; } } class AttrRowTests { ATTR_ROW* pSingle; ATTR_ROW* pChain; short _sDefaultLength = 80; short _sDefaultChainLength = 6; short sChainSegLength; short sChainLeftover; short sChainSegmentsNeeded; WORD __wDefaultAttr = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; WORD __wDefaultChainAttr = BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY; TextAttribute _DefaultAttr = TextAttribute(__wDefaultAttr); TextAttribute _DefaultChainAttr = TextAttribute(__wDefaultChainAttr); TEST_CLASS(AttrRowTests); TEST_METHOD_SETUP(MethodSetup) { pSingle = new ATTR_ROW(_sDefaultLength, _DefaultAttr); // Segment length is the expected length divided by the row length // E.g. row of 80, 4 segments, 20 segment length each sChainSegLength = _sDefaultLength / _sDefaultChainLength; // Leftover is spaces that don't fit evenly // E.g. row of 81, 4 segments, 1 leftover length segment sChainLeftover = _sDefaultLength % _sDefaultChainLength; // Start with the number of segments we expect sChainSegmentsNeeded = _sDefaultChainLength; // If we had a remainder, add one more segment if (sChainLeftover) { sChainSegmentsNeeded++; } // Create the chain pChain = new ATTR_ROW(_sDefaultLength, _DefaultAttr); pChain->_list.resize(sChainSegmentsNeeded); // Attach all chain segments that are even multiples of the row length for (short iChain = 0; iChain < _sDefaultChainLength; iChain++) { TextAttributeRun* pRun = &pChain->_list[iChain]; pRun->SetAttributes(TextAttribute{ gsl::narrow_cast(iChain) }); // Just use the chain position as the value pRun->SetLength(sChainSegLength); } if (sChainLeftover > 0) { // If we had a leftover, then this chain is one longer than we expected (the default length) // So use it as the index (because indices start at 0) TextAttributeRun* pRun = &pChain->_list[_sDefaultChainLength]; pRun->SetAttributes(_DefaultChainAttr); pRun->SetLength(sChainLeftover); } return true; } TEST_METHOD_CLEANUP(MethodCleanup) { delete pSingle; delete pChain; return true; } TEST_METHOD(TestInitialize) { // Properties needed for test const WORD wAttr = FOREGROUND_RED | BACKGROUND_BLUE; TextAttribute attr = TextAttribute(wAttr); // Cases to test ATTR_ROW* pTestItems[]{ pSingle, pChain }; // Loop cases for (UINT iIndex = 0; iIndex < ARRAYSIZE(pTestItems); iIndex++) { ATTR_ROW* pUnderTest = pTestItems[iIndex]; pUnderTest->Reset(attr); VERIFY_ARE_EQUAL(pUnderTest->_list.size(), 1u); VERIFY_ARE_EQUAL(pUnderTest->_list[0].GetAttributes(), attr); VERIFY_ARE_EQUAL(pUnderTest->_list[0].GetLength(), (unsigned int)_sDefaultLength); } } // Routine Description: // - Packs an array of words representing attributes into the more compact storage form used by the row. // Arguments: // - rgAttrs - Array of words representing the attribute associated with each character position in the row. // - cRowLength - Length of preceding array. // - outAttrRun - reference to unique_ptr that will contain packed attr run on success. // Return Value: // - Success if success. Buffer too small if row length is incorrect. HRESULT PackAttrs(_In_reads_(cRowLength) const TextAttribute* const rgAttrs, const size_t cRowLength, _Inout_ std::unique_ptr& outAttrRun, _Out_ size_t* const cOutAttrRun) { RETURN_HR_IF(E_NOT_SUFFICIENT_BUFFER, cRowLength == 0); // first count up the deltas in the array size_t cDeltas = 1; const TextAttribute* pPrevAttr = &rgAttrs[0]; for (size_t i = 1; i < cRowLength; i++) { const TextAttribute* pCurAttr = &rgAttrs[i]; if (*pCurAttr != *pPrevAttr) { cDeltas++; } pPrevAttr = pCurAttr; } // This whole situation was too complicated with a one off holder for one row run // new method: // delete the old buffer // make a new buffer, one run + one run for each change // set the values for each run one run index at a time std::unique_ptr attrRun = std::make_unique(cDeltas); RETURN_HR_IF_NULL(E_OUTOFMEMORY, attrRun); TextAttributeRun* pCurrentRun = attrRun.get(); pCurrentRun->SetAttributes(rgAttrs[0]); pCurrentRun->SetLength(1); for (size_t i = 1; i < cRowLength; i++) { if (pCurrentRun->GetAttributes() == rgAttrs[i]) { pCurrentRun->SetLength(pCurrentRun->GetLength() + 1); } else { pCurrentRun++; pCurrentRun->SetAttributes(rgAttrs[i]); pCurrentRun->SetLength(1); } } attrRun.swap(outAttrRun); *cOutAttrRun = cDeltas; return S_OK; } NoThrowString LogRunElement(_In_ TextAttributeRun& run) { return NoThrowString().Format(L"%wc%d", run.GetAttributes().GetLegacyAttributes(), run.GetLength()); } void LogChain(_In_ PCWSTR pwszPrefix, boost::container::small_vector_base& chain) { NoThrowString str(pwszPrefix); if (chain.size() > 0) { str.Append(LogRunElement(chain[0])); for (size_t i = 1; i < chain.size(); i++) { str.AppendFormat(L"->%s", (const wchar_t*)(LogRunElement(chain[i]))); } } Log::Comment(str); } void LogChain(_In_ PCWSTR pwszPrefix, std::vector& chain) { NoThrowString str(pwszPrefix); if (chain.size() > 0) { str.Append(LogRunElement(chain[0])); for (size_t i = 1; i < chain.size(); i++) { str.AppendFormat(L"->%s", (const wchar_t*)(LogRunElement(chain[i]))); } } Log::Comment(str); } void DoTestInsertAttrRuns(UINT& uiStartPos, WORD& ch1, UINT& uiChar1Length, WORD& ch2, UINT& uiChar2Length) { Log::Comment(String().Format(L"StartPos: %d, Char1: %wc, Char1Length: %d, Char2: %wc, Char2Length: %d", uiStartPos, ch1, uiChar1Length, ch2, uiChar2Length)); bool const fUseStr2 = (ch2 != L'0'); // Set up our "original row" that we are going to try to insert into. // This will represent a 10 column run of R3->B5->G2 that we will use for all tests. ATTR_ROW originalRow{ static_cast(_sDefaultLength), _DefaultAttr }; originalRow._list.resize(3); originalRow._cchRowWidth = 10; originalRow._list[0].SetAttributes(TextAttribute{ 'R' }); originalRow._list[0].SetLength(3); originalRow._list[1].SetAttributes(TextAttribute{ 'B' }); originalRow._list[1].SetLength(5); originalRow._list[2].SetAttributes(TextAttribute{ 'G' }); originalRow._list[2].SetLength(2); LogChain(L"Original: ", originalRow._list); // Set up our "insertion run" size_t cInsertRow = 1; if (fUseStr2) { cInsertRow++; } std::vector insertRow; insertRow.resize(cInsertRow); insertRow[0].SetAttributes(TextAttribute{ ch1 }); insertRow[0].SetLength(uiChar1Length); if (fUseStr2) { insertRow[1].SetAttributes(TextAttribute{ ch2 }); insertRow[1].SetLength(uiChar2Length); } LogChain(L"Insert: ", insertRow); Log::Comment(NoThrowString().Format(L"At Index: %d", uiStartPos)); UINT uiTotalLength = uiChar1Length; if (fUseStr2) { uiTotalLength += uiChar2Length; } VERIFY_IS_TRUE((uiStartPos + uiTotalLength) >= 1); // assert we won't underflow. UINT const uiEndPos = uiStartPos + uiTotalLength - 1; // Calculate our expected final/result run by unpacking original, laying our insertion on it at the index // then using our pack function to repack it. // This method is easy to understand and very reliable, but its performance is bad. // The InsertAttrRuns method we test against below is hard to understand but very high performance in production. // - 1. Unpack std::vector unpackedOriginal = { originalRow.begin(), originalRow.end() }; // - 2. Overlay insertion UINT uiInsertedCount = 0; UINT uiInsertIndex = 0; // --- Walk through the unpacked run from start to end.... for (UINT uiUnpackedIndex = uiStartPos; uiUnpackedIndex <= uiEndPos; uiUnpackedIndex++) { // Pull the item from the insert run to analyze. TextAttributeRun run = insertRow[uiInsertIndex]; // Copy the attribute from the run into the unpacked array unpackedOriginal[uiUnpackedIndex] = run.GetAttributes(); // Increment how many times we've copied this particular portion of the run uiInsertedCount++; // If we've now inserted enough of them to match the length, advance the insert index and reset the counter. if (uiInsertedCount >= run.GetLength()) { uiInsertIndex++; uiInsertedCount = 0; } } // - 3. Pack. std::unique_ptr packedRun; size_t cPackedRun = 0; VERIFY_SUCCEEDED(PackAttrs(unpackedOriginal.data(), originalRow._cchRowWidth, packedRun, &cPackedRun)); // Now send parameters into InsertAttrRuns and get its opinion on the subject. VERIFY_SUCCEEDED(originalRow.InsertAttrRuns({ insertRow.data(), insertRow.size() }, uiStartPos, uiEndPos, (UINT)originalRow._cchRowWidth)); // Compare and ensure that the expected and actual match. VERIFY_ARE_EQUAL(cPackedRun, originalRow._list.size(), L"Ensure that number of array elements required for RLE are the same."); std::vector packedRunExpected; std::copy_n(packedRun.get(), cPackedRun, std::back_inserter(packedRunExpected)); LogChain(L"Expected: ", packedRunExpected); LogChain(L"Actual: ", originalRow._list); for (size_t testIndex = 0; testIndex < cPackedRun; testIndex++) { VERIFY_ARE_EQUAL(packedRun[testIndex], originalRow._list[testIndex]); } } TEST_METHOD(TestInsertAttrRunsSingle) { UINT const uiTestRunLength = 10; UINT uiStartPos = 0; WORD ch1 = L'0'; UINT uiChar1Length = 0; WORD ch2 = L'0'; UINT uiChar2Length = 0; Log::Comment(L"Test inserting a single item of a variable length into the run."); WORD rgch1Options[] = { L'X', L'R', L'G', L'B' }; for (size_t iCh1Option = 0; iCh1Option < ARRAYSIZE(rgch1Options); iCh1Option++) { ch1 = rgch1Options[iCh1Option]; for (UINT iCh1Length = 1; iCh1Length <= uiTestRunLength; iCh1Length++) { uiChar1Length = iCh1Length; // We can't try to insert a run that's longer than would fit. // If the run is of length 10 and we're trying to insert a length of 10, // we can only insert at position 0. // For the run length of 10 and an insert length of 9, we can try positions 0 and 1. // And so on... UINT const uiMaxPos = uiTestRunLength - uiChar1Length; for (UINT iStartPos = 0; iStartPos < uiMaxPos; iStartPos++) { uiStartPos = iStartPos; DoTestInsertAttrRuns(uiStartPos, ch1, uiChar1Length, ch2, uiChar2Length); } } } } TEST_METHOD(TestInsertAttrRunsMultiple) { UINT const uiTestRunLength = 10; UINT uiStartPos = 0; WORD ch1 = L'0'; UINT uiChar1Length = 0; WORD ch2 = L'0'; UINT uiChar2Length = 0; Log::Comment(L"Test inserting a multiple item run with each piece having variable length into the existing run."); WORD rgch1Options[] = { L'X', L'R', L'G', L'B' }; for (size_t iCh1Option = 0; iCh1Option < ARRAYSIZE(rgch1Options); iCh1Option++) { ch1 = rgch1Options[iCh1Option]; UINT const uiMaxCh1Length = uiTestRunLength - 1; // leave at least 1 space for the second piece of the insert run. for (UINT iCh1Length = 1; iCh1Length <= uiMaxCh1Length; iCh1Length++) { uiChar1Length = iCh1Length; WORD rgch2Options[] = { L'Y' }; for (size_t iCh2Option = 0; iCh2Option < ARRAYSIZE(rgch2Options); iCh2Option++) { ch2 = rgch2Options[iCh2Option]; // When choosing the length of the second item, it can't be bigger than the remaining space in the run // when accounting for the length of the first piece chosen. // For example if the total run length is 10 and the first piece chosen was 8 long, // the second piece can only be 1 or 2 long. UINT const uiMaxCh2Length = uiTestRunLength - uiMaxCh1Length; for (UINT iCh2Length = 1; iCh2Length <= uiMaxCh2Length; iCh2Length++) { uiChar2Length = iCh2Length; // We can't try to insert a run that's longer than would fit. // If the run is of length 10 and we're trying to insert a total length of 10, // we can only insert at position 0. // For the run length of 10 and an insert length of 9, we can try positions 0 and 1. // And so on... UINT const uiMaxPos = uiTestRunLength - (uiChar1Length + uiChar2Length); for (UINT iStartPos = 0; iStartPos <= uiMaxPos; iStartPos++) { uiStartPos = iStartPos; DoTestInsertAttrRuns(uiStartPos, ch1, uiChar1Length, ch2, uiChar2Length); } } } } } } TEST_METHOD(TestUnpackAttrs) { Log::Comment(L"Checking unpack of a single color for the entire length"); { const std::vector attrs{ pSingle->begin(), pSingle->end() }; for (auto& attr : attrs) { VERIFY_ARE_EQUAL(attr, _DefaultAttr); } } Log::Comment(L"Checking unpack of the multiple color chain"); const std::vector attrs{ pChain->begin(), pChain->end() }; short cChainRun = 0; // how long we've been looking at the current piece of the chain short iChainSegIndex = 0; // which piece of the chain we should be on right now for (auto& attr : attrs) { // by default the chain was assembled above to have the chain segment index be the attribute TextAttribute MatchingAttr = TextAttribute(iChainSegIndex); // However, if the index is greater than the expected chain length, a remainder piece was made with a default attribute if (iChainSegIndex >= _sDefaultChainLength) { MatchingAttr = _DefaultChainAttr; } VERIFY_ARE_EQUAL(attr, MatchingAttr); // Add to the chain run cChainRun++; // If the chain run is greater than the length the segments were specified to be if (cChainRun >= sChainSegLength) { // reset to 0 cChainRun = 0; // move to the next chain segment down the line iChainSegIndex++; } } } TEST_METHOD(TestReverseIteratorWalkFromMiddle) { // GH #3409, walking backwards through color range runs out of bounds // We're going to create an attribute row with assorted colors and varying lengths // just like the row of text on the Ubuntu prompt line that triggered this bug being found. // Then we're going to walk backwards through the iterator like a selection-expand-to-left // operation and ensure we don't run off the bounds. // walk the chain, from index, stepSize at a time // ensure we don't crash auto testWalk = [](ATTR_ROW* chain, size_t index, int stepSize) { // move to starting index auto iter = chain->cbegin(); iter += index; // Now walk backwards in a loop until 0. while (iter) { iter -= stepSize; } Log::Comment(L"We made it through without crashing!"); }; // take one step of size stepSize on the chain // index is where we start from // expectedAttribute is what we expect to read here auto verifyStep = [](ATTR_ROW* chain, size_t index, int stepSize, TextAttribute expectedAttribute) { // move to starting index auto iter = chain->cbegin(); iter += index; // Now step backwards iter -= stepSize; VERIFY_ARE_EQUAL(expectedAttribute, *iter); }; Log::Comment(L"Reverse iterate through ubuntu prompt"); { // Create attr row representing a buffer that's 121 wide. auto chain = std::make_unique(121, _DefaultAttr); // The repro case had 4 chain segments. chain->_list.resize(4); // The color 10 went for the first 18. chain->_list[0].SetAttributes(TextAttribute(0xA)); chain->_list[0].SetLength(18); // Default color for the next 1 chain->_list[1].SetAttributes(TextAttribute()); chain->_list[1].SetLength(1); // Color 12 for the next 29 chain->_list[2].SetAttributes(TextAttribute(0xC)); chain->_list[2].SetLength(29); // Then default color to end the run chain->_list[3].SetAttributes(TextAttribute()); chain->_list[3].SetLength(73); // The sum of the lengths should be 121. VERIFY_ARE_EQUAL(chain->_cchRowWidth, chain->_list[0]._cchLength + chain->_list[1]._cchLength + chain->_list[2]._cchLength + chain->_list[3]._cchLength); auto index = chain->_list[0].GetLength(); auto stepSize = 1; testWalk(chain.get(), index, stepSize); } Log::Comment(L"Reverse iterate across a text run in the chain"); { // Create attr row representing a buffer that's 3 wide. auto chain = std::make_unique(3, _DefaultAttr); // The repro case had 3 chain segments. chain->_list.resize(3); // The color 10 went for the first 1. chain->_list[0].SetAttributes(TextAttribute(0xA)); chain->_list[0].SetLength(1); // The color 11 for the next 1 chain->_list[1].SetAttributes(TextAttribute(0xB)); chain->_list[1].SetLength(1); // Color 12 for the next 1 chain->_list[2].SetAttributes(TextAttribute(0xC)); chain->_list[2].SetLength(1); // The sum of the lengths should be 3. VERIFY_ARE_EQUAL(chain->_cchRowWidth, chain->_list[0]._cchLength + chain->_list[1]._cchLength + chain->_list[2]._cchLength); // on 'ABC', step from B to A auto index = 1; auto stepSize = 1; verifyStep(chain.get(), index, stepSize, TextAttribute(0xA)); } Log::Comment(L"Reverse iterate across two text runs in the chain"); { // Create attr row representing a buffer that's 3 wide. auto chain = std::make_unique(3, _DefaultAttr); // The repro case had 3 chain segments. chain->_list.resize(3); // The color 10 went for the first 1. chain->_list[0].SetAttributes(TextAttribute(0xA)); chain->_list[0].SetLength(1); // The color 11 for the next 1 chain->_list[1].SetAttributes(TextAttribute(0xB)); chain->_list[1].SetLength(1); // Color 12 for the next 1 chain->_list[2].SetAttributes(TextAttribute(0xC)); chain->_list[2].SetLength(1); // The sum of the lengths should be 3. VERIFY_ARE_EQUAL(chain->_cchRowWidth, chain->_list[0]._cchLength + chain->_list[1]._cchLength + chain->_list[2]._cchLength); // on 'ABC', step from C to A auto index = 2; auto stepSize = 2; verifyStep(chain.get(), index, stepSize, TextAttribute(0xA)); } } TEST_METHOD(TestSetAttrToEnd) { const WORD wTestAttr = FOREGROUND_BLUE | BACKGROUND_GREEN; TextAttribute TestAttr = TextAttribute(wTestAttr); Log::Comment(L"FIRST: Set index to > 0 to test making/modifying chains"); const short iTestIndex = 50; VERIFY_IS_TRUE(iTestIndex >= 0 && iTestIndex < _sDefaultLength); Log::Comment(L"SetAttrToEnd for single color applied to whole string."); pSingle->SetAttrToEnd(iTestIndex, TestAttr); // Was 1 (single), should now have 2 segments VERIFY_ARE_EQUAL(pSingle->_list.size(), 2u); VERIFY_ARE_EQUAL(pSingle->_list[0].GetAttributes(), _DefaultAttr); VERIFY_ARE_EQUAL(pSingle->_list[0].GetLength(), (unsigned int)(_sDefaultLength - (_sDefaultLength - iTestIndex))); VERIFY_ARE_EQUAL(pSingle->_list[1].GetAttributes(), TestAttr); VERIFY_ARE_EQUAL(pSingle->_list[1].GetLength(), (unsigned int)(_sDefaultLength - iTestIndex)); Log::Comment(L"SetAttrToEnd for existing chain of multiple colors."); pChain->SetAttrToEnd(iTestIndex, TestAttr); // From 7 segments down to 5. VERIFY_ARE_EQUAL(pChain->_list.size(), 5u); // Verify chain colors and lengths VERIFY_ARE_EQUAL(TextAttribute(0), pChain->_list[0].GetAttributes()); VERIFY_ARE_EQUAL(pChain->_list[0].GetLength(), (unsigned int)13); VERIFY_ARE_EQUAL(TextAttribute(1), pChain->_list[1].GetAttributes()); VERIFY_ARE_EQUAL(pChain->_list[1].GetLength(), (unsigned int)13); VERIFY_ARE_EQUAL(TextAttribute(2), pChain->_list[2].GetAttributes()); VERIFY_ARE_EQUAL(pChain->_list[2].GetLength(), (unsigned int)13); VERIFY_ARE_EQUAL(TextAttribute(3), pChain->_list[3].GetAttributes()); VERIFY_ARE_EQUAL(pChain->_list[3].GetLength(), (unsigned int)11); VERIFY_ARE_EQUAL(TestAttr, pChain->_list[4].GetAttributes()); VERIFY_ARE_EQUAL(pChain->_list[4].GetLength(), (unsigned int)30); Log::Comment(L"SECOND: Set index to 0 to test replacing anything with a single"); ATTR_ROW* pTestItems[]{ pSingle, pChain }; for (UINT iIndex = 0; iIndex < ARRAYSIZE(pTestItems); iIndex++) { ATTR_ROW* pUnderTest = pTestItems[iIndex]; pUnderTest->SetAttrToEnd(0, TestAttr); // should be down to 1 attribute set from beginning to end of string VERIFY_ARE_EQUAL(pUnderTest->_list.size(), 1u); // singular pair should contain the color VERIFY_ARE_EQUAL(pUnderTest->_list[0].GetAttributes(), TestAttr); // and its length should be the length of the whole string VERIFY_ARE_EQUAL(pUnderTest->_list[0].GetLength(), (unsigned int)_sDefaultLength); } } TEST_METHOD(TestTotalLength) { ATTR_ROW* pTestItems[]{ pSingle, pChain }; for (UINT iIndex = 0; iIndex < ARRAYSIZE(pTestItems); iIndex++) { ATTR_ROW* pUnderTest = pTestItems[iIndex]; const size_t Result = pUnderTest->_cchRowWidth; VERIFY_ARE_EQUAL((short)Result, _sDefaultLength); } } TEST_METHOD(TestResize) { pSingle->Resize(240); pChain->Resize(240); pSingle->Resize(255); pChain->Resize(255); pSingle->Resize(255); pChain->Resize(255); pSingle->Resize(60); pChain->Resize(60); pSingle->Resize(60); pChain->Resize(60); VERIFY_THROWS_SPECIFIC(pSingle->Resize(0), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); VERIFY_THROWS_SPECIFIC(pChain->Resize(0), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); } };