diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9fc7f84a394dcb444e3ef6086f6eba04aee74c40..76b18755f735c0b4c7fa4f47a7c0f0bd7c64d721 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,7 @@ set(BASE_SOURCES util/debug.cpp util/hostname.cpp util/path.cpp + util/prefixbuf.cpp util/unwind.cpp ) set(CUDA_SOURCES diff --git a/src/util/prefixbuf.cpp b/src/util/prefixbuf.cpp new file mode 100644 index 0000000000000000000000000000000000000000..71dfea842abf53c1d058775a31065259c9bb7f59 --- /dev/null +++ b/src/util/prefixbuf.cpp @@ -0,0 +1,144 @@ +#include <iostream> +#include <stack> +#include <string> +#include <vector> + +#include <util/prefixbuf.hpp> + +namespace arb { +namespace util { + +// prefixbuf implementation: + +std::streamsize prefixbuf::xsputn(const char_type* s, std::streamsize count) { + std::streamsize written = 0; + + while (count>0) { + if (bol_) { + inner_->sputn(&prefix[0], prefix.size()); + bol_ = false; + } + + std::streamsize i = 0; + while (i<count && s[i]!='\n') { + ++i; + } + + if (i<count) { // encountered '\n' + ++i; + bol_ = true; + } + + std::streamsize n = inner_->sputn(s, i); + written += n; + if (n<i) { + break; + } + + s += i; + count -= i; + } + + return written; +} + +prefixbuf::int_type prefixbuf::overflow(int_type ch) { + static int_type eof = traits_type::eof(); + + if (ch!=eof) { + char_type c = (char_type)ch; + return xsputn(&c, 1)? 0: eof; + } + + return eof; +} + +// setprefix implementation: + +std::ostream& operator<<(std::ostream& os, const setprefix& sp) { + if (auto pbuf = dynamic_cast<prefixbuf*>(os.rdbuf())) { + pbuf->prefix = sp.prefix_; + } + + return os; +} + +// indent_manip implementation: + +using indent_stack = std::stack<unsigned, std::vector<unsigned>>; + +int indent_manip::xindex() { + static int i = std::ios_base::xalloc(); + return i; +} + +static void apply_indent_prefix(std::ios& s, int index) { + if (auto pbuf = dynamic_cast<prefixbuf*>(s.rdbuf())) { + indent_stack* stack_ptr = static_cast<indent_stack*>(s.pword(index)); + unsigned tabwidth = s.iword(index); + + unsigned tabs = (!stack_ptr || stack_ptr->empty())? 0: stack_ptr->top(); + pbuf->prefix = std::string(tabs*tabwidth, ' '); + } +} + +static void indent_stack_callback(std::ios_base::event ev, std::ios_base& ios, int index) { + void*& pword = ios.pword(index); + + switch (ev) { + case std::ios_base::erase_event: + if (pword) { + indent_stack* stack_ptr = static_cast<indent_stack*>(pword); + delete stack_ptr; + pword = nullptr; + } + break; + case std::ios_base::copyfmt_event: + if (pword) { + // Clone stack: + indent_stack* stack_ptr = static_cast<indent_stack*>(pword); + pword = new indent_stack(*stack_ptr); + + // Set prefix if streambuf is a prefixbuf: + if (auto stream_ptr = dynamic_cast<std::ios*>(&ios)) { + apply_indent_prefix(*stream_ptr, index); + } + } + break; + default: + ; + } +} + +std::ostream& operator<<(std::ostream& os, indent_manip in) { + int xindex = indent_manip::xindex(); + void*& pword = os.pword(xindex); + long& iword = os.iword(xindex); + + if (!pword) { + os.register_callback(&indent_stack_callback, xindex); + pword = new indent_stack(); + iword = static_cast<long>(indent_manip::default_tabwidth); + } + + indent_stack& stack = *static_cast<indent_stack*>(pword); + switch (in.action_) { + case indent_manip::pop: + while (!stack.empty() && in.value_--) { + stack.pop(); + } + break; + case indent_manip::push: + stack.push(stack.empty()? in.value_: in.value_+stack.top()); + break; + case indent_manip::settab: + iword = static_cast<long>(in.value_); + break; + } + + apply_indent_prefix(os, xindex); + return os; +} + +} // namespace util +} // namespace arb diff --git a/src/util/prefixbuf.hpp b/src/util/prefixbuf.hpp new file mode 100644 index 0000000000000000000000000000000000000000..5f4c342e78cab89f69029e8a6a72b930891cd594 --- /dev/null +++ b/src/util/prefixbuf.hpp @@ -0,0 +1,135 @@ +#pragma once + +// Output-only stream buffer that prepends a prefix to each line of output, +// together with stream manipulators for setting the prefix and managing +// an indentation level. + +#include <ostream> +#include <sstream> +#include <string> + +namespace arb { +namespace util { + +// `prefixbuf` acts an output-only filter for another streambuf, inserting +// the contents of the `prefix` string before the first character in a line. +// +// The following code, for example: +// +// prefixbuf p(std::cout.rdbuf()); +// std::cout.rdbuf(&p); +// p.prefix = ">>> "; +// std::cout << "hello\nworld\n"; +// +// would emit to stdout: +// +// >>> hello +// >>> world + +class prefixbuf: public std::streambuf { +public: + explicit prefixbuf(std::streambuf* inner): inner_(inner) {} + + prefixbuf(prefixbuf&&) = default; + prefixbuf(const prefixbuf&) = delete; + + prefixbuf& operator=(prefixbuf&&) = default; + prefixbuf& operator=(const prefixbuf&) = delete; + + std::streambuf* inner() { return inner_; } + std::string prefix; + +protected: + std::streambuf* inner_; + bool bol_ = true; + + std::streamsize xsputn(const char_type* s, std::streamsize count) override; + int_type overflow(int_type ch) override; +}; + +// Manipulators: +// +// setprefix(s): explicitly set prefix string on corresponding prefixbuf. +// indent: increase indentation level by one tab width. +// indent(n): increase indentation level by n tab widths. +// popindent: undo last `indent` operation. +// popindent(n) undo last n `indent` operations. +// settab(w): set tab width to w (default is 4). +// +// Note that the prefix string is a property of the prefixbuf, not the stream, +// and so will not be preserved by e.g. `copyfmt`. +// +// All but `setprefix` are implemented as values of type `indent_manip` +// below. +// +// The manipulator `indent(0)` can be used to reset the prefix of the underlying +// stream to match the current indentation level. + +class setprefix { +public: + explicit setprefix(std::string prefix): prefix_(std::move(prefix)) {} + + friend std::ostream& operator<<(std::ostream& os, const setprefix& sp); + +private: + std::string prefix_; +}; + +struct indent_manip { + enum action_enum {push, pop, settab}; + + explicit constexpr indent_manip(action_enum action, unsigned value=0): + action_(action), value_(value) + {} + + // convenience interface: allows using both `indent` and `indent(n)` + // as stream manipulators. + indent_manip operator()(unsigned n) const { + return indent_manip(action_, n); + } + + friend std::ostream& operator<<(std::ostream& os, indent_manip in); + +private: + static constexpr unsigned default_tabwidth = 4; + static int xindex(); + + action_enum action_; + unsigned value_; +}; + +constexpr indent_manip indent{indent_manip::push, 1u}; +constexpr indent_manip popindent{indent_manip::pop, 1u}; + +inline indent_manip settab(unsigned w) { + return indent_manip{indent_manip::settab, w}; +} + +// Wrap an stringbuf with a prefixbuf, and present as a stream. +// Acts very much like a `std::ostringstream`, but with prefix +// and indent functionality. + +class pfxstringstream: public std::ostream { +public: + pfxstringstream(): + std::ostream(&pbuf_), + sbuf_(std::ios_base::out), + pbuf_(&sbuf_) + {} + + pfxstringstream(pfxstringstream&&) = default; + pfxstringstream& operator=(pfxstringstream&&) = default; + + std::string str() const { return sbuf_.str(); } + void str(const std::string& s) { sbuf_.str(s); } + + prefixbuf* rdbuf() { return &pbuf_; } + +private: + std::stringbuf sbuf_; + prefixbuf pbuf_; +}; + + +} // namespace util +} // namespace arb diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 62eb4695dbda122a0d5807ecd85d1d77238d1615..3c64b0a497dad0e282b77c28f57fbfe3b106992b 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -61,6 +61,7 @@ set(TEST_SOURCES test_partition.cpp test_path.cpp test_point.cpp + test_prefixbuf.cpp test_probe.cpp test_range.cpp test_segment.cpp diff --git a/tests/unit/test_prefixbuf.cpp b/tests/unit/test_prefixbuf.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b211c8161338da03ea164f599c5fd066a52a9436 --- /dev/null +++ b/tests/unit/test_prefixbuf.cpp @@ -0,0 +1,169 @@ +#include "../gtest.h" + +#include <cstring> +#include <iomanip> +#include <string> +#include <sstream> + +#include <util/prefixbuf.hpp> + +using namespace arb::util; + +// Test public std::stringbuf 'put' interfaces on prefixbuf. + +TEST(prefixbuf, prefix) { + // Write a C-string with `std::steambuf::sputn` --- + // exercises `prefixbuf::xsputn`. + + auto write_sputn = [](std::streambuf& b, const char* c) { + b.sputn(c, std::strlen(c)); + }; + + // Write a C-string with `std::steambuf::sputc` --- + // exercises `prefixbuf::overflow`. + + auto write_sputc = [](std::streambuf& b, const char* c) { + while (*c) { + b.sputc(*c++); + } + }; + + std::stringbuf s; + write_sputn(s, "starting text\n"); + + prefixbuf p(&s); + p.prefix = ":) "; + + write_sputn(p, "foo\nbar "); + write_sputc(p, "quux\nbaz "); + p.prefix = "^^ "; + write_sputn(p, "xyzzy\nplugh\n"); + + std::string expected = + "starting text\n" + ":) foo\n" + ":) bar quux\n" + ":) baz xyzzy\n" + "^^ plugh\n"; + + EXPECT_EQ(expected, s.str()); +} + +// Test `pfxstringstream` basic functionality: +// +// 1. `rdbuf()` method gives pointer to `prefixbuf`. +// +// 2. Formatted write operations behave as expected, +// but with the prefixbuf prefix-inserting behaviour. +// +// 3. `str()` method gives string from wrapped `std::stringbuf`. + +TEST(prefixbuf, pfxstringstream) { + pfxstringstream p; + + p.rdbuf()->prefix = "..."; + + p << "_foo_ " << std::setw(5) << 123 << "\n"; + p << std::showbase << std::hex << 42; + + std::string expected = + "..._foo_ 123\n" + "...0x2a"; + + EXPECT_EQ(expected, p.str()); +} + +// Test that the `pfxstringstream::str(const std::string&)` method +// behaves analagously to that of `std::ostringstream`, viz. initializing +// the contents of the buffer. + +TEST(prefixbuf, pfxstringstream_str) { + pfxstringstream p; + p.str("0123456789"); + p.rdbuf()->prefix = "__"; + p << "a\nb"; + + std::string expected = "__a\n__b789"; + EXPECT_EQ(expected, p.str()); +} + +// Test indent manipulators against expected prefixes. + +TEST(prefixbuf, indent_manip) { + pfxstringstream p; + + p << settab(2); + p << "0\n" + << indent + << "1\n" + << indent(2) + << "3\n" + << indent + << "4\n" + << popindent(2) + << "1\n" + << popindent + << "0"; + + std::string expected = + "0\n" + " 1\n" + " 3\n" + " 4\n" + " 1\n" + "0"; + + EXPECT_EQ(expected, p.str()); +} + +// `setprefix` goes behind the stream's back and sets the prefix +// in the underling streambuf directly. +// +// The stream's indentation will be re-applied to the streambuf +// when it encounters an indent manipulator; `indent(0)` would +// otherwise constitute a NOP. + +TEST(prefixbuf, setprefix) { + pfxstringstream p; + + p << indent << "one\ntwo "; + p << setprefix("--->"); // override prefix + p << "three\nfour "; + p << indent(0) // restore indentation + << "five\nsix"; + + std::string expected = + " one\n" + " two three\n" + "--->four five\n" + " six"; + + EXPECT_EQ(expected, p.str()); +} + +// Confirm the callback associated with the indentation state +// propagates the full indentation stack after `copyfmt`, +// and imposes the corresponding prefix on the underlying +// `prefixbuf`. + +TEST(prefixbuf, copyfmt) { + pfxstringstream p1; + pfxstringstream p2; + + p1 << indent << "1\n" << indent << "2\n"; + p2 << "0\n"; + + p2.copyfmt(p1); + p2 << "2\n" << popindent << "1\n"; + + p1 << indent << "3\n"; + + std::string expected1 = + " 1\n 2\n 3\n"; + + std::string expected2 = + "0\n 2\n 1\n"; + + EXPECT_EQ(expected1, p1.str()); + EXPECT_EQ(expected2, p2.str()); +}