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