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