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