100.00% Lines (46/46) 100.00% Functions (9/9)
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   // 4   //
4   // Distributed under the Boost Software License, Version 1.0. (See accompanying 5   // Distributed under the Boost Software License, Version 1.0. (See accompanying
5   // 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)
6   // 7   //
7   // Official repository: https://github.com/cppalliance/capy 8   // Official repository: https://github.com/cppalliance/capy
8   // 9   //
9   10  
10   #ifndef BOOST_CAPY_TEST_WRITE_STREAM_HPP 11   #ifndef BOOST_CAPY_TEST_WRITE_STREAM_HPP
11   #define BOOST_CAPY_TEST_WRITE_STREAM_HPP 12   #define BOOST_CAPY_TEST_WRITE_STREAM_HPP
12   13  
13   #include <boost/capy/detail/config.hpp> 14   #include <boost/capy/detail/config.hpp>
14   #include <boost/capy/buffers.hpp> 15   #include <boost/capy/buffers.hpp>
15   #include <boost/capy/buffers/buffer_copy.hpp> 16   #include <boost/capy/buffers/buffer_copy.hpp>
16   #include <boost/capy/buffers/make_buffer.hpp> 17   #include <boost/capy/buffers/make_buffer.hpp>
17   #include <coroutine> 18   #include <coroutine>
18   #include <boost/capy/ex/io_env.hpp> 19   #include <boost/capy/ex/io_env.hpp>
19   #include <boost/capy/io_result.hpp> 20   #include <boost/capy/io_result.hpp>
20   #include <boost/capy/error.hpp> 21   #include <boost/capy/error.hpp>
21   #include <boost/capy/test/fuse.hpp> 22   #include <boost/capy/test/fuse.hpp>
22   23  
23   #include <algorithm> 24   #include <algorithm>
24   #include <string> 25   #include <string>
25   #include <string_view> 26   #include <string_view>
26   27  
27   namespace boost { 28   namespace boost {
28   namespace capy { 29   namespace capy {
29   namespace test { 30   namespace test {
30   31  
31   /** A mock stream for testing write operations. 32   /** A mock stream for testing write operations.
32   33  
33   Use this to verify code that performs writes without needing 34   Use this to verify code that performs writes without needing
34   real I/O. Call @ref write_some to write data, then @ref data 35   real I/O. Call @ref write_some to write data, then @ref data
35   to retrieve what was written. The associated @ref fuse enables 36   to retrieve what was written. The associated @ref fuse enables
36   error injection at controlled points. An optional 37   error injection at controlled points. An optional
37   `max_write_size` constructor parameter limits bytes per write 38   `max_write_size` constructor parameter limits bytes per write
38   to simulate chunked delivery. 39   to simulate chunked delivery.
39   40  
40   This class satisfies the @ref WriteStream concept. 41   This class satisfies the @ref WriteStream concept.
41   42  
42   @par Thread Safety 43   @par Thread Safety
43   Not thread-safe. 44   Not thread-safe.
44   45  
45   @par Example 46   @par Example
46   @code 47   @code
47   fuse f; 48   fuse f;
48   write_stream ws( f ); 49   write_stream ws( f );
49   50  
50   auto r = f.armed( [&]( fuse& ) -> task<void> { 51   auto r = f.armed( [&]( fuse& ) -> task<void> {
51   auto [ec, n] = co_await ws.write_some( 52   auto [ec, n] = co_await ws.write_some(
52   const_buffer( "Hello", 5 ) ); 53   const_buffer( "Hello", 5 ) );
53   if( ec ) 54   if( ec )
54   co_return; 55   co_return;
55   // ws.data() returns "Hello" 56   // ws.data() returns "Hello"
56   } ); 57   } );
57   @endcode 58   @endcode
58   59  
59   @see fuse, WriteStream 60   @see fuse, WriteStream
60   */ 61   */
61   class write_stream 62   class write_stream
62   { 63   {
63   fuse f_; 64   fuse f_;
64   std::string data_; 65   std::string data_;
65   std::string expect_; 66   std::string expect_;
66   std::size_t max_write_size_; 67   std::size_t max_write_size_;
67   68  
68   std::error_code 69   std::error_code
HITCBC 69   959 consume_match_() noexcept 70   959 consume_match_() noexcept
70   { 71   {
HITCBC 71   959 if(data_.empty() || expect_.empty()) 72   959 if(data_.empty() || expect_.empty())
HITCBC 72   943 return {}; 73   943 return {};
HITCBC 73   16 std::size_t const n = (std::min)(data_.size(), expect_.size()); 74   16 std::size_t const n = (std::min)(data_.size(), expect_.size());
HITCBC 74   16 if(std::string_view(data_.data(), n) != 75   16 if(std::string_view(data_.data(), n) !=
HITCBC 75   32 std::string_view(expect_.data(), n)) 76   32 std::string_view(expect_.data(), n))
HITCBC 76   4 return error::test_failure; 77   4 return error::test_failure;
HITCBC 77   12 data_.erase(0, n); 78   12 data_.erase(0, n);
HITCBC 78   12 expect_.erase(0, n); 79   12 expect_.erase(0, n);
HITCBC 79   12 return {}; 80   12 return {};
80   } 81   }
81   82  
82   public: 83   public:
83   /** Construct a write stream. 84   /** Construct a write stream.
84   85  
85   @param f The fuse used to inject errors during writes. 86   @param f The fuse used to inject errors during writes.
86   87  
87   @param max_write_size Maximum bytes transferred per write. 88   @param max_write_size Maximum bytes transferred per write.
88   Use to simulate chunked network delivery. 89   Use to simulate chunked network delivery.
89   */ 90   */
HITCBC 90   1191 explicit write_stream( 91   1192 explicit write_stream(
91   fuse f = {}, 92   fuse f = {},
92   std::size_t max_write_size = std::size_t(-1)) noexcept 93   std::size_t max_write_size = std::size_t(-1)) noexcept
HITCBC 93   1191 : f_(std::move(f)) 94   1192 : f_(std::move(f))
HITCBC 94   1191 , max_write_size_(max_write_size) 95   1192 , max_write_size_(max_write_size)
95   { 96   {
HITCBC 96   1191 } 97   1192 }
97   98  
98   /// Return the written data as a string view. 99   /// Return the written data as a string view.
99   std::string_view 100   std::string_view
HITCBC 100   962 data() const noexcept 101   962 data() const noexcept
101   { 102   {
HITCBC 102   962 return data_; 103   962 return data_;
103   } 104   }
104   105  
105   /** Set the expected data for subsequent writes. 106   /** Set the expected data for subsequent writes.
106   107  
107   Stores the expected data and immediately tries to match 108   Stores the expected data and immediately tries to match
108   against any data already written. Matched data is consumed 109   against any data already written. Matched data is consumed
109   from both buffers. 110   from both buffers.
110   111  
111   @param sv The expected data. 112   @param sv The expected data.
112   113  
113   @return An error if existing data does not match. 114   @return An error if existing data does not match.
114   */ 115   */
115   std::error_code 116   std::error_code
HITCBC 116   30 expect(std::string_view sv) 117   30 expect(std::string_view sv)
117   { 118   {
HITCBC 118   30 expect_.assign(sv); 119   30 expect_.assign(sv);
HITCBC 119   30 return consume_match_(); 120   30 return consume_match_();
120   } 121   }
121   122  
122   /// Return the number of bytes written. 123   /// Return the number of bytes written.
123   std::size_t 124   std::size_t
HITCBC 124   6 size() const noexcept 125   7 size() const noexcept
125   { 126   {
HITCBC 126   6 return data_.size(); 127   7 return data_.size();
127   } 128   }
128   129  
129   /** Asynchronously write data to the stream. 130   /** Asynchronously write data to the stream.
130   131  
131   Transfers up to `buffer_size( buffers )` bytes from the provided 132   Transfers up to `buffer_size( buffers )` bytes from the provided
132   const buffer sequence to the internal buffer. Before every write, 133   const buffer sequence to the internal buffer. Before every write,
133   the attached @ref fuse is consulted to possibly inject an error 134   the attached @ref fuse is consulted to possibly inject an error
134   for testing fault scenarios. The returned `std::size_t` is the 135   for testing fault scenarios. The returned `std::size_t` is the
135   number of bytes transferred. 136   number of bytes transferred.
136   137  
137   @par Effects 138   @par Effects
138   On success, appends the written bytes to the internal buffer. 139   On success, appends the written bytes to the internal buffer.
139   If an error is injected by the fuse, the internal buffer remains 140   If an error is injected by the fuse, the internal buffer remains
140   unchanged. 141   unchanged.
141   142  
142   @par Exception Safety 143   @par Exception Safety
143   No-throw guarantee. 144   No-throw guarantee.
144   145  
  146 + @par Cancellation
  147 + If the environment's stop token has been requested, the write
  148 + completes immediately with `error::canceled` and transfers no
  149 + data. An empty buffer sequence is a no-op that completes
  150 + successfully regardless of the stop token.
  151 +
145   @param buffers The const buffer sequence containing data to write. 152   @param buffers The const buffer sequence containing data to write.
146   153  
147   @return An awaitable that await-returns `(error_code,std::size_t)`. 154   @return An awaitable that await-returns `(error_code,std::size_t)`.
148   155  
149   @see fuse 156   @see fuse
150   */ 157   */
151   template<ConstBufferSequence CB> 158   template<ConstBufferSequence CB>
152   auto 159   auto
HITCBC 153   1193 write_some(CB buffers) 160   1194 write_some(CB buffers)
154   { 161   {
155   struct awaitable 162   struct awaitable
156   { 163   {
157   write_stream* self_; 164   write_stream* self_;
158   CB buffers_; 165   CB buffers_;
  166 + bool canceled_ = false;
159   167  
HITCBC 160 - 1193 bool await_ready() const noexcept { return true; } 168 + 1194 bool await_ready() const noexcept { return false; }
161   169  
162 - // This method is required to satisfy Capy's IoAwaitable concept, 170 + // The operation completes synchronously, but await_suspend is
163 - // but is never called because await_ready() returns true. 171 + // the only place io_env is delivered (the promise's
164 - // 172 + // transform_awaiter forwards it here). Returning false means
165 - // Capy uses a two-layer awaitable system: the promise's 173 + // the coroutine does not actually suspend; it resumes
166 - // await_transform wraps awaitables in a transform_awaiter whose 174 + // immediately, having observed the stop token. See io_env,
167 - // standard await_suspend(coroutine_handle) calls this custom 175 + // IoAwaitable.
168 - // 2-argument overload, passing the io_env from the coroutine's 176 + bool
HITGIC 169 - // context. For synchronous test awaitables like this one, the 177 + 1194 await_suspend(
170 - // coroutine never suspends, so this is not invoked. The signature  
171 - // exists to allow the same awaitable type to work with both  
172 - // synchronous (test) and asynchronous (real I/O) code.  
DUB 173 - void await_suspend(  
174   std::coroutine_handle<>, 178   std::coroutine_handle<>,
175 - io_env const*) const noexcept 179 + io_env const* env) noexcept
176   { 180   {
HITGNC   181 + 1194 canceled_ = env->stop_token.stop_requested();
HITGNC   182 + 1194 return false;
EUB 177   } 183   }
178   184  
179   io_result<std::size_t> 185   io_result<std::size_t>
HITCBC 180   1193 await_resume() 186   1194 await_resume()
181   { 187   {
HITCBC 182   1193 if(buffer_empty(buffers_)) 188   1194 if(buffer_empty(buffers_))
HITCBC 183   2 return {{}, 0}; 189   2 return {{}, 0};
  190 +
HITGNC   191 + 1192 if(canceled_)
HITGNC   192 + 1 return {error::canceled, 0};
184   193  
HITCBC 185   1191 auto ec = self_->f_.maybe_fail(); 194   1191 auto ec = self_->f_.maybe_fail();
HITCBC 186   1060 if(ec) 195   1060 if(ec)
HITCBC 187   131 return {ec, 0}; 196   131 return {ec, 0};
188   197  
HITCBC 189   929 std::size_t n = buffer_size(buffers_); 198   929 std::size_t n = buffer_size(buffers_);
HITCBC 190   929 n = (std::min)(n, self_->max_write_size_); 199   929 n = (std::min)(n, self_->max_write_size_);
191   200  
HITCBC 192   929 std::size_t const old_size = self_->data_.size(); 201   929 std::size_t const old_size = self_->data_.size();
HITCBC 193   929 self_->data_.resize(old_size + n); 202   929 self_->data_.resize(old_size + n);
HITCBC 194   929 buffer_copy(make_buffer( 203   929 buffer_copy(make_buffer(
HITCBC 195   929 self_->data_.data() + old_size, n), buffers_, n); 204   929 self_->data_.data() + old_size, n), buffers_, n);
196   205  
HITCBC 197   929 ec = self_->consume_match_(); 206   929 ec = self_->consume_match_();
HITCBC 198   929 if(ec) 207   929 if(ec)
199   { 208   {
HITCBC 200   2 self_->data_.resize(old_size); 209   2 self_->data_.resize(old_size);
HITCBC 201   2 return {ec, 0}; 210   2 return {ec, 0};
202   } 211   }
203   212  
HITCBC 204   927 return {{}, n}; 213   927 return {{}, n};
205   } 214   }
206   }; 215   };
HITCBC 207   1193 return awaitable{this, buffers}; 216   1194 return awaitable{this, buffers};
208   } 217   }
209   }; 218   };
210   219  
211   } // test 220   } // test
212   } // capy 221   } // capy
213   } // boost 222   } // boost
214   223  
215   #endif 224   #endif