diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ea33e7..02b8a91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,7 @@ target_sources( src/ctrace.cpp src/exceptions.cpp src/from_current.cpp + src/formatting.cpp src/options.cpp src/utils.cpp src/demangle/demangle_with_cxxabi.cpp diff --git a/include/cpptrace/basic.hpp b/include/cpptrace/basic.hpp index d6240a6..b5fb4ea 100644 --- a/include/cpptrace/basic.hpp +++ b/include/cpptrace/basic.hpp @@ -182,8 +182,6 @@ namespace cpptrace { inline const_iterator cbegin() const noexcept { return frames.cbegin(); } inline const_iterator cend() const noexcept { return frames.cend(); } private: - void print(std::ostream& stream, bool color, bool newline_at_end, const char* header) const; - void print_with_snippets(std::ostream& stream, bool color, bool newline_at_end, const char* header) const; friend void print_terminate_trace(); }; diff --git a/include/cpptrace/formatting.hpp b/include/cpptrace/formatting.hpp new file mode 100644 index 0000000..7ddf4ea --- /dev/null +++ b/include/cpptrace/formatting.hpp @@ -0,0 +1,67 @@ +#ifndef CPPTRACE_FORMATTING_HPP +#define CPPTRACE_FORMATTING_HPP + +#include + +#include +#include +#include + +namespace cpptrace { + class CPPTRACE_EXPORT formatter { + class impl; + // can't be a std::unique_ptr due to msvc awfulness with dllimport/dllexport and https://stackoverflow.com/q/4145605/15675011 + impl* pimpl; + + public: + formatter(); + ~formatter(); + + formatter(formatter&&); + formatter(const formatter&); + formatter& operator=(formatter&&); + formatter& operator=(const formatter&); + + formatter& set_header(std::string); + enum class color_mode { + always, + none, + automatic, + }; + formatter& set_color_mode(color_mode); + enum class address_mode { + raw, + object, + none, + }; + formatter& set_address_mode(address_mode); + formatter& set_snippets(bool); + formatter& set_snippet_context(int); + formatter& include_column(bool); + formatter& set_filter(std::function); + + std::string format(const stacktrace_frame&) const; + std::string format(const stacktrace_frame&, bool color) const; + + std::string format(const stacktrace&) const; + std::string format(const stacktrace&, bool color) const; + + void print(const stacktrace_frame&) const; + void print(const stacktrace_frame&, bool color) const; + void print(std::ostream&, const stacktrace_frame&) const; + void print(std::ostream&, const stacktrace_frame&, bool color) const; + void print(std::FILE*, const stacktrace_frame&) const; + void print(std::FILE*, const stacktrace_frame&, bool color) const; + + void print(const stacktrace&) const; + void print(const stacktrace&, bool color) const; + void print(std::ostream&, const stacktrace&) const; + void print(std::ostream&, const stacktrace&, bool color) const; + void print(std::FILE*, const stacktrace&) const; + void print(std::FILE*, const stacktrace&, bool color) const; + }; + + CPPTRACE_EXPORT const formatter& get_default_formatter(); +} + +#endif diff --git a/src/cpptrace.cpp b/src/cpptrace.cpp index a30bd5c..ed445b0 100644 --- a/src/cpptrace.cpp +++ b/src/cpptrace.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -9,6 +10,7 @@ #include #include +#include "cpptrace/basic.hpp" #include "symbols/symbols.hpp" #include "unwind/unwind.hpp" #include "demangle/demangle.hpp" @@ -129,41 +131,13 @@ namespace cpptrace { return detail::get_frame_object_info(raw_address); } - static std::string frame_to_string( - bool color, - const stacktrace_frame& frame - ) { - const auto reset = color ? RESET : ""; - const auto green = color ? GREEN : ""; - const auto yellow = color ? YELLOW : ""; - const auto blue = color ? BLUE : ""; - std::string str; - if(frame.is_inline) { - str += microfmt::format("{<{}}", 2 * sizeof(frame_ptr) + 2, "(inlined)"); - } else { - str += microfmt::format("{}0x{>{}:0h}{}", blue, 2 * sizeof(frame_ptr), frame.raw_address, reset); - } - if(!frame.symbol.empty()) { - str += microfmt::format(" in {}{}{}", yellow, frame.symbol, reset); - } - if(!frame.filename.empty()) { - str += microfmt::format(" at {}{}{}", green, frame.filename, reset); - if(frame.line.has_value()) { - str += microfmt::format(":{}{}{}", blue, frame.line.value(), reset); - if(frame.column.has_value()) { - str += microfmt::format(":{}{}{}", blue, frame.column.value(), reset); - } - } - } - return str; - } - std::string stacktrace_frame::to_string() const { return to_string(false); } std::string stacktrace_frame::to_string(bool color) const { - return frame_to_string(color, *this); + // return frame_to_string(color, *this); + return get_default_formatter().format(*this, color); } std::ostream& operator<<(std::ostream& stream, const stacktrace_frame& frame) { @@ -195,89 +169,34 @@ namespace cpptrace { } void stacktrace::print() const { - print(std::cerr, true); + get_default_formatter().print(*this); } void stacktrace::print(std::ostream& stream) const { - print(stream, true); + get_default_formatter().print(stream, *this); } void stacktrace::print(std::ostream& stream, bool color) const { - print(stream, color, true, nullptr); + get_default_formatter().print(stream, *this, color); } - static void print_frame( - std::ostream& stream, - bool color, - unsigned frame_number_width, - std::size_t counter, - const stacktrace_frame& frame - ) { - std::string line = microfmt::format("#{<{}} {}", frame_number_width, counter, frame.to_string(color)); - stream << line; - } - - void stacktrace::print(std::ostream& stream, bool color, bool newline_at_end, const char* header) const { - if( - color && ( - (&stream == &std::cout && isatty(stdout_fileno)) || (&stream == &std::cerr && isatty(stderr_fileno)) - ) - ) { - detail::enable_virtual_terminal_processing_if_needed(); - } - stream << (header ? header : "Stack trace (most recent call first):") << '\n'; - std::size_t counter = 0; - if(frames.empty()) { - stream << "\n"; - return; - } - const auto frame_number_width = detail::n_digits(static_cast(frames.size()) - 1); - for(const auto& frame : frames) { - print_frame(stream, color, frame_number_width, counter, frame); - if(newline_at_end || &frame != &frames.back()) { - stream << '\n'; - } - counter++; + namespace detail { + const formatter& get_default_snippet_formatter() { + static formatter snippet_formatter = formatter{}.set_snippets(true); + return snippet_formatter; } } void stacktrace::print_with_snippets() const { - print_with_snippets(std::cerr, true); + detail::get_default_snippet_formatter().print(*this); } void stacktrace::print_with_snippets(std::ostream& stream) const { - print_with_snippets(stream, true); + detail::get_default_snippet_formatter().print(stream, *this); } void stacktrace::print_with_snippets(std::ostream& stream, bool color) const { - print_with_snippets(stream, color, true, nullptr); - } - - void stacktrace::print_with_snippets(std::ostream& stream, bool color, bool newline_at_end, const char* header) const { - if( - color && ( - (&stream == &std::cout && isatty(stdout_fileno)) || (&stream == &std::cerr && isatty(stderr_fileno)) - ) - ) { - detail::enable_virtual_terminal_processing_if_needed(); - } - stream << (header ? header : "Stack trace (most recent call first):") << '\n'; - std::size_t counter = 0; - if(frames.empty()) { - stream << "" << '\n'; - return; - } - const auto frame_number_width = detail::n_digits(static_cast(frames.size()) - 1); - for(const auto& frame : frames) { - print_frame(stream, color, frame_number_width, counter, frame); - if(newline_at_end || &frame != &frames.back()) { - stream << '\n'; - } - if(frame.line.has_value() && !frame.filename.empty()) { - stream << detail::get_snippet(frame.filename, frame.line.value(), 2, color); - } - counter++; - } + detail::get_default_snippet_formatter().print(stream, *this, color); } void stacktrace::clear() { @@ -289,13 +208,12 @@ namespace cpptrace { } std::string stacktrace::to_string(bool color) const { - std::ostringstream oss; - print(oss, color, false, nullptr); - return std::move(oss).str(); + return get_default_formatter().format(*this, color); } std::ostream& operator<<(std::ostream& stream, const stacktrace& trace) { - return stream << trace.to_string(); + get_default_formatter().print(stream, trace); + return stream; } CPPTRACE_FORCE_NO_INLINE diff --git a/src/formatting.cpp b/src/formatting.cpp new file mode 100644 index 0000000..eb17bbb --- /dev/null +++ b/src/formatting.cpp @@ -0,0 +1,322 @@ +#include +#include + +#include "utils/optional.hpp" +#include "utils/utils.hpp" +#include "snippets/snippet.hpp" + +#include +#include +#include +#include +#include +#include + +namespace cpptrace { + class formatter::impl { + struct { + std::string header = "Stack trace (most recent call first):"; + color_mode color = color_mode::automatic; + address_mode addresses = address_mode::raw; + bool snippets = false; + int context_lines = 2; + bool columns = true; + std::function filter; + } options; + + public: + void set_header(std::string header) { + options.header = std::move(header); + } + void set_color_mode(formatter::color_mode mode) { + options.color = mode; + } + void set_address_mode(formatter::address_mode mode) { + options.addresses = mode; + } + void set_snippets(bool snippets) { + options.snippets = snippets; + } + void set_snippet_context(int lines) { + options.context_lines = lines; + } + void include_column(bool columns) { + options.columns = columns; + } + void set_filter(std::function filter) { + options.filter = filter; + } + + std::string format(const stacktrace_frame& frame, detail::optional color_override = detail::nullopt) const { + return frame_to_string(frame, color_override.value_or(options.color == color_mode::always)); + } + + std::string format(const stacktrace& trace, detail::optional color_override = detail::nullopt) const { + std::ostringstream oss; + print_internal(oss, trace, false, color_override); + return std::move(oss).str(); + } + + void print(const stacktrace_frame& frame, detail::optional color_override = detail::nullopt) const { + print(std::cout, frame, color_override); + } + void print( + std::ostream& stream, + const stacktrace_frame& frame, + detail::optional color_override = detail::nullopt + ) const { + print_frame_internal(stream, frame, color_override); + } + void print( + std::FILE* file, + const stacktrace_frame& frame, + detail::optional color_override = detail::nullopt + ) const { + auto str = format(frame, color_override); + std::fwrite(str.data(), 1, str.size(), file); + } + + void print(const stacktrace& trace, detail::optional color_override = detail::nullopt) const { + print(std::cout, trace, color_override); + } + void print( + std::ostream& stream, + const stacktrace& trace, + detail::optional color_override = detail::nullopt + ) const { + print_internal(stream, trace, true, color_override); + } + void print( + std::FILE* file, + const stacktrace& trace, + detail::optional color_override = detail::nullopt + ) const { + auto str = format(trace, color_override); + std::fwrite(str.data(), 1, str.size(), file); + } + + private: + bool stream_is_tty(std::ostream& stream) const { + // not great, but it'll have to do + return (&stream == &std::cout && isatty(stdout_fileno)) + || (&stream == &std::cerr && isatty(stderr_fileno)); + } + + void maybe_ensure_virtual_terminal_processing(std::ostream& stream, bool color) const { + if(color && stream_is_tty(stream)) { + detail::enable_virtual_terminal_processing_if_needed(); + } + } + + bool should_do_color(std::ostream& stream, detail::optional color_override) const { + bool do_color = options.color == color_mode::always || color_override.value_or(false); + if( + (options.color == color_mode::automatic || options.color == color_mode::always) && + (!color_override || color_override.unwrap() != false) && + stream_is_tty(stream) + ) { + detail::enable_virtual_terminal_processing_if_needed(); + do_color = true; + } + return do_color; + } + + void print_internal(std::ostream& stream, const stacktrace& trace, bool newline_at_end, detail::optional color_override) const { + bool do_color = should_do_color(stream, color_override); + maybe_ensure_virtual_terminal_processing(stream, do_color); + print_internal(stream, trace, newline_at_end, do_color); + } + + void print_internal(std::ostream& stream, const stacktrace& trace, bool newline_at_end, bool color) const { + if(!options.header.empty()) { + stream << options.header << '\n'; + } + std::size_t counter = 0; + const auto& frames = trace.frames; + if(frames.empty()) { + stream << "\n"; + return; + } + const auto frame_number_width = detail::n_digits(static_cast(frames.size()) - 1); + for(const auto& frame : frames) { + if(options.filter && !options.filter(frame)) { + counter++; + continue; + } + print_frame_internal(stream, frame, color, frame_number_width, counter); + if(frame.line.has_value() && !frame.filename.empty() && options.snippets) { + auto snippet = detail::get_snippet( + frame.filename, + frame.line.value(), + options.context_lines, + color + ); + if(!snippet.empty()) { + stream << '\n'; + stream << snippet; + } + } + if(newline_at_end || &frame != &frames.back()) { + stream << '\n'; + } + counter++; + } + } + + void print_frame_internal( + std::ostream& stream, + const stacktrace_frame& frame, + bool color, + unsigned frame_number_width, + std::size_t counter + ) const { + std::string line = microfmt::format("#{<{}} {}", frame_number_width, counter, frame_to_string(frame, color)); + stream << line; + } + + void print_frame_internal( + std::ostream& stream, + const stacktrace_frame& frame, + detail::optional color_override + ) const { + bool do_color = should_do_color(stream, color_override); + maybe_ensure_virtual_terminal_processing(stream, do_color); + stream << frame_to_string(frame, do_color); + } + + std::string frame_to_string(const stacktrace_frame& frame, bool color) const { + const auto reset = color ? RESET : ""; + const auto green = color ? GREEN : ""; + const auto yellow = color ? YELLOW : ""; + const auto blue = color ? BLUE : ""; + std::string str; + if(frame.is_inline) { + str += microfmt::format("{<{}}", 2 * sizeof(frame_ptr) + 2, "(inlined)"); + } else { + auto address = options.addresses == address_mode::raw ? frame.raw_address : frame.object_address; + str += microfmt::format("{}0x{>{}:0h}{}", blue, 2 * sizeof(frame_ptr), address, reset); + } + if(!frame.symbol.empty()) { + str += microfmt::format(" in {}{}{}", yellow, frame.symbol, reset); + } + if(!frame.filename.empty()) { + str += microfmt::format(" at {}{}{}", green, frame.filename, reset); + if(frame.line.has_value()) { + str += microfmt::format(":{}{}{}", blue, frame.line.value(), reset); + if(frame.column.has_value() && options.columns) { + str += microfmt::format(":{}{}{}", blue, frame.column.value(), reset); + } + } + } + return str; + } + }; + + formatter::formatter() : pimpl(new impl) {} + formatter::~formatter() { + delete pimpl; + } + + formatter::formatter(formatter&& other) : pimpl(detail::exchange(other.pimpl, nullptr)) {} + formatter::formatter(const formatter& other) : pimpl(new impl(*other.pimpl)) {} + formatter& formatter::operator=(formatter&& other) { + if(pimpl) { + delete pimpl; + } + pimpl = detail::exchange(other.pimpl, nullptr); + return *this; + } + formatter& formatter::operator=(const formatter& other) { + if(pimpl) { + delete pimpl; + } + pimpl = new impl(*other.pimpl); + return *this; + } + + formatter& formatter::set_header(std::string header) { + pimpl->set_header(std::move(header)); + return *this; + } + formatter& formatter::set_color_mode(color_mode mode) { + pimpl->set_color_mode(mode); + return *this; + } + formatter& formatter::set_address_mode(address_mode mode) { + pimpl->set_address_mode(mode); + return *this; + } + formatter& formatter::set_snippets(bool snippets) { + pimpl->set_snippets(snippets); + return *this; + } + formatter& formatter::set_snippet_context(int lines) { + pimpl->set_snippet_context(lines); + return *this; + } + formatter& formatter::include_column(bool columns) { + pimpl->include_column(columns); + return *this; + } + formatter& formatter::set_filter(std::function filter) { + pimpl->set_filter(std::move(filter)); + return *this; + } + + std::string formatter::format(const stacktrace_frame& frame) const { + return pimpl->format(frame); + } + std::string formatter::format(const stacktrace_frame& frame, bool color) const { + return pimpl->format(frame, color); + } + + std::string formatter::format(const stacktrace& trace) const { + return pimpl->format(trace); + } + std::string formatter::format(const stacktrace& trace, bool color) const { + return pimpl->format(trace, color); + } + + void formatter::print(const stacktrace& trace) const { + pimpl->print(trace); + } + void formatter::print(const stacktrace& trace, bool color) const { + pimpl->print(trace, color); + } + void formatter::print(std::ostream& stream, const stacktrace& trace) const { + pimpl->print(stream, trace); + } + void formatter::print(std::ostream& stream, const stacktrace& trace, bool color) const { + pimpl->print(stream, trace, color); + } + void formatter::print(std::FILE* file, const stacktrace& trace) const { + pimpl->print(file, trace); + } + void formatter::print(std::FILE* file, const stacktrace& trace, bool color) const { + pimpl->print(file, trace, color); + } + + void formatter::print(const stacktrace_frame& frame) const { + pimpl->print(frame); + } + void formatter::print(const stacktrace_frame& frame, bool color) const { + pimpl->print(frame, color); + } + void formatter::print(std::ostream& stream, const stacktrace_frame& frame) const { + pimpl->print(stream, frame); + } + void formatter::print(std::ostream& stream, const stacktrace_frame& frame, bool color) const { + pimpl->print(stream, frame, color); + } + void formatter::print(std::FILE* file, const stacktrace_frame& frame) const { + pimpl->print(file, frame); + } + void formatter::print(std::FILE* file, const stacktrace_frame& frame, bool color) const { + pimpl->print(file, frame, color); + } + + const formatter& get_default_formatter() { + static formatter formatter; + return formatter; + } +} diff --git a/src/snippets/snippet.cpp b/src/snippets/snippet.cpp index c776d3e..5b819d8 100644 --- a/src/snippets/snippet.cpp +++ b/src/snippets/snippet.cpp @@ -135,7 +135,10 @@ namespace detail { if(color && line == target_line) { snippet += RESET; } - snippet += lines[line - original_begin] + "\n"; + snippet += lines[line - original_begin]; + if(line != end) { + snippet += '\n'; + } } return snippet; } diff --git a/src/utils.cpp b/src/utils.cpp index 0841841..c0a76bb 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,5 +1,6 @@ #include #include +#include #include @@ -27,12 +28,9 @@ namespace cpptrace { CPPTRACE_FORCE_NO_INLINE void print_terminate_trace() { try { // try/catch can never be hit but it's needed to prevent TCO - generate_trace(1).print( - std::cerr, - isatty(stderr_fileno), - true, - "Stack trace to reach terminate handler (most recent call first):" - ); + formatter{} + .set_header("Stack trace to reach terminate handler (most recent call first):") + .print(std::cerr, generate_trace(1)); } catch(...) { if(!detail::should_absorb_trace_exceptions()) { throw; diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp index f4af8f3..a81521a 100644 --- a/src/utils/utils.hpp +++ b/src/utils/utils.hpp @@ -189,6 +189,13 @@ namespace detail { return static_cast(v); } + template + T exchange(T& obj, U&& value) { + T old = std::move(obj); + obj = std::forward(value); + return old; + } + struct monostate {}; // TODO: Rework some stuff here. Not sure deleters should be optional or moved. diff --git a/test/unit/lib/formatting.cpp b/test/unit/lib/formatting.cpp new file mode 100644 index 0000000..a2cdee4 --- /dev/null +++ b/test/unit/lib/formatting.cpp @@ -0,0 +1,258 @@ +#include + +#include +#include +#include +#include + +#include "utils/microfmt.hpp" +#include "utils/utils.hpp" + +using cpptrace::detail::split; +using testing::ElementsAre; + +namespace { + +cpptrace::stacktrace make_test_stacktrace() { + cpptrace::stacktrace trace; + trace.frames.push_back({0x1, 0x1001, {20}, {30}, "foo.cpp", "foo()", false}); + trace.frames.push_back({0x2, 0x1002, {30}, {40}, "bar.cpp", "bar()", false}); + trace.frames.push_back({0x3, 0x1003, {40}, {25}, "foo.cpp", "main", false}); + return trace; +} + +TEST(FormatterTest, Basic) { + auto res = split(cpptrace::get_default_formatter().format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + "#0 0x0000000000000001 in foo() at foo.cpp:20:30", + "#1 0x0000000000000002 in bar() at bar.cpp:30:40", + "#2 0x0000000000000003 in main at foo.cpp:40:25" + ) + ); +} + +TEST(FormatterTest, Inlines) { + auto trace = make_test_stacktrace(); + trace.frames[1].is_inline = true; + trace.frames[1].raw_address = 0; + trace.frames[1].object_address = 0; + auto res = split(cpptrace::get_default_formatter().format(trace), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + "#0 0x0000000000000001 in foo() at foo.cpp:20:30", + "#1 (inlined) in bar() at bar.cpp:30:40", + "#2 0x0000000000000003 in main at foo.cpp:40:25" + ) + ); +} + +TEST(FormatterTest, Header) { + auto formatter = cpptrace::formatter{} + .set_header("Stack trace:"); + auto res = split(formatter.format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace:", + "#0 0x0000000000000001 in foo() at foo.cpp:20:30", + "#1 0x0000000000000002 in bar() at bar.cpp:30:40", + "#2 0x0000000000000003 in main at foo.cpp:40:25" + ) + ); +} + +TEST(FormatterTest, NoColumn) { + auto formatter = cpptrace::formatter{} + .include_column(false); + auto res = split(formatter.format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + "#0 0x0000000000000001 in foo() at foo.cpp:20", + "#1 0x0000000000000002 in bar() at bar.cpp:30", + "#2 0x0000000000000003 in main at foo.cpp:40" + ) + ); +} + +TEST(FormatterTest, ObjectAddresses) { + auto formatter = cpptrace::formatter{} + .set_address_mode(cpptrace::formatter::address_mode::object); + auto res = split(formatter.format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + "#0 0x0000000000001001 in foo() at foo.cpp:20:30", + "#1 0x0000000000001002 in bar() at bar.cpp:30:40", + "#2 0x0000000000001003 in main at foo.cpp:40:25" + ) + ); +} + +TEST(FormatterTest, Snippets) { + cpptrace::stacktrace trace; + unsigned line = __LINE__ + 1; + trace.frames.push_back({0x1, 0x1001, {line}, {20}, __FILE__, "foo()", false}); + trace.frames.push_back({0x2, 0x1002, {line + 1}, {20}, __FILE__, "foo()", false}); + auto formatter = cpptrace::formatter{} + .set_snippets(true); + auto res = split(formatter.format(trace), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + // frame 1 + cpptrace::microfmt::format("#0 0x0000000000000001 in foo() at {}:{}:20", __FILE__, line), + cpptrace::microfmt::format(" {}: cpptrace::stacktrace trace;", line - 2), + cpptrace::microfmt::format(" {}: unsigned line = __LINE__ + 1;", line - 1), + cpptrace::microfmt::format( + " {}: trace.frames.push_back({0x1, 0x1001, {line}, {{20}}, __FILE__, \"foo()\", false});", + line + ), + cpptrace::microfmt::format( + " {}: trace.frames.push_back({0x2, 0x1002, {line + 1}, {{20}}, __FILE__, \"foo()\", false});", + line + 1 + ), + cpptrace::microfmt::format(" {}: auto formatter = cpptrace::formatter{{}}", line + 2), + // frame 2 + cpptrace::microfmt::format("#1 0x0000000000000002 in foo() at {}:{}:20", __FILE__, line + 1), + cpptrace::microfmt::format(" {}: unsigned line = __LINE__ + 1;", line - 1), + cpptrace::microfmt::format( + " {}: trace.frames.push_back({0x1, 0x1001, {line}, {{20}}, __FILE__, \"foo()\", false});", + line + ), + cpptrace::microfmt::format( + " {}: trace.frames.push_back({0x2, 0x1002, {line + 1}, {{20}}, __FILE__, \"foo()\", false});", + line + 1 + ), + cpptrace::microfmt::format(" {}: auto formatter = cpptrace::formatter{{}}", line + 2), + cpptrace::microfmt::format(" {}: .set_snippets(true);", line + 3) + ) + ); + formatter.set_snippet_context(1); + res = split(formatter.format(trace), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + // frame 1 + cpptrace::microfmt::format("#0 0x0000000000000001 in foo() at {}:{}:20", __FILE__, line), + cpptrace::microfmt::format(" {}: unsigned line = __LINE__ + 1;", line - 1), + cpptrace::microfmt::format( + " {}: trace.frames.push_back({0x1, 0x1001, {line}, {{20}}, __FILE__, \"foo()\", false});", + line + ), + cpptrace::microfmt::format( + " {}: trace.frames.push_back({0x2, 0x1002, {line + 1}, {{20}}, __FILE__, \"foo()\", false});", + line + 1 + ), + // frame 2 + cpptrace::microfmt::format("#1 0x0000000000000002 in foo() at {}:{}:20", __FILE__, line + 1), + cpptrace::microfmt::format( + " {}: trace.frames.push_back({0x1, 0x1001, {line}, {{20}}, __FILE__, \"foo()\", false});", + line + ), + cpptrace::microfmt::format( + " {}: trace.frames.push_back({0x2, 0x1002, {line + 1}, {{20}}, __FILE__, \"foo()\", false});", + line + 1 + ), + cpptrace::microfmt::format(" {}: auto formatter = cpptrace::formatter{{}}", line + 2) + ) + ); +} + +TEST(FormatterTest, Colors) { + auto formatter = cpptrace::formatter{} + .set_color_mode(cpptrace::formatter::color_mode::always); + auto res = split(formatter.format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + "#0 \x1B[34m0x0000000000000001\x1B[0m in \x1B[33mfoo()\x1B[0m at \x1B[32mfoo.cpp\x1B[0m:\x1B[34m20\x1B[0m:\x1B[34m30\x1B[0m", + "#1 \x1B[34m0x0000000000000002\x1B[0m in \x1B[33mbar()\x1B[0m at \x1B[32mbar.cpp\x1B[0m:\x1B[34m30\x1B[0m:\x1B[34m40\x1B[0m", + "#2 \x1B[34m0x0000000000000003\x1B[0m in \x1B[33mmain\x1B[0m at \x1B[32mfoo.cpp\x1B[0m:\x1B[34m40\x1B[0m:\x1B[34m25\x1B[0m" + ) + ); +} + +TEST(FormatterTest, Filtering) { + auto formatter = cpptrace::formatter{} + .set_filter([] (const cpptrace::stacktrace_frame& frame) -> bool { + return frame.filename.find("foo.cpp") != std::string::npos; + }); + auto res = split(formatter.format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + "#0 0x0000000000000001 in foo() at foo.cpp:20:30", + "#2 0x0000000000000003 in main at foo.cpp:40:25" + ) + ); +} + +TEST(FormatterTest, MoveSemantics) { + auto formatter = cpptrace::formatter{} + .set_filter([] (const cpptrace::stacktrace_frame& frame) -> bool { + return frame.filename.find("foo.cpp") != std::string::npos; + }); + auto formatter2 = std::move(formatter); + auto res = split(formatter2.format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + "#0 0x0000000000000001 in foo() at foo.cpp:20:30", + "#2 0x0000000000000003 in main at foo.cpp:40:25" + ) + ); + cpptrace::formatter formatter3; + formatter3 = std::move(formatter); + auto res2 = split(formatter2.format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res2, + ElementsAre( + "Stack trace (most recent call first):", + "#0 0x0000000000000001 in foo() at foo.cpp:20:30", + "#2 0x0000000000000003 in main at foo.cpp:40:25" + ) + ); +} + +TEST(FormatterTest, CopySemantics) { + auto formatter = cpptrace::formatter{} + .set_filter([] (const cpptrace::stacktrace_frame& frame) -> bool { + return frame.filename.find("foo.cpp") != std::string::npos; + }); + auto formatter2 = formatter; + auto res = split(formatter2.format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res, + ElementsAre( + "Stack trace (most recent call first):", + "#0 0x0000000000000001 in foo() at foo.cpp:20:30", + "#2 0x0000000000000003 in main at foo.cpp:40:25" + ) + ); + cpptrace::formatter formatter3; + formatter3 = formatter; + auto res2 = split(formatter2.format(make_test_stacktrace()), "\n"); + EXPECT_THAT( + res2, + ElementsAre( + "Stack trace (most recent call first):", + "#0 0x0000000000000001 in foo() at foo.cpp:20:30", + "#2 0x0000000000000003 in main at foo.cpp:40:25" + ) + ); +} + +}