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   /** A test utility for systematic error injection. 55   /** A test utility for systematic error injection.
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   3154 explicit operator bool() const noexcept 221   1216 explicit operator bool() const noexcept
222   { 222   {
HITCBC 223   3154 auto& s = *p_; 223   1216 auto& s = *p_;
HITCBC 224   3154 if(s.n == (std::numeric_limits<std::size_t>::max)()) 224   1216 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   683 s.n = 0; 227   287 s.n = 0;
HITCBC 228   683 return true; 228   287 return true;
229   } 229   }
HITCBC 230   2471 if(s.triggered) 230   929 if(s.triggered)
231   { 231   {
232   // Previous round triggered, try next failure point 232   // Previous round triggered, try next failure point
HITCBC 233   1795 s.n++; 233   649 s.n++;
HITCBC 234   1795 s.i = 0; 234   649 s.i = 0;
HITCBC 235   1795 s.triggered = false; 235   649 s.triggered = false;
HITCBC 236   1795 return true; 236   649 return true;
237   } 237   }
238   // Test completed without trigger: success 238   // Test completed without trigger: success
HITCBC 239   676 return false; 239   280 return false;
240   } 240   }
241   241  
242   public: 242   public:
243   /** Result of a fuse operation. 243   /** Result of a fuse operation.
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 @ref success. 279   /// Return @ref success.
HITCBC 280   42 constexpr explicit operator bool() const noexcept 280   42 constexpr explicit operator bool() const noexcept
281   { 281   {
HITCBC 282   42 return success; 282   42 return success;
283   } 283   }
284   }; 284   };
285   285  
286   /** Construct a fuse with a custom error code. 286   /** Construct a fuse with a custom error code.
287   287  
288   @par Example 288   @par Example
289   289  
290   @code 290   @code
291   auto custom_ec = make_error_code( 291   auto custom_ec = make_error_code(
292   std::errc::operation_canceled); 292   std::errc::operation_canceled);
293   fuse f(custom_ec); 293   fuse f(custom_ec);
294   294  
295   std::error_code captured_ec; 295   std::error_code captured_ec;
296   auto r = f([&](fuse& f) { 296   auto r = f([&](fuse& f) {
297   auto ec = f.maybe_fail(); 297   auto ec = f.maybe_fail();
298   if(ec) 298   if(ec)
299   { 299   {
300   captured_ec = ec; 300   captured_ec = ec;
301   return; 301   return;
302   } 302   }
303   }); 303   });
304   304  
305   assert(captured_ec == custom_ec); 305   assert(captured_ec == custom_ec);
306   @endcode 306   @endcode
307   307  
308   @param ec The error code to deliver at failure points. 308   @param ec The error code to deliver at failure points.
309   */ 309   */
HITCBC 310   450 explicit fuse(std::error_code ec) 310   216 explicit fuse(std::error_code ec)
HITCBC 311   450 : p_(std::make_shared<state>()) 311   216 : p_(std::make_shared<state>())
312   { 312   {
HITCBC 313   450 p_->ec = ec; 313   216 p_->ec = ec;
HITCBC 314   450 } 314   216 }
315   315  
316   /** Construct a fuse with the default error code. 316   /** Construct a fuse with the default error code.
317   317  
318   The default error code is `error::test_failure`. 318   The default error code is `error::test_failure`.
319   319  
320   @par Example 320   @par Example
321   321  
322   @code 322   @code
323   fuse f; 323   fuse f;
324   std::error_code captured_ec; 324   std::error_code captured_ec;
325   325  
326   auto r = f([&](fuse& f) { 326   auto r = f([&](fuse& f) {
327   auto ec = f.maybe_fail(); 327   auto ec = f.maybe_fail();
328   if(ec) 328   if(ec)
329   { 329   {
330   captured_ec = ec; 330   captured_ec = ec;
331   return; 331   return;
332   } 332   }
333   }); 333   });
334   334  
335   assert(captured_ec == error::test_failure); 335   assert(captured_ec == error::test_failure);
336   @endcode 336   @endcode
337   */ 337   */
HITCBC 338   448 fuse() 338   214 fuse()
HITCBC 339   448 : fuse(error::test_failure) 339   214 : fuse(error::test_failure)
340   { 340   {
HITCBC 341   448 } 341   214 }
342   342  
343   /** Return an error or throw at the current failure point. 343   /** Return an error or throw at the current failure point.
344   344  
345   When running under @ref armed, increments the internal 345   When running under @ref armed, increments the internal
346   counter. When the counter reaches the current failure 346   counter. When the counter reaches the current failure
347   point, returns the stored error code (or throws 347   point, returns the stored error code (or throws
348   `std::system_error` in exception mode) and records 348   `std::system_error` in exception mode) and records
349   the source location. 349   the source location.
350   350  
351   When called outside of @ref armed or @ref inert (standalone 351   When called outside of @ref armed or @ref inert (standalone
352   usage), or when running under @ref inert, always returns 352   usage), or when running under @ref inert, always returns
353   an empty error code. This enables dependency injection 353   an empty error code. This enables dependency injection
354   where the fuse is a no-op in production code. 354   where the fuse is a no-op in production code.
355   355  
356   @par Example 356   @par Example
357   357  
358   @code 358   @code
359   fuse f; 359   fuse f;
360   auto r = f([](fuse& f) { 360   auto r = f([](fuse& f) {
361   // Error code mode: returns the error 361   // Error code mode: returns the error
362   auto ec = f.maybe_fail(); 362   auto ec = f.maybe_fail();
363   if(ec) 363   if(ec)
364   return; 364   return;
365   365  
366   // Exception mode: throws system_error 366   // Exception mode: throws system_error
367   ec = f.maybe_fail(); 367   ec = f.maybe_fail();
368   if(ec) 368   if(ec)
369   return; 369   return;
370   }); 370   });
371   @endcode 371   @endcode
372   372  
373   @par Standalone Usage 373   @par Standalone Usage
374   374  
375   @code 375   @code
376   fuse f; 376   fuse f;
377   auto ec = f.maybe_fail(); // Always returns {} (no-op) 377   auto ec = f.maybe_fail(); // Always returns {} (no-op)
378   @endcode 378   @endcode
379   379  
380   @param loc The source location of the call site, 380   @param loc The source location of the call site,
381   captured automatically. 381   captured automatically.
382   382  
383   @return The stored error code if at the failure point, 383   @return The stored error code if at the failure point,
384   otherwise an empty error code. In exception mode, 384   otherwise an empty error code. In exception mode,
385   throws instead of returning an error. When called 385   throws instead of returning an error. When called
386   outside @ref armed, or when running under @ref inert, 386   outside @ref armed, or when running under @ref inert,
387   always returns an empty error code. 387   always returns an empty error code.
388   388  
389   @throws std::system_error When in exception mode 389   @throws std::system_error When in exception mode
390   and at the failure point (not thrown outside @ref armed). 390   and at the failure point (not thrown outside @ref armed).
391   */ 391   */
392   std::error_code 392   std::error_code
HITCBC 393   4873 maybe_fail( 393   1531 maybe_fail(
394   std::source_location loc = std::source_location::current()) 394   std::source_location loc = std::source_location::current())
395   { 395   {
HITCBC 396   4873 auto& s = *p_; 396   1531 auto& s = *p_;
HITCBC 397   4873 if(s.inert) 397   1531 if(s.inert)
HITCBC 398   236 return {}; 398   236 return {};
HITCBC 399   4637 if(s.i < s.n) 399   1295 if(s.i < s.n)
HITCBC 400   3960 ++s.i; 400   1046 ++s.i;
HITCBC 401   4637 if(s.i == s.n) 401   1295 if(s.i == s.n)
402   { 402   {
HITCBC 403   1873 s.triggered = true; 403   649 s.triggered = true;
HITCBC 404   1873 s.loc = loc; 404   649 s.loc = loc;
HITCBC 405   1873 if(s.throws) 405   649 if(s.throws)
HITCBC 406   891 throw std::system_error(s.ec); 406   318 throw std::system_error(s.ec);
HITCBC 407   982 return s.ec; 407   331 return s.ec;
408   } 408   }
HITCBC 409   2764 return {}; 409   646 return {};
410   } 410   }
411   411  
412   /** Signal a test failure and stop execution. 412   /** Signal a test failure and stop execution.
413   413  
414   Call this from the test function to indicate a failure 414   Call this from the test function to indicate a failure
415   condition. Both @ref armed and @ref inert will return 415   condition. Both @ref armed and @ref inert will return
416   a failed @ref result immediately. 416   a failed @ref result immediately.
417   417  
418   @par Example 418   @par Example
419   419  
420   @code 420   @code
421   fuse f; 421   fuse f;
422   auto r = f([](fuse& f) { 422   auto r = f([](fuse& f) {
423   auto ec = f.maybe_fail(); 423   auto ec = f.maybe_fail();
424   if(ec) 424   if(ec)
425   return; 425   return;
426   426  
427   // Explicit failure when a condition is not met 427   // Explicit failure when a condition is not met
428   if(some_value != expected) 428   if(some_value != expected)
429   { 429   {
430   f.fail(); 430   f.fail();
431   return; 431   return;
432   } 432   }
433   }); 433   });
434   434  
435   if(!r) 435   if(!r)
436   { 436   {
437   std::cerr << "Test failed at " 437   std::cerr << "Test failed at "
438   << r.loc.file_name() << ":" 438   << r.loc.file_name() << ":"
439   << r.loc.line() << "\n"; 439   << r.loc.line() << "\n";
440   } 440   }
441   @endcode 441   @endcode
442   442  
443   @param loc The source location of the call site, 443   @param loc The source location of the call site,
444   captured automatically. 444   captured automatically.
445   */ 445   */
446   void 446   void
HITCBC 447   3 fail( 447   3 fail(
448   std::source_location loc = 448   std::source_location loc =
449   std::source_location::current()) noexcept 449   std::source_location::current()) noexcept
450   { 450   {
HITCBC 451   3 p_->loc = loc; 451   3 p_->loc = loc;
HITCBC 452   3 p_->stopped = true; 452   3 p_->stopped = true;
HITCBC 453   3 } 453   3 }
454   454  
455   /** Signal a test failure with an exception and stop execution. 455   /** Signal a test failure with an exception and stop execution.
456   456  
457   Call this from the test function to indicate a failure 457   Call this from the test function to indicate a failure
458   condition with an associated exception. Both @ref armed 458   condition with an associated exception. Both @ref armed
459   and @ref inert will return a failed @ref result with 459   and @ref inert will return a failed @ref result with
460   the captured exception pointer. 460   the captured exception pointer.
461   461  
462   @par Example 462   @par Example
463   463  
464   @code 464   @code
465   fuse f; 465   fuse f;
466   auto r = f([](fuse& f) { 466   auto r = f([](fuse& f) {
467   try 467   try
468   { 468   {
469   do_something(); 469   do_something();
470   } 470   }
471   catch(...) 471   catch(...)
472   { 472   {
473   f.fail(std::current_exception()); 473   f.fail(std::current_exception());
474   return; 474   return;
475   } 475   }
476   }); 476   });
477   477  
478   if(!r) 478   if(!r)
479   { 479   {
480   try 480   try
481   { 481   {
482   if(r.ep) 482   if(r.ep)
483   std::rethrow_exception(r.ep); 483   std::rethrow_exception(r.ep);
484   } 484   }
485   catch(std::exception const& e) 485   catch(std::exception const& e)
486   { 486   {
487   std::cerr << "Exception: " << e.what() << "\n"; 487   std::cerr << "Exception: " << e.what() << "\n";
488   } 488   }
489   } 489   }
490   @endcode 490   @endcode
491   491  
492   @param ep The exception pointer to capture. 492   @param ep The exception pointer to capture.
493   493  
494   @param loc The source location of the call site, 494   @param loc The source location of the call site,
495   captured automatically. 495   captured automatically.
496   */ 496   */
497   void 497   void
HITCBC 498   2 fail( 498   2 fail(
499   std::exception_ptr ep, 499   std::exception_ptr ep,
500   std::source_location loc = 500   std::source_location loc =
501   std::source_location::current()) noexcept 501   std::source_location::current()) noexcept
502   { 502   {
HITCBC 503   2 p_->ep = ep; 503   2 p_->ep = ep;
HITCBC 504   2 p_->loc = loc; 504   2 p_->loc = loc;
HITCBC 505   2 p_->stopped = true; 505   2 p_->stopped = true;
HITCBC 506   2 } 506   2 }
507   507  
508   private: 508   private:
509   /* Drive the two-phase armed loop, invoking `do_iter` once per round. 509   /* Drive the two-phase armed loop, invoking `do_iter` once per round.
510   510  
511   Phase 1 delivers injected failures as error codes; phase 2 as 511   Phase 1 delivers injected failures as error codes; phase 2 as
512   exceptions. Shared by the two coroutine `armed` overloads: each 512   exceptions. Shared by the two coroutine `armed` overloads: each
513   supplies a nullary `do_iter` that runs one iteration — via 513   supplies a nullary `do_iter` that runs one iteration — via
514   @ref run_blocking, or via a caller-supplied runner — so the round 514   @ref run_blocking, or via a caller-supplied runner — so the round
515   sequence and failure handling stay identical across them. 515   sequence and failure handling stay identical across them.
516   */ 516   */
517   template<class DoIter> 517   template<class DoIter>
518   result 518   result
HITCBC 519   313 run_phases(DoIter&& do_iter) 519   124 run_phases(DoIter&& do_iter)
520   { 520   {
HITCBC 521   313 result r; 521   124 result r;
522   522  
523   // Phase 1: error code mode 523   // Phase 1: error code mode
HITCBC 524   313 p_->throws = false; 524   124 p_->throws = false;
HITCBC 525   313 p_->inert = false; 525   124 p_->inert = false;
HITCBC 526   313 p_->n = (std::numeric_limits<std::size_t>::max)(); 526   124 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 527   1490 while(*this) 527   539 while(*this)
528   { 528   {
529   try 529   try
530   { 530   {
HITCBC 531   1178 do_iter(); 531   416 do_iter();
532   } 532   }
HITCBC 533   2 catch(...) 533   2 catch(...)
534   { 534   {
HITCBC 535   1 r.success = false; 535   1 r.success = false;
HITCBC 536   1 r.loc = p_->loc; 536   1 r.loc = p_->loc;
HITCBC 537   1 r.ep = p_->ep; 537   1 r.ep = p_->ep;
HITCBC 538   1 p_->inert = true; 538   1 p_->inert = true;
HITCBC 539   1 return r; 539   1 return r;
540   } 540   }
HITCBC 541   1177 if(p_->stopped) 541   415 if(p_->stopped)
542   { 542   {
MISUBC 543   r.success = false; 543   r.success = false;
MISUBC 544   r.loc = p_->loc; 544   r.loc = p_->loc;
MISUBC 545   r.ep = p_->ep; 545   r.ep = p_->ep;
MISUBC 546   p_->inert = true; 546   p_->inert = true;
MISUBC 547   return r; 547   return r;
548   } 548   }
549   } 549   }
550   550  
551   // Phase 2: exception mode 551   // Phase 2: exception mode
HITCBC 552   312 p_->throws = true; 552   123 p_->throws = true;
HITCBC 553   312 p_->n = (std::numeric_limits<std::size_t>::max)(); 553   123 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 554   312 p_->i = 0; 554   123 p_->i = 0;
HITCBC 555   312 p_->triggered = false; 555   123 p_->triggered = false;
HITCBC 556   1487 while(*this) 556   536 while(*this)
557   { 557   {
558   try 558   try
559   { 559   {
HITCBC 560   1175 do_iter(); 560   413 do_iter();
561   } 561   }
HITCBC 562   1726 catch(std::system_error const& ex) 562   580 catch(std::system_error const& ex)
563   { 563   {
HITCBC 564   863 if(ex.code() != p_->ec) 564   290 if(ex.code() != p_->ec)
565   { 565   {
MISUBC 566   r.success = false; 566   r.success = false;
MISUBC 567   r.loc = p_->loc; 567   r.loc = p_->loc;
MISUBC 568   r.ep = p_->ep; 568   r.ep = p_->ep;
MISUBC 569   p_->inert = true; 569   p_->inert = true;
MISUBC 570   return r; 570   return r;
571   } 571   }
572   } 572   }
MISUBC 573   catch(...) 573   catch(...)
574   { 574   {
MISUBC 575   r.success = false; 575   r.success = false;
MISUBC 576   r.loc = p_->loc; 576   r.loc = p_->loc;
MISUBC 577   r.ep = p_->ep; 577   r.ep = p_->ep;
MISUBC 578   p_->inert = true; 578   p_->inert = true;
MISUBC 579   return r; 579   return r;
580   } 580   }
HITCBC 581   1175 if(p_->stopped) 581   413 if(p_->stopped)
582   { 582   {
MISUBC 583   r.success = false; 583   r.success = false;
MISUBC 584   r.loc = p_->loc; 584   r.loc = p_->loc;
MISUBC 585   r.ep = p_->ep; 585   r.ep = p_->ep;
MISUBC 586   p_->inert = true; 586   p_->inert = true;
MISUBC 587   return r; 587   return r;
588   } 588   }
589   } 589   }
HITCBC 590   312 p_->inert = true; 590   123 p_->inert = true;
HITCBC 591   312 return r; 591   123 return r;
MISUBC 592   } 592   }
593   593  
594   public: 594   public:
595   /** Run a test function with systematic failure injection. 595   /** Run a test function with systematic failure injection.
596   596  
597   Repeatedly invokes the provided function, failing at 597   Repeatedly invokes the provided function, failing at
598   successive points until the function completes without 598   successive points until the function completes without
599   encountering a failure. First runs the complete loop 599   encountering a failure. First runs the complete loop
600   using error codes, then runs using exceptions. 600   using error codes, then runs using exceptions.
601   601  
602   @par Example 602   @par Example
603   603  
604   @code 604   @code
605   fuse f; 605   fuse f;
606   auto r = f.armed([](fuse& f) { 606   auto r = f.armed([](fuse& f) {
607   auto ec = f.maybe_fail(); 607   auto ec = f.maybe_fail();
608   if(ec) 608   if(ec)
609   return; 609   return;
610   610  
611   ec = f.maybe_fail(); 611   ec = f.maybe_fail();
612   if(ec) 612   if(ec)
613   return; 613   return;
614   }); 614   });
615   615  
616   if(!r) 616   if(!r)
617   { 617   {
618   std::cerr << "Failure at " 618   std::cerr << "Failure at "
619   << r.loc.file_name() << ":" 619   << r.loc.file_name() << ":"
620   << r.loc.line() << "\n"; 620   << r.loc.line() << "\n";
621   } 621   }
622   @endcode 622   @endcode
623   623  
624   @param fn The test function to invoke. It receives 624   @param fn The test function to invoke. It receives
625   a reference to the fuse and should call @ref maybe_fail 625   a reference to the fuse and should call @ref maybe_fail
626   at each potential failure point. 626   at each potential failure point.
627   627  
628   @return A @ref result indicating success or failure. 628   @return A @ref result indicating success or failure.
629   On failure, `result::loc` contains the source location 629   On failure, `result::loc` contains the source location
630   of the last @ref maybe_fail or @ref fail call. 630   of the last @ref maybe_fail or @ref fail call.
631   */ 631   */
632   template<class F> 632   template<class F>
633   result 633   result
HITCBC 634   32 armed(F&& fn) 634   23 armed(F&& fn)
635   { 635   {
HITCBC 636   32 result r; 636   23 result r;
637   637  
638   // Phase 1: error code mode 638   // Phase 1: error code mode
HITCBC 639   32 p_->throws = false; 639   23 p_->throws = false;
HITCBC 640   32 p_->inert = false; 640   23 p_->inert = false;
HITCBC 641   32 p_->n = (std::numeric_limits<std::size_t>::max)(); 641   23 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 642   97 while(*this) 642   79 while(*this)
643   { 643   {
644   try 644   try
645   { 645   {
HITCBC 646   71 fn(*this); 646   62 fn(*this);
647   } 647   }
HITCBC 648   6 catch(...) 648   6 catch(...)
649   { 649   {
HITCBC 650   3 r.success = false; 650   3 r.success = false;
HITCBC 651   3 r.loc = p_->loc; 651   3 r.loc = p_->loc;
HITCBC 652   3 r.ep = p_->ep; 652   3 r.ep = p_->ep;
HITCBC 653   3 p_->inert = true; 653   3 p_->inert = true;
HITCBC 654   3 return r; 654   3 return r;
655   } 655   }
HITCBC 656   68 if(p_->stopped) 656   59 if(p_->stopped)
657   { 657   {
HITCBC 658   3 r.success = false; 658   3 r.success = false;
HITCBC 659   3 r.loc = p_->loc; 659   3 r.loc = p_->loc;
HITCBC 660   3 r.ep = p_->ep; 660   3 r.ep = p_->ep;
HITCBC 661   3 p_->inert = true; 661   3 p_->inert = true;
HITCBC 662   3 return r; 662   3 return r;
663   } 663   }
664   } 664   }
665   665  
666   // Phase 2: exception mode 666   // Phase 2: exception mode
HITCBC 667   26 p_->throws = true; 667   17 p_->throws = true;
HITCBC 668   26 p_->n = (std::numeric_limits<std::size_t>::max)(); 668   17 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 669   26 p_->i = 0; 669   17 p_->i = 0;
HITCBC 670   26 p_->triggered = false; 670   17 p_->triggered = false;
HITCBC 671   80 while(*this) 671   62 while(*this)
672   { 672   {
673   try 673   try
674   { 674   {
HITCBC 675   54 fn(*this); 675   45 fn(*this);
676   } 676   }
HITCBC 677   56 catch(std::system_error const& ex) 677   56 catch(std::system_error const& ex)
678   { 678   {
HITCBC 679   28 if(ex.code() != p_->ec) 679   28 if(ex.code() != p_->ec)
680   { 680   {
MISUBC 681   r.success = false; 681   r.success = false;
MISUBC 682   r.loc = p_->loc; 682   r.loc = p_->loc;
MISUBC 683   r.ep = p_->ep; 683   r.ep = p_->ep;
MISUBC 684   p_->inert = true; 684   p_->inert = true;
MISUBC 685   return r; 685   return r;
686   } 686   }
687   } 687   }
MISUBC 688   catch(...) 688   catch(...)
689   { 689   {
MISUBC 690   r.success = false; 690   r.success = false;
MISUBC 691   r.loc = p_->loc; 691   r.loc = p_->loc;
MISUBC 692   r.ep = p_->ep; 692   r.ep = p_->ep;
MISUBC 693   p_->inert = true; 693   p_->inert = true;
MISUBC 694   return r; 694   return r;
695   } 695   }
HITCBC 696   54 if(p_->stopped) 696   45 if(p_->stopped)
697   { 697   {
MISUBC 698   r.success = false; 698   r.success = false;
MISUBC 699   r.loc = p_->loc; 699   r.loc = p_->loc;
MISUBC 700   r.ep = p_->ep; 700   r.ep = p_->ep;
MISUBC 701   p_->inert = true; 701   p_->inert = true;
MISUBC 702   return r; 702   return r;
703   } 703   }
704   } 704   }
HITCBC 705   26 p_->inert = true; 705   17 p_->inert = true;
HITCBC 706   26 return r; 706   17 return r;
MISUBC 707   } 707   }
708   708  
709   /** Run a coroutine test function with systematic failure injection. 709   /** Run a coroutine test function with systematic failure injection.
710   710  
711   Repeatedly invokes the provided coroutine function, failing at 711   Repeatedly invokes the provided coroutine function, failing at
712   successive points until the function completes without 712   successive points until the function completes without
713   encountering a failure. First runs the complete loop 713   encountering a failure. First runs the complete loop
714   using error codes, then runs using exceptions. 714   using error codes, then runs using exceptions.
715   715  
716   This overload handles lambdas that return an @ref IoRunnable 716   This overload handles lambdas that return an @ref IoRunnable
717   (such as `task<void>`), executing them synchronously via 717   (such as `task<void>`), executing them synchronously via
718   @ref run_blocking. 718   @ref run_blocking.
719   719  
720   @par Example 720   @par Example
721   721  
722   @code 722   @code
723   fuse f; 723   fuse f;
724   auto r = f.armed([&](fuse&) -> task<void> { 724   auto r = f.armed([&](fuse&) -> task<void> {
725   auto ec = f.maybe_fail(); 725   auto ec = f.maybe_fail();
726   if(ec) 726   if(ec)
727   co_return; 727   co_return;
728   728  
729   ec = f.maybe_fail(); 729   ec = f.maybe_fail();
730   if(ec) 730   if(ec)
731   co_return; 731   co_return;
732   }); 732   });
733   733  
734   if(!r) 734   if(!r)
735   { 735   {
736   std::cerr << "Failure at " 736   std::cerr << "Failure at "
737   << r.loc.file_name() << ":" 737   << r.loc.file_name() << ":"
738   << r.loc.line() << "\n"; 738   << r.loc.line() << "\n";
739   } 739   }
740   @endcode 740   @endcode
741   741  
742   @param fn The coroutine test function to invoke. It receives 742   @param fn The coroutine test function to invoke. It receives
743   a reference to the fuse and should call @ref maybe_fail 743   a reference to the fuse and should call @ref maybe_fail
744   at each potential failure point. 744   at each potential failure point.
745   745  
746   @return A @ref result indicating success or failure. 746   @return A @ref result indicating success or failure.
747   On failure, `result::loc` contains the source location 747   On failure, `result::loc` contains the source location
748   of the last @ref maybe_fail or @ref fail call. 748   of the last @ref maybe_fail or @ref fail call.
749   */ 749   */
750   template<class F> 750   template<class F>
751   requires IoRunnable<std::invoke_result_t<F, fuse&>> 751   requires IoRunnable<std::invoke_result_t<F, fuse&>>
752   result 752   result
HITCBC 753   310 armed(F&& fn) 753   121 armed(F&& fn)
754   { 754   {
HITCBC 755   3814 return run_phases([&]{ run_blocking()(fn(*this)); }); 755   1339 return run_phases([&]{ run_blocking()(fn(*this)); });
756   } 756   }
757   757  
758   /** Run a coroutine test function on a caller-supplied runner. 758   /** Run a coroutine test function on a caller-supplied runner.
759   759  
760   Behaves like the @ref IoRunnable overload of @ref armed, but 760   Behaves like the @ref IoRunnable overload of @ref armed, but
761   instead of driving each iteration through @ref run_blocking, it 761   instead of driving each iteration through @ref run_blocking, it
762   hands the coroutine to `run_one`. This lets a caller run each 762   hands the coroutine to `run_one`. This lets a caller run each
763   iteration on any execution context it chooses — in particular an 763   iteration on any execution context it chooses — in particular an
764   `io_context`, which operations built on `corosio::timeout` or 764   `io_context`, which operations built on `corosio::timeout` or
765   `corosio::delay` require, since those abort on a 765   `corosio::delay` require, since those abort on a
766   non-`io_context` executor. `fuse` never learns about the context; 766   non-`io_context` executor. `fuse` never learns about the context;
767   the caller owns the drive loop. 767   the caller owns the drive loop.
768   768  
769   @par Runner contract 769   @par Runner contract
770   `run_one` is invoked once per round with the @ref IoRunnable 770   `run_one` is invoked once per round with the @ref IoRunnable
771   produced by `fn`. It must run that task to completion 771   produced by `fn`. It must run that task to completion
772   synchronously and *return* any exception the task raised as a 772   synchronously and *return* any exception the task raised as a
773   `std::exception_ptr` (null on success). It must not rethrow: 773   `std::exception_ptr` (null on success). It must not rethrow:
774   `armed` rethrows the returned pointer from its own synchronous 774   `armed` rethrows the returned pointer from its own synchronous
775   code so the exception phase observes injected failures, whereas 775   code so the exception phase observes injected failures, whereas
776   an exception escaping a `run_async` completion handler would call 776   an exception escaping a `run_async` completion handler would call
777   `std::terminate`. Capture the exception in the error handler and 777   `std::terminate`. Capture the exception in the error handler and
778   return it once the run loop is done. 778   return it once the run loop is done.
779   779  
780   @par Example 780   @par Example
781   @code 781   @code
782   // Drive each iteration on a fresh io_context. 782   // Drive each iteration on a fresh io_context.
783   auto io_runner = [](capy::task<> t) -> std::exception_ptr 783   auto io_runner = [](capy::task<> t) -> std::exception_ptr
784   { 784   {
785   corosio::io_context ioc; 785   corosio::io_context ioc;
786   std::exception_ptr ep; 786   std::exception_ptr ep;
787   capy::run_async(ioc.get_executor(), 787   capy::run_async(ioc.get_executor(),
788   [](auto&&...){}, 788   [](auto&&...){},
789   [&ep](std::exception_ptr e){ ep = e; } 789   [&ep](std::exception_ptr e){ ep = e; }
790   )(std::move(t)); 790   )(std::move(t));
791   ioc.run(); 791   ioc.run();
792   return ep; 792   return ep;
793   }; 793   };
794   auto r = f.armed(io_runner, 794   auto r = f.armed(io_runner,
795   [&](capy::test::fuse&) -> capy::task<> 795   [&](capy::test::fuse&) -> capy::task<>
796   { 796   {
797   co_await corosio::timeout(some_op(), 5s); 797   co_await corosio::timeout(some_op(), 5s);
798   }); 798   });
799   @endcode 799   @endcode
800   800  
801   @param run_one A callable invoked with each iteration's task; it 801   @param run_one A callable invoked with each iteration's task; it
802   runs the task to completion and returns any escaped exception 802   runs the task to completion and returns any escaped exception
803   (null on success) without rethrowing. 803   (null on success) without rethrowing.
804   804  
805   @param fn The coroutine test function to invoke. 805   @param fn The coroutine test function to invoke.
806   806  
807   @return A @ref result indicating success or failure. 807   @return A @ref result indicating success or failure.
808   */ 808   */
809   template<class Runner, class F> 809   template<class Runner, class F>
810   requires IoRunnable<std::invoke_result_t<F, fuse&>> 810   requires IoRunnable<std::invoke_result_t<F, fuse&>>
811   && std::same_as< 811   && std::same_as<
812   std::invoke_result_t<Runner&, std::invoke_result_t<F, fuse&>>, 812   std::invoke_result_t<Runner&, std::invoke_result_t<F, fuse&>>,
813   std::exception_ptr> 813   std::exception_ptr>
814   result 814   result
HITCBC 815   3 armed(Runner&& run_one, F&& fn) 815   3 armed(Runner&& run_one, F&& fn)
816   { 816   {
HITCBC 817   14 return run_phases([&]{ 817   14 return run_phases([&]{
HITCBC 818   23 if(auto ep = run_one(fn(*this))) 818   23 if(auto ep = run_one(fn(*this)))
HITCBC 819   12 std::rethrow_exception(ep); 819   12 std::rethrow_exception(ep);
HITCBC 820   6 }); 820   6 });
821   } 821   }
822   822  
823   /** Alias for @ref armed. 823   /** Alias for @ref armed.
824   824  
825   Allows the fuse to be invoked directly as a function 825   Allows the fuse to be invoked directly as a function
826   object for more concise syntax. 826   object for more concise syntax.
827   827  
828   @par Example 828   @par Example
829   829  
830   @code 830   @code
831   // These are equivalent: 831   // These are equivalent:
832   fuse f; 832   fuse f;
833   auto r1 = f.armed([](fuse& f) { ... }); 833   auto r1 = f.armed([](fuse& f) { ... });
834   auto r2 = f([](fuse& f) { ... }); 834   auto r2 = f([](fuse& f) { ... });
835   835  
836   // Inline usage: 836   // Inline usage:
837   auto r3 = fuse()([](fuse& f) { 837   auto r3 = fuse()([](fuse& f) {
838   auto ec = f.maybe_fail(); 838   auto ec = f.maybe_fail();
839   if(ec) 839   if(ec)
840   return; 840   return;
841   }); 841   });
842   @endcode 842   @endcode
843   843  
844   @see armed 844   @see armed
845   */ 845   */
846   template<class F> 846   template<class F>
847   result 847   result
HITCBC 848   15 operator()(F&& fn) 848   15 operator()(F&& fn)
849   { 849   {
HITCBC 850   15 return armed(std::forward<F>(fn)); 850   15 return armed(std::forward<F>(fn));
851   } 851   }
852   852  
853   /** Alias for @ref armed (coroutine overload). 853   /** Alias for @ref armed (coroutine overload).
854   854  
855   @see armed 855   @see armed
856   */ 856   */
857   template<class F> 857   template<class F>
858   requires IoRunnable<std::invoke_result_t<F, fuse&>> 858   requires IoRunnable<std::invoke_result_t<F, fuse&>>
859   result 859   result
860   operator()(F&& fn) 860   operator()(F&& fn)
861   { 861   {
862   return armed(std::forward<F>(fn)); 862   return armed(std::forward<F>(fn));
863   } 863   }
864   864  
865   /** Run a test function once without failure injection. 865   /** Run a test function once without failure injection.
866   866  
867   Invokes the provided function exactly once. Calls to 867   Invokes the provided function exactly once. Calls to
868   @ref maybe_fail always return an empty error code and 868   @ref maybe_fail always return an empty error code and
869   never throw. Only explicit calls to @ref fail can 869   never throw. Only explicit calls to @ref fail can
870   signal a test failure. 870   signal a test failure.
871   871  
872   This is useful for running tests where you want to 872   This is useful for running tests where you want to
873   manually control failures, or for quick single-run 873   manually control failures, or for quick single-run
874   tests without systematic error injection. 874   tests without systematic error injection.
875   875  
876   @par Example 876   @par Example
877   877  
878   @code 878   @code
879   fuse f; 879   fuse f;
880   auto r = f.inert([](fuse& f) { 880   auto r = f.inert([](fuse& f) {
881   auto ec = f.maybe_fail(); // Always succeeds 881   auto ec = f.maybe_fail(); // Always succeeds
882   assert(!ec); 882   assert(!ec);
883   883  
884   // Only way to signal failure: 884   // Only way to signal failure:
885   if(some_condition) 885   if(some_condition)
886   { 886   {
887   f.fail(); 887   f.fail();
888   return; 888   return;
889   } 889   }
890   }); 890   });
891   891  
892   if(!r) 892   if(!r)
893   { 893   {
894   std::cerr << "Test failed at " 894   std::cerr << "Test failed at "
895   << r.loc.file_name() << ":" 895   << r.loc.file_name() << ":"
896   << r.loc.line() << "\n"; 896   << r.loc.line() << "\n";
897   } 897   }
898   @endcode 898   @endcode
899   899  
900   @param fn The test function to invoke. It receives 900   @param fn The test function to invoke. It receives
901   a reference to the fuse. Calls to @ref maybe_fail 901   a reference to the fuse. Calls to @ref maybe_fail
902   will always succeed. 902   will always succeed.
903   903  
904   @return A @ref result indicating success or failure. 904   @return A @ref result indicating success or failure.
905   On failure, `result::loc` contains the source location 905   On failure, `result::loc` contains the source location
906   of the @ref fail call. 906   of the @ref fail call.
907   */ 907   */
908   template<class F> 908   template<class F>
909   result 909   result
HITCBC 910   8 inert(F&& fn) 910   8 inert(F&& fn)
911   { 911   {
HITCBC 912   8 result r; 912   8 result r;
HITCBC 913   8 p_->inert = true; 913   8 p_->inert = true;
914   try 914   try
915   { 915   {
HITCBC 916   8 fn(*this); 916   8 fn(*this);
917   } 917   }
HITCBC 918   2 catch(...) 918   2 catch(...)
919   { 919   {
HITCBC 920   1 r.success = false; 920   1 r.success = false;
HITCBC 921   1 r.loc = p_->loc; 921   1 r.loc = p_->loc;
HITCBC 922   1 r.ep = std::current_exception(); 922   1 r.ep = std::current_exception();
HITCBC 923   1 return r; 923   1 return r;
924   } 924   }
HITCBC 925   7 if(p_->stopped) 925   7 if(p_->stopped)
926   { 926   {
HITCBC 927   2 r.success = false; 927   2 r.success = false;
HITCBC 928   2 r.loc = p_->loc; 928   2 r.loc = p_->loc;
HITCBC 929   2 r.ep = p_->ep; 929   2 r.ep = p_->ep;
930   } 930   }
HITCBC 931   7 return r; 931   7 return r;
MISUBC 932   } 932   }
933   933  
934   /** Run a coroutine test function once without failure injection. 934   /** Run a coroutine test function once without failure injection.
935   935  
936   Invokes the provided coroutine function exactly once using 936   Invokes the provided coroutine function exactly once using
937   @ref run_blocking. Calls to @ref maybe_fail always return 937   @ref run_blocking. Calls to @ref maybe_fail always return
938   an empty error code and never throw. Only explicit calls 938   an empty error code and never throw. Only explicit calls
939   to @ref fail can signal a test failure. 939   to @ref fail can signal a test failure.
940   940  
941   @par Example 941   @par Example
942   942  
943   @code 943   @code
944   fuse f; 944   fuse f;
945   auto r = f.inert([](fuse& f) -> task<void> { 945   auto r = f.inert([](fuse& f) -> task<void> {
946   auto ec = f.maybe_fail(); // Always succeeds 946   auto ec = f.maybe_fail(); // Always succeeds
947   assert(!ec); 947   assert(!ec);
948   948  
949   // Only way to signal failure: 949   // Only way to signal failure:
950   if(some_condition) 950   if(some_condition)
951   { 951   {
952   f.fail(); 952   f.fail();
953   co_return; 953   co_return;
954   } 954   }
955   }); 955   });
956   956  
957   if(!r) 957   if(!r)
958   { 958   {
959   std::cerr << "Test failed at " 959   std::cerr << "Test failed at "
960   << r.loc.file_name() << ":" 960   << r.loc.file_name() << ":"
961   << r.loc.line() << "\n"; 961   << r.loc.line() << "\n";
962   } 962   }
963   @endcode 963   @endcode
964   964  
965   @param fn The coroutine test function to invoke. It receives 965   @param fn The coroutine test function to invoke. It receives
966   a reference to the fuse. Calls to @ref maybe_fail 966   a reference to the fuse. Calls to @ref maybe_fail
967   will always succeed. 967   will always succeed.
968   968  
969   @return A @ref result indicating success or failure. 969   @return A @ref result indicating success or failure.
970   On failure, `result::loc` contains the source location 970   On failure, `result::loc` contains the source location
971   of the @ref fail call. 971   of the @ref fail call.
972   */ 972   */
973   template<class F> 973   template<class F>
974   requires IoRunnable<std::invoke_result_t<F, fuse&>> 974   requires IoRunnable<std::invoke_result_t<F, fuse&>>
975   result 975   result
HITCBC 976   29 inert(F&& fn) 976   29 inert(F&& fn)
977   { 977   {
HITCBC 978   29 result r; 978   29 result r;
HITCBC 979   29 p_->inert = true; 979   29 p_->inert = true;
980   try 980   try
981   { 981   {
HITCBC 982   29 run_blocking()(fn(*this)); 982   29 run_blocking()(fn(*this));
983   } 983   }
MISUBC 984   catch(...) 984   catch(...)
985   { 985   {
MISUBC 986   r.success = false; 986   r.success = false;
MISUBC 987   r.loc = p_->loc; 987   r.loc = p_->loc;
MISUBC 988   r.ep = std::current_exception(); 988   r.ep = std::current_exception();
MISUBC 989   return r; 989   return r;
990   } 990   }
HITCBC 991   29 if(p_->stopped) 991   29 if(p_->stopped)
992   { 992   {
MISUBC 993   r.success = false; 993   r.success = false;
MISUBC 994   r.loc = p_->loc; 994   r.loc = p_->loc;
MISUBC 995   r.ep = p_->ep; 995   r.ep = p_->ep;
996   } 996   }
HITCBC 997   29 return r; 997   29 return r;
MISUBC 998   } 998   }
999   }; 999   };
1000   1000  
1001   } // test 1001   } // test
1002   } // capy 1002   } // capy
1003   } // boost 1003   } // boost
1004   1004  
1005   #endif 1005   #endif