-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add tests for FMT_ENFORCE_COMPILE_STRING, fix several errors #2038
Conversation
1b97d1d
to
930c554
Compare
include/fmt/chrono.h
Outdated
return format_to(out, compile_string_to_view(format), val); | ||
|
||
// Workaround a compiler error in MSVC < 16.8.2 | ||
#if defined(FMT_MSC_VER) && FMT_MSC_VER <= 1928 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My last fix was actually in error and did not hold up to CI once a test was in place. Microsoft states that this is fixed as of 16.8.2 however I cannot verify since the most recent version on godbolt is 19.27
include/fmt/core.h
Outdated
@@ -1841,6 +1841,15 @@ inline auto format_to_n(OutputIt out, size_t n, const S& format_str, | |||
Returns the number of characters in the output of | |||
``format(format_str, args...)``. | |||
*/ | |||
template <typename S, typename... Args, | |||
FMT_ENABLE_IF(is_compile_string<S>::value)> | |||
inline size_t formatted_size(const S& format_str, Args&&... args) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appeared to be a gap in the API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks unrelated to the current PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could break it out into a separate followup PR, but it seemed relevant since without it there's no way to call formatted_size while FMT_ENFORCE_COMPILE_STRING is set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move to a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR.
Testing FMT_ENFORCE_COMPILE_STRING
is a good idea but I think we should avoid duplicating tests. The new test should only check that the public headers compile with FMT_ENFORCE_COMPILE_STRING
set with no or few checks of its own.
include/fmt/chrono.h
Outdated
// Workaround a compiler error in MSVC <= 16.8.2 | ||
#if FMT_MSC_VER && FMT_MSC_VER <= 1928 | ||
return vformat_to(out, to_string_view(format), | ||
make_format_args<buffer_context<Char>>(val)); | ||
#else | ||
return format_to(out, FMT_STRING(format), val); | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you move this workaround to a separate PR and provide more details?
I agree we should avoid duplicating tests, but we kinda have to to a certain extent since without a file exercising the APIs nothing will actually get instantiated? One option might be making a separate file for all the tests of FMT_STRING specifically that could then be compiled with and without FMT_ENFORCE_COMPILE_STRING, would that work for you? |
b7ab26d
to
270f664
Compare
Sure but we don't need to replicate the tests, only force the necessary instantiations. Therefore I suggest removing all assertions to make it clear that this is not testing formatting functionality (which should be tested elsewhere) and only leave function calls that trigger instantiations. |
doc/api.rst
Outdated
Enforcing Compile-time Format Checks | ||
------------------------------------ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this doesn't need to be a separate section. Instead add a paragraph to the Compile-time Format String Checks section above.
doc/api.rst
Outdated
@@ -165,6 +165,14 @@ functions in their ``formatter`` specializations. | |||
|
|||
.. _udt: | |||
|
|||
Enforcing Compile-time Format Checks | |||
------------------------------------ | |||
To force the use of Compile-time checks, compile with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compile -> compile
#2049 was closed, but the modifications to chrono.h are required. An alternative here might be to say that FMT_ENFORCE_COMPILE_STRING is simply not supported at all in MSVC, and disable the enforce_compile_string test for that compiler |
include/fmt/chrono.h
Outdated
// Note(12/3/2020): Workaround an as-of-yet unfixed compiler error in MSVC. | ||
// See https://developercommunity.visualstudio.com/content/problem/1277597/internal-compiler-c0001-error-on-complex-nested-la.html | ||
#if FMT_MSC_VER | ||
return vformat_to(out, to_string_view(format), | ||
make_format_args<buffer_context<Char>>(val)); | ||
#else | ||
return format_to(out, FMT_STRING(format), val); | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As commented in #2049, I don't think it's worth doing.
test/CMakeLists.txt
Outdated
@@ -106,6 +106,10 @@ add_fmt_test(printf-test) | |||
add_fmt_test(ranges-test) | |||
add_fmt_test(scan-test) | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: extra newline
test/CMakeLists.txt
Outdated
@@ -106,6 +106,10 @@ add_fmt_test(printf-test) | |||
add_fmt_test(ranges-test) | |||
add_fmt_test(scan-test) | |||
|
|||
|
|||
add_fmt_test(enforce-compiletime-test) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enforce-compiletime-test -> enforce-compile-string-test for consistency with the macro name
test/CMakeLists.txt
Outdated
@@ -106,6 +106,10 @@ add_fmt_test(printf-test) | |||
add_fmt_test(ranges-test) | |||
add_fmt_test(scan-test) | |||
|
|||
|
|||
add_fmt_test(enforce-compiletime-test) | |||
target_compile_definitions(enforce-compiletime-test PRIVATE "-DFMT_ENFORCE_COMPILE_STRING") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please break the long line.
test/enforce-compiletime-test.cc
Outdated
#ifdef WIN32 | ||
# define _CRT_SECURE_NO_WARNINGS | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed, please remove.
test/enforce-compiletime-test.cc
Outdated
struct test_output_iterator { | ||
char* data; | ||
|
||
using iterator_category = std::output_iterator_tag; | ||
using value_type = void; | ||
using difference_type = void; | ||
using pointer = void; | ||
using reference = void; | ||
|
||
test_output_iterator& operator++() { | ||
++data; | ||
return *this; | ||
} | ||
test_output_iterator operator++(int) { | ||
auto tmp = *this; | ||
++data; | ||
return tmp; | ||
} | ||
char& operator*() { return *data; } | ||
}; | ||
|
||
void FormatToNOutputIteratorTest() { | ||
char buf[10] = {}; | ||
fmt::format_to_n(test_output_iterator{buf}, 10, FMT_STRING("{}"), 42); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is redundant, please remove.
test/enforce-compiletime-test.cc
Outdated
} | ||
|
||
void TestChrono() { | ||
#ifndef FMT_STATIC_THOUSANDS_SEPARATOR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why conditionally compiled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH, I'm not sure - all the tests covering formatting of chrono types in chrono_test.cc were too so I figured it was important? I'll try removing it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I lack context for this particular test, but: FMT_STATIC_THOUSANDS_SEPARATOR
is "overloaded" to mean "I don't like <locale>
and want nothing to do with it", and so the locale_ref
functions are not implemented if FMT_STATIC_THOUSANDS_SEPARATOR
is defined. However, chrono_formatter::format_localized
always tries to use the locale_ref
functions, so chrono-test
won't link in that situation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the explanation. It seems to work just fine without the ifdef in this situation so I'll leave them removed.
test/enforce-compiletime-test.cc
Outdated
struct zstring_sentinel {}; | ||
|
||
bool operator==(const char* p, zstring_sentinel) { return *p == '\0'; } | ||
bool operator!=(const char* p, zstring_sentinel) { return *p != '\0'; } | ||
|
||
struct zstring { | ||
const char* p; | ||
const char* begin() const { return p; } | ||
zstring_sentinel end() const { return {}; } | ||
}; | ||
|
||
void TestZString() { | ||
zstring hello{"hello"}; | ||
(void)fmt::format(FMT_STRING("{}"), hello); | ||
(void)fmt::format(FMT_STRING("{}"), fmt::join(hello, "_")); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the 2nd overload but this actually exposed an issue with ranges.h:294
test/enforce-compiletime-test.cc
Outdated
(void)fmt::format(FMT_STRING("{}"), fmt::join(hello, "_")); | ||
} | ||
|
||
int main(int, char**) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arguments are unused, please remove.
test/enforce-compiletime-test.cc
Outdated
|
||
return 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed.
e4459fa
to
82c47d3
Compare
I've tried to solve the issue with GCC & calls to format with no arguments, but I'm not super familiar with the constexpr machinery at play & how to debug it. All the fixes I've tried so far also allow |
include/fmt/chrono.h
Outdated
@@ -769,18 +769,24 @@ template <typename Char, typename Rep, typename OutputIt, | |||
FMT_ENABLE_IF(std::is_integral<Rep>::value)> | |||
OutputIt format_duration_value(OutputIt out, Rep val, int) { | |||
static FMT_CONSTEXPR_DECL const Char format[] = {'{', '}', 0}; | |||
return format_to(out, compile_string_to_view(format), val); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: extra newline
include/fmt/chrono.h
Outdated
@@ -769,18 +769,24 @@ template <typename Char, typename Rep, typename OutputIt, | |||
FMT_ENABLE_IF(std::is_integral<Rep>::value)> | |||
OutputIt format_duration_value(OutputIt out, Rep val, int) { | |||
static FMT_CONSTEXPR_DECL const Char format[] = {'{', '}', 0}; | |||
return format_to(out, compile_string_to_view(format), val); | |||
|
|||
return vformat_to(out, to_string_view(format), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and below: why not wrap format
in FMT_STRING
and use format_to
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because then it wouldn't compile on windows because of the MSVC error? Thats why I was submitting that bug report and had that here before
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICS the MSVC bug is specific to static constexpr which we don't need here. I suggest replacing with:
return format_to(out, FMT_STRING("{}"), val);
which seems to work: https://godbolt.org/z/Teqnhq.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we needed the static constexpr array so that the format string type would match the type of Char
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a second thought, we don't need format_to
at all. This should be just write<Char>(out, val)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent! I had no idea about that API. I've redone the other calls that were using format_to in chrono as well.
include/fmt/core.h
Outdated
@@ -1841,6 +1841,7 @@ inline auto format_to_n(OutputIt out, size_t n, const S& format_str, | |||
Returns the number of characters in the output of | |||
``format(format_str, args...)``. | |||
*/ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra newline
include/fmt/ranges.h
Outdated
out = format_to(out, | ||
detail::format_str_quoted( | ||
(formatting.add_delimiter_spaces && i > 0), v), | ||
v); | ||
out = vformat_to(out, | ||
detail::format_str_quoted( | ||
(formatting.add_delimiter_spaces && i > 0), v), | ||
make_format_args<FormatContext>(v)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and below: it would be better to refactor this to use compile string and format_to
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered that but given that the format string used appeared to be determined at run-time I wasn't sure it was possible. I'll give it a go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this more I'm reaching the same conclusion - formatting
can't be used from within FMT_STRING
- doing so causes
/home/walter.gray/development/walrus/fmt/include/fmt/ranges.h:371:29: error: use of non-static data member 'formatting' of 'formatter<T, Char, enable_if_t<fmt::is_range<T, Char>::value && (has_formatter<detail::value_type<T>, format_context>::value || detail::has_fallback_formatter<detail::value_type<T>, format_context>::value)> >' from nested type 'FMT_COMPILE_STRING'
(formatting.add_delimiter_spaces && i > 0), *it)),
I couldn't have a function that returned one of several format strings, and I don't think I could have a function that returned a constexpr string that would work with FMT_STRING either -
constexpr const char* getString() {
return "{} bananna";
}
int main(int argc, char** argv) {
fmt::print(FMT_STRING(getString()), "fried");
}
for example will not compile in godbolt. I also cant return the result of FMT_STRING since they'd be different types between branches. My best guess for how to do this would be to have a wrapping function that used template specialization on a bool for add_delimiter_spaces and runtime switching on i > 0 and called format_to with the appropriate FMT_STRING directly. Am I missing an option here or should I try that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use write
instead of format_to
here too.
test/CMakeLists.txt
Outdated
@@ -106,6 +106,14 @@ add_fmt_test(printf-test) | |||
add_fmt_test(ranges-test) | |||
add_fmt_test(scan-test) | |||
|
|||
if(NOT MSVC) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing space after if
.
test/enforce-compile-string-test.cc
Outdated
#include <array> | ||
#include <iterator> | ||
#include <list> | ||
#include <map> | ||
#include <sstream> | ||
#include <string> | ||
#include <utility> | ||
#include <vector> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of these are unused.
test/enforce-compile-string-test.cc
Outdated
|
||
// Exercise the API to verify that everything we expect to can compile. | ||
void test_format_api() { | ||
(void)fmt::format(FMT_STRING("{}"), 42); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think cast is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC apple clang complains about an unused return value if it's not present, I'll try it again though.
test/enforce-compile-string-test.cc
Outdated
#if !FMT_GCC_VERSION // Currently will not compile: See | ||
// https://github.com/fmtlib/fmt/issues/2039 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should compile now, please rebase and remove.
test/enforce-compile-string-test.cc
Outdated
(void)udl_format; | ||
(void)udl_format_w; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variables are not needed, please remove.
test/enforce-compile-string-test.cc
Outdated
struct zstring_sentinel {}; | ||
|
||
bool operator==(const char* p, zstring_sentinel) { return *p == '\0'; } | ||
bool operator!=(const char* p, zstring_sentinel) { return *p != '\0'; } | ||
|
||
struct zstring { | ||
const char* p; | ||
const char* begin() const { return p; } | ||
zstring_sentinel end() const { return {}; } | ||
}; | ||
|
||
void test_zstring() { | ||
zstring hello{"hello"}; | ||
(void)fmt::format(FMT_STRING("{}"), hello); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks unnecessary. If you want to check ranges, you can use a standard container.
82c47d3
to
b9063f9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly looks good, just a few minor comments.
include/fmt/chrono.h
Outdated
basic_memory_buffer<Char> buffer; | ||
auto bufOut = std::back_inserter(buffer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unnecessary. You can write to out
directly.
include/fmt/ranges.h
Outdated
out = format_to(out, | ||
detail::format_str_quoted( | ||
(formatting.add_delimiter_spaces && i > 0), v), | ||
v); | ||
out = vformat_to(out, | ||
detail::format_str_quoted( | ||
(formatting.add_delimiter_spaces && i > 0), v), | ||
make_format_args<FormatContext>(v)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use write
instead of format_to
here too.
test/enforce-compile-string-test.cc
Outdated
} | ||
|
||
void test_range() { | ||
std::array<char, 5> hello = {'h','e','l','l','o'}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please apply clang-format.
Done! Good call on refactoring to use write - worked like a charm and IMO the code is easier to follow now as well. |
include/fmt/locale.h
Outdated
inline auto format_to(OutputIt out, const std::locale& loc, | ||
const S& format_str, Args&&... args) -> | ||
inline auto format_to(OutputIt out, const std::locale& loc, const S& format_str, | ||
Args&&... args) -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated, please revert.
include/fmt/ranges.h
Outdated
OutputIt add_range_formatting_spaces(OutputIt out, | ||
const Formatting& formatting) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add_range_formatting_spaces -> write_delimiter
include/fmt/ranges.h
Outdated
if (const_check(formatting.add_prepostfix_space)) { | ||
*out++ = ' '; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just remove this since add_prepostfix_space
is always false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My impression was that in user-declared overloads it could not be? (nm, I see thats impossible)
include/fmt/ranges.h
Outdated
if (const_check(formatting.add_delimiter_spaces)) { | ||
*out++ = ' '; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's write space unconditionally since add_delimiter_spaces
is always true and remove formatting
argument.
include/fmt/ranges.h
Outdated
return add_space ? " \"{}\"" : "\"{}\""; | ||
template < | ||
typename Char, typename OutputIt, typename Arg, | ||
FMT_ENABLE_IF(std::is_same<Arg, const char*>::value || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is std::is_same<Arg, const char*>::value
for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that this was required to make it so that for example a const char * array_of_literals[]
would be formatted with "
s, but I appear to be incorrect. I've added a test-case to cover that and you're correct, it appears unnecessary, though I couldn't say why.
include/fmt/ranges.h
Outdated
if (formatting.add_prepostfix_space) *out++ = ' '; | ||
if (formatting.add_prepostfix_space) { | ||
*out++ = ' '; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert (or remove).
Thank you! |
Though all format strings will be compile time checked by default in c++20, FMT_ENFORCE_COMPILE_STRING will still be useful for users stuck on older c++ versions.
This PR adds a test file with API coverage taken from other existing tests, uncovering 4 places where FMT_ENFORCE_COMPILE_STRING caused unexpected breakages. It additionally resolves those errors, allowing the test to pass.