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