diff --git a/src/engine/src/queryresultprinter.cpp b/src/engine/src/queryresultprinter.cpp index 34009dd8..0d0679a7 100644 --- a/src/engine/src/queryresultprinter.cpp +++ b/src/engine/src/queryresultprinter.cpp @@ -56,6 +56,7 @@ json ToJson(CoincenterCommandType commandType, json &&in, json &&out) { in.emplace("req", CoincenterCommandTypeToString(commandType)); json ret; + ret.emplace("in", std::move(in)); ret.emplace("out", std::move(out)); @@ -1154,7 +1155,7 @@ void QueryResultPrinter::printWithdrawFees(const MonetaryAmountByCurrencySetPerE for (const auto &[e, withdrawFees] : withdrawFeesPerExchange) { auto it = withdrawFees.find(cur); if (it == withdrawFees.end()) { - row.emplace_back(); + row.emplace_back(""); } else { row.emplace_back(it->str()); } @@ -1314,16 +1315,17 @@ void QueryResultPrinter::printDustSweeper( switch (_apiOutputType) { case ApiOutputType::kFormattedTable: { SimpleTable simpleTable("Exchange", "Account", "Trades", "Final Amount"); + + simpleTable.reserve(1U + tradedAmountsVectorWithFinalAmountPerExchange.size()); for (const auto &[exchangePtr, tradedAmountsVectorWithFinalAmount] : tradedAmountsVectorWithFinalAmountPerExchange) { - string tradesStr; - for (const auto &tradedAmounts : tradedAmountsVectorWithFinalAmount.tradedAmountsVector) { - if (!tradesStr.empty()) { - tradesStr.append(", "); - } - tradesStr.append(tradedAmounts.str()); + SimpleTable::Cell tradesCell; + const auto &tradedAmountsVector = tradedAmountsVectorWithFinalAmount.tradedAmountsVector; + tradesCell.reserve(tradedAmountsVector.size()); + for (const auto &tradedAmounts : tradedAmountsVector) { + tradesCell.emplace_back(tradedAmounts.str()); } - simpleTable.emplace_back(exchangePtr->name(), exchangePtr->keyName(), std::move(tradesStr), + simpleTable.emplace_back(exchangePtr->name(), exchangePtr->keyName(), std::move(tradesCell), tradedAmountsVectorWithFinalAmount.finalAmount.str()); } printTable(simpleTable); diff --git a/src/engine/test/queryresultprinter_private_test.cpp b/src/engine/test/queryresultprinter_private_test.cpp index bb052a03..efbbf434 100644 --- a/src/engine/test/queryresultprinter_private_test.cpp +++ b/src/engine/test/queryresultprinter_private_test.cpp @@ -11,7 +11,6 @@ #include "exchangename.hpp" #include "exchangeprivateapitypes.hpp" #include "monetaryamount.hpp" -#include "order.hpp" #include "ordersconstraints.hpp" #include "priceoptions.hpp" #include "priceoptionsdef.hpp" @@ -1660,13 +1659,15 @@ TEST_F(QueryResultPrinterDustSweeperTest, FormattedTable) { basicQueryResultPrinter(ApiOutputType::kFormattedTable) .printDustSweeper(tradedAmountsVectorWithFinalAmountPerExchange, cur); static constexpr std::string_view kExpected = R"( -+----------+-----------+-------------------------------------------------------+--------------+ -| Exchange | Account | Trades | Final Amount | -+----------+-----------+-------------------------------------------------------+--------------+ -| binance | testuser1 | 98.47 ETH -> 0.00005 BTC | 0 ETH | -| huobi | testuser1 | | 1.56 ETH | -| huobi | testuser2 | 0.45609 EUR -> 98.47 ETH, 1509.45 ETH -> 0.000612 BTC | 0 ETH | -+----------+-----------+-------------------------------------------------------+--------------+ ++----------+-----------+-----------------------------+--------------+ +| Exchange | Account | Trades | Final Amount | ++----------+-----------+-----------------------------+--------------+ +| binance | testuser1 | 98.47 ETH -> 0.00005 BTC | 0 ETH | +| huobi | testuser1 | | 1.56 ETH | +|~~~~~~~~~~|~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~| +| huobi | testuser2 | 0.45609 EUR -> 98.47 ETH | 0 ETH | +| | | 1509.45 ETH -> 0.000612 BTC | | ++----------+-----------+-----------------------------+--------------+ )"; expectStr(kExpected); } diff --git a/src/tech/include/simpletable.hpp b/src/tech/include/simpletable.hpp index 1f5269e4..48a99dd4 100644 --- a/src/tech/include/simpletable.hpp +++ b/src/tech/include/simpletable.hpp @@ -22,16 +22,24 @@ namespace cct { /// Simple, lightweight and fast table with dynamic number of columns. /// No checks are made about the number of columns for each Row, it's up to client's responsibility to make sure they /// match. -/// Multi line rows are *not* supported. +/// The SimpleTable is made up of 'Row's, themselves made up of 'Cell's, themselves made of 'CellLine's. +/// SimpleTable, Row, Cell behave like standard C++ vector-like containers, with support of most common methods. +/// The first Row is constructed like any other, but it will print an additional line separator to appear like a header. +/// No line separator will be placed between two single line only cells. +/// However, multi line Rows ('Row' containing at least a 'Cell' with several 'CellLine's) will have line separators +/// before and after them. +/// It is also possible to force a line separator between any row in the print. For that, you can insert the special Row +/// Row::kDivider at the desired place in the SimpleTable. +/// See the unit test to have an overview of its usage and the look and feel of the print. class SimpleTable { public: class Row; + class Cell; - /// Cell in a SimpleTable. + /// Cell in a SimpleTable on a single line. /// Can currently hold only 4 types of values: a string, a string_view, a int64_t and a bool. - class Cell { + class CellLine { public: - using IntegralType = int64_t; #ifdef CCT_MSVC // folly::string does not support operator<< correctly with alignments with MSVC. Hence we use std::string // in SimpleTable to guarantee correct alignment of formatted table. Referenced in this issue: @@ -40,39 +48,106 @@ class SimpleTable { #else using string_type = string; #endif - using value_type = std::variant; + using value_type = std::variant; using size_type = uint32_t; - explicit Cell(std::string_view sv = std::string_view()) : _data(sv) {} + CellLine() noexcept = default; + + explicit CellLine(std::string_view sv) : _data(sv) {} - explicit Cell(const char *cstr) : _data(std::string_view(cstr)) {} + explicit CellLine(const char *cstr) : _data(std::string_view(cstr)) {} #ifdef CCT_MSVC - explicit Cell(const string &v) : _data(std::string(v.data(), v.size())) {} + explicit CellLine(const string &v) : _data(string_type(v.data(), v.size())) {} #else - explicit Cell(const string_type &str) : _data(str) {} + explicit CellLine(const string_type &str) : _data(str) {} - explicit Cell(string_type &&str) : _data(std::move(str)) {} + explicit CellLine(string_type &&str) : _data(std::move(str)) {} #endif - explicit Cell(std::integral auto val) : _data(val) {} + explicit CellLine(std::integral auto val) : _data(val) {} - size_type size() const noexcept; + // Number of chars of this single line cell value. + size_type width() const noexcept; - void swap(Cell &rhs) noexcept { _data.swap(rhs._data); } + void swap(CellLine &rhs) noexcept { _data.swap(rhs._data); } using trivially_relocatable = is_trivially_relocatable::type; - bool operator==(const Cell &) const noexcept = default; + std::strong_ordering operator<=>(const CellLine &) const noexcept = default; + + friend std::ostream &operator<<(std::ostream &os, const CellLine &singleLineCell); + + private: + value_type _data; + }; + + class Cell { + public: + using value_type = CellLine; + using size_type = uint32_t; + + private: + using CellLineVector = SmallVector; + + public: + using iterator = CellLineVector::iterator; + using const_iterator = CellLineVector::const_iterator; + + Cell() noexcept = default; + + /// Implicit constructor of a Cell from a CellLine. + Cell(CellLine singleLineCell) { _singleLineCells.push_back(std::move(singleLineCell)); } + + /// Creates a new Row with given list of cells. + template + explicit Cell(Args &&...singleLineCells) { + ([&](auto &&input) { _singleLineCells.emplace_back(std::forward(input)); }( + std::forward(singleLineCells)), + ...); + } + + iterator begin() noexcept { return _singleLineCells.begin(); } + const_iterator begin() const noexcept { return _singleLineCells.begin(); } + + iterator end() noexcept { return _singleLineCells.end(); } + const_iterator end() const noexcept { return _singleLineCells.end(); } + + value_type &front() { return _singleLineCells.front(); } + const value_type &front() const { return _singleLineCells.front(); } + + value_type &back() { return _singleLineCells.back(); } + const value_type &back() const { return _singleLineCells.back(); } + + void push_back(const value_type &cell) { _singleLineCells.push_back(cell); } + void push_back(value_type &&cell) { _singleLineCells.push_back(std::move(cell)); } + + template + value_type &emplace_back(Args &&...args) { + return _singleLineCells.emplace_back(std::forward(args)...); + } + + size_type size() const noexcept { return _singleLineCells.size(); } + + size_type width() const noexcept; + + value_type &operator[](size_type cellPos) { return _singleLineCells[cellPos]; } + const value_type &operator[](size_type cellPos) const { return _singleLineCells[cellPos]; } + + void reserve(size_type sz) { _singleLineCells.reserve(sz); } + + void swap(Cell &rhs) noexcept { _singleLineCells.swap(rhs._singleLineCells); } + + using trivially_relocatable = is_trivially_relocatable::type; std::strong_ordering operator<=>(const Cell &) const noexcept = default; private: friend class Row; - void print(std::ostream &os, size_type maxCellWidth) const; + void print(std::ostream &os, size_type linePos, size_type maxCellWidth) const; - value_type _data; + CellLineVector _singleLineCells; }; /// Row in a SimpleTable. @@ -80,15 +155,23 @@ class SimpleTable { public: using value_type = Cell; using size_type = uint32_t; - using iterator = vector::iterator; - using const_iterator = vector::const_iterator; + + private: + using CellVector = vector; + + public: + using iterator = CellVector::iterator; + using const_iterator = CellVector::const_iterator; static const Row kDivider; + Row() noexcept = default; + + /// Creates a new Row with given list of cells. template - explicit Row(Args &&...args) { - // Usage of C++17 fold expressions to make it possible to set a Row directly from a variadic input arguments - ([&](auto &&input) { _cells.emplace_back(std::forward(input)); }(std::forward(args)), ...); + explicit Row(Args &&...cells) { + ([&](auto &&input) { _cells.emplace_back(std::forward(input)); }(std::forward(cells)), + ...); } iterator begin() noexcept { return _cells.begin(); } @@ -103,8 +186,8 @@ class SimpleTable { value_type &back() { return _cells.back(); } const value_type &back() const { return _cells.back(); } - void push_back(const Cell &cell) { _cells.push_back(cell); } - void push_back(Cell &&cell) { _cells.push_back(std::move(cell)); } + void push_back(const value_type &cell) { _cells.push_back(cell); } + void push_back(value_type &&cell) { _cells.push_back(std::move(cell)); } template value_type &emplace_back(Args &&...args) { @@ -113,30 +196,38 @@ class SimpleTable { size_type size() const noexcept { return _cells.size(); } + bool isMultiLine() const noexcept; + bool isDivider() const noexcept { return _cells.empty(); } + void reserve(size_type sz) { _cells.reserve(sz); } + value_type &operator[](size_type cellPos) { return _cells[cellPos]; } const value_type &operator[](size_type cellPos) const { return _cells[cellPos]; } - using trivially_relocatable = is_trivially_relocatable>::type; + void swap(Row &rhs) noexcept { _cells.swap(rhs._cells); } - bool operator==(const Row &) const noexcept = default; + using trivially_relocatable = is_trivially_relocatable::type; std::strong_ordering operator<=>(const Row &) const noexcept = default; private: - friend class SimpleTable; friend std::ostream &operator<<(std::ostream &, const SimpleTable &); void print(std::ostream &os, std::span maxWidthPerColumn) const; - vector _cells; + CellVector _cells; }; using value_type = Row; using size_type = uint32_t; - using iterator = vector::iterator; - using const_iterator = vector::const_iterator; + + private: + using RowVector = vector; + + public: + using iterator = RowVector::iterator; + using const_iterator = RowVector::const_iterator; SimpleTable() noexcept = default; @@ -165,21 +256,20 @@ class SimpleTable { size_type size() const noexcept { return _rows.size(); } bool empty() const noexcept { return _rows.empty(); } + value_type &operator[](size_type rowPos) { return _rows[rowPos]; } const value_type &operator[](size_type rowPos) const { return _rows[rowPos]; } void reserve(size_type sz) { _rows.reserve(sz); } - friend std::ostream &operator<<(std::ostream &os, const SimpleTable &t); + friend std::ostream &operator<<(std::ostream &os, const SimpleTable &table); - using trivially_relocatable = is_trivially_relocatable>::type; + using trivially_relocatable = is_trivially_relocatable::type; private: using MaxWidthPerColumnVector = SmallVector; - static Cell::string_type ComputeLineSep(std::span maxWidthPerColumnVector); - MaxWidthPerColumnVector computeMaxWidthPerColumn() const; - vector _rows; + RowVector _rows; }; } // namespace cct \ No newline at end of file diff --git a/src/tech/src/simpletable.cpp b/src/tech/src/simpletable.cpp index 244775d6..6eae1e01 100644 --- a/src/tech/src/simpletable.cpp +++ b/src/tech/src/simpletable.cpp @@ -24,6 +24,7 @@ constexpr char kColumnSep = '|'; constexpr std::string_view kBoolValueTrue = "yes"; constexpr std::string_view kBoolValueFalse = "no"; +constexpr char kEmptyValueChar = ' '; enum class AlignTo : int8_t { kLeft, kRight }; @@ -40,108 +41,167 @@ class Align { return os; } }; + +template +constexpr bool always_false_v = false; } // namespace -SimpleTable::size_type SimpleTable::Cell::size() const noexcept { +SimpleTable::size_type SimpleTable::CellLine::width() const noexcept { return std::visit( [](auto &&val) -> size_type { using T = std::decay_t; + if constexpr (std::is_same_v || std::is_same_v) { - return val.size(); + return val.length(); } else if constexpr (std::is_same_v) { - return val ? kBoolValueTrue.size() : kBoolValueFalse.size(); + return val ? kBoolValueTrue.length() : kBoolValueFalse.length(); } else if constexpr (std::is_integral_v) { return nchars(val); } else { - // Note: below ugly template lambda can be replaced with 'static_assert(false);' in C++23 - []() { static_assert(flag, "no match"); } - (); + // Note: can be replaced with 'static_assert(false);' in C++23 + static_assert(always_false_v, "non-exhaustive visitor!"); } }, _data); } -void SimpleTable::Cell::print(std::ostream &os, size_type maxCellWidth) const { - os << ' ' << Align(AlignTo::kLeft) << std::setw(maxCellWidth); - +std::ostream &operator<<(std::ostream &os, const SimpleTable::CellLine &singleLineCell) { std::visit( [&os](auto &&val) { using T = std::decay_t; + if constexpr (std::is_same_v) { os << (val ? kBoolValueTrue : kBoolValueFalse); - } else if constexpr (std::is_same_v || std::is_same_v || - std::is_integral_v) { + } else if constexpr (std::is_same_v || + std::is_same_v || std::is_integral_v) { os << val; } else { - // Note: below ugly template lambda can be replaced with 'static_assert(false);' in C++23 - []() { static_assert(flag, "no match"); } - (); + // Note: can be replaced with 'static_assert(false);' in C++23 + static_assert(always_false_v, "non-exhaustive visitor!"); } }, - _data); + singleLineCell._data); + return os; +} + +SimpleTable::Cell::size_type SimpleTable::Cell::width() const noexcept { + const auto maxWidthLineIt = std::ranges::max_element( + _singleLineCells, [](const auto &lhs, const auto &rhs) { return lhs.width() < rhs.width(); }); + return maxWidthLineIt == _singleLineCells.end() ? size_type{} : maxWidthLineIt->width(); +} + +void SimpleTable::Cell::print(std::ostream &os, size_type linePos, size_type maxCellWidth) const { + os << ' ' << Align(AlignTo::kLeft) << std::setw(maxCellWidth); + + if (linePos < size()) { + os << _singleLineCells[linePos]; + } else { + // No value for this line pos for given cell, just print spaces + os << kEmptyValueChar; + } os << ' ' << kColumnSep; } +bool SimpleTable::Row::isMultiLine() const noexcept { + return std::ranges::any_of(_cells, [](const Cell &cell) { return cell.size() > 1U; }); +} + void SimpleTable::Row::print(std::ostream &os, std::span maxWidthPerColumn) const { - os << kColumnSep; - size_type columnPos = 0; - for (const Cell &cell : _cells) { - cell.print(os, maxWidthPerColumn[columnPos++]); + const auto maxSingleLineCellsIt = + std::ranges::max_element(_cells, [](const Cell &lhs, const Cell &rhs) { return lhs.size() < rhs.size(); }); + const auto maxNbSingleLineCells = maxSingleLineCellsIt == _cells.end() ? size_type{} : maxSingleLineCellsIt->size(); + for (std::remove_const_t linePos = 0; linePos < maxNbSingleLineCells; ++linePos) { + os << kColumnSep; + + size_type columnPos{}; + + for (const Cell &cell : _cells) { + cell.print(os, linePos, maxWidthPerColumn[columnPos]); + ++columnPos; + } + + os << '\n'; } - os << '\n'; } SimpleTable::MaxWidthPerColumnVector SimpleTable::computeMaxWidthPerColumn() const { - // We assume that each row has same number of cells, no silly checks here + // We assume that each row has same number of cells const size_type nbColumns = _rows.front().size(); MaxWidthPerColumnVector res(nbColumns, 0); for (const Row &row : _rows) { - if (!row.isDivider()) { - for (size_type columnPos = 0; columnPos < nbColumns; ++columnPos) { - res[columnPos] = std::max(res[columnPos], static_cast(row[columnPos].size())); - } + if (row.isDivider()) { + continue; + } + for (size_type columnPos = 0; columnPos < nbColumns; ++columnPos) { + const Cell &cell = row[columnPos]; + + res[columnPos] = std::max(res[columnPos], static_cast(cell.width())); } } return res; } -SimpleTable::Cell::string_type SimpleTable::ComputeLineSep(std::span maxWidthPerColumnVector) { - const size_type sumWidths = std::accumulate(maxWidthPerColumnVector.begin(), maxWidthPerColumnVector.end(), 0U); +namespace { +auto ComputeLineSep(std::span maxWidthPerColumnVector, char cellFiller, char columnSep) { + const SimpleTable::size_type sumWidths = + std::accumulate(maxWidthPerColumnVector.begin(), maxWidthPerColumnVector.end(), 0U); // 3 as one space before, one space after the field name and column separator. +1 for the first column separator - const size_type tableWidth = sumWidths + maxWidthPerColumnVector.size() * 3 + 1; - Cell::string_type lineSep(tableWidth, '-'); + const SimpleTable::size_type tableWidth = sumWidths + maxWidthPerColumnVector.size() * 3 + 1; - size_type curWidth = 0; - lineSep[curWidth] = '+'; + SimpleTable::value_type::value_type::value_type::string_type lineSep(tableWidth, cellFiller); + + SimpleTable::size_type curWidth{}; + lineSep[curWidth] = columnSep; for (auto maxWidth : maxWidthPerColumnVector) { curWidth += maxWidth + 3; - lineSep[curWidth] = '+'; + lineSep[curWidth] = columnSep; } return lineSep; } +} // namespace std::ostream &operator<<(std::ostream &os, const SimpleTable &table) { if (table._rows.empty()) { return os; } + const auto maxWidthPerColumnVector = table.computeMaxWidthPerColumn(); - const auto lineSep = SimpleTable::ComputeLineSep(maxWidthPerColumnVector); + const auto lineSep = ComputeLineSep(maxWidthPerColumnVector, '-', '+'); + const auto multiLineSep = ComputeLineSep(maxWidthPerColumnVector, '~', '|'); os << lineSep << '\n'; - bool printHeader = table._rows.size() > 1U; - for (const auto &row : table._rows) { + bool isLastLineSep = false; + for (SimpleTable::size_type rowPos{}, nbRows = table.size(); rowPos < nbRows; ++rowPos) { + const SimpleTable::Row &row = table[rowPos]; + if (row.isDivider()) { os << lineSep << '\n'; - } else { - row.print(os, maxWidthPerColumnVector); + isLastLineSep = true; + continue; } - if (printHeader) { + + const bool isMultiLine = row.isMultiLine(); + + if (isMultiLine && !isLastLineSep) { + os << multiLineSep << '\n'; + } + + row.print(os, maxWidthPerColumnVector); + + if (rowPos == 0 && nbRows > 1) { + // header sep os << lineSep << '\n'; - printHeader = false; + isLastLineSep = true; + } else { + isLastLineSep = isMultiLine && rowPos + 1 < nbRows && !table[rowPos + 1].isDivider(); + + if (isLastLineSep) { + os << multiLineSep << '\n'; + } } } diff --git a/src/tech/test/simpletable_test.cpp b/src/tech/test/simpletable_test.cpp index 85e868f3..106e725f 100644 --- a/src/tech/test/simpletable_test.cpp +++ b/src/tech/test/simpletable_test.cpp @@ -83,23 +83,142 @@ class SimpleTableTest : public ::testing::Test { TEST_F(SimpleTableTest, SettingRowDirectly) { EXPECT_EQ(table.size(), 1U); fill(); - EXPECT_EQ(table[2].front().size(), 7U); - EXPECT_EQ(table.back().front().size(), 13U); + EXPECT_EQ(table[2].front().size(), 1U); + EXPECT_EQ(table[2].front().front().width(), 7U); + + EXPECT_EQ(table.back().front().size(), 1U); + EXPECT_EQ(table.back().front().front().width(), 13U); + + std::ostringstream ss; + + ss << '\n' << table; + static constexpr std::string_view kExpected = R"( ++---------------+----------+-----------------------+ +| Amount | Currency | This header is longer | ++---------------+----------+-----------------------+ +| 1235 | EUR | Nothing here | +| 3456.78 | USD | 42 | +| -677234.67 | SUSHI | -12 | +| -677256340000 | KEBAB | -34.09 | ++---------------+----------+-----------------------+)"; + + EXPECT_EQ(ss.view(), kExpected); +} + +TEST_F(SimpleTableTest, MultiLineFields) { + fill(); + + table[1][2].push_back(SimpleTable::CellLine("... but another line!")); + table[3][0].push_back(SimpleTable::CellLine(true)); + + table.emplace_back("999.25", "KRW", 16820100000000000000UL); std::ostringstream ss; ss << '\n' << table; + static constexpr std::string_view kExpected = R"( +---------------+----------+-----------------------+ | Amount | Currency | This header is longer | +---------------+----------+-----------------------+ | 1235 | EUR | Nothing here | +| | | ... but another line! | +|~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~| | 3456.78 | USD | 42 | +|~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~| | -677234.67 | SUSHI | -12 | +| yes | | | +|~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~| | -677256340000 | KEBAB | -34.09 | +| 999.25 | KRW | 16820100000000000000 | +---------------+----------+-----------------------+)"; EXPECT_EQ(ss.view(), kExpected); } +TEST_F(SimpleTableTest, EmptyCellShouldBePossible) { + fill(); + + table.emplace_back(SimpleTable::Cell{12, -4}, SimpleTable::Cell{}, "Nothing here"); + + std::ostringstream ss; + + ss << '\n' << table; + + static constexpr std::string_view kExpected = R"( ++---------------+----------+-----------------------+ +| Amount | Currency | This header is longer | ++---------------+----------+-----------------------+ +| 1235 | EUR | Nothing here | +| 3456.78 | USD | 42 | +| -677234.67 | SUSHI | -12 | +| -677256340000 | KEBAB | -34.09 | +|~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~| +| 12 | | Nothing here | +| -4 | | | ++---------------+----------+-----------------------+)"; + + EXPECT_EQ(ss.view(), kExpected); +} + +class DividerLineTest : public ::testing::Test { + protected: + void SetUp() override { fill(); } + + void fill() { + table.emplace_back(1); + table.emplace_back(2); + table.emplace_back(""); + table.emplace_back(SimpleTable::Row::kDivider); + table.emplace_back(4); + table.emplace_back(SimpleTable::Row::kDivider); + } + + SimpleTable table; + std::ostringstream ss; +}; + +TEST_F(DividerLineTest, SingleLineRows) { + ss << '\n' << table; + + static constexpr std::string_view kExpected = R"( ++---+ +| 1 | ++---+ +| 2 | +| | ++---+ +| 4 | ++---+ ++---+)"; + + EXPECT_EQ(ss.view(), kExpected); +} + +TEST_F(DividerLineTest, WithMultiLine) { + table[1][0].push_back(SimpleTable::CellLine(42)); + table[1][0].push_back(SimpleTable::CellLine(true)); + + table[4][0].push_back(SimpleTable::CellLine(false)); + + ss << '\n' << table; + + static constexpr std::string_view kExpected = R"( ++-----+ +| 1 | ++-----+ +| 2 | +| 42 | +| yes | +|~~~~~| +| | ++-----+ +| 4 | +| no | ++-----+ ++-----+)"; + + EXPECT_EQ(ss.view(), kExpected); +} + } // namespace cct \ No newline at end of file