Skip to content

Commit

Permalink
Print strings with unusual character types as an array of characters
Browse files Browse the repository at this point in the history
  • Loading branch information
jimporter committed Jul 22, 2024
1 parent 39c6489 commit e79ec88
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 112 deletions.
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
- `bencode.hpp` is now installed alongside mettle when `--vendorize` is used
- Arithmetic matchers now use ADL to find `max` and `abs`, allowing custom
arithmetic types to use their own implementations of these functions
- Attempt to print strings of arbitrary character type in messages
- Print strings with unusual character types as an array of characters

### Breaking changes
- Implementation updated to require C++17
Expand Down
2 changes: 1 addition & 1 deletion doc/about/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ in progress
- `bencode.hpp` is now installed alongside mettle when `--vendorize` is used
- Arithmetic matchers now use ADL to find `max` and `abs`, allowing custom
arithmetic types to use their own implementations of these functions
- Attempt to print strings of arbitrary character type in messages
- Print strings with unusual character types as an array of characters

### Breaking changes
- Implementation updated to require C++17
Expand Down
10 changes: 10 additions & 0 deletions include/mettle/matchers/result.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ namespace mettle {
return {!m.matched, std::move(m.message)};
}

// Ignore warnings about const-qualified function types
#if defined(_MSC_VER) && !defined(__clang__)
# pragma warning(push)
# pragma warning(disable:4180)
#endif

namespace detail {
template<typename T>
class message_impl {
Expand All @@ -49,6 +55,10 @@ namespace mettle {
};
}

#if defined(_MSC_VER) && !defined(__clang__)
# pragma warning(pop)
#endif

template<typename T>
inline const detail::message_impl<T>
matcher_message(const match_result &result, const T &fallback) {
Expand Down
46 changes: 13 additions & 33 deletions include/mettle/output/string.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,6 @@ namespace mettle {
return s;
}

inline std::string_view
convert_string(const std::basic_string_view<unsigned char> &s) {
auto begin = reinterpret_cast<const char *>(s.data());
return std::string_view(begin, s.size());
}

inline std::string_view
convert_string(const std::basic_string_view<signed char> &s) {
auto begin = reinterpret_cast<const char *>(s.data());
return std::string_view(begin, s.size());
}

inline std::string_view
convert_string(const std::basic_string_view<std::byte> &s) {
auto begin = reinterpret_cast<const char *>(s.data());
return std::string_view(begin, s.size());
}

// Ignore warnings about deprecated <codecvt>.
#if defined(_MSC_VER) && !defined(__clang__)
# pragma warning(push)
Expand Down Expand Up @@ -122,25 +104,23 @@ namespace mettle {
# pragma GCC diagnostic pop
#endif

namespace detail {
template<typename, typename = std::void_t<>>
struct is_string_convertible : std::false_type {};
template<typename, typename = std::void_t<>>
struct is_string_convertible : std::false_type {};

template<typename T>
struct is_string_convertible<T, std::void_t<
decltype(convert_string(std::declval<T&>()))
>> : std::true_type {};
}
template<typename T>
struct is_string_convertible<T, std::void_t<
decltype(convert_string(std::declval<T&>()))
>> : std::true_type {};

template<typename T>
inline constexpr bool is_string_convertible_v =
is_string_convertible<T>::value;

template<typename String>
inline auto
represent_string(const String &s,
[[maybe_unused]] char delim = '"') { // Silence GCC < 10.
if constexpr(detail::is_string_convertible<String>::value) {
return escape_string(convert_string(s), delim);
} else {
return std::string("(unrepresentable string)");
}
represent_string(const String &s, char delim = '"') {
static_assert(is_string_convertible_v<String>);
return escape_string(convert_string(s), delim);
}

} // namespace mettle
Expand Down
48 changes: 34 additions & 14 deletions include/mettle/output/to_printable.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#ifndef INC_METTLE_OUTPUT_TO_PRINTABLE_HPP
#define INC_METTLE_OUTPUT_TO_PRINTABLE_HPP

#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <locale>
#include <iomanip>
#include <sstream>
Expand All @@ -26,29 +28,23 @@ namespace mettle {
}

template<typename Char, typename Traits, typename Alloc>
inline std::string
to_printable(const std::basic_string<Char, Traits, Alloc> &s) {
inline auto to_printable(const std::basic_string<Char, Traits, Alloc> &s) ->
std::enable_if_t<is_string_convertible_v<
std::basic_string<Char, Traits, Alloc>>, std::string> {
return represent_string(s);
}

template<typename Char, typename Traits>
inline std::string
to_printable(const std::basic_string_view<Char, Traits> &s) {
inline auto to_printable(const std::basic_string_view<Char, Traits> &s) ->
std::enable_if_t<is_string_convertible_v<
std::basic_string_view<Char, Traits>>, std::string> {
return represent_string(s);
}

inline std::string to_printable(char c) {
return represent_string(std::string(1, c), '\'');
}

inline std::string to_printable(unsigned char c) {
return represent_string(std::string(1, c), '\'');
}

inline std::string to_printable(signed char c) {
return represent_string(std::string(1, c), '\'');
}

inline std::string to_printable(wchar_t c) {
return represent_string(std::wstring(1, c), '\'');
}
Expand All @@ -61,6 +57,24 @@ namespace mettle {
return represent_string(std::u32string(1, c), '\'');
}

inline std::string to_printable(unsigned char c) {
std::ostringstream ss;
ss << "0x" << std::setw(2) << std::setfill('0') << std::hex
<< static_cast<unsigned int>(c);
return ss.str();
}

inline std::string to_printable(signed char c) {
std::ostringstream ss;
ss << (c >= 0 ? '+' : '-') << "0x" << std::setw(2) << std::setfill('0')
<< std::hex << std::abs(static_cast<int>(c));
return ss.str();
}

inline std::string to_printable(std::byte b) {
return to_printable(static_cast<unsigned char>(b));
}

template<typename T>
inline auto to_printable(const T *s) -> std::enable_if_t<
is_any_char_v<T>, std::string
Expand Down Expand Up @@ -107,11 +121,17 @@ namespace mettle {
if constexpr(std::is_pointer_v<T>) {
using ValueType = std::remove_pointer_t<T>;
if constexpr(!std::is_const_v<ValueType>) {
return to_printable(const_cast<const ValueType*>(t));
return to_printable(const_cast<const ValueType *>(t));
} else {
if(!t) return to_printable(nullptr);
std::ostringstream ss;
ss << t;
if constexpr (std::is_same_v<ValueType, const unsigned char> ||
std::is_same_v<ValueType, const signed char>) {
// Don't print signed/unsigned char* as regular strings.
ss << static_cast<const void *>(t);
} else {
ss << t;
}
return ss.str();
}
} else if constexpr(std::is_enum_v<T>) {
Expand Down
3 changes: 0 additions & 3 deletions include/mettle/output/traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ namespace mettle {
template<typename T> struct is_any_char<const volatile T> : is_any_char<T> {};

template<> struct is_any_char<char> : std::true_type {};
template<> struct is_any_char<signed char> : std::true_type {};
template<> struct is_any_char<unsigned char> : std::true_type {};

template<> struct is_any_char<wchar_t> : std::true_type {};
template<> struct is_any_char<char16_t> : std::true_type {};
template<> struct is_any_char<char32_t> : std::true_type {};
Expand Down
35 changes: 13 additions & 22 deletions test/output/test_string_output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ suite<> test_string_output("string output", [](auto &_) {
});

subsuite<
char, unsigned char, signed char, wchar_t, char16_t, char32_t, std::byte
char, wchar_t, char16_t, char32_t
>(_, "convert_string()", type_only, [](auto &_) {
using C = fixture_type_t<decltype(_)>;
auto T = &make_string<C>;
Expand All @@ -68,32 +68,23 @@ suite<> test_string_output("string output", [](auto &_) {
});
});

subsuite<>(_, "represent_string()", [](auto &_) {
_.test("char", []() {
expect(represent_string("text"), equal_to("\"text\""));
});

_.test("wchar_t", []() {
expect(represent_string(L"text"), equal_to("\"text\""));
});

_.test("char16_t", []() {
expect(represent_string(u"text"), equal_to("\"text\""));
});
subsuite<
char, wchar_t, char16_t, char32_t
>(_, "represent_string()", type_only, [](auto &_) {
using C = fixture_type_t<decltype(_)>;
auto T = &make_string<C>;
using SV = std::basic_string_view<C>;

_.test("char32_t", []() {
expect(represent_string(U"text"), equal_to("\"text\""));
_.test("std::basic_string", [T]() {
expect(represent_string(T("text")), equal_to("\"text\""));
});

_.test("std::byte", []() {
using B = std::byte;
B s[] = {B('t'), B('e'), B('x'), B('t'), B('\0')};
expect(represent_string(s), equal_to("\"text\""));
_.test("std::basic_string_view", [T]() {
expect(represent_string(SV(T("text"))), equal_to("\"text\""));
});

_.test("int", []() {
expect(represent_string(std::basic_string<int>{'t', 'e', 'x', 't'}),
equal_to("(unrepresentable string)"));
_.test("C string", [T]() {
expect(represent_string(T("text").c_str()), equal_to("\"text\""));
});
});
});
Loading

0 comments on commit e79ec88

Please sign in to comment.