72.00% Lines (126/175) 100.00% Functions (16/16)
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_FUSE_HPP 11   #ifndef BOOST_CAPY_TEST_FUSE_HPP
12   #define BOOST_CAPY_TEST_FUSE_HPP 12   #define BOOST_CAPY_TEST_FUSE_HPP
13   13  
14   #include <boost/capy/detail/config.hpp> 14   #include <boost/capy/detail/config.hpp>
15   #include <boost/capy/concept/io_runnable.hpp> 15   #include <boost/capy/concept/io_runnable.hpp>
16   #include <boost/capy/error.hpp> 16   #include <boost/capy/error.hpp>
17   #include <boost/capy/test/run_blocking.hpp> 17   #include <boost/capy/test/run_blocking.hpp>
18   #include <system_error> 18   #include <system_error>
19   #include <concepts> 19   #include <concepts>
20   #include <cstddef> 20   #include <cstddef>
21   #include <exception> 21   #include <exception>
22   #include <limits> 22   #include <limits>
23   #include <memory> 23   #include <memory>
24   #include <source_location> 24   #include <source_location>
25   #include <type_traits> 25   #include <type_traits>
26   26  
27   /* 27   /*
28   LLM/AI Instructions for fuse-based test patterns: 28   LLM/AI Instructions for fuse-based test patterns:
29   29  
30   When f.armed() runs a test, it injects errors at successive points 30   When f.armed() runs a test, it injects errors at successive points
31   via maybe_fail(). Operations like read_stream::read_some() and 31   via maybe_fail(). Operations like read_stream::read_some() and
32   write_stream::write_some() call maybe_fail() internally. 32   write_stream::write_some() call maybe_fail() internally.
33   33  
34   CORRECT pattern - early return on injected error: 34   CORRECT pattern - early return on injected error:
35   35  
36   auto [ec, n] = co_await rs.read_some(buf); 36   auto [ec, n] = co_await rs.read_some(buf);
37   if(ec) 37   if(ec)
38   co_return; // fuse injected error, exit gracefully 38   co_return; // fuse injected error, exit gracefully
39   // ... continue with success path 39   // ... continue with success path
40   40  
41   WRONG pattern - asserting success unconditionally: 41   WRONG pattern - asserting success unconditionally:
42   42  
43   auto [ec, n] = co_await rs.read_some(buf); 43   auto [ec, n] = co_await rs.read_some(buf);
44   BOOST_TEST(! ec); // FAILS when fuse injects error! 44   BOOST_TEST(! ec); // FAILS when fuse injects error!
45   45  
46   The fuse mechanism tests error handling by failing at each point 46   The fuse mechanism tests error handling by failing at each point
47   in sequence. Tests must handle injected errors by returning early, 47   in sequence. Tests must handle injected errors by returning early,
48   not by asserting that operations always succeed. 48   not by asserting that operations always succeed.
49   */ 49   */
50   50  
51   namespace boost { 51   namespace boost {
52   namespace capy { 52   namespace capy {
53   namespace test { 53   namespace test {
54   54  
55   /** Reruns a code path, injecting a failure at one later point on each pass. 55   /** Reruns a code path, injecting a failure at one later point on each pass.
56   56  
57   This class enables exhaustive testing of error handling 57   This class enables exhaustive testing of error handling
58   paths by injecting failures at successive points in code. 58   paths by injecting failures at successive points in code.
59   Each iteration fails at a later point until the code path 59   Each iteration fails at a later point until the code path
60   completes without encountering a failure. The @ref armed 60   completes without encountering a failure. The @ref armed
61   method runs in two phases: first with error codes, then 61   method runs in two phases: first with error codes, then
62   with exceptions. The @ref inert method runs once without 62   with exceptions. The @ref inert method runs once without
63   automatic failure injection. 63   automatic failure injection.
64   64  
65   @par Thread Safety 65   @par Thread Safety
66   66  
67   @b Not @b thread @b safe. Instances must not be accessed 67   @b Not @b thread @b safe. Instances must not be accessed
68   from different logical threads of operation concurrently. 68   from different logical threads of operation concurrently.
69   This includes coroutines - accessing the same fuse from 69   This includes coroutines - accessing the same fuse from
70   multiple concurrent coroutines causes non-deterministic 70   multiple concurrent coroutines causes non-deterministic
71   test behavior. 71   test behavior.
72   72  
73   @par Basic Inline Usage 73   @par Basic Inline Usage
74   74  
75 - @code 75 + @par !example example_1
76 - fuse()([](fuse& f) {  
77 - auto ec = f.maybe_fail();  
78 - if(ec)  
79 - return;  
80 - ec = f.maybe_fail();  
81 - if(ec)  
82 - return;  
83 - });  
84 - @endcode  
85   76  
86   77  
87   @par Named Fuse with armed() 78   @par Named Fuse with armed()
88   79  
89 - @code 80 + @par !example example_2
90 - fuse f; 81 +
91 - MyObject obj(f);  
92 - auto r = f.armed([&](fuse&) {  
93 - obj.do_something();  
94 - });  
95 - @endcode  
96   82  
97   @par Using inert() for Single-Run Tests 83   @par Using inert() for Single-Run Tests
98   84  
99 - @code 85 + @par !example example_3
100 - fuse f; 86 +
101 - auto r = f.inert([](fuse& f) {  
102 - auto ec = f.maybe_fail(); // Always succeeds  
103 - if(some_condition)  
104 - f.fail(); // Only way to signal failure  
105 - });  
106 - @endcode  
107   87  
108   @par Dependency Injection (Standalone Usage) 88   @par Dependency Injection (Standalone Usage)
109   89  
110   A default-constructed fuse is a no-op when used outside 90   A default-constructed fuse is a no-op when used outside
111   of @ref armed or @ref inert. This enables passing a fuse 91   of @ref armed or @ref inert. This enables passing a fuse
112   to classes for dependency injection without affecting 92   to classes for dependency injection without affecting
113   normal operation. 93   normal operation.
114   94  
115 - @code 95 + @par !example example_4
116 - class MyService  
117 - {  
118 - fuse& f_;  
119 - public:  
120 - explicit MyService(fuse& f) : f_(f) {}  
121 -  
122 - std::error_code do_work()  
123 - {  
124 - auto ec = f_.maybe_fail(); // No-op outside armed/inert  
125 - if(ec)  
126 - return ec;  
127 - // ... actual work ...  
128 - return {};  
129 - }  
130 - };  
131 -  
132 - // Production usage - fuse is no-op  
133 - fuse f;  
134 - MyService svc(f);  
135 - svc.do_work(); // maybe_fail() returns {} always  
136 - // Test usage - failures are injected  
137 - auto r = f.armed([&](fuse&) {  
138 - svc.do_work(); // maybe_fail() triggers failures  
139 - });  
140 - @endcode  
141   96  
142   97  
143   @par Custom Error Code 98   @par Custom Error Code
144   99  
145 - @code 100 + @par !example example_5
146 - auto custom_ec = make_error_code( 101 +
147 - std::errc::operation_canceled);  
148 - fuse f(custom_ec);  
149 - auto r = f.armed([](fuse& f) {  
150 - auto ec = f.maybe_fail();  
151 - if(ec)  
152 - return;  
153 - });  
154 - @endcode  
155   102  
156   @par Checking the Result 103   @par Checking the Result
157   104  
158 - @code 105 + @par !example example_6
159 - fuse f;  
160 - auto r = f([](fuse& f) {  
161 - auto ec = f.maybe_fail();  
162 - if(ec)  
163 - return;  
164 - });  
165 - if(!r)  
166 - {  
167 - std::cerr << "Failure at "  
168 - << r.loc.file_name() << ":"  
169 - << r.loc.line() << "\n";  
170 - }  
171 - @endcode  
172   106  
173   107  
174   @par Test Framework Integration 108   @par Test Framework Integration
175   109  
176 - @code 110 + @par !example example_7
177 - fuse f;  
178 - auto r = f([](fuse& f) {  
179 - auto ec = f.maybe_fail();  
180 - if(ec)  
181 - return;  
182 - });  
183 -  
184 - // Boost.Test  
185 - BOOST_TEST(r.success);  
186 - if(!r)  
187 - BOOST_TEST_MESSAGE("Failed at " << r.loc.file_name()  
188 - << ":" << r.loc.line());  
189 - // Catch2  
190 - REQUIRE(r.success);  
191 - if(!r)  
192 - INFO("Failed at " << r.loc.file_name()  
193 - << ":" << r.loc.line());  
194 - @endcode  
195   111  
196   */ 112   */
197   class fuse 113   class fuse
198   { 114   {
199   struct state 115   struct state
200   { 116   {
201   std::size_t n = (std::numeric_limits<std::size_t>::max)(); 117   std::size_t n = (std::numeric_limits<std::size_t>::max)();
202   std::size_t i = 0; 118   std::size_t i = 0;
203   bool triggered = false; 119   bool triggered = false;
204   bool throws = false; 120   bool throws = false;
205   bool stopped = false; 121   bool stopped = false;
206   bool inert = true; 122   bool inert = true;
207   std::error_code ec; 123   std::error_code ec;
208   std::source_location loc; 124   std::source_location loc;
209   std::exception_ptr ep; 125   std::exception_ptr ep;
210   }; 126   };
211   127  
212   std::shared_ptr<state> p_; 128   std::shared_ptr<state> p_;
213   129  
214   /** Return true if testing should continue. 130   /** Return true if testing should continue.
215   131  
216   On the first call, initializes the failure point to 0. 132   On the first call, initializes the failure point to 0.
217   After a triggered failure, increments the failure point 133   After a triggered failure, increments the failure point
218   and resets for the next iteration. Returns false when 134   and resets for the next iteration. Returns false when
219   the test completes without triggering a failure. 135   the test completes without triggering a failure.
220   */ 136   */
HITCBC 221   1326 explicit operator bool() const noexcept 137   1344 explicit operator bool() const noexcept
222   { 138   {
HITCBC 223   1326 auto& s = *p_; 139   1344 auto& s = *p_;
HITCBC 224   1326 if(s.n == (std::numeric_limits<std::size_t>::max)()) 140   1344 if(s.n == (std::numeric_limits<std::size_t>::max)())
225   { 141   {
226   // First call: start round 0 142   // First call: start round 0
HITCBC 227   313 s.n = 0; 143   317 s.n = 0;
HITCBC 228   313 return true; 144   317 return true;
229   } 145   }
HITCBC 230   1013 if(s.triggered) 146   1027 if(s.triggered)
231   { 147   {
232   // Previous round triggered, try next failure point 148   // Previous round triggered, try next failure point
HITCBC 233   707 s.n++; 149   717 s.n++;
HITCBC 234   707 s.i = 0; 150   717 s.i = 0;
HITCBC 235   707 s.triggered = false; 151   717 s.triggered = false;
HITCBC 236   707 return true; 152   717 return true;
237   } 153   }
238   // Test completed without trigger: success 154   // Test completed without trigger: success
HITCBC 239   306 return false; 155   310 return false;
240   } 156   }
241   157  
242   public: 158   public:
243   /** Converts to `bool`, reporting success, and carries the failure point on failure. 159   /** Converts to `bool`, reporting success, and carries the failure point on failure.
244   160  
245   Contains the outcome of @ref armed or @ref inert 161   Contains the outcome of @ref armed or @ref inert
246   and, on failure, the source location of the failing 162   and, on failure, the source location of the failing
247   point. Converts to `bool` for convenient success 163   point. Converts to `bool` for convenient success
248   checking. 164   checking.
249   165  
250   @par Example 166   @par Example
251   167  
252 - @code 168 + @par !example example
253 - fuse f;  
254 - auto r = f([](fuse& f) {  
255 - auto ec = f.maybe_fail();  
256 - if(ec)  
257 - return;  
258 - });  
259 - if(!r)  
260 - {  
261 - std::cerr << "Failure at "  
262 - << r.loc.file_name() << ":"  
263 - << r.loc.line() << "\n";  
264 - }  
265 - @endcode  
266   169  
267   */ 170   */
268   struct result 171   struct result
269   { 172   {
270   /// Source location of the failing point, set only on failure. 173   /// Source location of the failing point, set only on failure.
271   std::source_location loc = {}; 174   std::source_location loc = {};
272   175  
273   /// Exception captured by @ref fail, or null if none. 176   /// Exception captured by @ref fail, or null if none.
274   std::exception_ptr ep = nullptr; 177   std::exception_ptr ep = nullptr;
275   178  
276   /// True if the test completed without a failure. 179   /// True if the test completed without a failure.
277   bool success = true; 180   bool success = true;
278   181  
279   /** Return whether the test completed without a failure. 182   /** Return whether the test completed without a failure.
280   183  
281   @return @ref success. 184   @return @ref success.
282   */ 185   */
HITCBC 283   42 constexpr explicit operator bool() const noexcept 186   42 constexpr explicit operator bool() const noexcept
284   { 187   {
HITCBC 285   42 return success; 188   42 return success;
286   } 189   }
287   }; 190   };
288   191  
289   /** Construct a fuse with a custom error code. 192   /** Construct a fuse with a custom error code.
290   193  
291   @par Example 194   @par Example
292   195  
293 - @code 196 + @par !example example_1
294 - auto custom_ec = make_error_code(  
295 - std::errc::operation_canceled);  
296 - fuse f(custom_ec);  
297 -  
298 - std::error_code captured_ec;  
299 - auto r = f([&](fuse& f) {  
300 - auto ec = f.maybe_fail();  
301 - if(ec)  
302 - {  
303 - captured_ec = ec;  
304 - return;  
305 - }  
306 - });  
307 - assert(captured_ec == custom_ec);  
308 - @endcode  
309   197  
310   198  
311   @param ec The error code to deliver at failure points. 199   @param ec The error code to deliver at failure points.
312   */ 200   */
HITCBC 313   274 explicit fuse(std::error_code ec) 201   277 explicit fuse(std::error_code ec)
HITCBC 314   274 : p_(std::make_shared<state>()) 202   277 : p_(std::make_shared<state>())
315   { 203   {
HITCBC 316   274 p_->ec = ec; 204   277 p_->ec = ec;
HITCBC 317   274 } 205   277 }
318   206  
319   /** Construct a fuse with the default error code. 207   /** Construct a fuse with the default error code.
320   208  
321   The default error code is `error::test_failure`. 209   The default error code is `error::test_failure`.
322   210  
323   @par Example 211   @par Example
324   212  
325 - @code 213 + @par !example example_2
326 - fuse f;  
327 - std::error_code captured_ec;  
328 -  
329 - auto r = f([&](fuse& f) {  
330 - auto ec = f.maybe_fail();  
331 - if(ec)  
332 - {  
333 - captured_ec = ec;  
334 - return;  
335 - }  
336 - });  
337 - assert(captured_ec == error::test_failure);  
338 - @endcode  
339   214  
340   */ 215   */
HITCBC 341   271 fuse() 216   273 fuse()
HITCBC 342   271 : fuse(error::test_failure) 217   273 : fuse(error::test_failure)
343   { 218   {
HITCBC 344   271 } 219   273 }
345   220  
346   /** Return an error or throw at the current failure point. 221   /** Return an error or throw at the current failure point.
347   222  
348   When running under @ref armed, increments the internal 223   When running under @ref armed, increments the internal
349   counter. When the counter reaches the current failure 224   counter. When the counter reaches the current failure
350   point, returns the stored error code (or throws 225   point, returns the stored error code (or throws
351   `std::system_error` in exception mode) and records 226   `std::system_error` in exception mode) and records
352   the source location. 227   the source location.
353   228  
354   When called outside of @ref armed or @ref inert (standalone 229   When called outside of @ref armed or @ref inert (standalone
355   usage), or when running under @ref inert, always returns 230   usage), or when running under @ref inert, always returns
356   an empty error code. This enables dependency injection 231   an empty error code. This enables dependency injection
357   where the fuse is a no-op in production code. 232   where the fuse is a no-op in production code.
358   233  
359   @par Example 234   @par Example
360   235  
361 - @code 236 + @par !example example_1
362 - fuse f;  
363 - auto r = f([](fuse& f) {  
364 - // Error code mode: returns the error  
365 - auto ec = f.maybe_fail();  
366 - if(ec)  
367 - return;  
368 - // Exception mode: throws system_error  
369 - ec = f.maybe_fail();  
370 - if(ec)  
371 - return;  
372 - });  
373 - @endcode  
374   237  
375   238  
376   @par Standalone Usage 239   @par Standalone Usage
377   240  
378 - @code 241 + @par !example example_2
379 - fuse f; 242 +
380 - auto ec = f.maybe_fail(); // Always returns {} (no-op)  
381 - @endcode  
382   243  
383   @param loc The source location of the call site, 244   @param loc The source location of the call site,
384   captured automatically. 245   captured automatically.
385   246  
386   @return The stored error code if at the failure point, 247   @return The stored error code if at the failure point,
387   otherwise an empty error code. In exception mode, 248   otherwise an empty error code. In exception mode,
388   throws instead of returning an error. When called 249   throws instead of returning an error. When called
389   outside @ref armed, or when running under @ref inert, 250   outside @ref armed, or when running under @ref inert,
390   always returns an empty error code. 251   always returns an empty error code.
391   252  
392   @throws std::system_error When in exception mode 253   @throws std::system_error When in exception mode
393   and at the failure point (not thrown outside @ref armed). 254   and at the failure point (not thrown outside @ref armed).
394   */ 255   */
395   std::error_code 256   std::error_code
HITCBC 396   1746 maybe_fail( 257   1765 maybe_fail(
397   std::source_location loc = std::source_location::current()) 258   std::source_location loc = std::source_location::current())
398   { 259   {
HITCBC 399   1746 auto& s = *p_; 260   1765 auto& s = *p_;
HITCBC 400   1746 if(s.inert) 261   1765 if(s.inert)
HITCBC 401   323 return {}; 262   324 return {};
HITCBC 402   1423 if(s.i < s.n) 263   1441 if(s.i < s.n)
HITCBC 403   1152 ++s.i; 264   1166 ++s.i;
HITCBC 404   1423 if(s.i == s.n) 265   1441 if(s.i == s.n)
405   { 266   {
HITCBC 406   707 s.triggered = true; 267   717 s.triggered = true;
HITCBC 407   707 s.loc = loc; 268   717 s.loc = loc;
HITCBC 408   707 if(s.throws) 269   717 if(s.throws)
HITCBC 409   347 throw std::system_error(s.ec); 270   352 throw std::system_error(s.ec);
HITCBC 410   360 return s.ec; 271   365 return s.ec;
411   } 272   }
HITCBC 412   716 return {}; 273   724 return {};
413   } 274   }
414   275  
415   /** Signal a test failure and stop execution. 276   /** Signal a test failure and stop execution.
416   277  
417   Call this from the test function to indicate a failure 278   Call this from the test function to indicate a failure
418   condition. Both @ref armed and @ref inert return 279   condition. Both @ref armed and @ref inert return
419   a failed @ref result immediately. 280   a failed @ref result immediately.
420   281  
421   @par Example 282   @par Example
422   283  
423 - @code 284 + @par !example example_1
424 - fuse f;  
425 - auto r = f([](fuse& f) {  
426 - auto ec = f.maybe_fail();  
427 - if(ec)  
428 - return;  
429 -  
430 - // Explicit failure when a condition is not met  
431 - if(some_value != expected)  
432 - {  
433 - f.fail();  
434 - return;  
435 - }  
436 - });  
437 - if(!r)  
438 - {  
439 - std::cerr << "Test failed at "  
440 - << r.loc.file_name() << ":"  
441 - << r.loc.line() << "\n";  
442 - }  
443 - @endcode  
444   285  
445   286  
446   @param loc The source location of the call site, 287   @param loc The source location of the call site,
447   captured automatically. 288   captured automatically.
448   */ 289   */
449   void 290   void
HITCBC 450   3 fail( 291   3 fail(
451   std::source_location loc = 292   std::source_location loc =
452   std::source_location::current()) noexcept 293   std::source_location::current()) noexcept
453   { 294   {
HITCBC 454   3 p_->loc = loc; 295   3 p_->loc = loc;
HITCBC 455   3 p_->stopped = true; 296   3 p_->stopped = true;
HITCBC 456   3 } 297   3 }
457   298  
458   /** Signal a test failure with an exception and stop execution. 299   /** Signal a test failure with an exception and stop execution.
459   300  
460   Call this from the test function to indicate a failure 301   Call this from the test function to indicate a failure
461   condition with an associated exception. Both @ref armed 302   condition with an associated exception. Both @ref armed
462   and @ref inert return a failed @ref result with 303   and @ref inert return a failed @ref result with
463   the captured exception pointer. 304   the captured exception pointer.
464   305  
465   @par Example 306   @par Example
466   307  
467 - @code 308 + @par !example example_2
468 - fuse f;  
469 - auto r = f([](fuse& f) {  
470 - try  
471 - {  
472 - do_something();  
473 - }  
474 - catch(...)  
475 - {  
476 - f.fail(std::current_exception());  
477 - return;  
478 - }  
479 - });  
480 - if(!r)  
481 - {  
482 - try  
483 - {  
484 - if(r.ep)  
485 - std::rethrow_exception(r.ep);  
486 - }  
487 - catch(std::exception const& e)  
488 - {  
489 - std::cerr << "Exception: " << e.what() << "\n";  
490 - }  
491 - }  
492 - @endcode  
493   309  
494   310  
495   @param ep The exception pointer to capture. 311   @param ep The exception pointer to capture.
496   312  
497   @param loc The source location of the call site, 313   @param loc The source location of the call site,
498   captured automatically. 314   captured automatically.
499   */ 315   */
500   void 316   void
HITCBC 501   2 fail( 317   2 fail(
502   std::exception_ptr ep, 318   std::exception_ptr ep,
503   std::source_location loc = 319   std::source_location loc =
504   std::source_location::current()) noexcept 320   std::source_location::current()) noexcept
505   { 321   {
HITCBC 506   2 p_->ep = ep; 322   2 p_->ep = ep;
HITCBC 507   2 p_->loc = loc; 323   2 p_->loc = loc;
HITCBC 508   2 p_->stopped = true; 324   2 p_->stopped = true;
HITCBC 509   2 } 325   2 }
510   326  
511   private: 327   private:
512   /* Drive the two-phase armed loop, invoking `do_iter` once per round. 328   /* Drive the two-phase armed loop, invoking `do_iter` once per round.
513   329  
514   Phase 1 delivers injected failures as error codes; phase 2 as 330   Phase 1 delivers injected failures as error codes; phase 2 as
515   exceptions. Shared by the two coroutine `armed` overloads: each 331   exceptions. Shared by the two coroutine `armed` overloads: each
516   supplies a nullary `do_iter` that runs one iteration — via 332   supplies a nullary `do_iter` that runs one iteration — via
517   @ref run_blocking, or via a caller-supplied runner — so the round 333   @ref run_blocking, or via a caller-supplied runner — so the round
518   sequence and failure handling stay identical across them. 334   sequence and failure handling stay identical across them.
519   */ 335   */
520   template<class DoIter> 336   template<class DoIter>
521   result 337   result
HITCBC 522   134 run_phases(DoIter&& do_iter) 338   134 run_phases(DoIter&& do_iter)
523   { 339   {
HITCBC 524   134 result r; 340   134 result r;
525   341  
526   // Phase 1: error code mode 342   // Phase 1: error code mode
HITCBC 527   134 p_->throws = false; 343   134 p_->throws = false;
HITCBC 528   134 p_->inert = false; 344   134 p_->inert = false;
HITCBC 529   134 p_->n = (std::numeric_limits<std::size_t>::max)(); 345   134 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 530   581 while(*this) 346   581 while(*this)
531   { 347   {
532   try 348   try
533   { 349   {
HITCBC 534   448 do_iter(); 350   448 do_iter();
535   } 351   }
HITCBC 536   2 catch(...) 352   2 catch(...)
537   { 353   {
HITCBC 538   1 r.success = false; 354   1 r.success = false;
HITCBC 539   1 r.loc = p_->loc; 355   1 r.loc = p_->loc;
HITCBC 540   1 r.ep = p_->ep; 356   1 r.ep = p_->ep;
HITCBC 541   1 p_->inert = true; 357   1 p_->inert = true;
HITCBC 542   1 return r; 358   1 return r;
543   } 359   }
HITCBC 544   447 if(p_->stopped) 360   447 if(p_->stopped)
545   { 361   {
MISUBC 546   r.success = false; 362   r.success = false;
MISUBC 547   r.loc = p_->loc; 363   r.loc = p_->loc;
MISUBC 548   r.ep = p_->ep; 364   r.ep = p_->ep;
MISUBC 549   p_->inert = true; 365   p_->inert = true;
MISUBC 550   return r; 366   return r;
551   } 367   }
552   } 368   }
553   369  
554   // Phase 2: exception mode 370   // Phase 2: exception mode
HITCBC 555   133 p_->throws = true; 371   133 p_->throws = true;
HITCBC 556   133 p_->n = (std::numeric_limits<std::size_t>::max)(); 372   133 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 557   133 p_->i = 0; 373   133 p_->i = 0;
HITCBC 558   133 p_->triggered = false; 374   133 p_->triggered = false;
HITCBC 559   578 while(*this) 375   578 while(*this)
560   { 376   {
561   try 377   try
562   { 378   {
HITCBC 563   445 do_iter(); 379   445 do_iter();
564   } 380   }
HITCBC 565   624 catch(std::system_error const& ex) 381   624 catch(std::system_error const& ex)
566   { 382   {
HITCBC 567   312 if(ex.code() != p_->ec) 383   312 if(ex.code() != p_->ec)
568   { 384   {
MISUBC 569   r.success = false; 385   r.success = false;
MISUBC 570   r.loc = p_->loc; 386   r.loc = p_->loc;
MISUBC 571   r.ep = p_->ep; 387   r.ep = p_->ep;
MISUBC 572   p_->inert = true; 388   p_->inert = true;
MISUBC 573   return r; 389   return r;
574   } 390   }
575   } 391   }
MISUBC 576   catch(...) 392   catch(...)
577   { 393   {
MISUBC 578   r.success = false; 394   r.success = false;
MISUBC 579   r.loc = p_->loc; 395   r.loc = p_->loc;
MISUBC 580   r.ep = p_->ep; 396   r.ep = p_->ep;
MISUBC 581   p_->inert = true; 397   p_->inert = true;
MISUBC 582   return r; 398   return r;
583   } 399   }
HITCBC 584   445 if(p_->stopped) 400   445 if(p_->stopped)
585   { 401   {
MISUBC 586   r.success = false; 402   r.success = false;
MISUBC 587   r.loc = p_->loc; 403   r.loc = p_->loc;
MISUBC 588   r.ep = p_->ep; 404   r.ep = p_->ep;
MISUBC 589   p_->inert = true; 405   p_->inert = true;
MISUBC 590   return r; 406   return r;
591   } 407   }
592   } 408   }
HITCBC 593   133 p_->inert = true; 409   133 p_->inert = true;
HITCBC 594   133 return r; 410   133 return r;
MISUBC 595   } 411   }
596   412  
597   public: 413   public:
598   /** Run a test function with systematic failure injection. 414   /** Run a test function with systematic failure injection.
599   415  
600   Repeatedly invokes the provided function, failing at 416   Repeatedly invokes the provided function, failing at
601   successive points until the function completes without 417   successive points until the function completes without
602   encountering a failure. First runs the complete loop 418   encountering a failure. First runs the complete loop
603   using error codes, then runs using exceptions. 419   using error codes, then runs using exceptions.
604   420  
605   @par Example 421   @par Example
606   422  
607 - @code 423 + @par !example example_2
608 - fuse f;  
609 - auto r = f.armed([](fuse& f) {  
610 - auto ec = f.maybe_fail();  
611 - if(ec)  
612 - return;  
613 -  
614 - ec = f.maybe_fail();  
615 - if(ec)  
616 - return;  
617 - });  
618 - if(!r)  
619 - {  
620 - std::cerr << "Failure at "  
621 - << r.loc.file_name() << ":"  
622 - << r.loc.line() << "\n";  
623 - }  
624 - @endcode  
625   424  
626   425  
627   @param fn The test function to invoke. It receives 426   @param fn The test function to invoke. It receives
628   a reference to the fuse and should call @ref maybe_fail 427   a reference to the fuse and should call @ref maybe_fail
629   at each potential failure point. 428   at each potential failure point.
630   429  
631   @return A @ref result indicating success or failure. 430   @return A @ref result indicating success or failure.
632   On failure, `result::loc` contains the source location 431   On failure, `result::loc` contains the source location
633   of the last @ref maybe_fail or @ref fail call. 432   of the last @ref maybe_fail or @ref fail call.
634   */ 433   */
635   template<class F> 434   template<class F>
636   result 435   result
HITCBC 637   26 armed(F&& fn) 436   28 armed(F&& fn)
638   { 437   {
HITCBC 639   26 result r; 438   28 result r;
640   439  
641   // Phase 1: error code mode 440   // Phase 1: error code mode
HITCBC 642   26 p_->throws = false; 441   28 p_->throws = false;
HITCBC 643   26 p_->inert = false; 442   28 p_->inert = false;
HITCBC 644   26 p_->n = (std::numeric_limits<std::size_t>::max)(); 443   28 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 645   92 while(*this) 444   101 while(*this)
646   { 445   {
647   try 446   try
648   { 447   {
HITCBC 649   72 fn(*this); 448   79 fn(*this);
650   } 449   }
HITCBC 651   6 catch(...) 450   6 catch(...)
652   { 451   {
HITCBC 653   3 r.success = false; 452   3 r.success = false;
HITCBC 654   3 r.loc = p_->loc; 453   3 r.loc = p_->loc;
HITCBC 655   3 r.ep = p_->ep; 454   3 r.ep = p_->ep;
HITCBC 656   3 p_->inert = true; 455   3 p_->inert = true;
HITCBC 657   3 return r; 456   3 return r;
658   } 457   }
HITCBC 659   69 if(p_->stopped) 458   76 if(p_->stopped)
660   { 459   {
HITCBC 661   3 r.success = false; 460   3 r.success = false;
HITCBC 662   3 r.loc = p_->loc; 461   3 r.loc = p_->loc;
HITCBC 663   3 r.ep = p_->ep; 462   3 r.ep = p_->ep;
HITCBC 664   3 p_->inert = true; 463   3 p_->inert = true;
HITCBC 665   3 return r; 464   3 return r;
666   } 465   }
667   } 466   }
668   467  
669   // Phase 2: exception mode 468   // Phase 2: exception mode
HITCBC 670   20 p_->throws = true; 469   22 p_->throws = true;
HITCBC 671   20 p_->n = (std::numeric_limits<std::size_t>::max)(); 470   22 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 672   20 p_->i = 0; 471   22 p_->i = 0;
HITCBC 673   20 p_->triggered = false; 472   22 p_->triggered = false;
HITCBC 674   75 while(*this) 473   84 while(*this)
675   { 474   {
676   try 475   try
677   { 476   {
HITCBC 678   55 fn(*this); 477   62 fn(*this);
679   } 478   }
HITCBC 680   70 catch(std::system_error const& ex) 479   80 catch(std::system_error const& ex)
681   { 480   {
HITCBC 682   35 if(ex.code() != p_->ec) 481   40 if(ex.code() != p_->ec)
683   { 482   {
MISUBC 684   r.success = false; 483   r.success = false;
MISUBC 685   r.loc = p_->loc; 484   r.loc = p_->loc;
MISUBC 686   r.ep = p_->ep; 485   r.ep = p_->ep;
MISUBC 687   p_->inert = true; 486   p_->inert = true;
MISUBC 688   return r; 487   return r;
689   } 488   }
690   } 489   }
MISUBC 691   catch(...) 490   catch(...)
692   { 491   {
MISUBC 693   r.success = false; 492   r.success = false;
MISUBC 694   r.loc = p_->loc; 493   r.loc = p_->loc;
MISUBC 695   r.ep = p_->ep; 494   r.ep = p_->ep;
MISUBC 696   p_->inert = true; 495   p_->inert = true;
MISUBC 697   return r; 496   return r;
698   } 497   }
HITCBC 699   55 if(p_->stopped) 498   62 if(p_->stopped)
700   { 499   {
MISUBC 701   r.success = false; 500   r.success = false;
MISUBC 702   r.loc = p_->loc; 501   r.loc = p_->loc;
MISUBC 703   r.ep = p_->ep; 502   r.ep = p_->ep;
MISUBC 704   p_->inert = true; 503   p_->inert = true;
MISUBC 705   return r; 504   return r;
706   } 505   }
707   } 506   }
HITCBC 708   20 p_->inert = true; 507   22 p_->inert = true;
HITCBC 709   20 return r; 508   22 return r;
MISUBC 710   } 509   }
711   510  
712   /** Run a coroutine test function with systematic failure injection. 511   /** Run a coroutine test function with systematic failure injection.
713   512  
714   Repeatedly invokes the provided coroutine function, failing at 513   Repeatedly invokes the provided coroutine function, failing at
715   successive points until the function completes without 514   successive points until the function completes without
716   encountering a failure. First runs the complete loop 515   encountering a failure. First runs the complete loop
717   using error codes, then runs using exceptions. 516   using error codes, then runs using exceptions.
718   517  
719   This overload handles lambdas that return an @ref IoRunnable 518   This overload handles lambdas that return an @ref IoRunnable
720   (such as `task<void>`), executing them synchronously via 519   (such as `task<void>`), executing them synchronously via
721   @ref run_blocking. 520   @ref run_blocking.
722   521  
723   @par Example 522   @par Example
724   523  
725 - @code 524 + @par !example example_3
726 - fuse f;  
727 - auto r = f.armed([&](fuse&) -> task<void> {  
728 - auto ec = f.maybe_fail();  
729 - if(ec)  
730 - co_return;  
731 - ec = f.maybe_fail();  
732 - if(ec)  
733 - co_return;  
734 - });  
735 -  
736 - if(!r)  
737 - {  
738 - std::cerr << "Failure at "  
739 - << r.loc.file_name() << ":"  
740 - << r.loc.line() << "\n";  
741 - }  
742 - @endcode  
743   525  
744   526  
745   @param fn The coroutine test function to invoke. It receives 527   @param fn The coroutine test function to invoke. It receives
746   a reference to the fuse and should call @ref maybe_fail 528   a reference to the fuse and should call @ref maybe_fail
747   at each potential failure point. 529   at each potential failure point.
748   530  
749   @return A @ref result indicating success or failure. 531   @return A @ref result indicating success or failure.
750   On failure, `result::loc` contains the source location 532   On failure, `result::loc` contains the source location
751   of the last @ref maybe_fail or @ref fail call. 533   of the last @ref maybe_fail or @ref fail call.
752   */ 534   */
753   template<class F> 535   template<class F>
754   requires IoRunnable<std::invoke_result_t<F, fuse&>> 536   requires IoRunnable<std::invoke_result_t<F, fuse&>>
755   result 537   result
HITCBC 756   131 armed(F&& fn) 538   131 armed(F&& fn)
757   { 539   {
HITCBC 758   1445 return run_phases([&]{ run_blocking()(fn(*this)); }); 540   1445 return run_phases([&]{ run_blocking()(fn(*this)); });
759   } 541   }
760   542  
761   /** Run a coroutine test function on a caller-supplied runner. 543   /** Run a coroutine test function on a caller-supplied runner.
762   544  
763   Behaves like the @ref IoRunnable overload of @ref armed, but 545   Behaves like the @ref IoRunnable overload of @ref armed, but
764   instead of driving each iteration through @ref run_blocking, it 546   instead of driving each iteration through @ref run_blocking, it
765   hands the coroutine to `run_one`. This lets a caller run each 547   hands the coroutine to `run_one`. This lets a caller run each
766   iteration on any execution context it chooses. Operations built 548   iteration on any execution context it chooses. Operations built
767   on `corosio::timeout` or `corosio::delay` in particular require 549   on `corosio::timeout` or `corosio::delay` in particular require
768   an `io_context`, because they abort on a non-`io_context` 550   an `io_context`, because they abort on a non-`io_context`
769   executor. `fuse` never learns about the context; 551   executor. `fuse` never learns about the context;
770   the caller owns the drive loop. 552   the caller owns the drive loop.
771   553  
772   @par Runner contract 554   @par Runner contract
773   `run_one` is invoked once per round with the @ref IoRunnable 555   `run_one` is invoked once per round with the @ref IoRunnable
774   produced by `fn`. It must run that task to completion 556   produced by `fn`. It must run that task to completion
775   synchronously and *return* any exception the task raised as a 557   synchronously and *return* any exception the task raised as a
776   `std::exception_ptr` (null on success). It must not rethrow. 558   `std::exception_ptr` (null on success). It must not rethrow.
777   `armed` rethrows the returned pointer from its own synchronous 559   `armed` rethrows the returned pointer from its own synchronous
778   code, so the exception phase observes injected failures. An 560   code, so the exception phase observes injected failures. An
779   exception escaping a `run_async` completion handler would 561   exception escaping a `run_async` completion handler would
780   instead call `std::terminate`. Capture the exception in the error 562   instead call `std::terminate`. Capture the exception in the error
781   handler and return it once the run loop is done. 563   handler and return it once the run loop is done.
782   564  
783   @par Example 565   @par Example
784 - @code 566 + @par !example example_1
785 - // Drive each iteration on a fresh io_context. 567 +
786 - auto io_runner = [](capy::task<> t) -> std::exception_ptr  
787 - {  
788 - corosio::io_context ioc;  
789 - std::exception_ptr ep;  
790 - capy::run_async(ioc.get_executor(),  
791 - [](auto&&...){},  
792 - [&ep](std::exception_ptr e){ ep = e; }  
793 - )(std::move(t));  
794 - ioc.run();  
795 - return ep;  
796 - };  
797 - auto r = f.armed(io_runner,  
798 - [&](capy::test::fuse&) -> capy::task<>  
799 - {  
800 - co_await corosio::timeout(some_op(), 5s);  
801 - });  
802 - @endcode  
803   568  
804   @param run_one A callable invoked with each iteration's task; it 569   @param run_one A callable invoked with each iteration's task; it
805   runs the task to completion and returns any escaped exception 570   runs the task to completion and returns any escaped exception
806   (null on success) without rethrowing. 571   (null on success) without rethrowing.
807   572  
808   @param fn The coroutine test function to invoke. 573   @param fn The coroutine test function to invoke.
809   574  
810   @return A @ref result indicating success or failure. 575   @return A @ref result indicating success or failure.
811   */ 576   */
812   template<class Runner, class F> 577   template<class Runner, class F>
813   requires IoRunnable<std::invoke_result_t<F, fuse&>> 578   requires IoRunnable<std::invoke_result_t<F, fuse&>>
814   && std::same_as< 579   && std::same_as<
815   std::invoke_result_t<Runner&, std::invoke_result_t<F, fuse&>>, 580   std::invoke_result_t<Runner&, std::invoke_result_t<F, fuse&>>,
816   std::exception_ptr> 581   std::exception_ptr>
817   result 582   result
HITCBC 818   3 armed(Runner&& run_one, F&& fn) 583   3 armed(Runner&& run_one, F&& fn)
819   { 584   {
HITCBC 820   14 return run_phases([&]{ 585   14 return run_phases([&]{
HITCBC 821   23 if(auto ep = run_one(fn(*this))) 586   23 if(auto ep = run_one(fn(*this)))
HITCBC 822   12 std::rethrow_exception(ep); 587   12 std::rethrow_exception(ep);
HITCBC 823   6 }); 588   6 });
824   } 589   }
825   590  
826   /** Alias for @ref armed. 591   /** Alias for @ref armed.
827   592  
828   Allows the fuse to be invoked directly as a function 593   Allows the fuse to be invoked directly as a function
829   object for more concise syntax. 594   object for more concise syntax.
830   595  
831   @par Example 596   @par Example
832   597  
833 - @code 598 + @par !example example
834 - // These are equivalent:  
835 - fuse f;  
836 - auto r1 = f.armed([](fuse& f) { ... });  
837 - auto r2 = f([](fuse& f) { ... });  
838 - // Inline usage:  
839 - auto r3 = fuse()([](fuse& f) {  
840 - auto ec = f.maybe_fail();  
841 - if(ec)  
842 - return;  
843 - });  
844 - @endcode  
845   599  
846   600  
847   @param fn The test function to run under failure injection. 601   @param fn The test function to run under failure injection.
848   602  
849   @return The @ref result of the armed run. 603   @return The @ref result of the armed run.
850   604  
851   @see armed 605   @see armed
852   */ 606   */
853   template<class F> 607   template<class F>
854   result 608   result
HITCBC 855   15 operator()(F&& fn) 609   16 operator()(F&& fn)
856   { 610   {
HITCBC 857   15 return armed(std::forward<F>(fn)); 611   16 return armed(std::forward<F>(fn));
858   } 612   }
859   613  
860   /** Alias for @ref armed (coroutine overload). 614   /** Alias for @ref armed (coroutine overload).
861   615  
862   @param fn The test coroutine factory to run under failure injection. 616   @param fn The test coroutine factory to run under failure injection.
863   617  
864   @return The @ref result of the armed run. 618   @return The @ref result of the armed run.
865   619  
866   @see armed 620   @see armed
867   */ 621   */
868   template<class F> 622   template<class F>
869   requires IoRunnable<std::invoke_result_t<F, fuse&>> 623   requires IoRunnable<std::invoke_result_t<F, fuse&>>
870   result 624   result
871   operator()(F&& fn) 625   operator()(F&& fn)
872   { 626   {
873   return armed(std::forward<F>(fn)); 627   return armed(std::forward<F>(fn));
874   } 628   }
875   629  
876   /** Run a test function once without failure injection. 630   /** Run a test function once without failure injection.
877   631  
878   Invokes the provided function exactly once. Calls to 632   Invokes the provided function exactly once. Calls to
879   @ref maybe_fail always return an empty error code and 633   @ref maybe_fail always return an empty error code and
880   never throw. Only explicit calls to @ref fail can 634   never throw. Only explicit calls to @ref fail can
881   signal a test failure. 635   signal a test failure.
882   636  
883   This is useful for running tests where you want to 637   This is useful for running tests where you want to
884   manually control failures, or for quick single-run 638   manually control failures, or for quick single-run
885   tests without systematic error injection. 639   tests without systematic error injection.
886   640  
887   @par Example 641   @par Example
888   642  
889 - @code 643 + @par !example example_1
890 - fuse f;  
891 - auto r = f.inert([](fuse& f) {  
892 - auto ec = f.maybe_fail(); // Always succeeds  
893 - assert(!ec);  
894 -  
895 - // Only way to signal failure:  
896 - if(some_condition)  
897 - {  
898 - f.fail();  
899 - return;  
900 - }  
901 - });  
902 - if(!r)  
903 - {  
904 - std::cerr << "Test failed at "  
905 - << r.loc.file_name() << ":"  
906 - << r.loc.line() << "\n";  
907 - }  
908 - @endcode  
909   644  
910   645  
911   @param fn The test function to invoke. It receives 646   @param fn The test function to invoke. It receives
912   a reference to the fuse. Calls to @ref maybe_fail 647   a reference to the fuse. Calls to @ref maybe_fail
913   always succeed. 648   always succeed.
914   649  
915   @return A @ref result indicating success or failure. 650   @return A @ref result indicating success or failure.
916   On failure, `result::loc` contains the source location 651   On failure, `result::loc` contains the source location
917   of the @ref fail call. 652   of the @ref fail call.
918   */ 653   */
919   template<class F> 654   template<class F>
920   result 655   result
HITCBC 921   9 inert(F&& fn) 656   9 inert(F&& fn)
922   { 657   {
HITCBC 923   9 result r; 658   9 result r;
HITCBC 924   9 p_->inert = true; 659   9 p_->inert = true;
925   try 660   try
926   { 661   {
HITCBC 927   9 fn(*this); 662   9 fn(*this);
928   } 663   }
HITCBC 929   2 catch(...) 664   2 catch(...)
930   { 665   {
HITCBC 931   1 r.success = false; 666   1 r.success = false;
HITCBC 932   1 r.loc = p_->loc; 667   1 r.loc = p_->loc;
HITCBC 933   1 r.ep = std::current_exception(); 668   1 r.ep = std::current_exception();
HITCBC 934   1 return r; 669   1 return r;
935   } 670   }
HITCBC 936   8 if(p_->stopped) 671   8 if(p_->stopped)
937   { 672   {
HITCBC 938   2 r.success = false; 673   2 r.success = false;
HITCBC 939   2 r.loc = p_->loc; 674   2 r.loc = p_->loc;
HITCBC 940   2 r.ep = p_->ep; 675   2 r.ep = p_->ep;
941   } 676   }
HITCBC 942   8 return r; 677   8 return r;
MISUBC 943   } 678   }
944   679  
945   /** Run a coroutine test function once without failure injection. 680   /** Run a coroutine test function once without failure injection.
946   681  
947   Invokes the provided coroutine function exactly once using 682   Invokes the provided coroutine function exactly once using
948   @ref run_blocking. Calls to @ref maybe_fail always return 683   @ref run_blocking. Calls to @ref maybe_fail always return
949   an empty error code and never throw. Only explicit calls 684   an empty error code and never throw. Only explicit calls
950   to @ref fail can signal a test failure. 685   to @ref fail can signal a test failure.
951   686  
952   @par Example 687   @par Example
953   688  
954 - @code 689 + @par !example example_2
955 - fuse f;  
956 - auto r = f.inert([](fuse& f) -> task<void> {  
957 - auto ec = f.maybe_fail(); // Always succeeds  
958 - assert(!ec);  
959 -  
960 - // Only way to signal failure:  
961 - if(some_condition)  
962 - {  
963 - f.fail();  
964 - co_return;  
965 - }  
966 - });  
967 - if(!r)  
968 - {  
969 - std::cerr << "Test failed at "  
970 - << r.loc.file_name() << ":"  
971 - << r.loc.line() << "\n";  
972 - }  
973 - @endcode  
974   690  
975   691  
976   @param fn The coroutine test function to invoke. It receives 692   @param fn The coroutine test function to invoke. It receives
977   a reference to the fuse. Calls to @ref maybe_fail 693   a reference to the fuse. Calls to @ref maybe_fail
978   always succeed. 694   always succeed.
979   695  
980   @return A @ref result indicating success or failure. 696   @return A @ref result indicating success or failure.
981   On failure, `result::loc` contains the source location 697   On failure, `result::loc` contains the source location
982   of the @ref fail call. 698   of the @ref fail call.
983   */ 699   */
984   template<class F> 700   template<class F>
985   requires IoRunnable<std::invoke_result_t<F, fuse&>> 701   requires IoRunnable<std::invoke_result_t<F, fuse&>>
986   result 702   result
HITCBC 987   39 inert(F&& fn) 703   39 inert(F&& fn)
988   { 704   {
HITCBC 989   39 result r; 705   39 result r;
HITCBC 990   39 p_->inert = true; 706   39 p_->inert = true;
991   try 707   try
992   { 708   {
HITCBC 993   39 run_blocking()(fn(*this)); 709   39 run_blocking()(fn(*this));
994   } 710   }
MISUBC 995   catch(...) 711   catch(...)
996   { 712   {
MISUBC 997   r.success = false; 713   r.success = false;
MISUBC 998   r.loc = p_->loc; 714   r.loc = p_->loc;
MISUBC 999   r.ep = std::current_exception(); 715   r.ep = std::current_exception();
MISUBC 1000   return r; 716   return r;
1001   } 717   }
HITCBC 1002   39 if(p_->stopped) 718   39 if(p_->stopped)
1003   { 719   {
MISUBC 1004   r.success = false; 720   r.success = false;
MISUBC 1005   r.loc = p_->loc; 721   r.loc = p_->loc;
MISUBC 1006   r.ep = p_->ep; 722   r.ep = p_->ep;
1007   } 723   }
HITCBC 1008   39 return r; 724   39 return r;
MISUBC 1009   } 725   }
1010   }; 726   };
1011   727  
1012   } // test 728   } // test
1013   } // capy 729   } // capy
1014   } // boost 730   } // boost
1015   731  
1016   #endif 732   #endif