On Fri, 2 Oct 2026 at 12:52, Tomasz Kamiński <[email protected]> wrote:
>
> From: Anlai Lu <[email protected]>
>
> Cover format_to, format_to_n truncation, writing to sentinel,
> write failure, and large writes that exceed the internal buffer.
> These paths were previously untested.
>
> Additionally includes examples of formatters that observe
> if the output is buffered (string is created by call to format),
> before inserting to stream.
>
> libstdc++-v3/ChangeLog:
>
>         * estsuite/27_io/basic_ostream/print/buffered.cc: New test.
>         * testsuite/std/format/functions/format_to_ostreambuf.cc:
>         New test.
>
> Co-authored-by: Tomasz Kamiński <[email protected]>
> Signed-off-by: Anlai Lu <[email protected]>
> Signed-off-by: Tomasz Kamiński <[email protected]>
> ---
> v4 add test for writting to sentinel.
>
> Rested the new test file. OK for trunk?

OK

> Why we want this backported to 16?

If we're about to change the implementation on trunk to use the new
sink, keeping the tests on gcc-16 ensures that these cases still work
on the branch.

>
>  .../27_io/basic_ostream/print/buffered.cc     |  90 +++++
>  .../format/functions/format_to_ostreambuf.cc  | 377 ++++++++++++++++++
>  2 files changed, 467 insertions(+)
>  create mode 100644 
> libstdc++-v3/testsuite/27_io/basic_ostream/print/buffered.cc
>  create mode 100644 
> libstdc++-v3/testsuite/std/format/functions/format_to_ostreambuf.cc
>
> diff --git a/libstdc++-v3/testsuite/27_io/basic_ostream/print/buffered.cc 
> b/libstdc++-v3/testsuite/27_io/basic_ostream/print/buffered.cc
> new file mode 100644
> index 00000000000..04ef159db39
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/27_io/basic_ostream/print/buffered.cc
> @@ -0,0 +1,90 @@
> +// { dg-additional-options "-lstdc++exp" { target { *-*-mingw* } } }
> +// { dg-do run { target c++23 } }
> +
> +#include <sstream>
> +#include <algorithm>
> +#include <testsuite_hooks.h>
> +
> +struct ThrowingFormat
> +{
> +  std::string val;
> +  bool throw_from_format = false;
> +};
> +
> +template<>
> +struct std::formatter<ThrowingFormat, char>
> +{
> +   constexpr
> +   std::format_parse_context::iterator
> +   parse(std::format_parse_context& ctx) const
> +   { return ctx.begin(); }
> +
> +   template<typename Out>
> +   Out
> +   format(const ThrowingFormat& t, std::basic_format_context<Out, char>& fc) 
> const
> +   {
> +     if (t.throw_from_format)
> +       throw std::logic_error("Formatting stopped");
> +     return std::ranges::copy(t.val, fc.out()).out;
> +   }
> +};
> +
> +void
> +test_throwing()
> +{
> +  std::string s;
> +  s.reserve(100);
> +  std::ostringstream out(std::move(s));
> +  ThrowingFormat tf1{std::string(500, 'a'), false}, tf2{std::string(500, 
> 'b'), true};
> +
> +  try
> +  {
> +    std::print(out, "{} {}", tf1, tf2);
> +    VERIFY(false);
> +  } catch (...) {
> +    VERIFY(true);
> +  }
> +  VERIFY( out.view().empty() );
> +}
> +
> +struct MixedFormat
> +{
> +  std::ostream* out;
> +  std::string val;
> +};
> +
> +template<>
> +struct std::formatter<MixedFormat, char>
> +{
> +   constexpr
> +   std::format_parse_context::iterator
> +   parse(std::format_parse_context& ctx) const
> +   { return ctx.begin(); }
> +
> +   template<typename Out>
> +   Out
> +   format(const MixedFormat& t, std::basic_format_context<Out, char>& fc) 
> const
> +   {
> +     if (t.out)
> +       *t.out << "<<[" << t.val << "]";
> +     return std::ranges::copy(t.val, fc.out()).out;
> +   }
> +};
> +
> +void
> +test_interleaved()
> +{
> +  std::string s;
> +  s.reserve(100);
> +  std::ostringstream out(std::move(s));
> +  MixedFormat mf1{&out, "abc"}, mf2{&out, "DEF"};
> +
> +  std::print(out, "f[{}]f[{}]", mf1, mf2);
> +  VERIFY( out.view() == "<<[abc]<<[DEF]f[abc]f[DEF]" );
> +}
> +
> +int main()
> +{
> +  test_throwing();
> +  test_interleaved();
> +}
> diff --git 
> a/libstdc++-v3/testsuite/std/format/functions/format_to_ostreambuf.cc 
> b/libstdc++-v3/testsuite/std/format/functions/format_to_ostreambuf.cc
> new file mode 100644
> index 00000000000..81ac3a6c3d9
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/std/format/functions/format_to_ostreambuf.cc
> @@ -0,0 +1,377 @@
> +// { dg-do run { target c++20 } }
> +
> +#include <format>
> +#include <sstream>
> +#include <string>
> +#include <testsuite_hooks.h>
> +
> +template<std::streamsize BufSize>
> +struct ShortWriteStreambuf : std::streambuf
> +{
> +  std::string data;
> +
> +  std::streamsize
> +  xsputn(const char_type* s, std::streamsize n) override
> +  {
> +    std::streamsize limit = std::max<std::streamsize>(0, BufSize - 
> data.size());
> +    std::streamsize to_write = std::min(n, limit);
> +    if (to_write > 0)
> +      data.append(s, to_write);
> +    return to_write;
> +  }
> +
> +  int_type
> +  overflow(int_type c) override
> +  {
> +    if (data.size() < BufSize)
> +      {
> +       data.push_back(traits_type::to_char_type(c));
> +       return c;
> +      }
> +    return traits_type::eof();
> +  }
> +};
> +
> +bool
> +check_suffix(std::string_view s, size_t n, char c)
> +{
> +  if (s.size() < n)
> +    return false;
> +
> +  s.remove_prefix(s.size() - n);
> +  for (char v : s)
> +    if (v != c)
> +      return !n;
> +    else if (!n)
> +      return false;
> +    else
> +      --n;
> +  return !n;
> +}
> +
> +void
> +test_format_to()
> +{
> +  {
> +    std::ostringstream os;
> +    std::format_to(std::ostreambuf_iterator<char>(os), "{}", 42);
> +    VERIFY( os.view() == "42" );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    std::string big(800, 'y');
> +    std::format_to(std::ostreambuf_iterator<char>(os), "{}", big);
> +    VERIFY( check_suffix(os.view(), 800, 'y') );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    std::string part(700, 'P');
> +    std::format_to(std::ostreambuf_iterator<char>(os),
> +                  "{}{}{}", part, part, part);
> +    VERIFY( check_suffix(os.view(), 2100, 'P') );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    std::string body(400, 'B');
> +    std::format_to(std::ostreambuf_iterator<char>(os),
> +                  "header-{}-footer", body);
> +
> +    std::string_view sv = os.view();
> +    VERIFY( sv.starts_with("header-") );
> +    sv.remove_prefix(7);
> +    VERIFY( sv.ends_with("-footer") );
> +    sv.remove_suffix(7);
> +    VERIFY( check_suffix(sv, 400, 'B') );
> +  }
> +}
> +
> +void
> +test_format_to_n()
> +{
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               1, "{}", "A");
> +    VERIFY( res.size == 1 );
> +    VERIFY( os.view() == "A" );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               1, "{}", "AAA");
> +    VERIFY( res.size == 3 );
> +    VERIFY( os.view() == "A" );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               3, "{}", "hello");
> +    VERIFY( res.size == 5 );
> +    VERIFY( os.view() == "hel" );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               10, "{}", std::string(100, 'x'));
> +    VERIFY( res.size == 100 );
> +    VERIFY( check_suffix(os.view(), 10, 'x') );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               10, "{}", std::string(1000, 'x'));
> +    VERIFY( res.size == 1000 );
> +    VERIFY( check_suffix(os.view(), 10, 'x') );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               256, "{}", std::string(1000, 'x'));
> +    VERIFY( res.size == 1000 );
> +    VERIFY( check_suffix(os.view(), 256, 'x') );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               257, "{}", std::string(1000, 'x'));
> +    VERIFY( res.size == 1000 );
> +    VERIFY( check_suffix(os.view(), 257, 'x') );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               8, "{}{}{}", "AAAA", "BBBB", "CCCC");
> +    VERIFY( res.size == 12 );
> +    VERIFY( os.view() == "AAAABBBB" );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               2, "{}{}", "AAAA", "BBBB");
> +    VERIFY( res.size == 8 );
> +    VERIFY( os.view() == "AA" );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               8, "{}{}", "AAAA", "BBBB");
> +    VERIFY( res.size == 8 );
> +    VERIFY( os.view() == "AAAABBBB" );
> +  }
> +
> +  {
> +    std::string big(257, 'A');
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                                257, "{}", big);
> +    VERIFY( res.size == 257 );
> +    std::string_view sv = os.view();
> +    VERIFY( os.view() == big );
> +  }
> +
> +  {
> +    std::string big(300, 'A');
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                                257, "{}", big);
> +    VERIFY( res.size == 300 );
> +    VERIFY( check_suffix(os.view(), 257, 'A') );
> +  }
> +}
> +
> +void
> +test_format_to_n_padding()
> +{
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               3, "{:>10}", "hello");
> +    VERIFY( res.size == 10 );
> +    VERIFY( os.view().size() == 3 );
> +  }
> +
> +  {
> +    std::ostringstream os;
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               3, "{:.5}", std::string(100, 'z'));
> +    VERIFY( res.size == 5 );
> +    VERIFY( check_suffix(os.view(), 3, 'z') );
> +  }
> +}
> +
> +void
> +test_format_to_n_prefilled()
> +{
> +  // Pre-filled ostringstream: after seekp(0) the streambuf has a put
> +  // area pointing into existing data.  This exercises the
> +  // _M_use_put_area code path combined with format_to_n truncation.
> +  {
> +    std::string init(100, 'Z');
> +    std::ostringstream os(init);
> +    os.seekp(0);
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               5, "{}", "1234567890");
> +    VERIFY( res.size == 10 );
> +    VERIFY( os.view().substr(0, 10) == "12345ZZZZZ" );
> +    VERIFY( check_suffix(os.view(), 90, 'Z') );
> +  }
> +  {
> +    std::string init(100, 'Z');
> +    std::ostringstream os(init);
> +    os.seekp(0);
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               8, "{:=>15}", "1234567890");
> +    VERIFY( res.size == 15 );
> +    VERIFY( os.view().substr(0, 10) == "=====123ZZ" );
> +    VERIFY( check_suffix(os.view(), 90, 'Z') );
> +  }
> +  {
> +    std::string init(100, 'Z');
> +    std::ostringstream os(init);
> +    os.seekp(0);
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               3, "{}", 12345);
> +    VERIFY( res.size == 5 );
> +    VERIFY( os.view().substr(0, 10) == "123ZZZZZZZ" );
> +    VERIFY( check_suffix(os.view(), 90, 'Z') );
> +  }
> +  {
> +    std::string init(100, 'Z');
> +    std::ostringstream os(init);
> +    os.seekp(0);
> +    auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
> +                               7, "{:=>10}", 12345);
> +    VERIFY( res.size == 10 );
> +    VERIFY( os.view().substr(0, 10) == "=====12ZZZ" );
> +    VERIFY( check_suffix(os.view(), 90, 'Z') );
> +  }
> +}
> +
> +void
> +test_format_to_sentinel()
> +{
> +  // Test cases writting of default constructed (sentinel)
> +  // ostream_iterator. No characters can be written.
> +  const std::ostreambuf_iterator<char> sent;
> +  {
> +    auto res = std::format_to(sent, "");
> +    VERIFY( res.failed() );
> +  }
> +  {
> +    auto res = std::format_to(sent, "{}", "");
> +    VERIFY( res.failed() );
> +  }
> +  {
> +    auto res = std::format_to_n(sent, 0, "");
> +    VERIFY( res.out.failed() );
> +    VERIFY( res.size == 0 );
> +  }
> +  {
> +    auto res = std::format_to_n(sent, 0, "{}", "");
> +    VERIFY( res.out.failed() );
> +    VERIFY( res.size == 0 );
> +  }
> +  {
> +    auto res = std::format_to_n(sent, 0, "{}", "abc");
> +    VERIFY( res.out.failed() );
> +    VERIFY( res.size == 3 );
> +  }
> +  {
> +    auto res = std::format_to_n(sent, 0, "{}", "10");
> +    VERIFY( res.out.failed() );
> +    VERIFY( res.size == 2 );
> +  }
> +}
> +
> +void
> +test_write_failure()
> +{
> +  const std::string payload(20, 'Z');
> +
> +  {
> +    ShortWriteStreambuf<20> buf;
> +    auto it = std::ostreambuf_iterator<char>(&buf);
> +    auto res = std::format_to(it, "{}", payload);
> +    VERIFY( !res.failed() );
> +    VERIFY( check_suffix(buf.data, 20, 'Z') );
> +  }
> +
> +  {
> +    ShortWriteStreambuf<19> buf;
> +    auto it = std::ostreambuf_iterator<char>(&buf);
> +    auto res = std::format_to(it, "{}", payload);
> +    VERIFY( res.failed() );
> +    VERIFY( check_suffix(buf.data, 19, 'Z') );
> +  }
> +
> +  {
> +    ShortWriteStreambuf<10> buf;
> +    auto it = std::ostreambuf_iterator<char>(&buf);
> +    auto res = std::format_to_n(it, 10, "{}", payload);
> +    VERIFY( !res.out.failed() );
> +    VERIFY( res.size == 20 );
> +    VERIFY( check_suffix(buf.data, 10, 'Z') );
> +  }
> +
> +  {
> +    ShortWriteStreambuf<10> buf;
> +    auto it = std::ostreambuf_iterator<char>(&buf);
> +    auto res = std::format_to_n(it, 11, "{}", payload);
> +    VERIFY( res.out.failed() );
> +    VERIFY( res.size == 20 );
> +    VERIFY( check_suffix(buf.data, 10, 'Z') );
> +  }
> +
> +  {
> +    ShortWriteStreambuf<100> buf;
> +    auto it = std::ostreambuf_iterator<char>(&buf);
> +    auto res = std::format_to(it, "{}", std::string(300, 'Y'));
> +    VERIFY( res.failed() );
> +    VERIFY( buf.data.size() == 100 );
> +    VERIFY( check_suffix(buf.data, 100, 'Y') );
> +  }
> +  // format_to_n after write failure: size must still be the total
> +  // number of characters that would have been produced.
> +  {
> +    ShortWriteStreambuf<50> buf;
> +    auto it = std::ostreambuf_iterator<char>(&buf);
> +    auto res = std::format_to_n(it, 100,
> +                               "{}{}{}",
> +                               std::string(20, 'A'),
> +                               std::string(30, 'B'),
> +                               std::string(40, 'C'));
> +    // The first two arguments fill the buffer (20 + 30 == 50).
> +    // The third argument (40 C's) triggers write failure and the
> +    // remaining chars must still be counted in res.size.
> +    VERIFY( res.out.failed() );
> +    VERIFY( res.size == 90 );
> +
> +    std::string_view sv = buf.data;
> +    VERIFY( buf.data.size() == 50 );
> +    VERIFY( check_suffix(sv, 30, 'B') );
> +    sv.remove_suffix(30);
> +    VERIFY( check_suffix(sv, 20, 'A') );
> +  }
> +}
> +
> +int main()
> +{
> +  test_format_to();
> +  test_format_to_n();
> +  test_format_to_n_padding();
> +  test_format_to_n_prefilled();
> +  test_format_to_sentinel();
> +  test_write_failure();
> +}
> --
> 2.55.0
>

Reply via email to