util_string_tests.cpp raw

   1  // Copyright (c) 2024-present The Limenka 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()
 150