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   3523 explicit operator bool() const noexcept 219   3523 explicit operator bool() const noexcept
220   { 220   {
HITCBC 221   3523 auto& s = *p_; 221   3523 auto& s = *p_;
HITCBC 222   3523 if(s.n == (std::numeric_limits<std::size_t>::max)()) 222   3523 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   736 s.n = 0; 225   736 s.n = 0;
HITCBC 226   736 return true; 226   736 return true;
227   } 227   }
HITCBC 228   2787 if(s.triggered) 228   2787 if(s.triggered)
229   { 229   {
230   // Previous round triggered, try next failure point 230   // Previous round triggered, try next failure point
HITCBC 231   2057 s.n++; 231   2057 s.n++;
HITCBC 232   2057 s.i = 0; 232   2057 s.i = 0;
HITCBC 233   2057 s.triggered = false; 233   2057 s.triggered = false;
HITCBC 234   2057 return true; 234   2057 return true;
235   } 235   }
236   // Test completed without trigger: success 236   // Test completed without trigger: success
HITCBC 237   730 return false; 237   730 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   std::source_location loc = {}; 268   std::source_location loc = {};
269   std::exception_ptr ep = nullptr; 269   std::exception_ptr ep = nullptr;
270   bool success = true; 270   bool success = true;
271   271  
HITCBC 272   56 constexpr explicit operator bool() const noexcept 272   56 constexpr explicit operator bool() const noexcept
273   { 273   {
HITCBC 274   56 return success; 274   56 return success;
275   } 275   }
276   }; 276   };
277   277  
278   /** Construct a fuse with a custom error code. 278   /** Construct a fuse with a custom error code.
279   279  
280   @par Example 280   @par Example
281   281  
282   @code 282   @code
283   auto custom_ec = make_error_code( 283   auto custom_ec = make_error_code(
284   std::errc::operation_canceled); 284   std::errc::operation_canceled);
285   fuse f(custom_ec); 285   fuse f(custom_ec);
286   286  
287   std::error_code captured_ec; 287   std::error_code captured_ec;
288   auto r = f([&](fuse& f) { 288   auto r = f([&](fuse& f) {
289   auto ec = f.maybe_fail(); 289   auto ec = f.maybe_fail();
290   if(ec) 290   if(ec)
291   { 291   {
292   captured_ec = ec; 292   captured_ec = ec;
293   return; 293   return;
294   } 294   }
295   }); 295   });
296   296  
297   assert(captured_ec == custom_ec); 297   assert(captured_ec == custom_ec);
298   @endcode 298   @endcode
299   299  
300   @param ec The error code to deliver at failure points. 300   @param ec The error code to deliver at failure points.
301   */ 301   */
HITCBC 302   433 explicit fuse(std::error_code ec) 302   433 explicit fuse(std::error_code ec)
HITCBC 303   433 : p_(std::make_shared<state>()) 303   433 : p_(std::make_shared<state>())
304   { 304   {
HITCBC 305   433 p_->ec = ec; 305   433 p_->ec = ec;
HITCBC 306   433 } 306   433 }
307   307  
308   /** Construct a fuse with the default error code. 308   /** Construct a fuse with the default error code.
309   309  
310   The default error code is `error::test_failure`. 310   The default error code is `error::test_failure`.
311   311  
312   @par Example 312   @par Example
313   313  
314   @code 314   @code
315   fuse f; 315   fuse f;
316   std::error_code captured_ec; 316   std::error_code captured_ec;
317   317  
318   auto r = f([&](fuse& f) { 318   auto r = f([&](fuse& f) {
319   auto ec = f.maybe_fail(); 319   auto ec = f.maybe_fail();
320   if(ec) 320   if(ec)
321   { 321   {
322   captured_ec = ec; 322   captured_ec = ec;
323   return; 323   return;
324   } 324   }
325   }); 325   });
326   326  
327   assert(captured_ec == error::test_failure); 327   assert(captured_ec == error::test_failure);
328   @endcode 328   @endcode
329   */ 329   */
HITCBC 330   431 fuse() 330   431 fuse()
HITCBC 331   431 : fuse(error::test_failure) 331   431 : fuse(error::test_failure)
332   { 332   {
HITCBC 333   431 } 333   431 }
334   334  
335   /** Return an error or throw at the current failure point. 335   /** Return an error or throw at the current failure point.
336   336  
337   When running under @ref armed, increments the internal 337   When running under @ref armed, increments the internal
338   counter. When the counter reaches the current failure 338   counter. When the counter reaches the current failure
339   point, returns the stored error code (or throws 339   point, returns the stored error code (or throws
340   `std::system_error` in exception mode) and records 340   `std::system_error` in exception mode) and records
341   the source location. 341   the source location.
342   342  
343   When called outside of @ref armed or @ref inert (standalone 343   When called outside of @ref armed or @ref inert (standalone
344   usage), or when running under @ref inert, always returns 344   usage), or when running under @ref inert, always returns
345   an empty error code. This enables dependency injection 345   an empty error code. This enables dependency injection
346   where the fuse is a no-op in production code. 346   where the fuse is a no-op in production code.
347   347  
348   @par Example 348   @par Example
349   349  
350   @code 350   @code
351   fuse f; 351   fuse f;
352   auto r = f([](fuse& f) { 352   auto r = f([](fuse& f) {
353   // Error code mode: returns the error 353   // Error code mode: returns the error
354   auto ec = f.maybe_fail(); 354   auto ec = f.maybe_fail();
355   if(ec) 355   if(ec)
356   return; 356   return;
357   357  
358   // Exception mode: throws system_error 358   // Exception mode: throws system_error
359   ec = f.maybe_fail(); 359   ec = f.maybe_fail();
360   if(ec) 360   if(ec)
361   return; 361   return;
362   }); 362   });
363   @endcode 363   @endcode
364   364  
365   @par Standalone Usage 365   @par Standalone Usage
366   366  
367   @code 367   @code
368   fuse f; 368   fuse f;
369   auto ec = f.maybe_fail(); // Always returns {} (no-op) 369   auto ec = f.maybe_fail(); // Always returns {} (no-op)
370   @endcode 370   @endcode
371   371  
372   @param loc The source location of the call site, 372   @param loc The source location of the call site,
373   captured automatically. 373   captured automatically.
374   374  
375   @return The stored error code if at the failure point, 375   @return The stored error code if at the failure point,
376   otherwise an empty error code. In exception mode, 376   otherwise an empty error code. In exception mode,
377   throws instead of returning an error. When called 377   throws instead of returning an error. When called
378   outside @ref armed, or when running under @ref inert, 378   outside @ref armed, or when running under @ref inert,
379   always returns an empty error code. 379   always returns an empty error code.
380   380  
381   @throws std::system_error When in exception mode 381   @throws std::system_error When in exception mode
382   and at the failure point (not thrown outside @ref armed). 382   and at the failure point (not thrown outside @ref armed).
383   */ 383   */
384   std::error_code 384   std::error_code
HITCBC 385   6403 maybe_fail( 385   6403 maybe_fail(
386   std::source_location loc = std::source_location::current()) 386   std::source_location loc = std::source_location::current())
387   { 387   {
HITCBC 388   6403 auto& s = *p_; 388   6403 auto& s = *p_;
HITCBC 389   6403 if(s.inert) 389   6403 if(s.inert)
HITCBC 390   233 return {}; 390   233 return {};
HITCBC 391   6170 if(s.i < s.n) 391   6170 if(s.i < s.n)
HITCBC 392   5444 ++s.i; 392   5444 ++s.i;
HITCBC 393   6170 if(s.i == s.n) 393   6170 if(s.i == s.n)
394   { 394   {
HITCBC 395   2135 s.triggered = true; 395   2135 s.triggered = true;
HITCBC 396   2135 s.loc = loc; 396   2135 s.loc = loc;
HITCBC 397   2135 if(s.throws) 397   2135 if(s.throws)
HITCBC 398   1023 throw std::system_error(s.ec); 398   1023 throw std::system_error(s.ec);
HITCBC 399   1112 return s.ec; 399   1112 return s.ec;
400   } 400   }
HITCBC 401   4035 return {}; 401   4035 return {};
402   } 402   }
403   403  
404   /** Signal a test failure and stop execution. 404   /** Signal a test failure and stop execution.
405   405  
406   Call this from the test function to indicate a failure 406   Call this from the test function to indicate a failure
407   condition. Both @ref armed and @ref inert will return 407   condition. Both @ref armed and @ref inert will return
408   a failed @ref result immediately. 408   a failed @ref result immediately.
409   409  
410   @par Example 410   @par Example
411   411  
412   @code 412   @code
413   fuse f; 413   fuse f;
414   auto r = f([](fuse& f) { 414   auto r = f([](fuse& f) {
415   auto ec = f.maybe_fail(); 415   auto ec = f.maybe_fail();
416   if(ec) 416   if(ec)
417   return; 417   return;
418   418  
419   // Explicit failure when a condition is not met 419   // Explicit failure when a condition is not met
420   if(some_value != expected) 420   if(some_value != expected)
421   { 421   {
422   f.fail(); 422   f.fail();
423   return; 423   return;
424   } 424   }
425   }); 425   });
426   426  
427   if(!r) 427   if(!r)
428   { 428   {
429   std::cerr << "Test failed at " 429   std::cerr << "Test failed at "
430   << r.loc.file_name() << ":" 430   << r.loc.file_name() << ":"
431   << r.loc.line() << "\n"; 431   << r.loc.line() << "\n";
432   } 432   }
433   @endcode 433   @endcode
434   434  
435   @param loc The source location of the call site, 435   @param loc The source location of the call site,
436   captured automatically. 436   captured automatically.
437   */ 437   */
438   void 438   void
HITCBC 439   3 fail( 439   3 fail(
440   std::source_location loc = 440   std::source_location loc =
441   std::source_location::current()) noexcept 441   std::source_location::current()) noexcept
442   { 442   {
HITCBC 443   3 p_->loc = loc; 443   3 p_->loc = loc;
HITCBC 444   3 p_->stopped = true; 444   3 p_->stopped = true;
HITCBC 445   3 } 445   3 }
446   446  
447   /** Signal a test failure with an exception and stop execution. 447   /** Signal a test failure with an exception and stop execution.
448   448  
449   Call this from the test function to indicate a failure 449   Call this from the test function to indicate a failure
450   condition with an associated exception. Both @ref armed 450   condition with an associated exception. Both @ref armed
451   and @ref inert will return a failed @ref result with 451   and @ref inert will return a failed @ref result with
452   the captured exception pointer. 452   the captured exception pointer.
453   453  
454   @par Example 454   @par Example
455   455  
456   @code 456   @code
457   fuse f; 457   fuse f;
458   auto r = f([](fuse& f) { 458   auto r = f([](fuse& f) {
459   try 459   try
460   { 460   {
461   do_something(); 461   do_something();
462   } 462   }
463   catch(...) 463   catch(...)
464   { 464   {
465   f.fail(std::current_exception()); 465   f.fail(std::current_exception());
466   return; 466   return;
467   } 467   }
468   }); 468   });
469   469  
470   if(!r) 470   if(!r)
471   { 471   {
472   try 472   try
473   { 473   {
474   if(r.ep) 474   if(r.ep)
475   std::rethrow_exception(r.ep); 475   std::rethrow_exception(r.ep);
476   } 476   }
477   catch(std::exception const& e) 477   catch(std::exception const& e)
478   { 478   {
479   std::cerr << "Exception: " << e.what() << "\n"; 479   std::cerr << "Exception: " << e.what() << "\n";
480   } 480   }
481   } 481   }
482   @endcode 482   @endcode
483   483  
484   @param ep The exception pointer to capture. 484   @param ep The exception pointer to capture.
485   485  
486   @param loc The source location of the call site, 486   @param loc The source location of the call site,
487   captured automatically. 487   captured automatically.
488   */ 488   */
489   void 489   void
HITCBC 490   2 fail( 490   2 fail(
491   std::exception_ptr ep, 491   std::exception_ptr ep,
492   std::source_location loc = 492   std::source_location loc =
493   std::source_location::current()) noexcept 493   std::source_location::current()) noexcept
494   { 494   {
HITCBC 495   2 p_->ep = ep; 495   2 p_->ep = ep;
HITCBC 496   2 p_->loc = loc; 496   2 p_->loc = loc;
HITCBC 497   2 p_->stopped = true; 497   2 p_->stopped = true;
HITCBC 498   2 } 498   2 }
499   499  
500   /** Run a test function with systematic failure injection. 500   /** Run a test function with systematic failure injection.
501   501  
502   Repeatedly invokes the provided function, failing at 502   Repeatedly invokes the provided function, failing at
503   successive points until the function completes without 503   successive points until the function completes without
504   encountering a failure. First runs the complete loop 504   encountering a failure. First runs the complete loop
505   using error codes, then runs using exceptions. 505   using error codes, then runs using exceptions.
506   506  
507   @par Example 507   @par Example
508   508  
509   @code 509   @code
510   fuse f; 510   fuse f;
511   auto r = f.armed([](fuse& f) { 511   auto r = f.armed([](fuse& f) {
512   auto ec = f.maybe_fail(); 512   auto ec = f.maybe_fail();
513   if(ec) 513   if(ec)
514   return; 514   return;
515   515  
516   ec = f.maybe_fail(); 516   ec = f.maybe_fail();
517   if(ec) 517   if(ec)
518   return; 518   return;
519   }); 519   });
520   520  
521   if(!r) 521   if(!r)
522   { 522   {
523   std::cerr << "Failure at " 523   std::cerr << "Failure at "
524   << r.loc.file_name() << ":" 524   << r.loc.file_name() << ":"
525   << r.loc.line() << "\n"; 525   << r.loc.line() << "\n";
526   } 526   }
527   @endcode 527   @endcode
528   528  
529   @param fn The test function to invoke. It receives 529   @param fn The test function to invoke. It receives
530   a reference to the fuse and should call @ref maybe_fail 530   a reference to the fuse and should call @ref maybe_fail
531   at each potential failure point. 531   at each potential failure point.
532   532  
533   @return A @ref result indicating success or failure. 533   @return A @ref result indicating success or failure.
534   On failure, `result::loc` contains the source location 534   On failure, `result::loc` contains the source location
535   of the last @ref maybe_fail or @ref fail call. 535   of the last @ref maybe_fail or @ref fail call.
536   */ 536   */
537   template<class F> 537   template<class F>
538   result 538   result
HITCBC 539   32 armed(F&& fn) 539   32 armed(F&& fn)
540   { 540   {
HITCBC 541   32 result r; 541   32 result r;
542   542  
543   // Phase 1: error code mode 543   // Phase 1: error code mode
HITCBC 544   32 p_->throws = false; 544   32 p_->throws = false;
HITCBC 545   32 p_->inert = false; 545   32 p_->inert = false;
HITCBC 546   32 p_->n = (std::numeric_limits<std::size_t>::max)(); 546   32 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 547   97 while(*this) 547   97 while(*this)
548   { 548   {
549   try 549   try
550   { 550   {
HITCBC 551   71 fn(*this); 551   71 fn(*this);
552   } 552   }
HITCBC 553   6 catch(...) 553   6 catch(...)
554   { 554   {
HITCBC 555   3 r.success = false; 555   3 r.success = false;
HITCBC 556   3 r.loc = p_->loc; 556   3 r.loc = p_->loc;
HITCBC 557   3 r.ep = p_->ep; 557   3 r.ep = p_->ep;
HITCBC 558   3 p_->inert = true; 558   3 p_->inert = true;
HITCBC 559   3 return r; 559   3 return r;
560   } 560   }
HITCBC 561   68 if(p_->stopped) 561   68 if(p_->stopped)
562   { 562   {
HITCBC 563   3 r.success = false; 563   3 r.success = false;
HITCBC 564   3 r.loc = p_->loc; 564   3 r.loc = p_->loc;
HITCBC 565   3 r.ep = p_->ep; 565   3 r.ep = p_->ep;
HITCBC 566   3 p_->inert = true; 566   3 p_->inert = true;
HITCBC 567   3 return r; 567   3 return r;
568   } 568   }
569   } 569   }
570   570  
571   // Phase 2: exception mode 571   // Phase 2: exception mode
HITCBC 572   26 p_->throws = true; 572   26 p_->throws = true;
HITCBC 573   26 p_->n = (std::numeric_limits<std::size_t>::max)(); 573   26 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 574   26 p_->i = 0; 574   26 p_->i = 0;
HITCBC 575   26 p_->triggered = false; 575   26 p_->triggered = false;
HITCBC 576   80 while(*this) 576   80 while(*this)
577   { 577   {
578   try 578   try
579   { 579   {
HITCBC 580   54 fn(*this); 580   54 fn(*this);
581   } 581   }
HITCBC 582   56 catch(std::system_error const& ex) 582   56 catch(std::system_error const& ex)
583   { 583   {
HITCBC 584   28 if(ex.code() != p_->ec) 584   28 if(ex.code() != p_->ec)
585   { 585   {
MISUBC 586   r.success = false; 586   r.success = false;
MISUBC 587   r.loc = p_->loc; 587   r.loc = p_->loc;
MISUBC 588   r.ep = p_->ep; 588   r.ep = p_->ep;
MISUBC 589   p_->inert = true; 589   p_->inert = true;
MISUBC 590   return r; 590   return r;
591   } 591   }
592   } 592   }
MISUBC 593   catch(...) 593   catch(...)
594   { 594   {
MISUBC 595   r.success = false; 595   r.success = false;
MISUBC 596   r.loc = p_->loc; 596   r.loc = p_->loc;
MISUBC 597   r.ep = p_->ep; 597   r.ep = p_->ep;
MISUBC 598   p_->inert = true; 598   p_->inert = true;
MISUBC 599   return r; 599   return r;
600   } 600   }
HITCBC 601   54 if(p_->stopped) 601   54 if(p_->stopped)
602   { 602   {
MISUBC 603   r.success = false; 603   r.success = false;
MISUBC 604   r.loc = p_->loc; 604   r.loc = p_->loc;
MISUBC 605   r.ep = p_->ep; 605   r.ep = p_->ep;
MISUBC 606   p_->inert = true; 606   p_->inert = true;
MISUBC 607   return r; 607   return r;
608   } 608   }
609   } 609   }
HITCBC 610   26 p_->inert = true; 610   26 p_->inert = true;
HITCBC 611   26 return r; 611   26 return r;
MISUBC 612   } 612   }
613   613  
614   /** Run a coroutine test function with systematic failure injection. 614   /** Run a coroutine test function with systematic failure injection.
615   615  
616   Repeatedly invokes the provided coroutine function, failing at 616   Repeatedly invokes the provided coroutine function, failing at
617   successive points until the function completes without 617   successive points until the function completes without
618   encountering a failure. First runs the complete loop 618   encountering a failure. First runs the complete loop
619   using error codes, then runs using exceptions. 619   using error codes, then runs using exceptions.
620   620  
621   This overload handles lambdas that return an @ref IoRunnable 621   This overload handles lambdas that return an @ref IoRunnable
622   (such as `task<void>`), executing them synchronously via 622   (such as `task<void>`), executing them synchronously via
623   @ref run_blocking. 623   @ref run_blocking.
624   624  
625   @par Example 625   @par Example
626   626  
627   @code 627   @code
628   fuse f; 628   fuse f;
629   auto r = f.armed([&](fuse&) -> task<void> { 629   auto r = f.armed([&](fuse&) -> task<void> {
630   auto ec = f.maybe_fail(); 630   auto ec = f.maybe_fail();
631   if(ec) 631   if(ec)
632   co_return; 632   co_return;
633   633  
634   ec = f.maybe_fail(); 634   ec = f.maybe_fail();
635   if(ec) 635   if(ec)
636   co_return; 636   co_return;
637   }); 637   });
638   638  
639   if(!r) 639   if(!r)
640   { 640   {
641   std::cerr << "Failure at " 641   std::cerr << "Failure at "
642   << r.loc.file_name() << ":" 642   << r.loc.file_name() << ":"
643   << r.loc.line() << "\n"; 643   << r.loc.line() << "\n";
644   } 644   }
645   @endcode 645   @endcode
646   646  
647   @param fn The coroutine test function to invoke. It receives 647   @param fn The coroutine test function to invoke. It receives
648   a reference to the fuse and should call @ref maybe_fail 648   a reference to the fuse and should call @ref maybe_fail
649   at each potential failure point. 649   at each potential failure point.
650   650  
651   @return A @ref result indicating success or failure. 651   @return A @ref result indicating success or failure.
652   On failure, `result::loc` contains the source location 652   On failure, `result::loc` contains the source location
653   of the last @ref maybe_fail or @ref fail call. 653   of the last @ref maybe_fail or @ref fail call.
654   */ 654   */
655   template<class F> 655   template<class F>
656   requires IoRunnable<std::invoke_result_t<F, fuse&>> 656   requires IoRunnable<std::invoke_result_t<F, fuse&>>
657   result 657   result
HITCBC 658   339 armed(F&& fn) 658   339 armed(F&& fn)
659   { 659   {
HITCBC 660   339 result r; 660   339 result r;
661   661  
662   // Phase 1: error code mode 662   // Phase 1: error code mode
HITCBC 663   339 p_->throws = false; 663   339 p_->throws = false;
HITCBC 664   339 p_->inert = false; 664   339 p_->inert = false;
HITCBC 665   339 p_->n = (std::numeric_limits<std::size_t>::max)(); 665   339 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 666   1673 while(*this) 666   1673 while(*this)
667   { 667   {
668   try 668   try
669   { 669   {
HITCBC 670   1334 run_blocking()(fn(*this)); 670   1334 run_blocking()(fn(*this));
671   } 671   }
MISUBC 672   catch(...) 672   catch(...)
673   { 673   {
MISUBC 674   r.success = false; 674   r.success = false;
MISUBC 675   r.loc = p_->loc; 675   r.loc = p_->loc;
MISUBC 676   r.ep = p_->ep; 676   r.ep = p_->ep;
MISUBC 677   p_->inert = true; 677   p_->inert = true;
MISUBC 678   return r; 678   return r;
679   } 679   }
HITCBC 680   1334 if(p_->stopped) 680   1334 if(p_->stopped)
681   { 681   {
MISUBC 682   r.success = false; 682   r.success = false;
MISUBC 683   r.loc = p_->loc; 683   r.loc = p_->loc;
MISUBC 684   r.ep = p_->ep; 684   r.ep = p_->ep;
MISUBC 685   p_->inert = true; 685   p_->inert = true;
MISUBC 686   return r; 686   return r;
687   } 687   }
688   } 688   }
689   689  
690   // Phase 2: exception mode 690   // Phase 2: exception mode
HITCBC 691   339 p_->throws = true; 691   339 p_->throws = true;
HITCBC 692   339 p_->n = (std::numeric_limits<std::size_t>::max)(); 692   339 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 693   339 p_->i = 0; 693   339 p_->i = 0;
HITCBC 694   339 p_->triggered = false; 694   339 p_->triggered = false;
HITCBC 695   1673 while(*this) 695   1673 while(*this)
696   { 696   {
697   try 697   try
698   { 698   {
HITCBC 699   3324 run_blocking()(fn(*this)); 699   3324 run_blocking()(fn(*this));
700   } 700   }
HITCBC 701   1990 catch(std::system_error const& ex) 701   1990 catch(std::system_error const& ex)
702   { 702   {
HITCBC 703   995 if(ex.code() != p_->ec) 703   995 if(ex.code() != p_->ec)
704   { 704   {
MISUBC 705   r.success = false; 705   r.success = false;
MISUBC 706   r.loc = p_->loc; 706   r.loc = p_->loc;
MISUBC 707   r.ep = p_->ep; 707   r.ep = p_->ep;
MISUBC 708   p_->inert = true; 708   p_->inert = true;
MISUBC 709   return r; 709   return r;
710   } 710   }
711   } 711   }
MISUBC 712   catch(...) 712   catch(...)
713   { 713   {
MISUBC 714   r.success = false; 714   r.success = false;
MISUBC 715   r.loc = p_->loc; 715   r.loc = p_->loc;
MISUBC 716   r.ep = p_->ep; 716   r.ep = p_->ep;
MISUBC 717   p_->inert = true; 717   p_->inert = true;
MISUBC 718   return r; 718   return r;
719   } 719   }
HITCBC 720   1334 if(p_->stopped) 720   1334 if(p_->stopped)
721   { 721   {
MISUBC 722   r.success = false; 722   r.success = false;
MISUBC 723   r.loc = p_->loc; 723   r.loc = p_->loc;
MISUBC 724   r.ep = p_->ep; 724   r.ep = p_->ep;
MISUBC 725   p_->inert = true; 725   p_->inert = true;
MISUBC 726   return r; 726   return r;
727   } 727   }
728   } 728   }
HITCBC 729   339 p_->inert = true; 729   339 p_->inert = true;
HITCBC 730   339 return r; 730   339 return r;
MISUBC 731   } 731   }
732   732  
733   /** Alias for @ref armed. 733   /** Alias for @ref armed.
734   734  
735   Allows the fuse to be invoked directly as a function 735   Allows the fuse to be invoked directly as a function
736   object for more concise syntax. 736   object for more concise syntax.
737   737  
738   @par Example 738   @par Example
739   739  
740   @code 740   @code
741   // These are equivalent: 741   // These are equivalent:
742   fuse f; 742   fuse f;
743   auto r1 = f.armed([](fuse& f) { ... }); 743   auto r1 = f.armed([](fuse& f) { ... });
744   auto r2 = f([](fuse& f) { ... }); 744   auto r2 = f([](fuse& f) { ... });
745   745  
746   // Inline usage: 746   // Inline usage:
747   auto r3 = fuse()([](fuse& f) { 747   auto r3 = fuse()([](fuse& f) {
748   auto ec = f.maybe_fail(); 748   auto ec = f.maybe_fail();
749   if(ec) 749   if(ec)
750   return; 750   return;
751   }); 751   });
752   @endcode 752   @endcode
753   753  
754   @see armed 754   @see armed
755   */ 755   */
756   template<class F> 756   template<class F>
757   result 757   result
HITCBC 758   15 operator()(F&& fn) 758   15 operator()(F&& fn)
759   { 759   {
HITCBC 760   15 return armed(std::forward<F>(fn)); 760   15 return armed(std::forward<F>(fn));
761   } 761   }
762   762  
763   /** Alias for @ref armed (coroutine overload). 763   /** Alias for @ref armed (coroutine overload).
764   764  
765   @see armed 765   @see armed
766   */ 766   */
767   template<class F> 767   template<class F>
768   requires IoRunnable<std::invoke_result_t<F, fuse&>> 768   requires IoRunnable<std::invoke_result_t<F, fuse&>>
769   result 769   result
770   operator()(F&& fn) 770   operator()(F&& fn)
771   { 771   {
772   return armed(std::forward<F>(fn)); 772   return armed(std::forward<F>(fn));
773   } 773   }
774   774  
775   /** Run a test function once without failure injection. 775   /** Run a test function once without failure injection.
776   776  
777   Invokes the provided function exactly once. Calls to 777   Invokes the provided function exactly once. Calls to
778   @ref maybe_fail always return an empty error code and 778   @ref maybe_fail always return an empty error code and
779   never throw. Only explicit calls to @ref fail can 779   never throw. Only explicit calls to @ref fail can
780   signal a test failure. 780   signal a test failure.
781   781  
782   This is useful for running tests where you want to 782   This is useful for running tests where you want to
783   manually control failures, or for quick single-run 783   manually control failures, or for quick single-run
784   tests without systematic error injection. 784   tests without systematic error injection.
785   785  
786   @par Example 786   @par Example
787   787  
788   @code 788   @code
789   fuse f; 789   fuse f;
790   auto r = f.inert([](fuse& f) { 790   auto r = f.inert([](fuse& f) {
791   auto ec = f.maybe_fail(); // Always succeeds 791   auto ec = f.maybe_fail(); // Always succeeds
792   assert(!ec); 792   assert(!ec);
793   793  
794   // Only way to signal failure: 794   // Only way to signal failure:
795   if(some_condition) 795   if(some_condition)
796   { 796   {
797   f.fail(); 797   f.fail();
798   return; 798   return;
799   } 799   }
800   }); 800   });
801   801  
802   if(!r) 802   if(!r)
803   { 803   {
804   std::cerr << "Test failed at " 804   std::cerr << "Test failed at "
805   << r.loc.file_name() << ":" 805   << r.loc.file_name() << ":"
806   << r.loc.line() << "\n"; 806   << r.loc.line() << "\n";
807   } 807   }
808   @endcode 808   @endcode
809   809  
810   @param fn The test function to invoke. It receives 810   @param fn The test function to invoke. It receives
811   a reference to the fuse. Calls to @ref maybe_fail 811   a reference to the fuse. Calls to @ref maybe_fail
812   will always succeed. 812   will always succeed.
813   813  
814   @return A @ref result indicating success or failure. 814   @return A @ref result indicating success or failure.
815   On failure, `result::loc` contains the source location 815   On failure, `result::loc` contains the source location
816   of the @ref fail call. 816   of the @ref fail call.
817   */ 817   */
818   template<class F> 818   template<class F>
819   result 819   result
HITCBC 820   8 inert(F&& fn) 820   8 inert(F&& fn)
821   { 821   {
HITCBC 822   8 result r; 822   8 result r;
HITCBC 823   8 p_->inert = true; 823   8 p_->inert = true;
824   try 824   try
825   { 825   {
HITCBC 826   8 fn(*this); 826   8 fn(*this);
827   } 827   }
HITCBC 828   2 catch(...) 828   2 catch(...)
829   { 829   {
HITCBC 830   1 r.success = false; 830   1 r.success = false;
HITCBC 831   1 r.loc = p_->loc; 831   1 r.loc = p_->loc;
HITCBC 832   1 r.ep = std::current_exception(); 832   1 r.ep = std::current_exception();
HITCBC 833   1 return r; 833   1 return r;
834   } 834   }
HITCBC 835   7 if(p_->stopped) 835   7 if(p_->stopped)
836   { 836   {
HITCBC 837   2 r.success = false; 837   2 r.success = false;
HITCBC 838   2 r.loc = p_->loc; 838   2 r.loc = p_->loc;
HITCBC 839   2 r.ep = p_->ep; 839   2 r.ep = p_->ep;
840   } 840   }
HITCBC 841   7 return r; 841   7 return r;
MISUBC 842   } 842   }
843   843  
844   /** Run a coroutine test function once without failure injection. 844   /** Run a coroutine test function once without failure injection.
845   845  
846   Invokes the provided coroutine function exactly once using 846   Invokes the provided coroutine function exactly once using
847   @ref run_blocking. Calls to @ref maybe_fail always return 847   @ref run_blocking. Calls to @ref maybe_fail always return
848   an empty error code and never throw. Only explicit calls 848   an empty error code and never throw. Only explicit calls
849   to @ref fail can signal a test failure. 849   to @ref fail can signal a test failure.
850   850  
851   @par Example 851   @par Example
852   852  
853   @code 853   @code
854   fuse f; 854   fuse f;
855   auto r = f.inert([](fuse& f) -> task<void> { 855   auto r = f.inert([](fuse& f) -> task<void> {
856   auto ec = f.maybe_fail(); // Always succeeds 856   auto ec = f.maybe_fail(); // Always succeeds
857   assert(!ec); 857   assert(!ec);
858   858  
859   // Only way to signal failure: 859   // Only way to signal failure:
860   if(some_condition) 860   if(some_condition)
861   { 861   {
862   f.fail(); 862   f.fail();
863   co_return; 863   co_return;
864   } 864   }
865   }); 865   });
866   866  
867   if(!r) 867   if(!r)
868   { 868   {
869   std::cerr << "Test failed at " 869   std::cerr << "Test failed at "
870   << r.loc.file_name() << ":" 870   << r.loc.file_name() << ":"
871   << r.loc.line() << "\n"; 871   << r.loc.line() << "\n";
872   } 872   }
873   @endcode 873   @endcode
874   874  
875   @param fn The coroutine test function to invoke. It receives 875   @param fn The coroutine test function to invoke. It receives
876   a reference to the fuse. Calls to @ref maybe_fail 876   a reference to the fuse. Calls to @ref maybe_fail
877   will always succeed. 877   will always succeed.
878   878  
879   @return A @ref result indicating success or failure. 879   @return A @ref result indicating success or failure.
880   On failure, `result::loc` contains the source location 880   On failure, `result::loc` contains the source location
881   of the @ref fail call. 881   of the @ref fail call.
882   */ 882   */
883   template<class F> 883   template<class F>
884   requires IoRunnable<std::invoke_result_t<F, fuse&>> 884   requires IoRunnable<std::invoke_result_t<F, fuse&>>
885   result 885   result
HITCBC 886   17 inert(F&& fn) 886   17 inert(F&& fn)
887   { 887   {
HITCBC 888   17 result r; 888   17 result r;
HITCBC 889   17 p_->inert = true; 889   17 p_->inert = true;
890   try 890   try
891   { 891   {
HITCBC 892   17 run_blocking()(fn(*this)); 892   17 run_blocking()(fn(*this));
893   } 893   }
MISUBC 894   catch(...) 894   catch(...)
895   { 895   {
MISUBC 896   r.success = false; 896   r.success = false;
MISUBC 897   r.loc = p_->loc; 897   r.loc = p_->loc;
MISUBC 898   r.ep = std::current_exception(); 898   r.ep = std::current_exception();
MISUBC 899   return r; 899   return r;
900   } 900   }
HITCBC 901   17 if(p_->stopped) 901   17 if(p_->stopped)
902   { 902   {
MISUBC 903   r.success = false; 903   r.success = false;
MISUBC 904   r.loc = p_->loc; 904   r.loc = p_->loc;
MISUBC 905   r.ep = p_->ep; 905   r.ep = p_->ep;
906   } 906   }
HITCBC 907   17 return r; 907   17 return r;
MISUBC 908   } 908   }
909   }; 909   };
910   910  
911   } // test 911   } // test
912   } // capy 912   } // capy
913   } // boost 913   } // boost
914   914  
915   #endif 915   #endif