util_string_tests.cpp
1 // Copyright (c) 2024-present The Bitcoin Core developers 2 // Distributed under the MIT software license, see the accompanying 3 // file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 5 #include <util/string.h> 6 7 #include <boost/test/unit_test.hpp> 8 #include <test/util/setup_common.h> 9 10 using namespace util; 11 using util::detail::CheckNumFormatSpecifiers; 12 13 BOOST_AUTO_TEST_SUITE(util_string_tests) 14 15 template <unsigned NumArgs> 16 void TfmFormatZeroes(const std::string& fmt) 17 { 18 std::apply([&](auto... args) { 19 (void)tfm::format(tfm::RuntimeFormat{fmt}, args...); 20 }, std::array<int, NumArgs>{}); 21 } 22 23 // Helper to allow compile-time sanity checks while providing the number of 24 // args directly. Normally PassFmt<sizeof...(Args)> would be used. 25 template <unsigned NumArgs> 26 void PassFmt(ConstevalFormatString<NumArgs> fmt) 27 { 28 // Execute compile-time check again at run-time to get code coverage stats 29 BOOST_CHECK_NO_THROW(CheckNumFormatSpecifiers<NumArgs>(fmt.fmt)); 30 31 // If ConstevalFormatString didn't throw above, make sure tinyformat doesn't 32 // throw either for the same format string and parameter count combination. 33 // Proves that we have some extent of protection from runtime errors 34 // (tinyformat may still throw for some type mismatches). 35 BOOST_CHECK_NO_THROW(TfmFormatZeroes<NumArgs>(fmt.fmt)); 36 } 37 template <unsigned WrongNumArgs> 38 void FailFmtWithError(const char* wrong_fmt, std::string_view error) 39 { 40 BOOST_CHECK_EXCEPTION(CheckNumFormatSpecifiers<WrongNumArgs>(wrong_fmt), const char*, HasReason{error}); 41 } 42 43 BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec) 44 { 45 PassFmt<0>(""); 46 PassFmt<0>("%%"); 47 PassFmt<1>("%s"); 48 PassFmt<1>("%c"); 49 PassFmt<0>("%%s"); 50 PassFmt<0>("s%%"); 51 PassFmt<1>("%%%s"); 52 PassFmt<1>("%s%%"); 53 PassFmt<0>(" 1$s"); 54 PassFmt<1>("%1$s"); 55 PassFmt<1>("%1$s%1$s"); 56 PassFmt<2>("%2$s"); 57 PassFmt<2>("%2$s 4$s %2$s"); 58 PassFmt<129>("%129$s 999$s %2$s"); 59 PassFmt<1>("%02d"); 60 PassFmt<1>("%+2s"); 61 PassFmt<1>("%.6i"); 62 PassFmt<1>("%5.2f"); 63 PassFmt<1>("%5.f"); 64 PassFmt<1>("%.f"); 65 PassFmt<1>("%#x"); 66 PassFmt<1>("%1$5i"); 67 PassFmt<1>("%1$-5i"); 68 PassFmt<1>("%1$.5i"); 69 // tinyformat accepts almost any "type" spec, even '%', or '_', or '\n'. 70 PassFmt<1>("%123%"); 71 PassFmt<1>("%123%s"); 72 PassFmt<1>("%_"); 73 PassFmt<1>("%\n"); 74 75 PassFmt<2>("%*c"); 76 PassFmt<2>("%+*c"); 77 PassFmt<2>("%.*f"); 78 PassFmt<3>("%*.*f"); 79 PassFmt<3>("%2$*3$d"); 80 PassFmt<3>("%2$*3$.9d"); 81 PassFmt<3>("%2$.*3$d"); 82 PassFmt<3>("%2$9.*3$d"); 83 PassFmt<3>("%2$+9.*3$d"); 84 PassFmt<4>("%3$*2$.*4$f"); 85 86 // Make sure multiple flag characters "- 0+" are accepted 87 PassFmt<3>("'%- 0+*.*f'"); 88 PassFmt<3>("'%1$- 0+*3$.*2$f'"); 89 90 auto err_mix{"Format specifiers must be all positional or all non-positional!"}; 91 FailFmtWithError<1>("%s%1$s", err_mix); 92 FailFmtWithError<2>("%2$*d", err_mix); 93 FailFmtWithError<2>("%*2$d", err_mix); 94 FailFmtWithError<2>("%.*3$d", err_mix); 95 FailFmtWithError<2>("%2$.*d", err_mix); 96 97 auto err_num{"Format specifier count must match the argument count!"}; 98 FailFmtWithError<1>("", err_num); 99 FailFmtWithError<0>("%s", err_num); 100 FailFmtWithError<2>("%s", err_num); 101 FailFmtWithError<0>("%1$s", err_num); 102 FailFmtWithError<2>("%1$s", err_num); 103 FailFmtWithError<1>("%*c", err_num); 104 105 auto err_0_pos{"Positional format specifier must have position of at least 1"}; 106 FailFmtWithError<1>("%$s", err_0_pos); 107 FailFmtWithError<1>("%$", err_0_pos); 108 FailFmtWithError<0>("%0$", err_0_pos); 109 FailFmtWithError<0>("%0$s", err_0_pos); 110 FailFmtWithError<2>("%2$*$d", err_0_pos); 111 FailFmtWithError<2>("%2$*0$d", err_0_pos); 112 FailFmtWithError<3>("%3$*2$.*$f", err_0_pos); 113 FailFmtWithError<3>("%3$*2$.*0$f", err_0_pos); 114 115 auto err_term{"Format specifier incorrectly terminated by end of string"}; 116 FailFmtWithError<1>("%", err_term); 117 FailFmtWithError<1>("%9", err_term); 118 FailFmtWithError<1>("%9.", err_term); 119 FailFmtWithError<1>("%9.9", err_term); 120 FailFmtWithError<1>("%*", err_term); 121 FailFmtWithError<1>("%+*", err_term); 122 FailFmtWithError<1>("%.*", err_term); 123 FailFmtWithError<1>("%9.*", err_term); 124 FailFmtWithError<1>("%1$", err_term); 125 FailFmtWithError<1>("%1$9", err_term); 126 FailFmtWithError<2>("%1$*2$", err_term); 127 FailFmtWithError<2>("%1$.*2$", err_term); 128 FailFmtWithError<2>("%1$9.*2$", err_term); 129 130 // Non-parity between tinyformat and ConstevalFormatString. 131 // tinyformat throws but ConstevalFormatString does not. 132 BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<1>{"%n"}, 0), tfm::format_error, 133 HasReason{"tinyformat: %n conversion spec not supported"}); 134 BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<2>{"%*s"}, "hi", "hi"), tfm::format_error, 135 HasReason{"tinyformat: Cannot convert from argument type to integer for use as variable width or precision"}); 136 BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<2>{"%.*s"}, "hi", "hi"), tfm::format_error, 137 HasReason{"tinyformat: Cannot convert from argument type to integer for use as variable width or precision"}); 138 139 // Ensure that tinyformat throws if format string contains wrong number 140 // of specifiers. PassFmt relies on this to verify tinyformat successfully 141 // formats the strings, and will need to be updated if tinyformat is changed 142 // not to throw on failure. 143 BOOST_CHECK_EXCEPTION(TfmFormatZeroes<2>("%s"), tfm::format_error, 144 HasReason{"tinyformat: Not enough conversion specifiers in format string"}); 145 BOOST_CHECK_EXCEPTION(TfmFormatZeroes<1>("%s %s"), tfm::format_error, 146 HasReason{"tinyformat: Too many conversion specifiers in format string"}); 147 } 148 149 BOOST_AUTO_TEST_SUITE_END()