83.81% Lines (88/105) 100.00% Functions (21/21)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) 2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3   // Copyright (c) 2026 Michael Vandeberg 3   // Copyright (c) 2026 Michael Vandeberg
4   // 4   //
5   // Distributed under the Boost Software License, Version 1.0. (See accompanying 5   // Distributed under the Boost Software License, Version 1.0. (See accompanying
6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7   // 7   //
8   // Official repository: https://github.com/cppalliance/http 8   // Official repository: https://github.com/cppalliance/http
9   // 9   //
10   10  
11   #ifndef BOOST_HTTP_TEST_WRITE_SINK_HPP 11   #ifndef BOOST_HTTP_TEST_WRITE_SINK_HPP
12   #define BOOST_HTTP_TEST_WRITE_SINK_HPP 12   #define BOOST_HTTP_TEST_WRITE_SINK_HPP
13   13  
14   #include <boost/capy/detail/config.hpp> 14   #include <boost/capy/detail/config.hpp>
15   #include <boost/capy/buffers.hpp> 15   #include <boost/capy/buffers.hpp>
16   #include <boost/capy/buffers/buffer_copy.hpp> 16   #include <boost/capy/buffers/buffer_copy.hpp>
17   #include <boost/capy/buffers/make_buffer.hpp> 17   #include <boost/capy/buffers/make_buffer.hpp>
18   #include <coroutine> 18   #include <coroutine>
19   #include <boost/capy/ex/io_env.hpp> 19   #include <boost/capy/ex/io_env.hpp>
20   #include <boost/capy/io_result.hpp> 20   #include <boost/capy/io_result.hpp>
21   #include <boost/capy/error.hpp> 21   #include <boost/capy/error.hpp>
22   #include <boost/capy/test/fuse.hpp> 22   #include <boost/capy/test/fuse.hpp>
23   23  
24   #include <algorithm> 24   #include <algorithm>
25   #include <string> 25   #include <string>
26   #include <string_view> 26   #include <string_view>
27   27  
28   namespace boost { 28   namespace boost {
29   namespace http { 29   namespace http {
30   namespace test { 30   namespace test {
31   31  
32   /** A mock sink for testing write operations. 32   /** A mock sink for testing write operations.
33   33  
34   Use this to verify code that performs complete writes without needing 34   Use this to verify code that performs complete writes without needing
35   real I/O. Call @ref write to write data, then @ref data to retrieve 35   real I/O. Call @ref write to write data, then @ref data to retrieve
36   what was written. The associated @ref capy::test::fuse enables error injection 36   what was written. The associated @ref capy::test::fuse enables error injection
37   at controlled points. 37   at controlled points.
38   38  
39   This class satisfies the @ref WriteSink concept by providing partial 39   This class satisfies the @ref WriteSink concept by providing partial
40   writes via `write_some` (satisfying @ref WriteStream), complete 40   writes via `write_some` (satisfying @ref WriteStream), complete
41   writes via `write`, and EOF signaling via `write_eof`. 41   writes via `write`, and EOF signaling via `write_eof`.
42   42  
43   @par Thread Safety 43   @par Thread Safety
44   Not thread-safe. 44   Not thread-safe.
45   45  
46   @par Example 46   @par Example
47   @code 47   @code
48   capy::test::fuse f; 48   capy::test::fuse f;
49   write_sink ws( f ); 49   write_sink ws( f );
50   50  
51   auto r = f.armed( [&]( capy::test::fuse& ) -> task<void> { 51   auto r = f.armed( [&]( capy::test::fuse& ) -> task<void> {
52   auto [ec, n] = co_await ws.write( 52   auto [ec, n] = co_await ws.write(
53   capy::const_buffer( "Hello", 5 ) ); 53   capy::const_buffer( "Hello", 5 ) );
54   if( ec ) 54   if( ec )
55   co_return; 55   co_return;
56   auto [ec2] = co_await ws.write_eof(); 56   auto [ec2] = co_await ws.write_eof();
57   if( ec2 ) 57   if( ec2 )
58   co_return; 58   co_return;
59   // ws.data() returns "Hello" 59   // ws.data() returns "Hello"
60   } ); 60   } );
61   @endcode 61   @endcode
62   62  
63   @see capy::test::fuse, WriteSink 63   @see capy::test::fuse, WriteSink
64   */ 64   */
65   class write_sink 65   class write_sink
66   { 66   {
67   capy::test::fuse f_; 67   capy::test::fuse f_;
68   std::string data_; 68   std::string data_;
69   std::string expect_; 69   std::string expect_;
70   std::size_t max_write_size_; 70   std::size_t max_write_size_;
71   bool eof_called_ = false; 71   bool eof_called_ = false;
72   72  
73   std::error_code 73   std::error_code
HITCBC 74   1056 consume_match_() noexcept 74   1056 consume_match_() noexcept
75   { 75   {
HITCBC 76   1056 if(data_.empty() || expect_.empty()) 76   1056 if(data_.empty() || expect_.empty())
HITCBC 77   1056 return {}; 77   1056 return {};
MISUBC 78   std::size_t const n = (std::min)(data_.size(), expect_.size()); 78   std::size_t const n = (std::min)(data_.size(), expect_.size());
MISUBC 79   if(std::string_view(data_.data(), n) != 79   if(std::string_view(data_.data(), n) !=
MISUBC 80   std::string_view(expect_.data(), n)) 80   std::string_view(expect_.data(), n))
MISUBC 81   return capy::error::test_failure; 81   return capy::error::test_failure;
MISUBC 82   data_.erase(0, n); 82   data_.erase(0, n);
MISUBC 83   expect_.erase(0, n); 83   expect_.erase(0, n);
MISUBC 84   return {}; 84   return {};
85   } 85   }
86   86  
87   public: 87   public:
88   /** Construct a write sink. 88   /** Construct a write sink.
89   89  
90   @param f The capy::test::fuse used to inject errors during writes. 90   @param f The capy::test::fuse used to inject errors during writes.
91   91  
92   @param max_write_size Maximum bytes transferred per write. 92   @param max_write_size Maximum bytes transferred per write.
93   Use to simulate chunked delivery. 93   Use to simulate chunked delivery.
94   */ 94   */
HITCBC 95   402 explicit write_sink( 95   402 explicit write_sink(
96   capy::test::fuse f = {}, 96   capy::test::fuse f = {},
97   std::size_t max_write_size = std::size_t(-1)) noexcept 97   std::size_t max_write_size = std::size_t(-1)) noexcept
HITCBC 98   402 : f_(std::move(f)) 98   402 : f_(std::move(f))
HITCBC 99   402 , max_write_size_(max_write_size) 99   402 , max_write_size_(max_write_size)
100   { 100   {
HITCBC 101   402 } 101   402 }
102   102  
103   /// Return the written data as a string view. 103   /// Return the written data as a string view.
104   std::string_view 104   std::string_view
HITCBC 105   54 data() const noexcept 105   54 data() const noexcept
106   { 106   {
HITCBC 107   54 return data_; 107   54 return data_;
108   } 108   }
109   109  
110   /** Set the expected data for subsequent writes. 110   /** Set the expected data for subsequent writes.
111   111  
112   Stores the expected data and immediately tries to match 112   Stores the expected data and immediately tries to match
113   against any data already written. Matched data is consumed 113   against any data already written. Matched data is consumed
114   from both buffers. 114   from both buffers.
115   115  
116   @param sv The expected data. 116   @param sv The expected data.
117   117  
118   @return An error if existing data does not match. 118   @return An error if existing data does not match.
119   */ 119   */
120   std::error_code 120   std::error_code
121   expect(std::string_view sv) 121   expect(std::string_view sv)
122   { 122   {
123   expect_.assign(sv); 123   expect_.assign(sv);
124   return consume_match_(); 124   return consume_match_();
125   } 125   }
126   126  
127   /// Return the number of bytes written. 127   /// Return the number of bytes written.
128   std::size_t 128   std::size_t
HITCBC 129   2 size() const noexcept 129   2 size() const noexcept
130   { 130   {
HITCBC 131   2 return data_.size(); 131   2 return data_.size();
132   } 132   }
133   133  
134   /// Return whether write_eof has been called. 134   /// Return whether write_eof has been called.
135   bool 135   bool
HITCBC 136   42 eof_called() const noexcept 136   42 eof_called() const noexcept
137   { 137   {
HITCBC 138   42 return eof_called_; 138   42 return eof_called_;
139   } 139   }
140   140  
141   /// Clear all data and reset state. 141   /// Clear all data and reset state.
142   void 142   void
143   clear() noexcept 143   clear() noexcept
144   { 144   {
145   data_.clear(); 145   data_.clear();
146   expect_.clear(); 146   expect_.clear();
147   eof_called_ = false; 147   eof_called_ = false;
148   } 148   }
149   149  
150   /** Asynchronously write some data to the sink. 150   /** Asynchronously write some data to the sink.
151   151  
152   Transfers up to `capy::buffer_size( buffers )` bytes from the provided 152   Transfers up to `capy::buffer_size( buffers )` bytes from the provided
153   const buffer sequence to the internal buffer. Before every write, 153   const buffer sequence to the internal buffer. Before every write,
154   the attached @ref capy::test::fuse is consulted to possibly inject an error. 154   the attached @ref capy::test::fuse is consulted to possibly inject an error.
155   155  
156   @param buffers The const buffer sequence containing data to write. 156   @param buffers The const buffer sequence containing data to write.
157   157  
158   @return An awaitable that await-returns `(error_code,std::size_t)`. 158   @return An awaitable that await-returns `(error_code,std::size_t)`.
159   159  
160   @par Cancellation 160   @par Cancellation
161   If the environment's stop token has been requested, the write 161   If the environment's stop token has been requested, the write
162   completes immediately with `capy::error::canceled` and transfers no 162   completes immediately with `capy::error::canceled` and transfers no
163   data. An empty buffer sequence is a no-op that completes 163   data. An empty buffer sequence is a no-op that completes
164   successfully regardless of the stop token. 164   successfully regardless of the stop token.
165   165  
166   @see capy::test::fuse 166   @see capy::test::fuse
167   */ 167   */
168   template<capy::ConstBufferSequence CB> 168   template<capy::ConstBufferSequence CB>
169   auto 169   auto
HITCBC 170   38 write_some(CB buffers) 170   38 write_some(CB buffers)
171   { 171   {
172   struct awaitable 172   struct awaitable
173   { 173   {
174   write_sink* self_; 174   write_sink* self_;
175   CB buffers_; 175   CB buffers_;
176   bool canceled_ = false; 176   bool canceled_ = false;
177   177  
HITCBC 178   38 bool await_ready() const noexcept { return false; } 178   38 bool await_ready() const noexcept { return false; }
179   179  
180   // The operation completes synchronously, but await_suspend is 180   // The operation completes synchronously, but await_suspend is
181   // the only place capy::io_env is delivered (the promise's 181   // the only place capy::io_env is delivered (the promise's
182   // transform_awaiter forwards it here). Returning false means 182   // transform_awaiter forwards it here). Returning false means
183   // the coroutine does not actually suspend; it resumes 183   // the coroutine does not actually suspend; it resumes
184   // immediately, having observed the stop token. See capy::io_env, 184   // immediately, having observed the stop token. See capy::io_env,
185   // IoAwaitable. 185   // IoAwaitable.
186   bool 186   bool
HITCBC 187   38 await_suspend( 187   38 await_suspend(
188   std::coroutine_handle<>, 188   std::coroutine_handle<>,
189   capy::io_env const* env) noexcept 189   capy::io_env const* env) noexcept
190   { 190   {
HITCBC 191   38 canceled_ = env->stop_token.stop_requested(); 191   38 canceled_ = env->stop_token.stop_requested();
HITCBC 192   38 return false; 192   38 return false;
193   } 193   }
194   194  
195   capy::io_result<std::size_t> 195   capy::io_result<std::size_t>
HITCBC 196   38 await_resume() 196   38 await_resume()
197   { 197   {
HITCBC 198   38 if(capy::buffer_empty(buffers_)) 198   38 if(capy::buffer_empty(buffers_))
MISUBC 199   return {std::error_code(), 0}; 199   return {std::error_code(), 0};
200   200  
HITCBC 201   38 if(canceled_) 201   38 if(canceled_)
MISUBC 202   return {capy::error::canceled, 0}; 202   return {capy::error::canceled, 0};
203   203  
HITCBC 204   38 auto ec = self_->f_.maybe_fail(); 204   38 auto ec = self_->f_.maybe_fail();
HITCBC 205   28 if(ec) 205   28 if(ec)
HITCBC 206   10 return {ec, 0}; 206   10 return {ec, 0};
207   207  
HITCBC 208   18 std::size_t n = capy::buffer_size(buffers_); 208   18 std::size_t n = capy::buffer_size(buffers_);
HITCBC 209   18 n = (std::min)(n, self_->max_write_size_); 209   18 n = (std::min)(n, self_->max_write_size_);
210   210  
HITCBC 211   18 std::size_t const old_size = self_->data_.size(); 211   18 std::size_t const old_size = self_->data_.size();
HITCBC 212   18 self_->data_.resize(old_size + n); 212   18 self_->data_.resize(old_size + n);
HITCBC 213   18 capy::buffer_copy(capy::make_buffer( 213   18 capy::buffer_copy(capy::make_buffer(
HITCBC 214   18 self_->data_.data() + old_size, n), buffers_, n); 214   18 self_->data_.data() + old_size, n), buffers_, n);
215   215  
HITCBC 216   18 ec = self_->consume_match_(); 216   18 ec = self_->consume_match_();
HITCBC 217   18 if(ec) 217   18 if(ec)
218   { 218   {
MISUBC 219   self_->data_.resize(old_size); 219   self_->data_.resize(old_size);
MISUBC 220   return {ec, 0}; 220   return {ec, 0};
221   } 221   }
222   222  
HITCBC 223   18 return {std::error_code(), n}; 223   18 return {std::error_code(), n};
224   } 224   }
225   }; 225   };
HITCBC 226   38 return awaitable{this, buffers}; 226   38 return awaitable{this, buffers};
227   } 227   }
228   228  
229   /** Asynchronously write data to the sink. 229   /** Asynchronously write data to the sink.
230   230  
231   Transfers all bytes from the provided const buffer sequence 231   Transfers all bytes from the provided const buffer sequence
232   to the internal buffer. Unlike @ref write_some, this ignores 232   to the internal buffer. Unlike @ref write_some, this ignores
233   `max_write_size` and writes all available data, matching the 233   `max_write_size` and writes all available data, matching the
234   @ref WriteSink semantic contract. 234   @ref WriteSink semantic contract.
235   235  
236   @par Exception Safety 236   @par Exception Safety
237   Injected I/O conditions are reported via the `error_code` 237   Injected I/O conditions are reported via the `error_code`
238   component of the result. Throws `std::system_error` only when 238   component of the result. Throws `std::system_error` only when
239   the attached @ref capy::test::fuse is in exception mode and reaches its 239   the attached @ref capy::test::fuse is in exception mode and reaches its
240   failure point; no-throw otherwise. 240   failure point; no-throw otherwise.
241   241  
242   @param buffers The const buffer sequence containing data to write. 242   @param buffers The const buffer sequence containing data to write.
243   243  
244   @return An awaitable that await-returns `(error_code,std::size_t)`. 244   @return An awaitable that await-returns `(error_code,std::size_t)`.
245   245  
246   @par Cancellation 246   @par Cancellation
247   If the environment's stop token has been requested, the write 247   If the environment's stop token has been requested, the write
248   completes immediately with `capy::error::canceled` and transfers no 248   completes immediately with `capy::error::canceled` and transfers no
249   data. 249   data.
250   250  
251   @throws std::system_error When the attached @ref capy::test::fuse is in 251   @throws std::system_error When the attached @ref capy::test::fuse is in
252   exception mode and reaches its failure point. 252   exception mode and reaches its failure point.
253   253  
254   @see capy::test::fuse 254   @see capy::test::fuse
255   */ 255   */
256   template<capy::ConstBufferSequence CB> 256   template<capy::ConstBufferSequence CB>
257   auto 257   auto
HITCBC 258   1150 write(CB buffers) 258   1150 write(CB buffers)
259   { 259   {
260   struct awaitable 260   struct awaitable
261   { 261   {
262   write_sink* self_; 262   write_sink* self_;
263   CB buffers_; 263   CB buffers_;
264   bool canceled_ = false; 264   bool canceled_ = false;
265   265  
HITCBC 266   1150 bool await_ready() const noexcept { return false; } 266   1150 bool await_ready() const noexcept { return false; }
267   267  
268   // Reads the stop token without suspending; see the comment 268   // Reads the stop token without suspending; see the comment
269   // on write_some() for details. 269   // on write_some() for details.
270   bool 270   bool
HITCBC 271   1150 await_suspend( 271   1150 await_suspend(
272   std::coroutine_handle<>, 272   std::coroutine_handle<>,
273   capy::io_env const* env) noexcept 273   capy::io_env const* env) noexcept
274   { 274   {
HITCBC 275   1150 canceled_ = env->stop_token.stop_requested(); 275   1150 canceled_ = env->stop_token.stop_requested();
HITCBC 276   1150 return false; 276   1150 return false;
277   } 277   }
278   278  
279   capy::io_result<std::size_t> 279   capy::io_result<std::size_t>
HITCBC 280   1150 await_resume() 280   1150 await_resume()
281   { 281   {
HITCBC 282   1150 if(canceled_) 282   1150 if(canceled_)
MISUBC 283   return {capy::error::canceled, 0}; 283   return {capy::error::canceled, 0};
284   284  
HITCBC 285   1150 auto ec = self_->f_.maybe_fail(); 285   1150 auto ec = self_->f_.maybe_fail();
HITCBC 286   1091 if(ec) 286   1091 if(ec)
HITCBC 287   59 return {ec, 0}; 287   59 return {ec, 0};
288   288  
HITCBC 289   1032 std::size_t n = capy::buffer_size(buffers_); 289   1032 std::size_t n = capy::buffer_size(buffers_);
HITCBC 290   1032 if(n == 0) 290   1032 if(n == 0)
MISUBC 291   return {std::error_code(), 0}; 291   return {std::error_code(), 0};
292   292  
HITCBC 293   1032 std::size_t const old_size = self_->data_.size(); 293   1032 std::size_t const old_size = self_->data_.size();
HITCBC 294   1032 self_->data_.resize(old_size + n); 294   1032 self_->data_.resize(old_size + n);
HITCBC 295   1032 capy::buffer_copy(capy::make_buffer( 295   1032 capy::buffer_copy(capy::make_buffer(
HITCBC 296   1032 self_->data_.data() + old_size, n), buffers_); 296   1032 self_->data_.data() + old_size, n), buffers_);
297   297  
HITCBC 298   1032 ec = self_->consume_match_(); 298   1032 ec = self_->consume_match_();
HITCBC 299   1032 if(ec) 299   1032 if(ec)
MISUBC 300   return {ec, n}; 300   return {ec, n};
301   301  
HITCBC 302   1032 return {std::error_code(), n}; 302   1032 return {std::error_code(), n};
303   } 303   }
304   }; 304   };
HITCBC 305   1150 return awaitable{this, buffers}; 305   1150 return awaitable{this, buffers};
306   } 306   }
307   307  
308   /** Atomically write data and signal end-of-stream. 308   /** Atomically write data and signal end-of-stream.
309   309  
310   Transfers all bytes from the provided const buffer sequence to 310   Transfers all bytes from the provided const buffer sequence to
311   the internal buffer and signals end-of-stream. Before the write, 311   the internal buffer and signals end-of-stream. Before the write,
312   the attached @ref capy::test::fuse is consulted to possibly inject an error 312   the attached @ref capy::test::fuse is consulted to possibly inject an error
313   for testing fault scenarios. 313   for testing fault scenarios.
314   314  
315   @par Effects 315   @par Effects
316   On success, appends the written bytes to the internal buffer 316   On success, appends the written bytes to the internal buffer
317   and marks the sink as finalized. 317   and marks the sink as finalized.
318   If an error is injected by the capy::test::fuse, the internal buffer remains 318   If an error is injected by the capy::test::fuse, the internal buffer remains
319   unchanged. 319   unchanged.
320   320  
321   @par Exception Safety 321   @par Exception Safety
322   Injected I/O conditions are reported via the `error_code` 322   Injected I/O conditions are reported via the `error_code`
323   component of the result. Throws `std::system_error` only when 323   component of the result. Throws `std::system_error` only when
324   the attached @ref capy::test::fuse is in exception mode and reaches its 324   the attached @ref capy::test::fuse is in exception mode and reaches its
325   failure point; no-throw otherwise. 325   failure point; no-throw otherwise.
326   326  
327   @par Cancellation 327   @par Cancellation
328   If the environment's stop token has been requested, the operation 328   If the environment's stop token has been requested, the operation
329   completes immediately with `capy::error::canceled`, transfers no data, 329   completes immediately with `capy::error::canceled`, transfers no data,
330   and does not signal end-of-stream. 330   and does not signal end-of-stream.
331   331  
332   @param buffers The const buffer sequence containing data to write. 332   @param buffers The const buffer sequence containing data to write.
333   333  
334   @return An awaitable that await-returns `(error_code,std::size_t)`. 334   @return An awaitable that await-returns `(error_code,std::size_t)`.
335   335  
336   @throws std::system_error When the attached @ref capy::test::fuse is in 336   @throws std::system_error When the attached @ref capy::test::fuse is in
337   exception mode and reaches its failure point. 337   exception mode and reaches its failure point.
338   338  
339   @see capy::test::fuse 339   @see capy::test::fuse
340   */ 340   */
341   template<capy::ConstBufferSequence CB> 341   template<capy::ConstBufferSequence CB>
342   auto 342   auto
HITCBC 343   16 write_eof(CB buffers) 343   16 write_eof(CB buffers)
344   { 344   {
345   struct awaitable 345   struct awaitable
346   { 346   {
347   write_sink* self_; 347   write_sink* self_;
348   CB buffers_; 348   CB buffers_;
349   bool canceled_ = false; 349   bool canceled_ = false;
350   350  
HITCBC 351   16 bool await_ready() const noexcept { return false; } 351   16 bool await_ready() const noexcept { return false; }
352   352  
353   // Reads the stop token without suspending; see the comment 353   // Reads the stop token without suspending; see the comment
354   // on write_some() for details. 354   // on write_some() for details.
355   bool 355   bool
HITCBC 356   16 await_suspend( 356   16 await_suspend(
357   std::coroutine_handle<>, 357   std::coroutine_handle<>,
358   capy::io_env const* env) noexcept 358   capy::io_env const* env) noexcept
359   { 359   {
HITCBC 360   16 canceled_ = env->stop_token.stop_requested(); 360   16 canceled_ = env->stop_token.stop_requested();
HITCBC 361   16 return false; 361   16 return false;
362   } 362   }
363   363  
364   capy::io_result<std::size_t> 364   capy::io_result<std::size_t>
HITCBC 365   16 await_resume() 365   16 await_resume()
366   { 366   {
HITCBC 367   16 if(canceled_) 367   16 if(canceled_)
MISUBC 368   return {capy::error::canceled, 0}; 368   return {capy::error::canceled, 0};
369   369  
HITCBC 370   16 auto ec = self_->f_.maybe_fail(); 370   16 auto ec = self_->f_.maybe_fail();
HITCBC 371   11 if(ec) 371   11 if(ec)
HITCBC 372   5 return {ec, 0}; 372   5 return {ec, 0};
373   373  
HITCBC 374   6 std::size_t n = capy::buffer_size(buffers_); 374   6 std::size_t n = capy::buffer_size(buffers_);
HITCBC 375   6 if(n > 0) 375   6 if(n > 0)
376   { 376   {
HITCBC 377   6 std::size_t const old_size = self_->data_.size(); 377   6 std::size_t const old_size = self_->data_.size();
HITCBC 378   6 self_->data_.resize(old_size + n); 378   6 self_->data_.resize(old_size + n);
HITCBC 379   6 capy::buffer_copy(capy::make_buffer( 379   6 capy::buffer_copy(capy::make_buffer(
HITCBC 380   6 self_->data_.data() + old_size, n), buffers_); 380   6 self_->data_.data() + old_size, n), buffers_);
381   381  
HITCBC 382   6 ec = self_->consume_match_(); 382   6 ec = self_->consume_match_();
HITCBC 383   6 if(ec) 383   6 if(ec)
MISUBC 384   return {ec, n}; 384   return {ec, n};
385   } 385   }
386   386  
HITCBC 387   6 self_->eof_called_ = true; 387   6 self_->eof_called_ = true;
388   388  
HITCBC 389   6 return {std::error_code(), n}; 389   6 return {std::error_code(), n};
390   } 390   }
391   }; 391   };
HITCBC 392   16 return awaitable{this, buffers}; 392   16 return awaitable{this, buffers};
393   } 393   }
394   394  
395   /** Signal end-of-stream. 395   /** Signal end-of-stream.
396   396  
397   Marks the sink as finalized, indicating no more data will be 397   Marks the sink as finalized, indicating no more data will be
398   written. Before signaling, the attached @ref capy::test::fuse is consulted 398   written. Before signaling, the attached @ref capy::test::fuse is consulted
399   to possibly inject an error for testing fault scenarios. 399   to possibly inject an error for testing fault scenarios.
400   400  
401   @par Effects 401   @par Effects
402   On success, marks the sink as finalized. 402   On success, marks the sink as finalized.
403   If an error is injected by the capy::test::fuse, the state remains unchanged. 403   If an error is injected by the capy::test::fuse, the state remains unchanged.
404   404  
405   @par Exception Safety 405   @par Exception Safety
406   Injected I/O conditions are reported via the `error_code` 406   Injected I/O conditions are reported via the `error_code`
407   component of the result. Throws `std::system_error` only when 407   component of the result. Throws `std::system_error` only when
408   the attached @ref capy::test::fuse is in exception mode and reaches its 408   the attached @ref capy::test::fuse is in exception mode and reaches its
409   failure point; no-throw otherwise. 409   failure point; no-throw otherwise.
410   410  
411   @par Cancellation 411   @par Cancellation
412   If the environment's stop token has been requested, the operation 412   If the environment's stop token has been requested, the operation
413   completes immediately with `capy::error::canceled` and does not signal 413   completes immediately with `capy::error::canceled` and does not signal
414   end-of-stream. 414   end-of-stream.
415   415  
416   @return An awaitable that await-returns `(error_code)`. 416   @return An awaitable that await-returns `(error_code)`.
417   417  
418   @throws std::system_error When the attached @ref capy::test::fuse is in 418   @throws std::system_error When the attached @ref capy::test::fuse is in
419   exception mode and reaches its failure point. 419   exception mode and reaches its failure point.
420   420  
421   @see capy::test::fuse 421   @see capy::test::fuse
422   */ 422   */
423   auto 423   auto
HITCBC 424   68 write_eof() 424   68 write_eof()
425   { 425   {
426   struct awaitable 426   struct awaitable
427   { 427   {
428   write_sink* self_; 428   write_sink* self_;
429   bool canceled_ = false; 429   bool canceled_ = false;
430   430  
HITCBC 431   68 bool await_ready() const noexcept { return false; } 431   68 bool await_ready() const noexcept { return false; }
432   432  
433   // Reads the stop token without suspending; see the comment 433   // Reads the stop token without suspending; see the comment
434   // on write_some() for details. 434   // on write_some() for details.
435   bool 435   bool
HITCBC 436   68 await_suspend( 436   68 await_suspend(
437   std::coroutine_handle<>, 437   std::coroutine_handle<>,
438   capy::io_env const* env) noexcept 438   capy::io_env const* env) noexcept
439   { 439   {
HITCBC 440   68 canceled_ = env->stop_token.stop_requested(); 440   68 canceled_ = env->stop_token.stop_requested();
HITCBC 441   68 return false; 441   68 return false;
442   } 442   }
443   443  
444   capy::io_result<> 444   capy::io_result<>
HITCBC 445   68 await_resume() 445   68 await_resume()
446   { 446   {
HITCBC 447   68 if(canceled_) 447   68 if(canceled_)
MISUBC 448   return {capy::error::canceled}; 448   return {capy::error::canceled};
449   449  
HITCBC 450   68 auto ec = self_->f_.maybe_fail(); 450   68 auto ec = self_->f_.maybe_fail();
HITCBC 451   50 if(ec) 451   50 if(ec)
HITCBC 452   18 return {ec}; 452   18 return {ec};
453   453  
HITCBC 454   32 self_->eof_called_ = true; 454   32 self_->eof_called_ = true;
HITCBC 455   32 return {}; 455   32 return {};
456   } 456   }
457   }; 457   };
HITCBC 458   68 return awaitable{this}; 458   68 return awaitable{this};
459   } 459   }
460   }; 460   };
461   461  
462   } // test 462   } // test
463   } // capy 463   } // capy
464   } // boost 464   } // boost
465   465  
466   #endif 466   #endif