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