97.35% Lines (110/113) 100.00% Functions (23/23)
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/capy 8   // Official repository: https://github.com/cppalliance/capy
9   // 9   //
10   10  
11   #ifndef BOOST_CAPY_TEST_WRITE_SINK_HPP 11   #ifndef BOOST_CAPY_TEST_WRITE_SINK_HPP
12   #define BOOST_CAPY_TEST_WRITE_SINK_HPP 12   #define BOOST_CAPY_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 capy { 29   namespace capy {
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 fuse enables error injection 36   what was written. The associated @ref 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   fuse f; 48   fuse f;
49   write_sink ws( f ); 49   write_sink ws( f );
50   50  
51   auto r = f.armed( [&]( fuse& ) -> task<void> { 51   auto r = f.armed( [&]( fuse& ) -> task<void> {
52   auto [ec, n] = co_await ws.write( 52   auto [ec, n] = co_await ws.write(
53   const_buffer( "Hello", 5 ) ); 53   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 fuse, WriteSink 63   @see fuse, WriteSink
64   */ 64   */
65   class write_sink 65   class write_sink
66   { 66   {
67   fuse f_; 67   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   236 consume_match_() noexcept 74   236 consume_match_() noexcept
75   { 75   {
HITCBC 76   236 if(data_.empty() || expect_.empty()) 76   236 if(data_.empty() || expect_.empty())
HITCBC 77   228 return {}; 77   228 return {};
HITCBC 78   8 std::size_t const n = (std::min)(data_.size(), expect_.size()); 78   8 std::size_t const n = (std::min)(data_.size(), expect_.size());
HITCBC 79   8 if(std::string_view(data_.data(), n) != 79   8 if(std::string_view(data_.data(), n) !=
HITCBC 80   16 std::string_view(expect_.data(), n)) 80   16 std::string_view(expect_.data(), n))
HITCBC 81   4 return error::test_failure; 81   4 return error::test_failure;
HITCBC 82   4 data_.erase(0, n); 82   4 data_.erase(0, n);
HITCBC 83   4 expect_.erase(0, n); 83   4 expect_.erase(0, n);
HITCBC 84   4 return {}; 84   4 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 fuse used to inject errors during writes. 90   @param f The 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   416 explicit write_sink( 95   416 explicit write_sink(
96   fuse f = {}, 96   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   416 : f_(std::move(f)) 98   416 : f_(std::move(f))
HITCBC 99   416 , max_write_size_(max_write_size) 99   416 , max_write_size_(max_write_size)
100   { 100   {
HITCBC 101   416 } 101   416 }
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   100 data() const noexcept 105   100 data() const noexcept
106   { 106   {
HITCBC 107   100 return data_; 107   100 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
HITCBC 121   16 expect(std::string_view sv) 121   16 expect(std::string_view sv)
122   { 122   {
HITCBC 123   16 expect_.assign(sv); 123   16 expect_.assign(sv);
HITCBC 124   16 return consume_match_(); 124   16 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   9 size() const noexcept 129   9 size() const noexcept
130   { 130   {
HITCBC 131   9 return data_.size(); 131   9 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   66 eof_called() const noexcept 136   66 eof_called() const noexcept
137   { 137   {
HITCBC 138   66 return eof_called_; 138   66 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
HITCBC 143   4 clear() noexcept 143   4 clear() noexcept
144   { 144   {
HITCBC 145   4 data_.clear(); 145   4 data_.clear();
HITCBC 146   4 expect_.clear(); 146   4 expect_.clear();
HITCBC 147   4 eof_called_ = false; 147   4 eof_called_ = false;
HITCBC 148   4 } 148   4 }
149   149  
150   /** Asynchronously write some data to the sink. 150   /** Asynchronously write some data to the sink.
151   151  
152   Transfers up to `buffer_size( buffers )` bytes from the provided 152   Transfers up to `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 fuse is consulted to possibly inject an error. 154   the attached @ref 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 `error::canceled` and transfers no 162   completes immediately with `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 fuse 166   @see fuse
167   */ 167   */
168   template<ConstBufferSequence CB> 168   template<ConstBufferSequence CB>
169   auto 169   auto
HITCBC 170   77 write_some(CB buffers) 170   77 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   77 bool await_ready() const noexcept { return false; } 178   77 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 io_env is delivered (the promise's 181   // the only place 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 io_env, 184   // immediately, having observed the stop token. See io_env,
185   // IoAwaitable. 185   // IoAwaitable.
186   bool 186   bool
HITCBC 187   77 await_suspend( 187   77 await_suspend(
188   std::coroutine_handle<>, 188   std::coroutine_handle<>,
189   io_env const* env) noexcept 189   io_env const* env) noexcept
190   { 190   {
HITCBC 191   77 canceled_ = env->stop_token.stop_requested(); 191   77 canceled_ = env->stop_token.stop_requested();
HITCBC 192   77 return false; 192   77 return false;
193   } 193   }
194   194  
195   io_result<std::size_t> 195   io_result<std::size_t>
HITCBC 196   77 await_resume() 196   77 await_resume()
197   { 197   {
HITCBC 198   77 if(buffer_empty(buffers_)) 198   77 if(buffer_empty(buffers_))
HITCBC 199   2 return {{}, 0}; 199   2 return {{}, 0};
200   200  
HITCBC 201   75 if(canceled_) 201   75 if(canceled_)
HITCBC 202   1 return {error::canceled, 0}; 202   1 return {error::canceled, 0};
203   203  
HITCBC 204   74 auto ec = self_->f_.maybe_fail(); 204   74 auto ec = self_->f_.maybe_fail();
HITCBC 205   53 if(ec) 205   53 if(ec)
HITCBC 206   21 return {ec, 0}; 206   21 return {ec, 0};
207   207  
HITCBC 208   32 std::size_t n = buffer_size(buffers_); 208   32 std::size_t n = buffer_size(buffers_);
HITCBC 209   32 n = (std::min)(n, self_->max_write_size_); 209   32 n = (std::min)(n, self_->max_write_size_);
210   210  
HITCBC 211   32 std::size_t const old_size = self_->data_.size(); 211   32 std::size_t const old_size = self_->data_.size();
HITCBC 212   32 self_->data_.resize(old_size + n); 212   32 self_->data_.resize(old_size + n);
HITCBC 213   32 buffer_copy(make_buffer( 213   32 buffer_copy(make_buffer(
HITCBC 214   32 self_->data_.data() + old_size, n), buffers_, n); 214   32 self_->data_.data() + old_size, n), buffers_, n);
215   215  
HITCBC 216   32 ec = self_->consume_match_(); 216   32 ec = self_->consume_match_();
HITCBC 217   32 if(ec) 217   32 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   32 return {{}, n}; 223   32 return {{}, n};
224   } 224   }
225   }; 225   };
HITCBC 226   77 return awaitable{this, buffers}; 226   77 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 fuse is in exception mode and reaches its 239   the attached @ref 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 `error::canceled` and transfers no 248   completes immediately with `error::canceled` and transfers no
249   data. 249   data.
250   250  
251   @throws std::system_error When the attached @ref fuse is in 251   @throws std::system_error When the attached @ref fuse is in
252   exception mode and reaches its failure point. 252   exception mode and reaches its failure point.
253   253  
254   @see fuse 254   @see fuse
255   */ 255   */
256   template<ConstBufferSequence CB> 256   template<ConstBufferSequence CB>
257   auto 257   auto
HITCBC 258   303 write(CB buffers) 258   303 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   303 bool await_ready() const noexcept { return false; } 266   303 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   303 await_suspend( 271   303 await_suspend(
272   std::coroutine_handle<>, 272   std::coroutine_handle<>,
273   io_env const* env) noexcept 273   io_env const* env) noexcept
274   { 274   {
HITCBC 275   303 canceled_ = env->stop_token.stop_requested(); 275   303 canceled_ = env->stop_token.stop_requested();
HITCBC 276   303 return false; 276   303 return false;
277   } 277   }
278   278  
279   io_result<std::size_t> 279   io_result<std::size_t>
HITCBC 280   303 await_resume() 280   303 await_resume()
281   { 281   {
HITCBC 282   303 if(canceled_) 282   303 if(canceled_)
HITCBC 283   1 return {error::canceled, 0}; 283   1 return {error::canceled, 0};
284   284  
HITCBC 285   302 auto ec = self_->f_.maybe_fail(); 285   302 auto ec = self_->f_.maybe_fail();
HITCBC 286   241 if(ec) 286   241 if(ec)
HITCBC 287   61 return {ec, 0}; 287   61 return {ec, 0};
288   288  
HITCBC 289   180 std::size_t n = buffer_size(buffers_); 289   180 std::size_t n = buffer_size(buffers_);
HITCBC 290   180 if(n == 0) 290   180 if(n == 0)
HITCBC 291   2 return {{}, 0}; 291   2 return {{}, 0};
292   292  
HITCBC 293   178 std::size_t const old_size = self_->data_.size(); 293   178 std::size_t const old_size = self_->data_.size();
HITCBC 294   178 self_->data_.resize(old_size + n); 294   178 self_->data_.resize(old_size + n);
HITCBC 295   178 buffer_copy(make_buffer( 295   178 buffer_copy(make_buffer(
HITCBC 296   178 self_->data_.data() + old_size, n), buffers_); 296   178 self_->data_.data() + old_size, n), buffers_);
297   297  
HITCBC 298   178 ec = self_->consume_match_(); 298   178 ec = self_->consume_match_();
HITCBC 299   178 if(ec) 299   178 if(ec)
HITCBC 300   2 return {ec, n}; 300   2 return {ec, n};
301   301  
HITCBC 302   176 return {{}, n}; 302   176 return {{}, n};
303   } 303   }
304   }; 304   };
HITCBC 305   303 return awaitable{this, buffers}; 305   303 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 fuse is consulted to possibly inject an error 312   the attached @ref 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 fuse, the internal buffer remains 318   If an error is injected by the 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 fuse is in exception mode and reaches its 324   the attached @ref 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 `error::canceled`, transfers no data, 329   completes immediately with `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 fuse is in 336   @throws std::system_error When the attached @ref fuse is in
337   exception mode and reaches its failure point. 337   exception mode and reaches its failure point.
338   338  
339   @see fuse 339   @see fuse
340   */ 340   */
341   template<ConstBufferSequence CB> 341   template<ConstBufferSequence CB>
342   auto 342   auto
HITCBC 343   35 write_eof(CB buffers) 343   35 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   35 bool await_ready() const noexcept { return false; } 351   35 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   35 await_suspend( 356   35 await_suspend(
357   std::coroutine_handle<>, 357   std::coroutine_handle<>,
358   io_env const* env) noexcept 358   io_env const* env) noexcept
359   { 359   {
HITCBC 360   35 canceled_ = env->stop_token.stop_requested(); 360   35 canceled_ = env->stop_token.stop_requested();
HITCBC 361   35 return false; 361   35 return false;
362   } 362   }
363   363  
364   io_result<std::size_t> 364   io_result<std::size_t>
HITCBC 365   35 await_resume() 365   35 await_resume()
366   { 366   {
HITCBC 367   35 if(canceled_) 367   35 if(canceled_)
HITCBC 368   1 return {error::canceled, 0}; 368   1 return {error::canceled, 0};
369   369  
HITCBC 370   34 auto ec = self_->f_.maybe_fail(); 370   34 auto ec = self_->f_.maybe_fail();
HITCBC 371   23 if(ec) 371   23 if(ec)
HITCBC 372   11 return {ec, 0}; 372   11 return {ec, 0};
373   373  
HITCBC 374   12 std::size_t n = buffer_size(buffers_); 374   12 std::size_t n = buffer_size(buffers_);
HITCBC 375   12 if(n > 0) 375   12 if(n > 0)
376   { 376   {
HITCBC 377   10 std::size_t const old_size = self_->data_.size(); 377   10 std::size_t const old_size = self_->data_.size();
HITCBC 378   10 self_->data_.resize(old_size + n); 378   10 self_->data_.resize(old_size + n);
HITCBC 379   10 buffer_copy(make_buffer( 379   10 buffer_copy(make_buffer(
HITCBC 380   10 self_->data_.data() + old_size, n), buffers_); 380   10 self_->data_.data() + old_size, n), buffers_);
381   381  
HITCBC 382   10 ec = self_->consume_match_(); 382   10 ec = self_->consume_match_();
HITCBC 383   10 if(ec) 383   10 if(ec)
MISUBC 384   return {ec, n}; 384   return {ec, n};
385   } 385   }
386   386  
HITCBC 387   12 self_->eof_called_ = true; 387   12 self_->eof_called_ = true;
388   388  
HITCBC 389   12 return {{}, n}; 389   12 return {{}, n};
390   } 390   }
391   }; 391   };
HITCBC 392   35 return awaitable{this, buffers}; 392   35 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 fuse is consulted 398   written. Before signaling, the attached @ref 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 fuse, the state remains unchanged. 403   If an error is injected by the 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 fuse is in exception mode and reaches its 408   the attached @ref 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 `error::canceled` and does not signal 413   completes immediately with `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 fuse is in 418   @throws std::system_error When the attached @ref fuse is in
419   exception mode and reaches its failure point. 419   exception mode and reaches its failure point.
420   420  
421   @see fuse 421   @see fuse
422   */ 422   */
423   auto 423   auto
HITCBC 424   83 write_eof() 424   83 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   83 bool await_ready() const noexcept { return false; } 431   83 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   83 await_suspend( 436   83 await_suspend(
437   std::coroutine_handle<>, 437   std::coroutine_handle<>,
438   io_env const* env) noexcept 438   io_env const* env) noexcept
439   { 439   {
HITCBC 440   83 canceled_ = env->stop_token.stop_requested(); 440   83 canceled_ = env->stop_token.stop_requested();
HITCBC 441   83 return false; 441   83 return false;
442   } 442   }
443   443  
444   io_result<> 444   io_result<>
HITCBC 445   83 await_resume() 445   83 await_resume()
446   { 446   {
HITCBC 447   83 if(canceled_) 447   83 if(canceled_)
HITCBC 448   1 return {error::canceled}; 448   1 return {error::canceled};
449   449  
HITCBC 450   82 auto ec = self_->f_.maybe_fail(); 450   82 auto ec = self_->f_.maybe_fail();
HITCBC 451   60 if(ec) 451   60 if(ec)
HITCBC 452   22 return {ec}; 452   22 return {ec};
453   453  
HITCBC 454   38 self_->eof_called_ = true; 454   38 self_->eof_called_ = true;
HITCBC 455   38 return {}; 455   38 return {};
456   } 456   }
457   }; 457   };
HITCBC 458   83 return awaitable{this}; 458   83 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