From 28172fc3d68f00d08eb7156f07daffd963ca99f9 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 13 Jul 2025 21:17:13 -0500 Subject: [PATCH] xo-indentlog: add general-purposee pretty-printing [WIP] --- CMakeLists.txt | 14 +- example/CMakeLists.txt | 8 +- include/xo/indentlog/log_config.hpp | 12 + include/xo/indentlog/log_state.hpp | 31 +- include/xo/indentlog/log_streambuf.hpp | 129 ++++++-- include/xo/indentlog/print/concat.hpp | 20 ++ include/xo/indentlog/print/hex.hpp | 7 + include/xo/indentlog/print/pad.hpp | 3 + include/xo/indentlog/print/ppconfig.hpp | 34 +++ .../xo/indentlog/print/ppdetail_atomic.hpp | 119 ++++++++ include/xo/indentlog/print/ppstr.hpp | 190 ++++++++++++ include/xo/indentlog/print/pretty.hpp | 275 ++++++++++++++++++ include/xo/indentlog/print/pretty_concat.hpp | 14 + include/xo/indentlog/print/pretty_tag.hpp | 56 ++++ include/xo/indentlog/print/pretty_vector.hpp | 67 +++++ include/xo/indentlog/print/quoted.hpp | 6 + include/xo/indentlog/print/tag.hpp | 102 ++++--- include/xo/indentlog/print/tostr.hpp | 6 +- include/xo/indentlog/print/vector.hpp | 3 +- include/xo/indentlog/scope.hpp | 90 ++++-- utest/CMakeLists.txt | 4 +- utest/log_streambuf.test.cpp | 107 +++++++ utest/pretty_vector.test.cpp | 97 ++++++ utest/toppstr.test.cpp | 55 ++++ 24 files changed, 1327 insertions(+), 122 deletions(-) create mode 100644 include/xo/indentlog/print/ppconfig.hpp create mode 100644 include/xo/indentlog/print/ppdetail_atomic.hpp create mode 100644 include/xo/indentlog/print/ppstr.hpp create mode 100644 include/xo/indentlog/print/pretty.hpp create mode 100644 include/xo/indentlog/print/pretty_concat.hpp create mode 100644 include/xo/indentlog/print/pretty_tag.hpp create mode 100644 include/xo/indentlog/print/pretty_vector.hpp create mode 100644 utest/log_streambuf.test.cpp create mode 100644 utest/pretty_vector.test.cpp create mode 100644 utest/toppstr.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a7ff77..f595834 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# indentlog/CMakeLists.txt +# xo-indentlog/CMakeLists.txt cmake_minimum_required(VERSION 3.10) @@ -28,12 +28,8 @@ add_subdirectory(utest) set(SELF_LIB indentlog) xo_add_headeronly_library(${SELF_LIB}) xo_install_library4(${SELF_LIB} ${PROJECT_NAME}Targets) -xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) -# ---------------------------------------------------------------- -# docs targets depend on all the other library/utest targets -# -#add_subdirectory(docs) +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) # ---------------------------------------------------------------- @@ -45,4 +41,10 @@ if (XO_ENABLE_EXAMPLES) install(TARGETS indentlog_ex4 DESTINATION bin/indentlog/example) endif() +# ---------------------------------------------------------------- +# docs targets depends on other library/utest/exec targets, +# must come after them +# +add_subdirectory(docs) + # end CMakeLists.txt diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 0f629f7..72eaa8b 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,9 +1,7 @@ -set(PROJECT_CXX_FLAGS "--std=c++20") -add_definitions(${PROJECT_CXX_FLAGS}) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED True) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") +#set(CMAKE_CXX_STANDARD 20) +#set(CMAKE_CXX_STANDARD_REQUIRED True) +#set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") #include(cmake/FindSphinx.cmake) diff --git a/include/xo/indentlog/log_config.hpp b/include/xo/indentlog/log_config.hpp index 751e62b..198d36a 100644 --- a/include/xo/indentlog/log_config.hpp +++ b/include/xo/indentlog/log_config.hpp @@ -19,6 +19,10 @@ namespace xo { static bool time_local_flag; /* true to log time-of-day with microsecond precision; false for millisecond precision */ static bool time_usec_flag; + /* true to enable pretty-printing (see basic_scope::log()) */ + static bool pretty_print_enabled; + /* when pretty-printing enabled, use newlines in effort to avoid writing beyond this margin */ + static std::uint32_t right_margin; /* spaces per nesting level. 0 -> no indenting */ static std::uint32_t indent_width; /* max #of spaces to introduce when indenting */ @@ -58,6 +62,14 @@ namespace xo { bool log_config_impl::time_usec_flag = true; + template + bool + log_config_impl::pretty_print_enabled = true; + + template + std::uint32_t + log_config_impl::right_margin = 130; + template std::uint32_t log_config_impl::indent_width = 2; diff --git a/include/xo/indentlog/log_state.hpp b/include/xo/indentlog/log_state.hpp index f188a7c..e28fb2c 100644 --- a/include/xo/indentlog/log_state.hpp +++ b/include/xo/indentlog/log_state.hpp @@ -35,6 +35,7 @@ namespace xo { void decr_nesting() { --nesting_level_; } std::ostream & ss() { return ss_; } + log_streambuf_type & sbuf() { return *p_sbuf_phase1_.get(); } void check_print_time(utc_nanos now_tm) { using xo::time::timeutil; @@ -116,12 +117,6 @@ namespace xo { */ std::unique_ptr p_sbuf_phase1_; - /* #of characters found in .p_sbuf_phase1 since last \n. - * this value is established+updated in .flush2sbuf(). - * (in particular ignored by stream .ss()) - */ - std::size_t lpos_ = 0; - /* whenever .set_location() is called: * - capture (file, line) * - print them near right margin with next output line @@ -256,9 +251,14 @@ namespace xo { state_impl::flush2sbuf(std::streambuf * p_sbuf) { log_streambuf_type * sbuf1 = this->p_sbuf_phase1_.get(); + + // instead of post-processing, rely on newline-aware log_streambuf + // to indent in advance + +#ifdef OBSOLETE log_streambuf_type * sbuf2 = this->p_sbuf_phase2_.get(); - /* generally expecting sbuf to contain one line of output. + /* often sbuf contains one line of output. * if it contains multiple newlines, need to indent * after each one. * @@ -269,8 +269,8 @@ namespace xo { * in the unlikely event that it's non-zero */ char const * s = sbuf1->lo(); - char const * e = s + sbuf1->pos(); + char const * e = s + sbuf1->pos(); char const * p = s; /* point to first space following a non-space character. @@ -290,8 +290,14 @@ namespace xo { /* for indenting, looking for first 'space following non-space, on first line', if any */ - std::size_t lpos_on_newline = 0; +#ifdef OBSOLETE + // ..multiline input should have already been indented by custom log_streambuf. + // may need to extend to recognize terminal control sequences like below + std::size_t lpos_on_newline = 0; +#endif + +#ifdef OBSOLETE while(p < e) { if(space_after_nonspace) { ; @@ -318,8 +324,10 @@ namespace xo { in_color_escape = false; +#ifdef OBSOLETE lpos_on_newline = this->lpos_; this->lpos_ = 0; +#endif ++p; break; @@ -329,6 +337,7 @@ namespace xo { ++p; } } +#endif /* p=e or *p=\n */ @@ -382,9 +391,11 @@ namespace xo { n_indent += std::min(this->nesting_level_ * log_config::indent_width, log_config::max_indent_width); +#ifdef OBSOLETE // nice try, broken for multiline input + written before log_streambuf calculated lpos /* this is just to indent for per-line entry/exit label */ if(space_after_nonspace) n_indent += (space_after_nonspace - s); +#endif for(std::uint32_t i = 0; i < n_indent; ++i) sbuf2->sputc(' '); @@ -394,6 +405,8 @@ namespace xo { /* now write entire contents of *sbuf2 to clog */ p_sbuf->sputn(sbuf2->lo(), sbuf2->pos()); +#endif + p_sbuf->sputn(sbuf1->lo(), sbuf1->pos()); /* reset streams for next message */ this->reset_stream(); diff --git a/include/xo/indentlog/log_streambuf.hpp b/include/xo/indentlog/log_streambuf.hpp index 1a76165..855fc05 100644 --- a/include/xo/indentlog/log_streambuf.hpp +++ b/include/xo/indentlog/log_streambuf.hpp @@ -2,6 +2,7 @@ #pragma once +#include "print/quoted_char.hpp" #include #include #include // e.g. for std::memcpy() @@ -9,13 +10,14 @@ #include namespace xo { - /* recycling buffer for logging. - * write to self-extending storage array; - */ - template + /** recycling buffer for logging and pretty-printing + * - write to self-extending storage array + * - track position relative to start of line + **/ + template > class log_streambuf : public std::streambuf { public: - log_streambuf(std::uint32_t buf_z) { + log_streambuf(std::uint32_t buf_z, bool debug_flag = false) : debug_flag_{debug_flag} { this->buf_v_.resize(buf_z); this->reset_stream(); } /*ctor*/ @@ -25,40 +27,93 @@ namespace xo { char const * hi() const { return this->lo() + this->capacity(); } std::uint32_t pos() const { return this->pptr() - this->pbase(); } + /** number of characters since start of line (last \n or \r) **/ + std::uint32_t lpos() const { return pos() - solpos_; } + + bool debug_flag() const { return debug_flag_; } + void reset_stream() { char * p_lo = &(this->buf_v_[0]); char * p_hi = p_lo + this->capacity(); /* tells parent our buffer extent */ this->setp(p_lo, p_hi); + + this->solpos_ = 0; } /*reset_stream*/ + void rewind_to(std::uint32_t p) { + /* .setp(): using for side effect: sets .pptr to .pbase */ + this->setp(this->pbase(), this->epptr()); + this->pbump(p); + } + protected: + /** expand buffer storage (by 2x), preserve current contents **/ + void + expand_to(std::size_t new_z) { + char * old_pptr = pptr(); + std::streamsize old_n = old_pptr - pbase(); + + assert(old_n <= static_cast(buf_v_.size())); + assert(new_z > buf_v_.capacity()); + + this->buf_v_.resize(new_z); + + char * p_base = &(this->buf_v_[0]); + char * p_hi = p_base + this->buf_v_.capacity(); + + this->setp(p_base, p_hi); + this->pbump(old_n); + } /*expand*/ + virtual std::streamsize xsputn(char const * s, std::streamsize n) override { /* s must be an address in [this->lo() .. this->lo() + capacity()] */ - assert(this->hi() >= this->pptr()); + assert(hi() >= pptr()); -#ifdef NOT_USING_DEBUG - std::cout << "xsputn: pbase=" << (void *)(this->pbase()) - << ", pptr=" << (void*)(this->pptr()) - << "(+" << (this->pptr() - this->lo()) << ")" - << ", n=" << n << " -> (+" << (this->pptr() + n - this->lo()) << ")" - << ", buf_v.size=" << this->buf_v_.size() - << std::endl; -#endif - //std::cout << "xsputn: s=" << quot(string_view(s, n)) << ", n=" << n << std::endl; + if (pptr() + n > hi()) { + this->expand_to(std::max(2 * this->buf_v_.capacity(), std::size_t(this->pos() + n + 1))); + } + + if (debug_flag_) { + std::cout << "xsputn: pbase=" << (void *)(this->pbase()) + << ", pptr=" << (void*)(this->pptr()) + << "(+" << (this->pptr() - this->lo()) << ")" + << ", n=" << n << " -> (+" << (this->pptr() + n - this->lo()) << ")" + << ", buf_v.size=" << this->buf_v_.size() + << std::endl; + } + + std::streamsize ncopied = 0; if (this->pptr() + n > this->hi()) { - n = this->hi() - this->pptr(); - std::memcpy(this->pptr(), s, n); + ncopied = this->hi() - this->pptr(); } else { - std::memcpy(this->pptr(), s, n); + ncopied = n; } - this->pbump(n); - return n; + if (debug_flag_) { + std::cout << "xsputn: copying ncopied=" << ncopied << " (/n=" << n << ") bytes into range [lo,hi)" + << ", lo=" << (void*)this->pptr() + << ", hi=" << (void*)(this->pptr() + n) + << std::endl; + } + + std::memcpy(this->pptr(), s, ncopied); + + /* scan range [pptr, pptr+n] backwards, to account for newline (if any) */ + for (char const * p_lo = this->pptr(), * p_hi = p_lo + n - 1, * p = p_hi; p >= p_lo; --p) { + if (*p == '\n' || *p == '\r') { + this->solpos_ = (p+1 - this->pbase()); + break; + } + } + + this->pbump(ncopied); + + return ncopied; } /*xsputn*/ virtual int_type @@ -69,25 +124,31 @@ namespace xo { assert(old_n <= static_cast(this->buf_v_.size())); - std::size_t new_z = 2 * this->buf_v_.size(); + if (debug_flag_) { + std::cout << "overflow: new_ch=" << quoted_char(new_ch) << std::endl; + } + + this->expand_to(2 * buf_v_.size()); - this->buf_v_.resize(new_z); this->buf_v_[old_n] = new_ch; + this->pbump(1); - /* 'buffered range' will now be .buf_v[old_n .. new_z] */ - char * p_base = &(this->buf_v_[0]); - //char * p_lo = &(this->buf_v_[old_n+1]); - char * p_hi = p_base + this->buf_v_.capacity(); + if ((new_ch == static_cast('\n')) || (new_ch == static_cast('\r'))) + this->solpos_ = this->pos(); - this->setp(p_base, p_hi); - this->pbump(old_n + 1); /*see 'this->buf_v_[old_n] - new_ch' above*/ - - return new_ch; + if (new_ch == Traits::eof()) { + /* reminder: returning eof sets badbit on ostream */ + return Traits::not_eof(new_ch); + } else { + return new_ch; + } } /*overflow*/ /* off. offset, relative to starting point dir. * dir. * which. in|out|both + * + * Note that off=0,dir=cur,which=out reads offset */ virtual pos_type seekoff(off_type off, std::ios_base::seekdir dir, @@ -114,8 +175,14 @@ namespace xo { } /*seekoff*/ private: - /* buffered output stored here */ + /** position (relative to pbase) one character after last \n or \r. + * Use to drive @ref lpos + **/ + std::size_t solpos_ = 0; + /** buffered output stored here **/ std::vector buf_v_; + /** true to debug log_streambuf itself **/ + bool debug_flag_ = false; }; /*log_streambuf*/ } /*namespace xo*/ diff --git a/include/xo/indentlog/print/concat.hpp b/include/xo/indentlog/print/concat.hpp index 223900e..3c166c4 100644 --- a/include/xo/indentlog/print/concat.hpp +++ b/include/xo/indentlog/print/concat.hpp @@ -2,6 +2,7 @@ #pragma once +#include "ppdetail_atomic.hpp" #include #include // for std::move() @@ -37,6 +38,25 @@ namespace xo { return os; } /*operator<<*/ +#ifndef ppdetail_atomic + namespace print { + /* concat expected to be used on short string-like things. + * i.e. don't want structure visible to pretty-printer. + * could be using it like concat("boeing", 777) + */ + template + struct ppdetail> { + using target_type = concat_impl; + + static bool print_upto(ppstate * pps, const target_type & x) { + return ppdetail_atomic::print_upto(pps, x); + } + static void print_pretty(ppstate * pps, const target_type & x) { + ppdetail_atomic::print_pretty(pps, x); + } + }; + } +#endif } /*namespace xo*/ /* end concat.hpp */ diff --git a/include/xo/indentlog/print/hex.hpp b/include/xo/indentlog/print/hex.hpp index 8a6517d..4fbf49f 100644 --- a/include/xo/indentlog/print/hex.hpp +++ b/include/xo/indentlog/print/hex.hpp @@ -2,6 +2,7 @@ #pragma once +#include "ppdetail_atomic.hpp" #include #include @@ -140,6 +141,12 @@ namespace xo { return os; } +#ifndef ppdetail_atomic + namespace print { + PPDETAIL_ATOMIC(hex_view); + } +#endif + } /*namespace xo*/ /* end hex.hpp */ diff --git a/include/xo/indentlog/print/pad.hpp b/include/xo/indentlog/print/pad.hpp index e85d9c2..43aa7e6 100644 --- a/include/xo/indentlog/print/pad.hpp +++ b/include/xo/indentlog/print/pad.hpp @@ -38,6 +38,9 @@ namespace xo { inline pad_impl pad(std::uint32_t n, char pad_char = ' ') { return pad_impl(n, pad_char); } + inline pad_impl + spaces(std::uint32_t n) { return pad_impl(n, ' '); } + inline std::ostream & operator<<(std::ostream & s, pad_impl const & pad) diff --git a/include/xo/indentlog/print/ppconfig.hpp b/include/xo/indentlog/print/ppconfig.hpp new file mode 100644 index 0000000..1cc6059 --- /dev/null +++ b/include/xo/indentlog/print/ppconfig.hpp @@ -0,0 +1,34 @@ +/* @file ppconfig.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include + +namespace xo { + namespace print { + + /** @class ppconfig + * @brief hold pretty-printer control parameters + * + * Need one read-only instance of this to invoke pretty printer + **/ + struct ppconfig { + /** @defgroup ppconfig-instance-vars ppconfig instance variables **/ + ///@{ + + /** max line length. + * Pretty-printer will introduce newlines if needed + * to stay to the left of this margin + **/ + std::uint32_t right_margin_ = 135; + + /** amount of additional indent per nesting level **/ + std::uint32_t indent_width_ = 2; + + ///@} + }; + } /*namespace print*/ +} /*namespace xo*/ diff --git a/include/xo/indentlog/print/ppdetail_atomic.hpp b/include/xo/indentlog/print/ppdetail_atomic.hpp new file mode 100644 index 0000000..05614ee --- /dev/null +++ b/include/xo/indentlog/print/ppdetail_atomic.hpp @@ -0,0 +1,119 @@ +/* ppdetail_atomic.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include +#include + +namespace xo { + namespace print { + struct ppstate; // see pretty.hpp + +// Defining this means ppdetail_atomic is not used. +// In that case where not explicitly specialized ppdetail will revert to ordinary printing for a type, +// instead of giving compile-time error about missing template specialization of ppdetail + +#define ppdetail_atomic ppdetail + + /** @class ppdetail + * @brief template for opt-in to pretty-printer + * + * Provide a specialization that covers each type you want participating in pretty-printing. + * Default treats T as atomic. + * + * Expectations for specializers: + * - don't print final newline: + * - parent container will do that + * - want option to put closing ),],>,} on the same line + **/ + template + struct ppdetail; + + template + struct ppdetail_atomic; + + template + struct ppdetail_atomic { + /** 1. print @p x to private stream @ref scratch_ss_ + * 2. return true iff N = number of characters printed satisfies N <= @p budget. + * content actually printed to *sbuf may be used as-is in this case + * 3. return false otherwise. Will trigger non-degenerate pretty-printing. + * 4. in any case consume some of @ref scratch_sbuf_ + **/ + static bool print_upto(ppstate * pps, const T & x); + /** print @p x to private stream @ref scratch_ss_ **/ + static void print_pretty(ppstate * pps, const T & x); + }; + +#ifndef ppdetail_atomic + template + struct ppdetail { + using target_type = const char[N]; + + static bool print_upto(ppstate * pps, const target_type & x) { + return ppdetail_atomic::print_upto(pps, x); + } + static void print_pretty(ppstate * pps, const target_type & x) { + ppdetail_atomic::print_pretty(pps, x); + } + }; + + template <> + struct ppdetail { + static bool print_upto(ppstate * pps, const char * x) { + return ppdetail_atomic::print_upto(pps, x); + } + static void print_pretty(ppstate * pps, const char * x) { + ppdetail_atomic::print_pretty(pps, x); + } + }; + + +#define PPDETAIL_ATOMIC_BODY(target_type) \ + struct ppdetail { \ + static bool print_upto(ppstate * pps, const target_type & x) { \ + return ppdetail_atomic::print_upto(pps, x); \ + } \ + \ + static void print_pretty(ppstate * pps, const target_type & x) { \ + ppdetail_atomic::print_pretty(pps, x); \ + } \ + } + +#define PPDETAIL_ATOMIC_BODY_CONST(target_type) \ + struct ppdetail { \ + static bool print_upto(ppstate * pps, const target_type & x) { \ + return ppdetail_atomic::print_upto(pps, x); \ + } \ + \ + static void print_pretty(ppstate * pps, const target_type & x) { \ + ppdetail_atomic::print_pretty(pps, x); \ + } \ + } + +#define PPDETAIL_ATOMIC(target_type) \ + template<> \ + PPDETAIL_ATOMIC_BODY(target_type) + +#define PPDETAIL_ATOMIC_CONST(target_type) \ + template<> \ + PPDETAIL_ATOMIC_BODY_CONST(target_type) + + PPDETAIL_ATOMIC(bool); + PPDETAIL_ATOMIC(char); +// PPDETAIL_ATOMIC_CONST(char); + PPDETAIL_ATOMIC(std::int64_t); + PPDETAIL_ATOMIC(std::uint64_t); + PPDETAIL_ATOMIC(std::int32_t); + PPDETAIL_ATOMIC(std::uint32_t); + PPDETAIL_ATOMIC(std::string); + + using voidptr = void*; + PPDETAIL_ATOMIC(voidptr); +#endif + + } +} diff --git a/include/xo/indentlog/print/ppstr.hpp b/include/xo/indentlog/print/ppstr.hpp new file mode 100644 index 0000000..358c14b --- /dev/null +++ b/include/xo/indentlog/print/ppstr.hpp @@ -0,0 +1,190 @@ +/* file ppstr.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include "xo/indentlog/print/pretty.hpp" +#include "xo/indentlog/print/pretty_vector.hpp" +#include +#include +#include + +namespace xo { + + /** @class ppconcat + * @brief Container for a tuple of values to be pretty-printed in order + * @tparam Args... parameter pack of argument types + * + * We don't use the tuple directly, so that we don't inadvertently + * usurp pretty-printing behavior for all tuples. + * + **/ + template + struct ppconcat { + /** tuple to be pretty-printed. See toppstr(), toppstr2() **/ + std::tuple contents_; + }; + + namespace print { + namespace detail { + inline bool + ppconcat_printupto_aux(print::ppstate *) { + return true; + } + + template + bool + ppconcat_printupto_aux(print::ppstate * pps, T && x) { + if (!pps->print_upto(x)) + return false; + if (!pps->has_margin()) + return false; + return true; + } + + template + bool + ppconcat_printupto_aux(print::ppstate * pps, T && x, Tn && ...rest) { + if (!pps->print_upto(x)) + return false; + if (!pps->has_margin()) + return false; + return ppconcat_printupto_aux(pps, rest...); + } + + inline void + ppconcat_print_pretty_rest(print::ppstate *, std::uint32_t) {} + + template + void + ppconcat_print_pretty_rest(print::ppstate * pps, std::uint32_t ci1, T && x) { + pps->newline_indent(ci1); + pps->pretty(x); + } + + template + void + ppconcat_print_pretty_rest(print::ppstate * pps, std::uint32_t ci1, T && x, Tn && ...rest) { + pps->newline_indent(ci1); + pps->pretty(x); + + ppconcat_print_pretty_rest(pps, ci1, rest...); + } + + inline void + ppconcat_print_pretty_aux(print::ppstate *, std::uint32_t /*ci1*/) {} + + template + void + ppconcat_print_pretty_aux(print::ppstate * pps, std::uint32_t ci1, T && x, Tn && ...rest) { + // first item doesn't get extra indent. Want pretty output like: + // + // pretty(first) + // pretty(second) + // pretty(third) + // .. + // + pps->pretty(x); + ppconcat_print_pretty_rest(pps, ci1, rest...); + } + } + + /** implement pretty-printing for template @c ppconcat r**/ + template + struct ppdetail> { + /** try to print @p target on one line. + * return false if budget (space until right margin) exhausted + * or if an embedded newline appears + * + * @return true on success, otherwise false. + **/ + static bool print_upto(ppstate * pps, ppconcat target) { + std::uint32_t saved = pps->pos(); + + if (!pps->has_margin()) + return false; + + if (std::apply( + [pps](auto &&... args) { + return detail::ppconcat_printupto_aux(pps, std::forward(args)...); + }, + std::forward>(target.contents_) + ) == false) + { + return false; + } + + return pps->scan_no_newline(saved); + } + + /** pretty-print @p target using multiple lines + **/ + static void print_pretty(ppstate * pps, ppconcat target) { + std::uint32_t ci0 = pps->lpos(); + std::uint32_t ci1 = ci0 + pps->indent_width(); + + std::apply( + [pps, ci1](auto &&... args) { + detail::ppconcat_print_pretty_aux(pps, ci1, + std::forward(args)...); + }, + std::forward>(target.contents_) + ); + } + }; + } /*namespace print*/ + + /** @return string comprised of pretty-printed sequence of values in @p args + * + * Example: + * @code + * toppstr("hello") -> "hello" + * toppstr("string1", .., "string99") + * -> "string1 + * string2 + * ... + * string99" + * @endcode + **/ + template + std::string toppstr(Tn&&... args) { + print::ppconfig ppc; + + std::stringstream ss; + print::ppstate_standalone pps(&ss, 0, &ppc); + + // - std::decay_t remove reference and cv-qualifiers + // - brace initialization of ppconcat and its contained tuple + pps.pretty(ppconcat...>{{std::forward(args)...}}); + + return ss.str(); + } + + /** like @ref toppstr, but use pretty-printing configuration @p ppc + * + * Example: + * @code + * ppconfig ppc{.right_margin_=20, .indent_width_=4}; + * toppstr2("string1", "string2", "string3") -> + * "string1 + * string2 + * string3" + * @endcode + **/ + template + std::string toppstr2(const print::ppconfig& ppc, Tn&&... args) { + std::stringstream ss; + print::ppstate_standalone pps(&ss, 0, &ppc); + + // - std::decay_t remove reference and cv-qualifiers + // - brace initialization of ppconcat and its contained tuple + pps.pretty(ppconcat...>{{std::forward(args)...}}); + + return ss.str(); + } + +} /*namespace xo*/ + +/* end ppstr.hpp */ diff --git a/include/xo/indentlog/print/pretty.hpp b/include/xo/indentlog/print/pretty.hpp new file mode 100644 index 0000000..369713b --- /dev/null +++ b/include/xo/indentlog/print/pretty.hpp @@ -0,0 +1,275 @@ +/* file pretty.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include "xo/indentlog/print/ppconfig.hpp" +#include "xo/indentlog/log_streambuf.hpp" +#include "ppdetail_atomic.hpp" +#include "pad.hpp" +#include +#include +#include +#include + +namespace xo { + namespace print { + + /** @class ppstate + * @brief hold pretty-printer state + * + * Use: + * + * ppconfig ppc; + * ppstate pps(&cout, 0, &ppc); + * + * pps.pretty("first"); + * pps.pretty("second"); + **/ + struct ppstate { + using streambuf_type = log_streambuf>; + + explicit ppstate(std::uint32_t ci, const ppconfig * config, std::ostream * scratch_ss, streambuf_type * scratch_sbuf) + : scratch_sbuf_{scratch_sbuf}, scratch_ss_{scratch_ss}, + current_indent_{ci}, config_{config} + {} + + uint32_t indent_width() const { return config_->indent_width_; } + + std::uint32_t pos() const { return scratch_sbuf_->pos(); } + std::uint32_t lpos() const { return scratch_sbuf_->lpos(); } + + /** space available from current position until @c ppconfig.right_margin_ + * 0 if current position is already beyond right margin + **/ + std::uint32_t avail_margin() const { + std::uint32_t p = lpos(); + std::uint32_t m = config_->right_margin_; + + return (p < m) ? (m - p) : 0; + } + + bool has_margin() const { return avail_margin() > 0; } + + /** true if at least @p budget chars of space available + * before reaching right margin @c ppconfig.right_margin_ + **/ + bool has_budget(std::uint32_t budget) { + return avail_margin() >= budget; + } + + /** true if no newlines in range [@p lo, pos), + * where pos is current position + **/ + bool scan_no_newline(std::uint32_t start) const { + char const * p = scratch_sbuf_->lo() + start; + char const * e = scratch_sbuf_->lo() + pos(); + + for (; p < e; ++p) { + if (*p == '\n') + return false; + } + + return true; + } + + void indent(std::uint32_t tab) { + (*scratch_ss_) << spaces(tab); + } + + void newline_indent(std::uint32_t tab) { + (*scratch_ss_) << "\n"; + this->indent(tab); + } + + template + void write(T && x) { + (*scratch_ss_) << x; + } + + uint32_t incr_nesting_level() { return ++nesting_level_; } + uint32_t decr_nesting_level() { return --nesting_level_; } + + virtual void commit() {} + + template + bool print_upto(T && x); + + template + ppstate& pretty(T && x); + + /** like pretty(x), but follow with trailing \n **/ + template + ppstate& prettyn(T && x); + + private: + /** scratch space for pretty-printed output. + * Shared across nesting pretty calls. + * Caller responsible for saving/restoring state. + **/ + streambuf_type * scratch_sbuf_ = nullptr; + + /** ostream pointing to @ref scratch_sbuf_ **/ + std::ostream * scratch_ss_ = nullptr; + + /** level of reentrant pretty calls. + * Collect formatted text in @ref scratch_sbuf_ + * until end of toplevel @ref pretty call + **/ + std::uint32_t nesting_level_ = 0; + + /** current desired indent from left margin **/ + std::uint32_t current_indent_ = 0; + + /** readonly pretty-printer config **/ + const ppconfig * config_ = nullptr; + }; + + /** @class ppstate_standalone + * @brief like ppstate, but also holds streambuf + */ + struct ppstate_standalone : public ppstate { + explicit ppstate_standalone(std::ostream * os, std::uint32_t ci, const ppconfig * config) + : ppstate(ci, config, &scratch_ss_, &scratch_sbuf_), output_{os}, scratch_sbuf_{1024*1024} + {} + + virtual void commit() override { + output_->write(scratch_sbuf_.lo(), scratch_sbuf_.pos()); + reset_scratch_sbuf(); + } + + void reset_scratch_sbuf() { + /* reset scratch_sbuf for next time */ + scratch_sbuf_.pubseekoff(0, std::ios_base::beg, std::ios_base::out); + } + + /** send final output to this stream **/ + std::ostream * output_ = nullptr; + + /** streambuf (with newline tracking) **/ + streambuf_type scratch_sbuf_; + + /** always attached to @ref scratch_sbuf_ **/ + std::ostream scratch_ss_{&scratch_sbuf_}; + }; + + struct ppcommitter { + ppcommitter(ppstate * pps) : pps_{pps} { + pps_->incr_nesting_level(); + } + ~ppcommitter() { + check_commit(); + } + + void check_commit() { + if (pps_) { + ppstate * pps = pps_; + pps_ = nullptr; + if (pps->decr_nesting_level() == 0) + pps->commit(); + } + } + + ppstate * pps_ = nullptr; + }; + + template + bool + ppdetail_atomic::print_upto(ppstate * pps, const T & x) + { + /* position before we write */ + if (!pps->has_margin()) + return false; + + std::uint32_t start = pps->pos(); + + /* if x is non-atomic may optimize by checking budget more often */ + pps->write(x); + + if (!pps->has_margin()) + return false; + + /* scan characters [lo, hi) in line buffer. + * Newline -> caller should use print_pretty() + */ + return (pps->scan_no_newline(start)); + } + + template + void + ppdetail_atomic::print_pretty(ppstate * pps, const T & x) + { + /* In default implementation we don't know where to introduce newlines. + * Still need to calculate lpos though + */ + pps->write(x); + } + + template + bool + ppstate::print_upto(T && value) { + return ppdetail>::print_upto(this, value); + } + + template + ppstate & + ppstate::pretty(T && value) { + /* just because value fits doesn't mean we should print it. + * Decision depends on ancestor context + */ + + std::uint32_t saved = pos(); + + ppcommitter ppc(this); + + /* print_upto() modifies @ref scratch_sbuf_ (and @ref scratch_ss_) */ + bool fits = ppdetail>::print_upto(this, static_cast(value)); + + if (fits) { + /* fits on 1 line -> pretty-printing maybe not required */ + return *this; + } + + /* here: didn't fit -> split over multiple lines */ + + this->scratch_sbuf_->rewind_to(saved); + ppdetail>::print_pretty(this, value); + ppc.check_commit(); + + return *this; + } + + template + ppstate & + ppstate::prettyn(T && value) { + /* just because value fits doesn't mean we should print it. + * Decision depends on ancestor context + */ + + std::uint32_t saved = pos(); + + ppcommitter ppc(this); + + /* print_upto() modifies @ref scratch_sbuf_ (and @ref scratch_ss_) */ + bool fits = ppdetail>::print_upto(this, value); + + if (fits) { + this->newline_indent(0); + /* fits on 1 line -> pretty-printing maybe not required */ + return *this; + } + + /* here: didn't fit -> split over multiple lines */ + + this->scratch_sbuf_->rewind_to(saved); + ppdetail>::print_pretty(this, value); + this->newline_indent(0); + ppc.check_commit(); + + return *this; + } + + } /*namespace print*/ +} /*namespace xo*/ diff --git a/include/xo/indentlog/print/pretty_concat.hpp b/include/xo/indentlog/print/pretty_concat.hpp new file mode 100644 index 0000000..8b48493 --- /dev/null +++ b/include/xo/indentlog/print/pretty_concat.hpp @@ -0,0 +1,14 @@ +/* @file pretty_concat.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include "concat.hpp" +#include "pretty.hpp" + +namespace xo { +} + +/* end pretty_concat.hpp */ diff --git a/include/xo/indentlog/print/pretty_tag.hpp b/include/xo/indentlog/print/pretty_tag.hpp new file mode 100644 index 0000000..7402136 --- /dev/null +++ b/include/xo/indentlog/print/pretty_tag.hpp @@ -0,0 +1,56 @@ +/* file pretty_tag.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include "pretty.hpp" +#include "tag.hpp" + +namespace xo { + namespace print { + template + struct ppdetail> { + static bool print_upto(ppstate * pps, const tag_impl & tag) { + std::uint32_t saved = pps->pos(); + if (!pps->has_margin()) + return false; + + if (PrefixSpace) + pps->write(" "); + + /* skil colorizing here, since doesn't consume visible space */ + if (!pps->print_upto(concat((char const *)":", concat(tag.name(), (char const *)" ")))) + return false; + + if (!pps->print_upto(quot_impl(TagStyle == tagstyle::autoescape, tag.value()))) + return false; + + return pps->scan_no_newline(saved); + } + + static void print_pretty(ppstate * pps, const tag_impl & tag) { + /* pretty-print like + * :somename + * pretty(value) + */ + std::uint32_t ci0 = pps->lpos(); + std::uint32_t ci1 = ci0 + pps->indent_width(); + + if (PrefixSpace) + pps->write(" "); + pps->write(with_color(tag_config::tag_color, + concat((char const *)":", tag.name()))); + + pps->newline_indent(ci1); + pps->pretty(tag.value()); + + pps->newline_indent(ci0); + } + }; + + } /*namespace print*/ +} /*namespace xo*/ + +/* end pretty_tag.hpp */ diff --git a/include/xo/indentlog/print/pretty_vector.hpp b/include/xo/indentlog/print/pretty_vector.hpp new file mode 100644 index 0000000..efccb80 --- /dev/null +++ b/include/xo/indentlog/print/pretty_vector.hpp @@ -0,0 +1,67 @@ +/* file pretty_vector.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include "pretty.hpp" +#include "pad.hpp" +#include + +namespace xo { + namespace print { + template + struct ppdetail> { + static bool print_upto(ppstate * pps, const std::vector & x) { + std::uint32_t saved = pps->pos(); + if (!pps->has_margin()) + return false; + + pps->write("["); + for (size_t i = 0, z = x.size(); i < z; ++i) { + if (i > 0) + pps->write(", "); + + if (!pps->print_upto(x[i])) + return false; + if (!pps->has_margin()) + return false; + } + pps->write("]"); + if (!pps->has_margin()) + return false; + + return pps->scan_no_newline(saved); + } + + static void print_pretty(ppstate * pps, const std::vector & x) { + std::uint32_t ci0 = pps->lpos(); + + pps->write('['); + + std::uint32_t ci1 = ci0 + pps->indent_width(); + for (size_t i = 0, z = x.size(); i < z; ++i) { + pps->newline_indent(ci1); + pps->pretty(x[i]); + + if (i+1 < z) + pps->write(','); + } + + pps->newline_indent(ci0); + pps->write(']'); + } + }; + + template + struct ppdetail> { + static bool print_upto(ppstate * pps, const std::vector & x) { + return ppdetail>::print_upto(pps, x); + } + static void print_pretty(ppstate * pps, const std::vector & x) { + ppdetail>::print_pretty(pps, x); + } + }; + } +} diff --git a/include/xo/indentlog/print/quoted.hpp b/include/xo/indentlog/print/quoted.hpp index 16e6626..6de2bb9 100644 --- a/include/xo/indentlog/print/quoted.hpp +++ b/include/xo/indentlog/print/quoted.hpp @@ -5,6 +5,7 @@ #pragma once +#include "ppdetail_atomic.hpp" #include "tostr.hpp" #include #include @@ -147,6 +148,11 @@ namespace xo { auto unq(T && x) { return quot_impl(true /*unq_flag*/, std::forward(x)); } + +#ifndef ppdetail_atomic + template + PPDETAIL_ATOMIC_BODY(quot_impl); +#endif } /*namespace print*/ } /*namespace xo*/ diff --git a/include/xo/indentlog/print/tag.hpp b/include/xo/indentlog/print/tag.hpp index 0973506..ad55f3c 100644 --- a/include/xo/indentlog/print/tag.hpp +++ b/include/xo/indentlog/print/tag.hpp @@ -18,18 +18,45 @@ #define TAG2(x, y) xo::make_tag(x, y) #define XTAG(x) xo::xtag(STRINGIFY(x), x) -//#define XTAG2(x, y) xo::xtag(x, y) namespace xo { - // associate a name with a value - // - // will print like - // :name value - // - // NOTE: will search for operator<< overloads in the logutil - // namespace - //*/ - template + enum class tagstyle { + /** print with automatic escapes for embedded special characters + * (any of ' ','"','\','\n','\r','\t'). + * print inside double-quotes if an escape is required. + **/ + autoescape, + /** print literally. caller responsible for machine-readability **/ + raw, + }; + + /** K,V pair for printing. + * + * @tparam PrefixSpace if true print one space before :K + * @tparam TagStyle controls printing format + * @tparam Name typename for key K. + * @tparam Value typename for value V. + * + * 1. Uses escapes to preserve machine-readability of V. + * 2. Optionally colors K for readability. + * Can disable with + * @code + * xo::tag_config::tag_color = xo::color_spec_type::none() + * @endcode + * + * Printing styles + * @pre + * :name value // unadorned + * :name "value needing\nescapes" // with escapes + * :name // autoescape disabled + * @endpre + * + * NOTE: will search for operator<< overloads in the xo + * namespace + * + * + **/ + template struct tag_impl { tag_impl(Name const & n, Value const & v) : name_{n}, value_{v} {} @@ -42,71 +69,58 @@ namespace xo { private: Name name_; Value value_; - }; /*tag_impl*/ + }; - /* deduce tag template-type from arguments */ - template - tag_impl - make_tag(Name && n, Value && v) - { - return tag_impl(n, v); - } /*make_tag*/ - - template - tag_impl - make_tag(char const * n, Value && v) { - return tag_impl(n, v); - } /*make_tag*/ + // ----- xtag ----- template - tag_impl + auto //tag_impl> xtag(Name && n, Value && v) { - return tag_impl(n, v); + return tag_impl>(n, v); } /*xtag*/ template - tag_impl + auto //tag_impl> xtag(char const * n, Value && v) { - return tag_impl(n, v); + return tag_impl>(n, v); } /*xtag*/ - inline - tag_impl - xtag_pre(char const * n) { - return tag_impl(n, ""); - } /*xtag_pre*/ - // ----- tag ----- template - tag_impl + tag_impl tag(Name && n, Value && v) { - return tag_impl(n, v); - } /*tag*/ + return tag_impl(n, v); + } template - tag_impl + tag_impl tag(char const * n, Value && v) { - return tag_impl(n, v); - } /*tag*/ + return tag_impl(n, v); + } // ----- operator<< on tag_impl ----- - template + template inline std::ostream & operator<<(std::ostream &s, - tag_impl const & tag) + tag_impl const & tag) { using xo::print::unq; - if(PrefixSpace) + if (PrefixSpace) s << " "; s << with_color(tag_config::tag_color, concat((char const *)":", tag.name())) - << " " << unq(tag.value()); + << " "; + + if (TagStyle == tagstyle::autoescape) + s << unq(tag.value()); + else + s << tag.value(); return s; } /*operator<<*/ diff --git a/include/xo/indentlog/print/tostr.hpp b/include/xo/indentlog/print/tostr.hpp index e395cfe..ca50e3a 100644 --- a/include/xo/indentlog/print/tostr.hpp +++ b/include/xo/indentlog/print/tostr.hpp @@ -60,7 +60,7 @@ namespace xo { */ /* no-op terminal case */ - template + template Stream & tos(Stream & s) { return s; } @@ -70,14 +70,14 @@ namespace xo { // is the same as // s << a << b << c; // - template + template Stream & tos(Stream & s, T && x) { s << x; return s; } /*tos*/ template - Stream &tos(Stream &s, T &&x, Tn &&...rest) { + Stream &tos(Stream & s, T && x, Tn && ...rest) { s << x; return tos(s, rest...); } /*tos*/ diff --git a/include/xo/indentlog/print/vector.hpp b/include/xo/indentlog/print/vector.hpp index be5f449..5e87754 100644 --- a/include/xo/indentlog/print/vector.hpp +++ b/include/xo/indentlog/print/vector.hpp @@ -7,12 +7,13 @@ #include #include +#include namespace std { template inline std::ostream & operator<<(std::ostream & os, - std::vector const & v) + const std::vector & v) { os << "["; for(size_t i=0, z=v.size(); i #include @@ -31,15 +34,18 @@ namespace xo { /* throw exception if condition not met*/ # define XO_EXPECT(f,msg) if(!(f)) { throw std::runtime_error(msg); } /* establish scope using current function name */ -# define XO_SCOPE(name, lvl) xo::scope name(xo::scope_setup(xo::log_level::lvl, xo::log_config::style, __PRETTY_FUNCTION__, __FILE__, __LINE__)) +# define XO_SCOPE(varname, lvl) xo::scope varname(xo::scope_setup(xo::log_level::lvl, xo::log_config::style, __PRETTY_FUNCTION__, __FILE__, __LINE__)) /* like XO_SCOPE(name), but also set enabled flag */ //# define XO_SCOPE2(name, debug_flag) xo::scope name(xo::scope_setup(xo::log_config::style, __PRETTY_FUNCTION__, __FILE__, __LINE__, debug_flag)) # define XO_SCOPE_DISABLED(name) xo::scope name(xo::scope_setup(xo::log_level::never, xo::log_config::style, __PRETTY_FUNCTION__, __FILE__, __LINE__)) # define XO_STUB() { XO_SCOPE(logr); logr.log("STUB"); } - /* convenience class for basic_scope<..> construction (see below). - * use to disambiguate setup from other arguments - */ + /** @class scope_setup + * @brief Collect code-location information. + * + * Typically used with logging macros like @ref XO_SCOPE + * Application code isn't expected to interact with this class directly + **/ struct scope_setup { scope_setup(log_level level, function_style style, std::string_view name1, std::string_view name2, std::string_view file, std::uint32_t line) @@ -53,21 +59,33 @@ namespace xo { //static scope_setup literal(std::string_view name1, bool enabled_flag = true) { return scope_setup(FS_Literal, name1, enabled_flag); } //static scope_setup literal(std::string_view name1, std::string_view name2, bool enabled_flag = true) { return scope_setup(FS_Literal, name1, name2, enabled_flag); } - /* threshold level for logging -- write messages with severity >= this level */ + /** @defgroup scope_setup-instance-vars scope_setup instance variables **/ + ///@{ + + /** minimum severity level for logging -- write messages with severity >= this level **/ log_level log_level_ = log_level::error; - /* FS_Pretty | FS_Streamlined | FS_Simple */ + /** @c FS_Pretty | @c FS_Streamlined | @c FS_Simple **/ function_style style_ = function_style::pretty; + /** name extracted from left-hand side of symbol split (e.g. foo in method foo::bar) **/ std::string_view name1_ = "<.name1>"; + /** name extracted from right-hand side of symbol split (e.g. bar in method foo::bar) **/ std::string_view name2_ = "<.name2>"; - /* __FILE__ */ + /** captured value of `__FILE__` **/ std::string_view file_ = "<.file>"; - /* __LINE__ */ + /** captured value of `__LINE__` **/ std::uint32_t line_ = 0; + + ///@} }; /*scope_setup*/ - /* nesting logger + /** @class basic_scope + * @brief Track nesting level across participating function calls use to drive indentation. * - * Use: + * @tparam CharT character representation type. Usually @c char + * @tparam Traits character traits, usually @c std::char_traits + * + * Example: + * @code * using xo::scope; * * void myfunc() { @@ -78,7 +96,7 @@ namespace xo { * } * * void anotherfunc() { - * XO_SCOPE(x); // or scope x("anotherfunc") + * XO_SCOPE(x); // or scope x("anotherfunc"). * x.log(y); * } * @@ -98,14 +116,15 @@ namespace xo { * -anotherfunc: * d,e,f * -myfunc: - */ + * @endcode + **/ template > class basic_scope { public: using state_impl_type = state_impl; + using log_streambuf_type = log_streambuf; public: - //basic_scope(std::string_view name1, bool enabled_flag); template basic_scope(scope_setup setup, Tn&&... rest); ~basic_scope(); @@ -119,6 +138,7 @@ namespace xo { void set_dest_sbuf(std::streambuf * x) { this->dest_sbuf_ = x; } + /** Log arguments in pack @p rest **/ template bool log(Tn&&... rest) { if(this->finalized_) { @@ -129,8 +149,23 @@ namespace xo { /* indent for timestamp (not printed on this line) */ logstate->time_indent(); - /* log to per-thread stream to prevent data races */ - tosn(logstate2stream(logstate), std::forward(rest)...); + if (log_config::pretty_print_enabled) { + print::ppconfig ppc = { + .right_margin_ = log_config::right_margin, + .indent_width_ = log_config::indent_width + }; + std::ostream& ss = logstate2stream(logstate); + log_streambuf_type & sbuf = logstate2streambuf(logstate); + + /* use 0 for indent because flush2sbuf responsible for basic_scope toplevel indent */ + print::ppstate pps(0 /*ci*/, &ppc, &ss, &sbuf); + + pps.prettyn(ppconcat...>{{std::forward(rest)...}}); + + } else { + /* log to per-thread stream to prevent data races */ + tosn(logstate2stream(logstate), std::forward(rest)...); + } this->flush2sbuf(logstate); } @@ -138,18 +173,21 @@ namespace xo { return true; } /*log*/ + /** Log argument in pack @p args **/ template bool operator()(Tn&&... args) { return this->log(std::forward(args)...); } - /* call once to end scope before dtor */ + /** Optionally, call once to end scope before dtor. + * Logs arguments in pack @p args + **/ template void end_scope(Tn&&... args); private: - /* establish stream for logging; use thread-local storage for threadsafetỵ - * stream, if recycled (ịẹ after 1st call to scopẹlog() from a particular thread), - * will be in 'reset-to-beginning of buffer' statẹ - */ + /** establish stream for logging; use thread-local storage for threadsafetỵ + * stream, if recycled (ịẹ after 1st call to scopẹlog() from a particular thread), + * will be in 'reset-to-beginning of buffer' statẹ + **/ static state_impl_type * require_indent_thread_local_state(); /* establish logging state; use thread-local storage for threadsafety */ @@ -158,6 +196,9 @@ namespace xo { /* retrieve permanently-associated ostream for logging-state */ static std::ostream & logstate2stream(state_impl_type * logstate); + /* retreive permanently-associated streambuf for logging-state */ + static log_streambuf_type & logstate2streambuf(state_impl_type * logstate); + /* write collected output to std::clog, or chosen streambuf */ void flush2sbuf(state_impl_type * logstate); @@ -261,6 +302,13 @@ namespace xo { return logstate->ss(); } /*logstate2stream*/ + template + log_streambuf & + basic_scope::logstate2streambuf(state_impl_type * logstate) + { + return logstate->sbuf(); + } + template void basic_scope::flush2sbuf(state_impl_type * logstate) diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index ea4bb81..c8ea729 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -3,8 +3,8 @@ set(SELF_EXECUTABLE_NAME utest.indentlog) set(SELF_SOURCE_FILES fixed.test.cpp quoted.test.cpp vector.test.cpp array.test.cpp timeutil.test.cpp tag.test.cpp - filename.test.cpp code_location.test.cpp function.test.cpp - indentlog_utest_main.cpp) + filename.test.cpp code_location.test.cpp function.test.cpp pretty_vector.test.cpp + indentlog_utest_main.cpp log_streambuf.test.cpp toppstr.test.cpp) xo_add_utest_executable(${SELF_EXECUTABLE_NAME} ${SELF_SOURCE_FILES}) diff --git a/utest/log_streambuf.test.cpp b/utest/log_streambuf.test.cpp new file mode 100644 index 0000000..e145bd9 --- /dev/null +++ b/utest/log_streambuf.test.cpp @@ -0,0 +1,107 @@ +/* @file log_streambuf.test.cpp */ + +#include "xo/indentlog/log_streambuf.hpp" +#include "xo/indentlog/print/tag.hpp" +#include "xo/indentlog/print/quoted.hpp" +#include +//#include + +namespace ut { + using xo::log_streambuf; + + TEST_CASE("log_streamhuf", "[log_streambuf]") { + std::size_t z = 16; + log_streambuf> sbuf(z); + std::ostream ss(&sbuf); + + REQUIRE(sbuf.capacity() == z); + REQUIRE(sbuf.pos() == 0); + REQUIRE(sbuf.lpos() == 0); + + ss << '\n'; + + REQUIRE(sbuf.capacity() == z); + REQUIRE(sbuf.pos() == 1); + REQUIRE(sbuf.lpos() == 1); + } + + // write test cases with some random strings. + // for each string a list of tuples {ch,pos,lpos} + + struct recd { + char const * text_ = nullptr; + std::size_t expect_pos_ = 0; + std::size_t expect_lpos_ = 0; + }; + + struct test_case { + std::size_t buf_capacity_; + std::vector steps_; + }; + + std::vector s_testcase_v = { + {64, {{"\n", 1, 0}, + {"abcd", 5, 4}, + {"abcd\nefg\nhij", 17, 3}, + {"klmnopqrstuvwxyz", 33, 19}, + }}, + {32, {{"\n", 1, 0}, + {"abcd", 5, 4}, + {"abcd\nefg\nhij", 17, 3}, + {"klmnopqrstuvwxyz", 33, 19}, + }}, + {16, {{"\n", 1, 0}, + {"abcd", 5, 4}, + {"abcd\nefg\nhij", 17, 3}, + {"klmnopqrstuvwxyz", 33, 19}, + }}, + {8, {{"\n", 1, 0}, + {"abcd", 5, 4}, + {"abcd\nefg\nhij", 17, 3}, + {"klmnopqrstuvwxyz", 33, 19}, + }}, + {4, {{"\n", 1, 0}, + {"abcd", 5, 4}, + {"abcd\nefg\nhij", 17, 3}, + {"klmnopqrstuvwxyz", 33, 19}, + }}, + }; + + TEST_CASE("log_streambuf2", "[log_streambuf]") { + using xo::xtag; + using xo::print::quot; + using xo::print::unq; + + for (std::size_t i = 0; i < s_testcase_v.size(); ++i) { + INFO(tostr(xtag("i", i))); + + const test_case & tc = s_testcase_v[i]; + + std::size_t z = tc.buf_capacity_; + log_streambuf sbuf(z, true /*debug_flag*/); + std::ostream ss(&sbuf); + + REQUIRE(sbuf.capacity() == z); + REQUIRE(sbuf.pos() == 0); + REQUIRE(sbuf.lpos() == 0); + REQUIRE(sbuf.lo() + sbuf.capacity() == sbuf.hi()); + + std::size_t j = 0; + for (const recd & r : tc.steps_) { + INFO(tostr(xtag("j", j), xtag("text", unq(r.text_)))); + + ss << r.text_; + + REQUIRE(ss.good()); + REQUIRE(sbuf.capacity() >= z); + REQUIRE(sbuf.lo() + sbuf.capacity() == sbuf.hi()); + REQUIRE(sbuf.pos() == r.expect_pos_); + REQUIRE(sbuf.lpos() == r.expect_lpos_); + + ++j; + } + } + } +} + +/* end log_streambuf.test.cpp */ diff --git a/utest/pretty_vector.test.cpp b/utest/pretty_vector.test.cpp new file mode 100644 index 0000000..dc608b2 --- /dev/null +++ b/utest/pretty_vector.test.cpp @@ -0,0 +1,97 @@ +/* @file pretty_vector.test.cpp */ + +#include "xo/indentlog/print/pretty.hpp" +#include "xo/indentlog/print/pretty_vector.hpp" +#include +#include + +namespace ut { + using xo::print::ppconfig; + using xo::print::ppstate_standalone; + + TEST_CASE("print-upto", "[pretty]") { + std::stringstream ss; + ppconfig cfg; + ppstate_standalone pps(&ss, 0, &cfg); + + REQUIRE(pps.pos() == 0); + REQUIRE(pps.lpos() == 0); + REQUIRE(pps.avail_margin() == cfg.right_margin_); + REQUIRE(pps.has_margin()); + REQUIRE(pps.has_budget(cfg.right_margin_)); + REQUIRE(pps.scan_no_newline(0)); + } + + TEST_CASE("pretty", "[pretty]") { + ppconfig ppc; + ppc.right_margin_ = 40; + ppc.indent_width_ = 2; + + std::stringstream ss; + ppstate_standalone pps(&ss, 0, &ppc); + + pps.pretty("hello"); + + REQUIRE(ss.str() == "hello"); + } + + TEST_CASE("prettyvec", "[pretty]") { + ppconfig ppc; + ppc.right_margin_ = 20; + ppc.indent_width_ = 2; + + std::stringstream ss; + ppstate_standalone pps(&ss, 0, &ppc); + + std::vector test = {1, 2, 3, 4, 5, 6}; + + pps.pretty(test); + + REQUIRE(ss.str() == "[1, 2, 3, 4, 5, 6]"); + } + + TEST_CASE("prettyvec2", "[pretty]") { + ppconfig ppc; + ppc.right_margin_ = 10; + ppc.indent_width_ = 2; + + std::stringstream ss; + ppstate_standalone pps(&ss, 0, &ppc); + + std::vector test = {1, 2, 3, 4, 5, 6}; + + pps.pretty(test); + + REQUIRE(ss.str() == "[\n 1,\n 2,\n 3,\n 4,\n 5,\n 6\n]"); + } + + TEST_CASE("prettyvec3", "[pretty]") { + ppconfig ppc; + ppc.right_margin_ = 20; + ppc.indent_width_ = 2; + + std::stringstream ss; + ppstate_standalone pps(&ss, 0, &ppc); + + std::vector> test = {{1, 2, 3, 4}, {4, 5, 6, 7}}; + + pps.pretty(test); + + REQUIRE(ss.str() == "[\n [1, 2, 3, 4],\n [4, 5, 6, 7]\n]"); + } + + TEST_CASE("prettyvec4", "[pretty]") { + ppconfig ppc; + ppc.right_margin_ = 10; + ppc.indent_width_ = 2; + + std::stringstream ss; + ppstate_standalone pps(&ss, 0, &ppc); + + std::vector> test = {{1, 2, 3, 4}, {4, 5, 6, 7}}; + + pps.pretty(test); + + REQUIRE(ss.str() == "[\n [\n 1,\n 2,\n 3,\n 4\n ],\n [\n 4,\n 5,\n 6,\n 7\n ]\n]"); + } +} diff --git a/utest/toppstr.test.cpp b/utest/toppstr.test.cpp new file mode 100644 index 0000000..b06f806 --- /dev/null +++ b/utest/toppstr.test.cpp @@ -0,0 +1,55 @@ +/* @file toppstr.cpp */ + +#include "xo/indentlog/print/ppstr.hpp" +#include "xo/indentlog/print/tag.hpp" +#include +#include + +namespace ut { + using xo::toppstr; + using xo::toppstr2; + using xo::print::ppconfig; + using xo::print::ppstate; + + TEST_CASE("toppstr_1", "[toppstr]") { + std::string s = toppstr(); + REQUIRE(s.empty()); + } + + TEST_CASE("toppstr_2", "[toppstr]") { + std::string s = toppstr("hello"); + REQUIRE(s == "hello"); + } + + TEST_CASE("toppstr_3", "[toppstr]") { + std::string s = toppstr("the", " quick", " brown", " fox", " jumps", " over", " the", " lazy", " dog"); + REQUIRE(s == "the quick brown fox jumps over the lazy dog"); + } + + TEST_CASE("toppstr2_0", "[toppstr2]") { + ppconfig ppc; + ppc.right_margin_ = 40; + ppc.indent_width_ = 0; + + std::string s = toppstr2(ppc, "the", " quick", " brown", " fox", " jumps", " over", " the", " lazy", " dog"); + REQUIRE(s == "the\n quick\n brown\n fox\n jumps\n over\n the\n lazy\n dog"); + } + + TEST_CASE("toppstr2_1", "[toppstr2]") { + ppconfig ppc; + ppc.right_margin_ = 40; + ppc.indent_width_ = 2; + + std::string s = toppstr2(ppc, "the", " quick", " brown", " fox", " jumps", " over", " the", " lazy", " dog"); + REQUIRE(s == "the\n quick\n brown\n fox\n jumps\n over\n the\n lazy\n dog"); + } + + TEST_CASE("toppstr2_2", "[toppstr2]") { + ppconfig ppc; + ppc.right_margin_ = 40; + ppc.indent_width_ = 4; + + std::string s = toppstr2(ppc, "the", " quick", " brown", " fox", " jumps", " over", " the", " lazy", " dog"); + REQUIRE(s == "the\n quick\n brown\n fox\n jumps\n over\n the\n lazy\n dog"); + } +}