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