Commit b19c8721 authored by Dominik Charousset's avatar Dominik Charousset

Add format API scaffolding

The new `detail::format` and `detail::format_to` functions enable
format-based outputs in the new unit testing framework and gives us the
option to re-write the logging API without relying on the `std::ostream`
interfaces. These functions provide a thin wrapper around `std::format`.
In case the standard version is not available, we fall back to a minimal
implementation that delegates formatting of floats and integers to
`snprintf`.
parent 9498fc8a
......@@ -6,6 +6,7 @@ project(CAF CXX)
include(CMakeDependentOption)
include(CMakePackageConfigHelpers)
include(CheckCXXSourceCompiles)
include(CheckCXXSourceRuns)
include(GNUInstallDirs)
include(GenerateExportHeader)
......@@ -158,6 +159,26 @@ if(NOT CMAKE_CROSSCOMPILING)
endif()
endif()
# -- check whether we can use std::format --------------------------------------
if(NOT DEFINED CAF_USE_STD_FORMAT)
set(CAF_USE_STD_FORMAT OFF CACHE BOOL "Enable std::format support" FORCE)
if(NOT CMAKE_CROSSCOMPILING)
set(snippet "#include <format>
#include <iostream>
int main() { std::cout << std::format(\"{}\", \"ok\"); }")
check_cxx_source_runs("${snippet}" snippet_output)
if("${snippet_output}" STREQUAL "ok")
message(STATUS "Enable std::format support")
set(CAF_USE_STD_FORMAT ON CACHE BOOL "Enable std::format support" FORCE)
else()
message(STATUS "Disable std::format support: NOT available")
endif()
endif()
else()
set(CAF_USE_STD_FORMAT OFF CACHE BOOL "Enable std::format support" FORCE)
endif()
# -- export internal target (may be useful for re-using compiler flags) --------
set_target_properties(caf_internal PROPERTIES EXPORT_NAME internal)
......
......@@ -11,3 +11,5 @@
#cmakedefine CAF_ENABLE_EXCEPTIONS
#cmakedefine CAF_ENABLE_ACTOR_PROFILER
#cmakedefine CAF_USE_STD_FORMAT
......@@ -255,6 +255,7 @@ caf_add_component(
detail.base64
detail.bounds_checker
detail.config_consumer
detail.format
detail.group_tunnel
detail.ieee_754
detail.json
......@@ -385,6 +386,10 @@ caf_add_component(
uri
uuid)
if(NOT CAF_USE_STD_FORMAT)
target_sources(libcaf_core_obj PRIVATE src/detail/format.cpp)
endif()
if(CAF_ENABLE_TESTING AND CAF_ENABLE_EXCEPTIONS)
caf_add_test_suites(caf-core-test custom_exception_handler)
endif()
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
// Thin wrapper around std::format. If that is not available, we provide a
// minimal implementation that is "good enough" for our purposes.
//
// TODO: Currently, this only wraps the format_to and format functions as-is.
// The wrappers should also add support for types that provide an
// `inspect` overload.
#include "caf/detail/build_config.hpp"
#include <string>
#ifdef CAF_USE_STD_FORMAT
# include <format>
namespace caf::detail {
template <class OutputIt, class... Args>
auto format_to(OutputIt out, std::format_string<Args...> fstr, Args&&... args) {
return std::format_to(out, fstr, std::forward<Args>(args)...);
}
template <class... Args>
std::string format(std::format_string<Args...> fstr, Args&&... args) {
return std::format(fstr, std::forward<Args>(args)...);
}
} // namespace caf::detail
#else // here comes the poor man's version
# include "caf/detail/core_export.hpp"
# include "caf/span.hpp"
# include <array>
# include <cstdint>
# include <iterator>
# include <memory>
# include <string_view>
# include <variant>
namespace caf::detail {
using format_arg = std::variant<bool, char, int64_t, uint64_t, double,
const char*, std::string_view, const void*>;
template <class T>
format_arg make_format_arg(const T& arg) {
if constexpr (std::is_same_v<T, bool>) {
return format_arg{arg};
} else if constexpr (std::is_same_v<T, char>) {
return format_arg{arg};
} else if constexpr (std::is_integral_v<T>) {
if constexpr (std::is_signed_v<T>) {
return format_arg{static_cast<int64_t>(arg)};
} else {
return format_arg{static_cast<uint64_t>(arg)};
}
} else if constexpr (std::is_floating_point_v<T>) {
return format_arg{static_cast<double>(arg)};
} else if constexpr (std::is_same_v<T, const char*>) {
return format_arg{arg};
} else if constexpr (std::is_same_v<T, std::string>) {
return format_arg{std::string_view{arg}};
} else if constexpr (std::is_same_v<T, std::string_view>) {
return format_arg{arg};
} else {
static_assert(std::is_pointer_v<T>, "unsupported argument type");
return format_arg{static_cast<const void*>(arg)};
}
}
template <size_t N>
format_arg make_format_arg(const char (&arg)[N]) {
return format_arg{std::string_view{arg, N - 1}};
}
// Interface traversing the formatting output chunk by chunk.
class CAF_CORE_EXPORT compiled_format_string {
public:
virtual ~compiled_format_string();
// Checks whether we reached the end of the format string.
virtual bool at_end() const noexcept = 0;
// Returns the next chunk of the formatted output.
virtual std::string_view next() = 0;
};
CAF_CORE_EXPORT
std::unique_ptr<compiled_format_string>
compile_format_string(std::string_view fstr, span<format_arg> args);
template <class OutputIt, class... Args>
auto format_to(OutputIt out, std::string_view fstr, Args&&... raw_args) {
std::array<format_arg, sizeof...(Args)> args{make_format_arg(raw_args)...};
auto compiled = compile_format_string(fstr, make_span(args));
while (!compiled->at_end()) {
auto chunk = compiled->next();
out = std::copy(chunk.begin(), chunk.end(), out);
}
return out;
}
template <class... Args>
std::string format(std::string_view fstr, Args&&... args) {
std::string result;
result.reserve(fstr.size());
format_to(std::back_inserter(result), fstr, std::forward<Args>(args)...);
return result;
}
} // namespace caf::detail
#endif
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#include "caf/detail/format.hpp"
#include "caf/config.hpp"
#include "caf/detail/consumer.hpp"
#include "caf/detail/parser/chars.hpp"
#include "caf/detail/parser/is_char.hpp"
#include "caf/detail/parser/read_unsigned_integer.hpp"
#include "caf/detail/print.hpp"
#include "caf/detail/scope_guard.hpp"
#include "caf/parser_state.hpp"
#include "caf/pec.hpp"
#include "caf/raise_error.hpp"
#include "caf/span.hpp"
#include <type_traits>
CAF_PUSH_UNUSED_LABEL_WARNING
#include "caf/detail/parser/fsm.hpp"
namespace caf::detail::parser {
struct copy_state {
using fn_t = void (*)(string_parser_state&, copy_state&);
using maybe_uint = std::optional<size_t>;
explicit copy_state(span<format_arg> arg_list);
size_t next_arg_index = 0; // The next argument index for "auto"-mode.
maybe_uint arg_index; // The current argument index.
maybe_uint width; // The current width for formatting.
bool is_number = false; // Whether we are currently parsing a number.
char fill = ' '; // The fill character when setting an alignment.
char align = 0; // Alignment; '<': left, '>': right, '^': center.
char type = 0; // Selects an integer/float representation.
span<format_arg> args; // The list of arguments to use for formatting.
fn_t fn; // The current state function.
std::vector<char> buf; // Stores the formatted string.
std::vector<char> fstr; // Assembles the format string for snprintf.
// Restores the default values.
void reset() {
arg_index = std::nullopt;
width = std::nullopt;
fill = ' ';
align = 0;
type = 0;
is_number = false;
fstr.clear();
fstr.push_back('%');
}
// Callback for the format string parser.
void after_width() {
CAF_ASSERT(width.has_value());
if (align == 0) // if alignment is set, we manage the width ourselves
print(fstr, *width);
}
// Callback for the format string parser.
void after_width_index(size_t index) {
if (index >= args.size())
CAF_RAISE_ERROR(std::logic_error,
"invalid format string: width index out of range");
const auto& arg = args[index];
if (const auto* i64 = std::get_if<int64_t>(&arg)) {
if (*i64 < 0)
CAF_RAISE_ERROR(std::logic_error,
"invalid format string: negative width");
width = static_cast<size_t>(*i64);
after_width();
return;
}
if (const auto* u64 = std::get_if<uint64_t>(&arg)) {
width = static_cast<size_t>(*u64);
after_width();
return;
}
CAF_RAISE_ERROR(std::logic_error,
"invalid format string: expected an integer for width");
}
// Callback for the format string parser.
void after_precision(size_t value) {
print(fstr, value);
}
// Callback for the format string parser.
void after_precision_index(size_t index) {
if (index >= args.size())
CAF_RAISE_ERROR(std::logic_error,
"invalid format string: precision index out of range");
const auto& arg = args[index];
if (const auto* i64 = std::get_if<int64_t>(&arg)) {
if (*i64 < 0)
CAF_RAISE_ERROR(std::logic_error,
"invalid format string: negative precision");
print(fstr, *i64);
return;
}
if (const auto* u64 = std::get_if<uint64_t>(&arg)) {
print(fstr, *u64);
return;
}
CAF_RAISE_ERROR(std::logic_error,
"invalid format string: expected an integer for precision");
}
// Callback for the format string parser.
void set_type(char c) {
type = c;
fstr.push_back(c);
}
// Callback for the format string parser.
void set_padding(char c) {
fstr.push_back(c);
if (align != 0)
CAF_RAISE_ERROR(std::logic_error, "invalid format string: padding and "
"alignment are mutually exclusive");
}
// Callback for the format string parser.
void set_align(char fill_char, char align_char) {
fill = fill_char;
align = align_char;
}
// Adds padding to the buffer and aligns the content.
void apply_alignment(size_t fixed_width) {
if (buf.size() < fixed_width) {
if (align == 0)
align = is_number ? '>' : '<';
switch (align) {
case '<':
buf.insert(buf.end(), fixed_width - buf.size(), fill);
break;
case '>':
buf.insert(buf.begin(), fixed_width - buf.size(), fill);
break;
default: // '^'
buf.insert(buf.begin(), (fixed_width - buf.size()) / 2, fill);
buf.resize(fixed_width, fill);
}
}
}
// Renders the next argument.
void render() {
if (arg_index) {
render_arg(*arg_index);
arg_index = std::nullopt;
} else {
render_arg(next_arg_index++);
}
}
// Renders the argument at `index`.
void render_arg(size_t index) {
if (index >= args.size())
CAF_RAISE_ERROR(std::logic_error, "index out of range");
std::visit([&](auto val) { render_val(val); }, args[index]);
apply_alignment(width.value_or(0));
}
// Writes "true" or "false" to the buffer.
void render_val(bool val) {
print(buf, val);
}
// Writes a single character to the buffer.
void render_val(char val) {
buf.push_back(val);
}
// Calls snprintf to render `val`.
template <class T>
void do_sprintf(T val) {
fstr.push_back('\0');
auto buf_size = std::snprintf(nullptr, 0, fstr.data(), val);
if (buf_size <= 0)
CAF_RAISE_ERROR(std::logic_error, "invalid format string");
buf.resize(static_cast<size_t>(buf_size) + 1);
buf_size = std::snprintf(buf.data(), buf_size + 1, fstr.data(), val);
if (buf_size < 0 || static_cast<size_t>(buf_size) >= buf.size())
CAF_RAISE_ERROR(std::logic_error, "snprintf failed");
buf.resize(static_cast<size_t>(buf_size));
}
// Calls snprintf to render `val`.
void render_val(int64_t val) {
is_number = true;
switch (type) {
case 0:
type = 'd';
fstr.push_back('d');
[[fallthrough]];
case 'd':
case 'i':
fstr.pop_back();
fstr.push_back('l');
fstr.push_back('l');
fstr.push_back(type);
do_sprintf(static_cast<long long int>(val));
break;
case 'c':
if (val >= -128 && val <= 127) {
buf.push_back(static_cast<char>(val));
return;
}
CAF_RAISE_ERROR(std::logic_error,
"cannot convert integer to character: too large");
case 'o':
case 'x':
case 'X':
if (val < 0)
CAF_RAISE_ERROR(std::logic_error,
"cannot render negative number as unsigned");
render_val(static_cast<uint64_t>(val));
break;
default:
CAF_RAISE_ERROR(std::logic_error,
"invalid format string for unsigned integer");
}
}
// Calls snprintf to render `val`.
void render_val(uint64_t val) {
is_number = true;
switch (type) {
case 'd':
fstr.pop_back();
[[fallthrough]];
case 0:
type = 'u';
fstr.push_back('u');
[[fallthrough]];
case 'u':
case 'o':
case 'x':
case 'X':
fstr.pop_back();
fstr.push_back('l');
fstr.push_back('l');
fstr.push_back(type);
do_sprintf(static_cast<long long unsigned int>(val));
break;
case 'c':
if (val <= 127) {
buf.push_back(static_cast<char>(val));
return;
}
CAF_RAISE_ERROR(std::logic_error,
"cannot convert integer to character: too large");
default:
CAF_RAISE_ERROR(std::logic_error,
"invalid format string for unsigned integer");
}
}
// Calls snprintf to render `val`.
void render_val(double val) {
is_number = true;
switch (type) {
case 0:
type = 'g';
fstr.push_back('g');
[[fallthrough]];
case 'a':
case 'A':
case 'e':
case 'E':
case 'f':
case 'F':
case 'g':
case 'G':
do_sprintf(val);
break;
default:
CAF_RAISE_ERROR(std::logic_error, "invalid type for floating point");
}
}
// Writes a string to the buffer.
void render_val(const char* val) {
for (auto ch = *val++; ch != '\0'; ch = *val++)
buf.push_back(ch);
}
// Writes a string to the buffer.
void render_val(std::string_view val) {
buf.insert(buf.end(), val.begin(), val.end());
}
// Writes a pointer (address) to the buffer.
void render_val(const void* val) {
print(buf, val);
}
};
template <class ParserState>
void copy_verbatim(ParserState& ps, copy_state& cs);
// Parses a format string and writes the result to `cs`.
template <class ParserState>
void copy_formatted(ParserState& ps, copy_state& cs) {
cs.reset();
auto guard = make_scope_guard([&] {
if (ps.code <= pec::trailing_character)
cs.fn = copy_verbatim<string_parser_state>;
});
size_t tmp = 0;
auto make_tmp_consumer = [&tmp] {
tmp = 0;
return make_consumer(tmp);
};
typename ParserState::iterator_type maybe_fill;
// spec = [[fill]align][sign]["#"]["0"][width]["." precision]["L"][type]
// fill = <a character other than '{' or '}'>
// align = "<" | ">" | "^"
// sign = "+" | "-" | " "
// width = integer | "{" [arg_id] "}"
// precision = integer | "{" [arg_id] "}"
// type = "a" | "A" | "b" | "B" | "c" | "d" | "e" | "E" | "f" | "F" |
// "g" | "G" | "o" | "p" | "s" | "x" | "X"
// clang-format off
start();
state(init) {
fsm_epsilon(read_unsigned_integer(ps, make_consumer(cs.arg_index)),
after_arg_index, decimal_chars)
epsilon(after_arg_index)
}
state(after_arg_index) {
transition(has_close_brace, '}', cs.render())
transition(read_format_spec, ':')
}
state(read_format_spec) {
transition(disambiguate_fill, any_char, maybe_fill = ps.i)
}
state(disambiguate_fill) {
transition(after_align, "<>^", cs.set_align(*maybe_fill, ch))
epsilon(after_fill, any_char, ch = *(ps.i = maybe_fill)) // go back one char
}
state(after_fill) {
transition(after_align, "<>^", cs.set_align(' ', ch))
epsilon(after_align, any_char)
}
state(after_align) {
transition(after_sign, "+- ", cs.fstr.push_back(ch))
epsilon(after_sign, any_char)
}
state(after_sign) {
transition(after_alt, '#', cs.fstr.push_back(ch))
epsilon(after_alt, any_char)
}
state(after_alt) {
transition(after_padding, '0', cs.set_padding(ch))
epsilon(after_padding, any_char)
}
state(after_padding) {
fsm_epsilon(read_unsigned_integer(ps, make_consumer(cs.width)),
after_width_ret, decimal_chars)
fsm_transition(read_unsigned_integer(ps, make_tmp_consumer()),
after_width_index, '{')
epsilon(after_width, any_char)
}
state(after_width_index) {
transition(after_width, '}', cs.after_width_index(tmp))
}
state(after_width_ret) {
epsilon(after_width, any_char, cs.after_width())
}
state(after_width) {
transition(read_precision, '.', cs.fstr.push_back(ch))
epsilon(after_precision, any_char)
}
state(read_precision) {
fsm_epsilon(read_unsigned_integer(ps, make_tmp_consumer()),
after_precision_ret, decimal_chars)
fsm_transition(read_unsigned_integer(ps, make_tmp_consumer()),
after_precision_index, '{')
epsilon(after_precision, any_char)
}
state(after_precision_ret) {
epsilon(after_width, any_char, cs.after_precision(tmp))
}
state(after_precision_index) {
transition(after_precision, '}', cs.after_precision_index(tmp))
}
state(after_precision) {
transition(after_locale, 'L') // not supported by snprintf
epsilon(after_locale, any_char)
}
state(after_locale) {
transition(after_type, "aAbBcdeEfFgGopsxX", cs.set_type(ch))
transition(has_close_brace, '}', cs.render())
}
state(after_type) {
transition(has_close_brace, '}', cs.render())
}
term_state(has_close_brace) {
// nop
}
fin();
// clang-format on
}
// Copies the input verbatim to the output buffer until finding a format string.
template <class ParserState>
void copy_verbatim(ParserState& ps, copy_state& cs) {
// clang-format off
start();
term_state(init) {
transition(has_open_brace, '{')
transition(has_close_brace, '}')
transition(init, any_char, cs.buf.push_back(ch))
}
state(has_open_brace) {
transition(init, '{', cs.buf.push_back('{'))
epsilon(at_fmt_start, any_char, cs.fn = copy_formatted<string_parser_state>)
}
state(has_close_brace) {
transition(init, '}', cs.buf.push_back('}'))
}
term_state(at_fmt_start) {
// nop
}
fin();
// clang-format on
}
copy_state::copy_state(span<format_arg> arg_list) : args(arg_list) {
fn = copy_verbatim<string_parser_state>;
buf.reserve(64);
}
} // namespace caf::detail::parser
#include "caf/detail/parser/fsm_undef.hpp"
CAF_POP_WARNINGS
namespace caf::detail {
class compiled_format_string_impl : public compiled_format_string {
public:
explicit compiled_format_string_impl(std::string_view fstr,
span<format_arg> args)
: begin_(fstr.begin()), state_(begin_, fstr.end()), copy_(args) {
// nop
}
bool at_end() const noexcept override {
return state_.at_end();
}
std::string_view next() override {
copy_.buf.clear();
copy_.fn(state_, copy_);
if (state_.code > pec::trailing_character) {
auto pos = std::distance(begin_, state_.i);
std::string err = "format error at offset ";
err += std::to_string(pos);
err += ": ";
err += to_string(state_.code);
CAF_RAISE_ERROR(std::logic_error, err.c_str());
}
return std::string_view{copy_.buf.data(), copy_.buf.size()};
}
private:
std::string_view::iterator begin_;
string_parser_state state_;
parser::copy_state copy_;
};
compiled_format_string::~compiled_format_string() {
// nop
}
std::unique_ptr<compiled_format_string>
compile_format_string(std::string_view fstr, span<format_arg> args) {
return std::make_unique<compiled_format_string_impl>(fstr, args);
}
} // namespace caf::detail
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#define CAF_SUITE detail.format
#include "caf/detail/format.hpp"
#include "core-test.hpp"
#if !defined(CAF_USE_STD_FORMAT) && !defined(CAF_USE_SYSTEM_LIBFMT)
# define CAF_MINIMAL_FORMATTING
#endif
using namespace std::literals;
using caf::detail::format;
using caf::detail::format_to;
TEST_CASE("format strings without placeholders copies verbatim") {
CHECK_EQ(format("hello world"), "hello world");
CHECK_EQ(format("foo {{bar}}"), "foo {bar}");
CHECK_EQ(format("foo {{bar}} baz"), "foo {bar} baz");
}
TEST_CASE("format strings without indexes iterate over their arguments") {
CHECK_EQ(format("foo: {}{}", true, '!'), "foo: true!");
CHECK_EQ(format("bar: {}{}", false, '?'), "bar: false?");
CHECK_EQ(format("{} {} {} {} {}", 1, 2u, 2.5f, 4.5, "5"s), "1 2 2.5 4.5 5");
}
TEST_CASE("format strings with indexes uses the specified arguments") {
CHECK_EQ(format("{1} {2} {0}", 3, 1, 2), "1 2 3");
CHECK_EQ(format("{1} {0} {1}", 1, 2), "2 1 2");
}
TEST_CASE("format strings can specify rendering of floating point numbers") {
CHECK_EQ(format("{}", 2.5), "2.5");
CHECK_EQ(format("{:.3f}", 2.5), "2.500");
CHECK_EQ(format("{:.3F}", 2.5), "2.500");
CHECK_EQ(format("{:g}", 2.5), "2.5");
CHECK_EQ(format("{:G}", 2.5), "2.5");
CHECK_EQ(format("{:.0e}", 10.0), "1e+01");
CHECK_EQ(format("{:.0E}", 10.0), "1E+01");
}
TEST_CASE("format strings can specify rendering of integers") {
CHECK_EQ(format("{}", 42), "42");
CHECK_EQ(format("{:d}", 42), "42");
CHECK_EQ(format("{:c}", 42), "*");
CHECK_EQ(format("{:o}", 42), "52");
CHECK_EQ(format("{:#o}", 42), "052");
CHECK_EQ(format("{:x}", 42), "2a");
CHECK_EQ(format("{:X}", 42), "2A");
CHECK_EQ(format("{:#x}", 42), "0x2a");
CHECK_EQ(format("{:#X}", 42), "0X2A");
CHECK_EQ(format("{}", 42u), "42");
CHECK_EQ(format("{:d}", 42u), "42");
CHECK_EQ(format("{:c}", 42u), "*");
CHECK_EQ(format("{:o}", 42u), "52");
CHECK_EQ(format("{:#o}", 42u), "052");
CHECK_EQ(format("{:x}", 42u), "2a");
CHECK_EQ(format("{:X}", 42u), "2A");
CHECK_EQ(format("{:#x}", 42u), "0x2a");
CHECK_EQ(format("{:#X}", 42u), "0X2A");
CHECK_EQ(format("'{:+}' '{:-}' '{: }'", 1, 1, 1), "'+1' '1' ' 1'");
CHECK_EQ(format("'{:+}' '{:-}' '{: }'", -1, -1, -1), "'-1' '-1' '-1'");
}
TEST_CASE("format strings may specify the width of the output") {
CHECK_EQ(format("{0:0{1}}", 1, 2), "01");
CHECK_EQ(format("{1:02} {0:02}", 1, 2), "02 01");
CHECK_EQ(format("{:!<3}?{:!>3}", 0, 0), "0!!?!!0");
CHECK_EQ(format("{:!^3}?{:!^3}", 'A', 'A'), "!A!?!A!");
CHECK_EQ(format("{0:!^{1}}", 'A', 5), "!!A!!");
CHECK_EQ(format("{:<3}?{:>3}", 0, 0), "0 ? 0");
}
TEST_CASE("format strings accept various string types as values") {
using namespace std::literals;
const auto* cstr = "C-string";
CHECK_EQ(format("{}", cstr), "C-string");
CHECK_EQ(format("{}", "string literal"), "string literal");
CHECK_EQ(format("{}", "std::string"s), "std::string");
CHECK_EQ(format("{}", "std::string_view"sv), "std::string_view");
}
TEST_CASE("format_to can incrementally build a string") {
std::string str;
format_to(std::back_inserter(str), "foo");
CHECK_EQ(str, "foo");
format_to(std::back_inserter(str), "bar");
CHECK_EQ(str, "foobar");
format_to(std::back_inserter(str), "baz");
CHECK_EQ(str, "foobarbaz");
}
#if defined(CAF_ENABLE_EXCEPTIONS) && defined(CAF_MINIMAL_FORMATTING)
// Note: the standard version as well as libfmt (should) raise a compile-time
// error for these test cases. Only our minimal implementation throws.
TEST_CASE("ill-formatted formatting strings throw") {
CHECK_THROWS_AS(format("foo {"), std::logic_error);
CHECK_THROWS_AS(format("foo } bar"), std::logic_error);
CHECK_THROWS_AS(format("{1}", 1), std::logic_error);
}
#endif
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment