Clarifies and puts the extent of parity under test.
Broken out from #30546 based on #30546 (review) and #30546 (review).
Clarifies and puts the extent of parity under test.
Broken out from #30546 based on #30546 (review) and #30546 (review).
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/30933.
See the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
Reviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/30403435103
Make sure to run all tests locally, according to the documentation.
The failure may happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
18 {
19- // This was already executed at compile-time, but is executed again at run-time to avoid -Wunused.
20- decltype(fmt)::Detail_CheckNumFormatSpecifiers(fmt.fmt);
21+ // Prove parity with tinyformat
22+ switch (NumArgs) {
23+ case 0:
style nit (feel free to ignore): I suspect you could reduce verbosity and having to hand-format all of this by using tuple_cat
, see #30546 (review)
This should also ensure that all cases are covered and wouldn’t require to change the tests vectors.
Not sure it would reduce verbosity that much, but would definitely consider switching if someone took the time to provide a diff.
Compiler warnings are triggered for missing cases.
This should also ensure that all cases are covered and wouldn’t require to change the tests vectors.
If you don’t want to take it, that is fine. However, it would be good to at least terminate in the default case instead of silently passing. Otherwise it is unclear that all cases are covered.
While posting this PR I was aware of your tuple_cat
suggestion. I am not against it on principle but TBH I haven’t had a clear reason to struggle through that corner of C++ and don’t see your suggestion compelling enough to work through it. Your suggestion in that comment is close to compiling but doesn’t handle sending in one arg less for the failure cases. A motivation behind this PR is to prove parity in a clear way, and I’m worried it might obfuscate it. Happy to learn more from a diff.
Added default cases in dbbc2e52284a151a33d553b4a4032503906a4774, agree it’s more robust.
12@@ -13,17 +13,50 @@ BOOST_AUTO_TEST_SUITE(util_string_tests)
13 // Helper to allow compile-time sanity checks while providing the number of
14 // args directly. Normally PassFmt<sizeof...(Args)> would be used.
15 template <unsigned NumArgs>
16-inline void PassFmt(util::ConstevalFormatString<NumArgs> fmt)
17+inline void PassFmt(ConstevalFormatString<NumArgs> fmt)
18 {
19- // This was already executed at compile-time, but is executed again at run-time to avoid -Wunused.
20- decltype(fmt)::Detail_CheckNumFormatSpecifiers(fmt.fmt);
FailFmtWithError
, but happy path might only be run at compile time. Will bring it back.
14@@ -15,6 +15,12 @@
15 FALSE_POSITIVES = [
16 ("src/clientversion.cpp", "strprintf(_(COPYRIGHT_HOLDERS).translated, COPYRIGHT_HOLDERS_SUBSTITUTION)"),
17 ("src/test/translation_tests.cpp", "strprintf(format, arg)"),
18+ ("src/test/util_string_tests.cpp", "tfm::format(fmt.fmt)"),
19+ ("src/test/util_string_tests.cpp", 'tfm::format(fmt.fmt, "foo", "bar")'),
20+ ("src/test/util_string_tests.cpp", 'tfm::format(fmt.fmt, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11")'),
21+ ("src/test/util_string_tests.cpp", 'tfm::format(fmt.fmt, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12")'),
22+ ("src/test/util_string_tests.cpp", "tfm::format(fmt.fmt, a, b)"),
23+ ("src/test/util_string_tests.cpp", "tfm::format(fmt.fmt, a, b, c)"),
I’ll be happy to remove them once the linter is no longer run on CI.
That seems like extra churn and merge conflicts for no good reason. It should be trivial to avoid adding lines to this file. For example a simple #define tfm_fmt tfm::format
in the test (and using it) should avoid merge conflicts with future changes, or at least make them one-line conflicts at most.
Current approach:
Suggested approach:
#define
’s to temporarily work around a linter that is about to be removed, only to clean it up in another change seems like extra churn.Regardless:
tfm::*format*
function based on #30928.Maybe I’m missing something.
I’m not in a hurry to get this merged before other PRs in this area. Happy to move this to draft for now so other PRs can merge and deal with churn myself before undrafting.
Yes, exactly. Depending on how #30928 (review) turns out, this test-only pull will have to be adjusted.
There are already 4 different follow-ups, all of which explicitly or implicitly conflict with each other.
I don’t really want to open a 5th one to remove the linter first.
The more pull requests exist that explicitly or implicitly conflict with each other, the more review will be wasted, because if one of them is merged, all others will have to be rebased and re-reviewed. I understand that it isn’t always possible to avoid conflicts, but this whole changeset around formatting should be low priority enough to not require wasting (re)-review.
12@@ -13,17 +13,57 @@ BOOST_AUTO_TEST_SUITE(util_string_tests)
13 // Helper to allow compile-time sanity checks while providing the number of
14 // args directly. Normally PassFmt<sizeof...(Args)> would be used.
15 template <unsigned NumArgs>
16-inline void PassFmt(util::ConstevalFormatString<NumArgs> fmt)
17+inline void PassFmt(ConstevalFormatString<NumArgs> fmt)
18 {
19- // This was already executed at compile-time, but is executed again at run-time to avoid -Wunused.
20+ // Exercise happy paths at run-time for code coverage metrics.
I’m not sure I understand why we need this.
Why not make them all runtime instead, why are we “testing” compile time behavior here at all, aren’t we already doing that in the rest of the source code?
It seems to me the whole situation would simplify a lot if we would only test runtime behavior here.
constexpr
functions are not guaranteed to be able to evaluate at compile time for all possible parameters, so exercising that path in the test still provides some value IMO.
19- // This was already executed at compile-time, but is executed again at run-time to avoid -Wunused.
20+ // Exercise happy paths at run-time for code coverage metrics.
21 decltype(fmt)::Detail_CheckNumFormatSpecifiers(fmt.fmt);
22+
23+ // Prove parity with tinyformat
24+ switch (NumArgs) {
I like the idea of testing the validator together with the implementation!
But I’m not in love with the current approach.
Why are we testing PassFmt<1>("%02d");
with foo
- I don’t think it helps with understanding how formatter works.
Also PassFmt<1>("%s")
made sense when we were only validating the number of args, but we’ve extended it since, I think we should extend examples with the actual parameters, e.g.
PassFmt("%s", "test");
PassFmt("%02d", 42);
PassFmt("%12$s %2$s %1$s", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12");
In which case each example would document the format that we’re exercising more specifically.
It would also obviate the parameter count by examples, would all be runtime (seems to me that would simplify a lot) and comparison would simply depend on the behavior of tfm::format
, e.g. validation would be something like:
0template <typename... Args>
1inline bool ValidateFmt(const char* fmt, const char* expected_error, const Args&... args)
2{
3 try {
4 tfm::format(fmt, args...);
5 ConstevalFormatString<sizeof...(Args)>::Detail_CheckNumFormatSpecifiers(fmt);
6 return true;
7 } catch (const tfm::format_error&) {
8 using ErrType = const char*;
9 auto check_throw = [expected_error](const ErrType& str) { return str == expected_error; };
10 BOOST_CHECK_EXCEPTION(ConstevalFormatString<sizeof...(Args)>::Detail_CheckNumFormatSpecifiers(fmt), ErrType, check_throw);
11 return false;
12 }
13}
14
15template <typename... Args>
16void PassFmt(const char* fmt, const Args&... args) { BOOST_CHECK(ValidateFmt(fmt, nullptr, args...)); }
17
18template <typename... Args>
19void FailFmt(const char* fmt, const char* expected_error, const Args&... args) { BOOST_CHECK(!ValidateFmt(fmt, expected_error, args...)); }
and usage would be something like
0 PassFmt("%%");
1 PassFmt("%s", "test");
2
3 auto err_num{"Format specifier count must match the argument count!"};
4 FailFmt("%s", err_num);
5 FailFmt("%s", err_num, "test", "extra");
tuple_cat
approach, leaving it feeling somewhat half-baked.
My aim is to retain the tests of leaving out one arg though (which your suggestion doesn’t include)
I didn’t include it since I though a single instance of those is enough in the tests.
I would prefer having concrete typed examples over retesting -1 and +1 args (for which explicit examples should likely suffice)
I prefer retaining negative tests to increase certainty and would rather not do the PassFmt<1>("%s")
-> PassFmt("%s", "test")
refactor in this PR if that’s okay with you.
Edit: I double-checked that tinyformat errors on both too many and too few conversion/format specifiers, and agree the negative case isn’t necessary - removed in latest push.
30+
31+ // Prove parity with tinyformat
32+ TfmF(fmt.fmt, std::tuple_cat(std::array<int, NumArgs>{}));
33+ if constexpr (NumArgs > 0) {
34+ BOOST_CHECK_THROW(TfmF(fmt.fmt, std::tuple_cat(std::array<int, NumArgs - 1>{})), tfm::format_error);
35+ }
0 {
1 BOOST_CHECK_THROW(TfmF(fmt.fmt, std::tuple_cat(std::array<int, NumArgs + 1>{})), tfm::format_error);
2 }
style nit?
But let’s wait for CI first. There is a good chance that tuple_cat
doesn’t compile on Windows or one of the older compilers :sweat_smile:
Surely that’s more than style? :)
Would be nice to have both -1 and +1. Tried out your suggestion but tinyformat fails to throw in 8 of the cases. For example in "%12$s 999$s %2$s"
with 13 arguments, only using the 12th is not considered an error.
Are you still working on this?
I am asking now, because there shouldn’t be any conflicts after rebase, I think.
37+inline void PassFmtIncorrect(ConstevalFormatString<WrongNumArgs> fmt)
38+{
39+ // Disprove parity with tinyformat
40+ static_assert(WrongNumArgs != CorrectArgs);
41+ TfmF(fmt.fmt, std::tuple_cat(std::array<int, CorrectArgs>{}));
42+ BOOST_CHECK_THROW(TfmF(fmt.fmt, std::tuple_cat(std::array<int, WrongNumArgs>{})), tfm::format_error);
In commit “test: Prove+document ConstevalFormatString/tinyformat parity” (a911efc6e868ab1a23d05ee8ab3e94f7ecb89e2b)
The name TfmF
doesn’t have any clear meaning to me, and usage of the function is also complicated by the need to use tuple_cat everywhere. I think the code would be clearer if the function were less generic and did not require passing a tuple. Maybe would suggest:
0--- a/src/test/util_string_tests.cpp
1+++ b/src/test/util_string_tests.cpp
2@@ -10,12 +10,12 @@ using namespace util;
3
4 BOOST_AUTO_TEST_SUITE(util_string_tests)
5
6-template <typename... Tt>
7-void TfmF(const char* fmt, const std::tuple<Tt...>& t)
8+template <unsigned NumArgs>
9+std::string FormatZeroes(const char* fmt)
10 {
11- std::apply([fmt](const Tt&... ta){
12- tfm::format(fmt, ta...);
13- }, t);
14+ return std::apply([fmt](auto... args) {
15+ return tfm::format(fmt, args...);
16+ }, std::tuple_cat(std::array<int, NumArgs>{}));
17 }
18
19 // Helper to allow compile-time sanity checks while providing the number of
20@@ -27,9 +27,11 @@ inline void PassFmt(ConstevalFormatString<NumArgs> fmt)
21 decltype(fmt)::Detail_CheckNumFormatSpecifiers(fmt.fmt);
22
23 // Prove parity with tinyformat
24- TfmF(fmt.fmt, std::tuple_cat(std::array<int, NumArgs>{}));
25+ FormatZeroes<NumArgs>(fmt.fmt);
26+
27+ // Make sure tinyformat would throw if an argument is missing
28 if constexpr (NumArgs > 0) {
29- BOOST_CHECK_THROW(TfmF(fmt.fmt, std::tuple_cat(std::array<int, NumArgs - 1>{})), tfm::format_error);
30+ BOOST_CHECK_THROW(FormatZeroes<NumArgs - 1>(fmt.fmt), tfm::format_error);
31 }
32 }
33 template <unsigned WrongNumArgs, unsigned CorrectArgs>
34@@ -37,8 +39,8 @@ inline void PassFmtIncorrect(ConstevalFormatString<WrongNumArgs> fmt)
35 {
36 // Disprove parity with tinyformat
37 static_assert(WrongNumArgs != CorrectArgs);
38- TfmF(fmt.fmt, std::tuple_cat(std::array<int, CorrectArgs>{}));
39- BOOST_CHECK_THROW(TfmF(fmt.fmt, std::tuple_cat(std::array<int, WrongNumArgs>{})), tfm::format_error);
40+ FormatZeroes<CorrectArgs>(fmt.fmt);
41+ BOOST_CHECK_THROW(FormatZeroes<WrongNumArgs>(fmt.fmt), tfm::format_error);
42 }
43 template <unsigned WrongNumArgs>
44 inline void FailFmtWithError(std::string_view wrong_fmt, std::string_view error)
79@@ -58,9 +80,9 @@ BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
80 // The `*` specifier behavior is unsupported and can lead to runtime
81 // errors when used in a ConstevalFormatString. Please refer to the
82 // note in the ConstevalFormatString docs.
83- PassFmt<1>("%*c");
84- PassFmt<2>("%2$*3$d");
85- PassFmt<1>("%.*f");
86+ PassFmtIncorrect<1, 2>("%*c");
In commit “test: Prove+document ConstevalFormatString/tinyformat parity” (a911efc6e868ab1a23d05ee8ab3e94f7ecb89e2b)
Maybe drop WrongNumArgs and just pass CorrectArgs? Unless I’m missing something it seems like PassFmtIncorrect could choose the wrong number of args arbitrarily like PassFmt does.
PassFmtIncorrect
have now become valid PassFmt
thanks to your #31174. PassFmtIncorrect
is no more.
So, I was a bit late reacting to the merge of #31174, but here we are - rebased and ready for review again.
Added a commit regarding non-parity of "%n"
as suggested in #31174 (comment).
121@@ -110,6 +122,12 @@ BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
122 FailFmtWithError<2>("%1$*2$", err_term);
123 FailFmtWithError<2>("%1$.*2$", err_term);
124 FailFmtWithError<2>("%1$9.*2$", err_term);
125+
126+ // Non-parity between tinyformat and ConstevalFormatString
127+ int n{};
128+ BOOST_CHECK_EXCEPTION(tfm::format(std::string{"%n"}, n), tfm::format_error,
👍 for checking %n
as well!
nit: we might inline 0
here:
0 BOOST_CHECK_EXCEPTION(tfm::format(std::string{"%n"}, 0), tfm::format_error,
29+ // Exercise happy paths at run-time for code coverage metrics.
30 decltype(fmt)::Detail_CheckNumFormatSpecifiers(fmt.fmt);
31+
32+ // Prove parity with tinyformat, which will throw if the number of
33+ // conversion specifiers is incorrect.
34+ TfmFormatZeroes<NumArgs>(fmt.fmt);
I was wondering how it will behave with %c
which would add a \0
which could end the const char*
prematurely, but since we have a string it seems to work correctly.
Are there any checks we could do with the result here? Or make TfmFormatZeroes
void if we don’t need the actual result. Maybe something like this:
0 auto result{TfmFormatZeroes<NumArgs>(fmt.fmt)};
1 BOOST_CHECK_EQUAL(result.empty(), std::string_view{fmt.fmt}.empty());
re: #30933 (review)
I think suggested check would be broken in the case of strprintf("%.0s", 0)
where result should be empty (because it is printing the first 0 characters of a string) even though the format string empty. It has hard to think of a reliable check that could be done here that would also be meaningful.
(void)
ed in latest push and added commit testing "%c"
.
10@@ -11,18 +11,30 @@ using namespace util;
11
12 BOOST_AUTO_TEST_SUITE(util_string_tests)
13
14+template <unsigned NumArgs>
15+std::string TfmFormatZeroes(const char* fmt)
0
as a valid substitution?
tfm::format(std::string{"%s"}, 1.6f)
results in "1.6"
instead of an error, so tinyformat isn’t too picky about types.
27- // This was already executed at compile-time, but is executed again at run-time to avoid -Wunused.
28+ // Exercise happy paths at run-time for code coverage metrics.
29 decltype(fmt)::Detail_CheckNumFormatSpecifiers(fmt.fmt);
30+
31+ // Prove parity with tinyformat, which will throw if the number of
32+ // conversion specifiers is incorrect.
In commit “test: Prove+document ConstevalFormatString/tinyformat parity” (db081bd66015f7899696e6eb4a76052446685b64)
Can there be a test here or somewhere in this file that verifies calls to format really do throw if the number of specifiers is wrong? I am concerned there could be a future change like #30928 where tinyformat errors start being handled differently and these tests become broken without someone noticing.
121@@ -122,6 +122,12 @@ BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
122 FailFmtWithError<2>("%1$*2$", err_term);
123 FailFmtWithError<2>("%1$.*2$", err_term);
124 FailFmtWithError<2>("%1$9.*2$", err_term);
125+
126+ // Non-parity between tinyformat and ConstevalFormatString
In commit “test: Document non-parity between tinyformat and ConstevalFormatstring for “%n”” (2c82ee3219ee186b9d3998a129bff1bc809894d3):
Another case where this is not parity is when a noninteger type is used as a width like strprintf("%*s", "hi", "hi"),
which throws a tinyformat::format_error
exception “tinyformat: Cannot convert from argument type to integer for use as variable width or precision”. A test could be added for this as well.
strprintf("%.*s", "hi", "hi")
… (I guess it should technically be %.*f
but tinyformat doesn’t care, might change iff I re-touch).
🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/33979284676
Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
review ACK 8e7b252d8adb2821b946bd9052f1a707c4edea4f 🔏
Signature:
0untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
1RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
2trusted comment: review ACK 8e7b252d8adb2821b946bd9052f1a707c4edea4f 🔏
3YEFop/SgVPN4plP8ZSV4N79ZGj/2ySsY/oG6RmO4QgkHBsA1pFiqOgX9y8+BjXJ88F+fUv4ErGkaKKct3s7MDA==
121@@ -110,6 +122,12 @@ BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
122 FailFmtWithError<2>("%1$*2$", err_term);
123 FailFmtWithError<2>("%1$.*2$", err_term);
124 FailFmtWithError<2>("%1$9.*2$", err_term);
125+
126+ // Belt & suspenders for tinyformat behavior
In commit “test: Prove+document ConstevalFormatString/tinyformat parity” (4352596f4167d3e0c1d39ee46c6d5096f0228c61)
I don’t think this is a belt and suspenders check (assuming belt and suspenders means redundant with another check here). Would suggest a more actionable comment like: // Ensure that tinyformat will throw if format string contains wrong number of specifiers. PassFmt relies on this to verify tinyformat successfully formats the strings, and will need to be updated if tinyformat is changed not to throw on failure.
27 // Execute compile-time check again at run-time to get code coverage stats
28 util::detail::CheckNumFormatSpecifiers<NumArgs>(fmt.fmt);
29+
30+ // Prove parity with tinyformat, which will throw if the number of
31+ // conversion specifiers is incorrect.
32+ TfmFormatZeroes<NumArgs>(fmt.fmt);
In commit “test: Prove+document ConstevalFormatString/tinyformat parity” (4352596f4167d3e0c1d39ee46c6d5096f0228c61)
Adding BOOST_CHECK_NO_THROW could be nice to make intent clearer
BOOST_CHECK_NO_THROW
to your suggested non-parity checks at the bottom. :) Good point, also added to the original CheckNumFormatSpecifiers
-call in PassFmt
as I ended up touching that line.
130+ BOOST_CHECK_NO_THROW(util::detail::CheckNumFormatSpecifiers<2>("%*s"));
131+ BOOST_CHECK_EXCEPTION(tfm::format(std::string{"%*s"}, "hi", "hi"), tfm::format_error,
132+ HasReason{"tinyformat: Cannot convert from argument type to integer for use as variable width or precision"});
133+ BOOST_CHECK_NO_THROW(util::detail::CheckNumFormatSpecifiers<2>("%.*s"));
134+ BOOST_CHECK_EXCEPTION(tfm::format(std::string{"%.*s"}, "hi", "hi"), tfm::format_error,
135+ HasReason{"tinyformat: Cannot convert from argument type to integer for use as variable width or precision"});
In commit “test: Document non-parity between tinyformat and ConstevalFormatstring” (f71933c82b14d2a2e619d02f25d60a1432c3093d):
Repeating each case two different ways makes this harder to understand. Would suggest consolidating:
0 // Non-parity between tinyformat and ConstevalFormatString
1 // tinyformat throws but ConstevalFormatString does not
2 BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<1>{"%n"}, 0), tfm::format_error,
3 HasReason{"tinyformat: %n conversion spec not supported"});
4 BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<2>{"%*s"}, "hi", "hi"), tfm::format_error,
5 HasReason{"tinyformat: Cannot convert from argument type to integer for use as variable width or precision"});
6 BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<2>{"%.*s"}, "hi", "hi"), tfm::format_error,
7 HasReason{"tinyformat: Cannot convert from argument type to integer for use as variable width or precision"});
I think it is valid to have the CheckNumFormatSpecifiers
calls to prove they work.
In the latest push I’ve at least removed the util::detail::
parts to decrease noise which happily makes the format strings line up.
re: #30933 (review)
I think it is valid to have the
CheckNumFormatSpecifiers
calls to prove they work.
Agree it’s good to have these but ConstevalFormatString is calling CheckNumFormatSpecifiers internally. So suggested version should make these cases easier to read, easier to extend in the future, more consistent with each other, and not lose any test coverage (actually increasing it a little)
ConstevalFormatString
instead of std::string
. A GitHub suggestion might have made it stand out more. I did visit the optician earlier in the year… anyway. Taken in latest push.
25 inline void PassFmt(ConstevalFormatString<NumArgs> fmt)
26 {
27 // Execute compile-time check again at run-time to get code coverage stats
28 util::detail::CheckNumFormatSpecifiers<NumArgs>(fmt.fmt);
29+
30+ // Prove parity with tinyformat, which will throw if the number of
In commit “test: Prove+document ConstevalFormatString/tinyformat parity” (4352596f4167d3e0c1d39ee46c6d5096f0228c61)
Not sure this is really parity, since it only shows that tinyformat doesn’t throw when ConstevalFormatString doesn’t throw, not that tinyformat throws when ConstevalFormatString does (which we probably don’t care about). A more literal comment like “Make sure tinyformat doesn’t throw if ConstevalFormat didn’t throw” might make it more obvious what benefit of this check is.
Updated with more verbose comment, let me know if you think it’s okay.
0// If ConstevalFormatString didn't throw above, make sure tinyformat doesn't
1// throw either for the same format string and parameter count combination.
2// Proves that we have some extent of protection from runtime errors
3// (tinyformat may still throw for some type mismatches).
re: #30933 (review)
Updated with more verbose comment, let me know if you think it’s okay.
Looks good! Very clear
Latest push:
using util::detail::CheckNumFormatSpecifiers;
, decreasing noise in the rest of the changes.BOOST_CHECK_NO_THROW
to both CheckNumFormatSpecifiers
and tinyformat calls in PassFmt
.Also updated PR summary: “Makes unequivocally clear the extent of parity.” -> “Clarifies and puts the extent of parity under test.”
18-inline void PassFmt(util::ConstevalFormatString<NumArgs> fmt)
19+inline void PassFmt(ConstevalFormatString<NumArgs> fmt)
20 {
21 // Execute compile-time check again at run-time to get code coverage stats
22- util::detail::CheckNumFormatSpecifiers<NumArgs>(fmt.fmt);
23+ BOOST_CHECK_NO_THROW(CheckNumFormatSpecifiers<NumArgs>(fmt.fmt));
BOOST_CHECK_NO_THROW
, documents the reason for the dangling call a lot better
14
15 // Helper to allow compile-time sanity checks while providing the number of
16 // args directly. Normally PassFmt<sizeof...(Args)> would be used.
17 template <unsigned NumArgs>
18-inline void PassFmt(util::ConstevalFormatString<NumArgs> fmt)
19+inline void PassFmt(ConstevalFormatString<NumArgs> fmt)
nit: given that PassFmt
is local to util_string_tests
, I think static
would make more sense here (and the other helpers which sometimes have inline and sometimes nothing):
0static void PassFmt(ConstevalFormatString<NumArgs> fmt)
PassFmt
and FailFmtWithError
are marked inline, but the new TfmFormatZeroes
is not.
I’m fine with not making them static, but we could at least remove the redundant inline
s - not critical, I won’t block, would just prefer consistency.
inline
s as I had to push again anyway. Consistency++
They are also template functions, so implicitly inline
Just as a note. I don’t think this is generally true. For example, explicit template specialization isn’t implicitly inline. This is even explicitly mentioned in the standard, see https://timsong-cpp.github.io/cppwp/temp.expl.spec#12
You can get a link failure with a diff like so:
0diff --git a/src/node/chainstatemanager_args.cpp b/src/node/chainstatemanager_args.cpp
1index 0ac96c5514..96bc418819 100644
2--- a/src/node/chainstatemanager_args.cpp
3+++ b/src/node/chainstatemanager_args.cpp
4@@ -21,6 +21,18 @@
5 #include <chrono>
6 #include <string>
7
8+template <class A>
9+void Afoo(const A& a)
10+{
11+ std::cout << "one" << std::endl;
12+}
13+
14+template <>
15+void Afoo<int>(const int& a)
16+{
17+ std::cout << "specialized for int" << std::endl;
18+}
19+
20 namespace node {
21 util::Result<void> ApplyArgsManOptions(const ArgsManager& args, ChainstateManager::Options& opts)
22 {
23diff --git a/src/node/coins_view_args.cpp b/src/node/coins_view_args.cpp
24index 5d55143e83..3a0a7a5e40 100644
25--- a/src/node/coins_view_args.cpp
26+++ b/src/node/coins_view_args.cpp
27@@ -7,6 +7,17 @@
28 #include <common/args.h>
29 #include <txdb.h>
30
31+template <class A>
32+void Afoo(const A& a)
33+{
34+ std::cout << "one" << std::endl;
35+}
36+template <>
37+void Afoo<int>(const int& a)
38+{
39+ std::cout << "specialized for int" << std::endl;
40+}
41+
42 namespace node {
43 void ReadCoinsViewArgs(const ArgsManager& args, CoinsViewOptions& options)
44 {
139+ HasReason{"tinyformat: Cannot convert from argument type to integer for use as variable width or precision"});
140+
141+ // Ensure that tinyformat will throws if format string contains wrong number
142+ // of specifiers. PassFmt relies on this to verify tinyformat successfully
143+ // formats the strings, and will need to be updated if tinyformat is changed
144+ // not to throw on failure.
nit in 920782a8531a3dae7423ca304cc1fe2ec5ee46bf: possible typo. Also could remove the note that the test may need to be updated if the underlying tested module is changed (This should be obvious). Even more so, given that tinyformat likely won’t be changed before its removal with C++26.
0 // Ensure that tinyformat throws if format string contains wrong number
1 // of specifiers. PassFmt relies on this to verify tinyformat successfully
2 // formats the strings.
re: #30933 (review)
Good catch on the typo, and suggestion is ok, but disagree with:
Also could remove the note that the test may need to be updated if the underlying tested module is changed (This should be obvious). Even more so, given that tinyformat likely won’t be changed before its removal with C++26.
I agree it should not be necessary to say that changes to tinyformat could break assumptions made by a tinyformat_tests.cpp
test file, but it doesn’t seem obvious that a change to tinyformat would break assumptions made by util_string_tests.cpp
tests. And there is an open PR which could break this in #30928 commit 49f73071a8ec4131595090a619d6198a5f8b7c3c, so it’s not just a theoretical concern. Fine to change this, but I think it’s better if the comment clearly explains why this check is here.
136+ HasReason{"tinyformat: Cannot convert from argument type to integer for use as variable width or precision"});
137+ BOOST_CHECK_NO_THROW(CheckNumFormatSpecifiers<2>("%.*s"));
138+ BOOST_CHECK_EXCEPTION(tfm::format(std::string{"%.*s"}, "hi", "hi"), tfm::format_error,
139+ HasReason{"tinyformat: Cannot convert from argument type to integer for use as variable width or precision"});
140+
141+ // Ensure that tinyformat will throws if format string contains wrong number
0 // Ensure that tinyformat will throw if format string contains wrong number
nothing blocking, just a nit.
re-ACK 2951532a2ca09ac8ebdea066fa20826fdb6fa8e5 💼
Signature:
0untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
1RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
2trusted comment: re-ACK 2951532a2ca09ac8ebdea066fa20826fdb6fa8e5 💼
3LJt7P3harWblu2dLt4t2t8Z3yPelpL/kKq/KB7AoO45V3vTgMr8HXd6YbZiCCx5Y3BTDTIlVohJmss9B0+l9DA==
129+ // Ensure that tinyformat will throws if format string contains wrong number
130+ // of specifiers. PassFmt relies on this to verify tinyformat successfully
131+ // formats the strings, and will need to be updated if tinyformat is changed
132+ // not to throw on failure.
133+ BOOST_CHECK_EXCEPTION(TfmFormatZeroes<2>("%s"), tfm::format_error,
134+ HasReason{"tinyformat: Not enough conversion specifiers in format string"});
nit: don’t feel strongly about it, just noting that HasReason
can do partial matches, we can also check something shorter like:
0 HasReason{"Not enough conversion specifiers"});
(which would enable this being a one-liner)
commit message typo:
0- For "%n", which is supposed to write to the argument for printf.
16+void TfmFormatZeroes(const char* fmt)
17+{
18+ std::apply([fmt](auto... args) {
19+ (void)tfm::format(std::string{fmt}, args...);
20+ }, std::tuple_cat(std::array<int, NumArgs>{}));
21+}
nit: we could avoid [fmt]
via [&]
here, make it static
and give it a std::string
param directly:
0template <unsigned NumArgs>
1static void TfmFormatZeroes(const std::string& fmt)
2{
3 std::apply([&](auto... args) { tfm::format(fmt, args...); }, std::array<int, NumArgs>{});
4}
Thanks, taken in broad strokes.
Temporarily dded
0BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
1{
2+ TfmFormatZeroes<10>("blah")
together with
0void TfmFormatZeroes(const std::string& fmt)
1{
2 std::apply([&](auto... args) {
3+ fprintf(stderr, "ARG COUNT: %d", sizeof...(args));
4+ assert(false);
5 (void)tfm::format(fmt, args...);
6 }, std::array<int, NumArgs>{});
7}
To verify that tuple_cat
was unnecessary.
14@@ -15,6 +15,8 @@
15 FALSE_POSITIVES = [
16 ("src/clientversion.cpp", "strprintf(_(COPYRIGHT_HOLDERS), COPYRIGHT_HOLDERS_SUBSTITUTION)"),
17 ("src/test/translation_tests.cpp", "strprintf(format, arg)"),
18+ ("src/test/util_string_tests.cpp", 'tfm::format(std::string{"%*s"}, "hi", "hi")'),
ConstevalFormatString
is getting further towards being able to remove the linter in the first place.
Ah like these new ones you mean?
0[20:48:24.756] src/test/util_string_tests.cpp: Expected 0 argument(s) after format string but found 1 argument(s): tfm::format(ConstevalFormatString<2>{"%*s"}, "hi", "hi")
1[20:48:24.756] src/test/util_string_tests.cpp: Expected 0 argument(s) after format string but found 1 argument(s): tfm::format(ConstevalFormatString<2>{"%.*s"}, "hi", "hi")
Exactly :D
Fixed in latest push. @maflcko I realize you were concerned about touching this linter in #30933 (review), do you have an alternate approach suggestion?
@maflcko I realize you were concerned about touching this linter in #30933 (comment), do you have an alternate approach suggestion?
The file should just be deleted. I went ahead and did that in #31061 (comment)
Also adds BOOST_CHECK_NO_THROW() while touching that line, clarifying part of what we are checking for.
Also removed redundant inline from template functions in .cpp file.
Co-Authored-By: Lőrinc <pap.lorinc@gmail.com>
Co-Authored-By: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>
Co-Authored-By: Ryan Ofsky <ryan@ofsky.org>
🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/34054153748
Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
- For "%n", which is supposed to write to the argument for printf.
- For string/integer mismatches of width/precision specifiers.
Co-Authored-By: Ryan Ofsky <ryan@ofsky.org>
Proves tinyformat doesn't trigger an exception for \0 characters.
Co-Authored-By: Lőrinc <pap.lorinc@gmail.com>
125@@ -110,6 +126,24 @@ BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
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,
Good point. Might go with something like this (slightly useful) if I re-touch:
0 BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<2>{"%*s"}, "one", "two"), tfm::format_error,
125@@ -110,6 +126,24 @@ BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
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,
ConstevalFormatString
parity check for the FUZZ tests as well (guess that should reveal other cases where it’s misaligned)
I guess a version of ConstevalFormatString/CheckNumFormatSpecifiers()
could be checked in a test together with tfm::format
, but I’m not sure it would be a fuzz-test, as tfm::format()
takes args specified at compile time.
It could conceivably be a pretty advanced fuzz-test that generated C++ code and compiled it as part of the test, not sure if we have that class of tests already or if this would be new, and if it would be accepted.
CheckNumFormatSpecifiers
?
CheckNumFormatSpecifiers
is currently a template, but sure, could be changed to take the number of specifiers at runtime instead.
If we want to check parity in a fuzzing context we should probably check tfm::format()
as well, with the same input?
It could conceivably be a pretty advanced fuzz-test that generated C++ code and compiled it as part of the test, not sure if we have that class of tests already or if this would be new, and if it would be accepted.
I don’t think you need to generate C++ code and compile it. It should be possible to just write it directly. See:
0#include <test/fuzz/FuzzedDataProvider.h>
1#include <test/fuzz/fuzz.h>
2#include <test/fuzz/util.h>
3#include <tinyformat.h>
4#include <util/strencodings.h>
5#include <util/translation.h>
6
7#include <algorithm>
8#include <cstdint>
9#include <string>
10#include <vector>
11
12template <unsigned NumArgs>
13void Check(const std::string& fmt)
14{
15 try {
16 util::detail::CheckNumFormatSpecifiers<NumArgs>(fmt.c_str());
17 try {
18 std::apply([&](auto... args) { tfm::format(tfm::RuntimeFormat{fmt}, args...); }, std::array<int, NumArgs>{});
19 } catch (const std::exception& e) {
20 std::string_view what{e.what()};
21 if (what == "tinyformat: %n conversion spec not supported") return;
22 std::cout << "mismatch: " << what << std::endl;
23 std::cout << "'" << fmt << "'" << std::endl;
24 assert(false);
25 }
26 } catch (...) {
27 // fine
28 }
29}
30
31FUZZ_TARGET(str_printf)
32{
33 FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
34 const std::string format_string = fuzzed_data_provider.ConsumeRandomLengthString(64);
35
36 const int digits_in_format_specifier = std::count_if(format_string.begin(), format_string.end(), IsDigit);
37
38 // Avoid triggering the following crash bug:
39 // * strprintf("%987654321000000:", 1);
40 //
41 // Avoid triggering the following OOM bug:
42 // * strprintf("%.222222200000000$", 1.1);
43 //
44 // Upstream bug report: https://github.com/c42f/tinyformat/issues/70
45 if (format_string.find('%') != std::string::npos && digits_in_format_specifier >= 7) {
46 return;
47 }
48
49 // Avoid triggering the following crash bug:
50 // * strprintf("%1$*1$*", -11111111);
51 //
52 // Upstream bug report: https://github.com/c42f/tinyformat/issues/70
53 if (format_string.find('%') != std::string::npos && format_string.find('$') != std::string::npos && format_string.find('*') != std::string::npos && digits_in_format_specifier > 0) {
54 return;
55 }
56
57 // Avoid triggering the following crash bug:
58 // * strprintf("%.1s", (char*)nullptr);
59 //
60 // (void)strprintf(format_string, (char*)nullptr);
61 //
62 // Upstream bug report: https://github.com/c42f/tinyformat/issues/70
63
64 Check<0>(format_string);
65 Check<1>(format_string);
66 Check<2>(format_string);
67 Check<3>(format_string);
68 Check<4>(format_string);
69 Check<5>(format_string);
70}
For clarity, running the fuzz test will actually spit out some ways to trick the consteval check, because it doesn’t fully implement tinyformat at compile time. For example, the consteval check doesn’t ignore c99 length modifiers.
0diff --git a/src/test/util_string_tests.cpp b/src/test/util_string_tests.cpp
1index 340c650fe3..2284e775c6 100644
2--- a/src/test/util_string_tests.cpp
3+++ b/src/test/util_string_tests.cpp
4@@ -129,6 +129,16 @@ BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
5
6 // Non-parity between tinyformat and ConstevalFormatString.
7 // tinyformat throws but ConstevalFormatString does not.
8+ BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<1>{"%h"}, 0), tfm::format_error,
9+ HasReason{"tinyformat: Conversion spec incorrectly terminated by end of string"});
10+ BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<1>{"%7#%o"}, 0), tfm::format_error,
11+ HasReason{"tinyformat: Too many conversion specifiers in format string"});
12+ BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<2>{"%*9%*"}, 0, 0), tfm::format_error,
13+ HasReason{"tinyformat: Not enough arguments to read variable width or precision"});
14+ BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<1>{"%7#%8$"}, 0), tfm::format_error,
15+ HasReason{"tinyformat: Positional argument out of range"});
16+ BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<1>{"%6#%1$%%"}, 0), tfm::format_error,
17+ HasReason{"tinyformat: Non-positional argument used after a positional one"});
18 BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<1>{"%n"}, 0), tfm::format_error,
19 HasReason{"tinyformat: %n conversion spec not supported"});
20 BOOST_CHECK_EXCEPTION(tfm::format(ConstevalFormatString<2>{"%*s"}, "hi", "hi"), tfm::format_error,
Didn’t think to lock-down the number of args in that way and still get value from fuzzing. Turned out great!
Are you going to open a PR for this or what do you suggest as next step?
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,
strprintf
is a bit shorter than tfm::format
ACK c93bf0e6e2cd0e9dc979ed996e41cc1034bc1ad1 Changes:
tfm::format
of ConstevalFormatString
Would be curious if we could extend this to fuzzing as well - but it can be explored in a different PR as well
re-ACK c93bf0e6e2cd0e9dc979ed996e41cc1034bc1ad1 🗜
Signature:
0untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
1RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
2trusted comment: re-ACK c93bf0e6e2cd0e9dc979ed996e41cc1034bc1ad1 🗜
32bT04GVn2EBycqrMStGo3ZV6cz0SmTdEN2rHArqcWSDe5w8c/2B7Zg6UJUSGT5QzDPVgUPspDPW773Kb5GMiAg==