Skip to content
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

Merged
merged 34 commits into from Dec 24, 2020

Conversation

yeswalrus
Copy link
Contributor

@yeswalrus yeswalrus commented Nov 30, 2020

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.

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
Copy link
Contributor Author

@yeswalrus yeswalrus Dec 1, 2020

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

@@ -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) {
Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor

@vitaut vitaut left a 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.

Comment on lines 773 to 779
// 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
Copy link
Contributor

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?

@yeswalrus
Copy link
Contributor Author

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?

@vitaut
Copy link
Contributor

vitaut commented Dec 4, 2020

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?

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
Comment on lines 168 to 169
Enforcing Compile-time Format Checks
------------------------------------
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compile -> compile

@yeswalrus yeswalrus requested a review from vitaut December 9, 2020 19:28
@yeswalrus
Copy link
Contributor Author

#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

Comment on lines 773 to 780
// 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
Copy link
Contributor

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.

@@ -106,6 +106,10 @@ add_fmt_test(printf-test)
add_fmt_test(ranges-test)
add_fmt_test(scan-test)


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra newline

@@ -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)
Copy link
Contributor

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

@@ -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")
Copy link
Contributor

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.

Comment on lines 17 to 19
#ifdef WIN32
# define _CRT_SECURE_NO_WARNINGS
#endif
Copy link
Contributor

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.

Comment on lines 67 to 91
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);
}
Copy link
Contributor

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.

}

void TestChrono() {
#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why conditionally compiled?

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines 111 to 126
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, "_"));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant.

Copy link
Contributor Author

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

(void)fmt::format(FMT_STRING("{}"), fmt::join(hello, "_"));
}

int main(int, char**) {
Copy link
Contributor

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.

Comment on lines 135 to 136

return 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed.

@yeswalrus
Copy link
Contributor Author

yeswalrus commented Dec 13, 2020

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 fmt::format(FMT_STRING("{}")) to compile, which it definitely shouldn't

@@ -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);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra newline

@@ -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),
Copy link
Contributor

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?

Copy link
Contributor Author

@yeswalrus yeswalrus Dec 20, 2020

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

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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).

Copy link
Contributor Author

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.

@@ -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...)``.
*/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra newline

Comment on lines 294 to 297
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));
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@yeswalrus yeswalrus Dec 21, 2020

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?

Copy link
Contributor

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.

@@ -106,6 +106,14 @@ add_fmt_test(printf-test)
add_fmt_test(ranges-test)
add_fmt_test(scan-test)

if(NOT MSVC)
Copy link
Contributor

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.

Comment on lines 8 to 15
#include <array>
#include <iterator>
#include <list>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
Copy link
Contributor

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.


// Exercise the API to verify that everything we expect to can compile.
void test_format_api() {
(void)fmt::format(FMT_STRING("{}"), 42);
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines 28 to 29
#if !FMT_GCC_VERSION // Currently will not compile: See
// https://github.com/fmtlib/fmt/issues/2039
Copy link
Contributor

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.

Comment on lines 52 to 53
(void)udl_format;
(void)udl_format_w;
Copy link
Contributor

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.

Comment on lines 73 to 67
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);
}
Copy link
Contributor

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.

Copy link
Contributor

@vitaut vitaut left a 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.

Comment on lines 801 to 802
basic_memory_buffer<Char> buffer;
auto bufOut = std::back_inserter(buffer);
Copy link
Contributor

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.

Comment on lines 294 to 297
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));
Copy link
Contributor

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.

}

void test_range() {
std::array<char, 5> hello = {'h','e','l','l','o'};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please apply clang-format.

@yeswalrus
Copy link
Contributor Author

Done! Good call on refactoring to use write - worked like a charm and IMO the code is easier to follow now as well.

Comment on lines 55 to 56
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) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, please revert.

Comment on lines 251 to 252
OutputIt add_range_formatting_spaces(OutputIt out,
const Formatting& formatting) {
Copy link
Contributor

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

Comment on lines 253 to 255
if (const_check(formatting.add_prepostfix_space)) {
*out++ = ' ';
}
Copy link
Contributor

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.

Copy link
Contributor Author

@yeswalrus yeswalrus Dec 24, 2020

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)

Comment on lines 257 to 259
if (const_check(formatting.add_delimiter_spaces)) {
*out++ = ' ';
}
Copy link
Contributor

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.

return add_space ? " \"{}\"" : "\"{}\"";
template <
typename Char, typename OutputIt, typename Arg,
FMT_ENABLE_IF(std::is_same<Arg, const char*>::value ||
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines 378 to 390
if (formatting.add_prepostfix_space) *out++ = ' ';
if (formatting.add_prepostfix_space) {
*out++ = ' ';
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert (or remove).

@vitaut vitaut merged commit 4fa4c92 into fmtlib:master Dec 24, 2020
@vitaut
Copy link
Contributor

vitaut commented Dec 24, 2020

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants