72.00% Lines (126/175) 100.00% Functions (16/16)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) 2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3   // Copyright (c) 2026 Michael Vandeberg 3   // Copyright (c) 2026 Michael Vandeberg
4   // 4   //
5   // Distributed under the Boost Software License, Version 1.0. (See accompanying 5   // Distributed under the Boost Software License, Version 1.0. (See accompanying
6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7   // 7   //
8   // Official repository: https://github.com/cppalliance/capy 8   // Official repository: https://github.com/cppalliance/capy
9   // 9   //
10   10  
11   #ifndef BOOST_CAPY_TEST_FUSE_HPP 11   #ifndef BOOST_CAPY_TEST_FUSE_HPP
12   #define BOOST_CAPY_TEST_FUSE_HPP 12   #define BOOST_CAPY_TEST_FUSE_HPP
13   13  
14   #include <boost/capy/detail/config.hpp> 14   #include <boost/capy/detail/config.hpp>
15   #include <boost/capy/concept/io_runnable.hpp> 15   #include <boost/capy/concept/io_runnable.hpp>
16   #include <boost/capy/error.hpp> 16   #include <boost/capy/error.hpp>
17   #include <boost/capy/test/run_blocking.hpp> 17   #include <boost/capy/test/run_blocking.hpp>
18   #include <system_error> 18   #include <system_error>
19   #include <concepts> 19   #include <concepts>
20   #include <cstddef> 20   #include <cstddef>
21   #include <exception> 21   #include <exception>
22   #include <limits> 22   #include <limits>
23   #include <memory> 23   #include <memory>
24   #include <source_location> 24   #include <source_location>
25   #include <type_traits> 25   #include <type_traits>
26   26  
27   /* 27   /*
28   LLM/AI Instructions for fuse-based test patterns: 28   LLM/AI Instructions for fuse-based test patterns:
29   29  
30   When f.armed() runs a test, it injects errors at successive points 30   When f.armed() runs a test, it injects errors at successive points
31   via maybe_fail(). Operations like read_stream::read_some() and 31   via maybe_fail(). Operations like read_stream::read_some() and
32   write_stream::write_some() call maybe_fail() internally. 32   write_stream::write_some() call maybe_fail() internally.
33   33  
34   CORRECT pattern - early return on injected error: 34   CORRECT pattern - early return on injected error:
35   35  
36   auto [ec, n] = co_await rs.read_some(buf); 36   auto [ec, n] = co_await rs.read_some(buf);
37   if(ec) 37   if(ec)
38   co_return; // fuse injected error, exit gracefully 38   co_return; // fuse injected error, exit gracefully
39   // ... continue with success path 39   // ... continue with success path
40   40  
41   WRONG pattern - asserting success unconditionally: 41   WRONG pattern - asserting success unconditionally:
42   42  
43   auto [ec, n] = co_await rs.read_some(buf); 43   auto [ec, n] = co_await rs.read_some(buf);
44   BOOST_TEST(! ec); // FAILS when fuse injects error! 44   BOOST_TEST(! ec); // FAILS when fuse injects error!
45   45  
46   The fuse mechanism tests error handling by failing at each point 46   The fuse mechanism tests error handling by failing at each point
47   in sequence. Tests must handle injected errors by returning early, 47   in sequence. Tests must handle injected errors by returning early,
48   not by asserting that operations always succeed. 48   not by asserting that operations always succeed.
49   */ 49   */
50   50  
51   namespace boost { 51   namespace boost {
52   namespace capy { 52   namespace capy {
53   namespace test { 53   namespace test {
54   54  
55   /** Reruns a code path, injecting a failure at one later point on each pass. 55   /** Reruns a code path, injecting a failure at one later point on each pass.
56   56  
57   This class enables exhaustive testing of error handling 57   This class enables exhaustive testing of error handling
58   paths by injecting failures at successive points in code. 58   paths by injecting failures at successive points in code.
59   Each iteration fails at a later point until the code path 59   Each iteration fails at a later point until the code path
60   completes without encountering a failure. The @ref armed 60   completes without encountering a failure. The @ref armed
61   method runs in two phases: first with error codes, then 61   method runs in two phases: first with error codes, then
62   with exceptions. The @ref inert method runs once without 62   with exceptions. The @ref inert method runs once without
63   automatic failure injection. 63   automatic failure injection.
64   64  
65   @par Thread Safety 65   @par Thread Safety
66   66  
67   @b Not @b thread @b safe. Instances must not be accessed 67   @b Not @b thread @b safe. Instances must not be accessed
68   from different logical threads of operation concurrently. 68   from different logical threads of operation concurrently.
69   This includes coroutines - accessing the same fuse from 69   This includes coroutines - accessing the same fuse from
70   multiple concurrent coroutines causes non-deterministic 70   multiple concurrent coroutines causes non-deterministic
71   test behavior. 71   test behavior.
72   72  
73   @par Basic Inline Usage 73   @par Basic Inline Usage
74   74  
75   @par !example example_1 75   @par !example example_1
76   76  
77   77  
78   @par Named Fuse with armed() 78   @par Named Fuse with armed()
79   79  
80   @par !example example_2 80   @par !example example_2
81   81  
82   82  
83   @par Using inert() for Single-Run Tests 83   @par Using inert() for Single-Run Tests
84   84  
85   @par !example example_3 85   @par !example example_3
86   86  
87   87  
88   @par Dependency Injection (Standalone Usage) 88   @par Dependency Injection (Standalone Usage)
89   89  
90   A default-constructed fuse is a no-op when used outside 90   A default-constructed fuse is a no-op when used outside
91   of @ref armed or @ref inert. This enables passing a fuse 91   of @ref armed or @ref inert. This enables passing a fuse
92   to classes for dependency injection without affecting 92   to classes for dependency injection without affecting
93   normal operation. 93   normal operation.
94   94  
95   @par !example example_4 95   @par !example example_4
96   96  
97   97  
98   @par Custom Error Code 98   @par Custom Error Code
99   99  
100   @par !example example_5 100   @par !example example_5
101   101  
102   102  
103   @par Checking the Result 103   @par Checking the Result
104   104  
105   @par !example example_6 105   @par !example example_6
106   106  
107   107  
108   @par Test Framework Integration 108   @par Test Framework Integration
109   109  
110   @par !example example_7 110   @par !example example_7
111   111  
112   */ 112   */
113   class fuse 113   class fuse
114   { 114   {
115   struct state 115   struct state
116   { 116   {
117   std::size_t n = (std::numeric_limits<std::size_t>::max)(); 117   std::size_t n = (std::numeric_limits<std::size_t>::max)();
118   std::size_t i = 0; 118   std::size_t i = 0;
119   bool triggered = false; 119   bool triggered = false;
120   bool throws = false; 120   bool throws = false;
121   bool stopped = false; 121   bool stopped = false;
122   bool inert = true; 122   bool inert = true;
123   std::error_code ec; 123   std::error_code ec;
124   std::source_location loc; 124   std::source_location loc;
125   std::exception_ptr ep; 125   std::exception_ptr ep;
126   }; 126   };
127   127  
128   std::shared_ptr<state> p_; 128   std::shared_ptr<state> p_;
129   129  
130   /** Return true if testing should continue. 130   /** Return true if testing should continue.
131   131  
132   On the first call, initializes the failure point to 0. 132   On the first call, initializes the failure point to 0.
133   After a triggered failure, increments the failure point 133   After a triggered failure, increments the failure point
134   and resets for the next iteration. Returns false when 134   and resets for the next iteration. Returns false when
135   the test completes without triggering a failure. 135   the test completes without triggering a failure.
136   */ 136   */
HITCBC 137   1326 explicit operator bool() const noexcept 137   1326 explicit operator bool() const noexcept
138   { 138   {
HITCBC 139   1326 auto& s = *p_; 139   1326 auto& s = *p_;
HITCBC 140   1326 if(s.n == (std::numeric_limits<std::size_t>::max)()) 140   1326 if(s.n == (std::numeric_limits<std::size_t>::max)())
141   { 141   {
142   // First call: start round 0 142   // First call: start round 0
HITCBC 143   313 s.n = 0; 143   313 s.n = 0;
HITCBC 144   313 return true; 144   313 return true;
145   } 145   }
HITCBC 146   1013 if(s.triggered) 146   1013 if(s.triggered)
147   { 147   {
148   // Previous round triggered, try next failure point 148   // Previous round triggered, try next failure point
HITCBC 149   707 s.n++; 149   707 s.n++;
HITCBC 150   707 s.i = 0; 150   707 s.i = 0;
HITCBC 151   707 s.triggered = false; 151   707 s.triggered = false;
HITCBC 152   707 return true; 152   707 return true;
153   } 153   }
154   // Test completed without trigger: success 154   // Test completed without trigger: success
HITCBC 155   306 return false; 155   306 return false;
156   } 156   }
157   157  
158   public: 158   public:
159   /** Converts to `bool`, reporting success, and carries the failure point on failure. 159   /** Converts to `bool`, reporting success, and carries the failure point on failure.
160   160  
161   Contains the outcome of @ref armed or @ref inert 161   Contains the outcome of @ref armed or @ref inert
162   and, on failure, the source location of the failing 162   and, on failure, the source location of the failing
163   point. Converts to `bool` for convenient success 163   point. Converts to `bool` for convenient success
164   checking. 164   checking.
165   165  
166   @par Example 166   @par Example
167   167  
168   @par !example example 168   @par !example example
169   169  
170   */ 170   */
171   struct result 171   struct result
172   { 172   {
173   /// Source location of the failing point, set only on failure. 173   /// Source location of the failing point, set only on failure.
174   std::source_location loc = {}; 174   std::source_location loc = {};
175   175  
176   /// Exception captured by @ref fail, or null if none. 176   /// Exception captured by @ref fail, or null if none.
177   std::exception_ptr ep = nullptr; 177   std::exception_ptr ep = nullptr;
178   178  
179   /// True if the test completed without a failure. 179   /// True if the test completed without a failure.
180   bool success = true; 180   bool success = true;
181   181  
182   /** Return whether the test completed without a failure. 182   /** Return whether the test completed without a failure.
183   183  
184   @return @ref success. 184   @return @ref success.
185   */ 185   */
HITCBC 186   42 constexpr explicit operator bool() const noexcept 186   42 constexpr explicit operator bool() const noexcept
187   { 187   {
HITCBC 188   42 return success; 188   42 return success;
189   } 189   }
190   }; 190   };
191   191  
192   /** Construct a fuse with a custom error code. 192   /** Construct a fuse with a custom error code.
193   193  
194   @par Example 194   @par Example
195   195  
196   @par !example example_1 196   @par !example example_1
197   197  
198   198  
199   @param ec The error code to deliver at failure points. 199   @param ec The error code to deliver at failure points.
200   */ 200   */
HITCBC 201   274 explicit fuse(std::error_code ec) 201   274 explicit fuse(std::error_code ec)
HITCBC 202   274 : p_(std::make_shared<state>()) 202   274 : p_(std::make_shared<state>())
203   { 203   {
HITCBC 204   274 p_->ec = ec; 204   274 p_->ec = ec;
HITCBC 205   274 } 205   274 }
206   206  
207   /** Construct a fuse with the default error code. 207   /** Construct a fuse with the default error code.
208   208  
209   The default error code is `error::test_failure`. 209   The default error code is `error::test_failure`.
210   210  
211   @par Example 211   @par Example
212   212  
213   @par !example example_2 213   @par !example example_2
214   214  
215   */ 215   */
HITCBC 216   271 fuse() 216   271 fuse()
HITCBC 217   271 : fuse(error::test_failure) 217   271 : fuse(error::test_failure)
218   { 218   {
HITCBC 219   271 } 219   271 }
220   220  
221   /** Return an error or throw at the current failure point. 221   /** Return an error or throw at the current failure point.
222   222  
223   When running under @ref armed, increments the internal 223   When running under @ref armed, increments the internal
224   counter. When the counter reaches the current failure 224   counter. When the counter reaches the current failure
225   point, returns the stored error code (or throws 225   point, returns the stored error code (or throws
226   `std::system_error` in exception mode) and records 226   `std::system_error` in exception mode) and records
227   the source location. 227   the source location.
228   228  
229   When called outside of @ref armed or @ref inert (standalone 229   When called outside of @ref armed or @ref inert (standalone
230   usage), or when running under @ref inert, always returns 230   usage), or when running under @ref inert, always returns
231   an empty error code. This enables dependency injection 231   an empty error code. This enables dependency injection
232   where the fuse is a no-op in production code. 232   where the fuse is a no-op in production code.
233   233  
234   @par Example 234   @par Example
235   235  
236   @par !example example_1 236   @par !example example_1
237   237  
238   238  
239   @par Standalone Usage 239   @par Standalone Usage
240   240  
241   @par !example example_2 241   @par !example example_2
242   242  
243   243  
244   @param loc The source location of the call site, 244   @param loc The source location of the call site,
245   captured automatically. 245   captured automatically.
246   246  
247   @return The stored error code if at the failure point, 247   @return The stored error code if at the failure point,
248   otherwise an empty error code. In exception mode, 248   otherwise an empty error code. In exception mode,
249   throws instead of returning an error. When called 249   throws instead of returning an error. When called
250   outside @ref armed, or when running under @ref inert, 250   outside @ref armed, or when running under @ref inert,
251   always returns an empty error code. 251   always returns an empty error code.
252   252  
253   @throws std::system_error When in exception mode 253   @throws std::system_error When in exception mode
254   and at the failure point (not thrown outside @ref armed). 254   and at the failure point (not thrown outside @ref armed).
255   */ 255   */
256   std::error_code 256   std::error_code
HITCBC 257   1746 maybe_fail( 257   1746 maybe_fail(
258   std::source_location loc = std::source_location::current()) 258   std::source_location loc = std::source_location::current())
259   { 259   {
HITCBC 260   1746 auto& s = *p_; 260   1746 auto& s = *p_;
HITCBC 261   1746 if(s.inert) 261   1746 if(s.inert)
HITCBC 262   323 return {}; 262   323 return {};
HITCBC 263   1423 if(s.i < s.n) 263   1423 if(s.i < s.n)
HITCBC 264   1152 ++s.i; 264   1152 ++s.i;
HITCBC 265   1423 if(s.i == s.n) 265   1423 if(s.i == s.n)
266   { 266   {
HITCBC 267   707 s.triggered = true; 267   707 s.triggered = true;
HITCBC 268   707 s.loc = loc; 268   707 s.loc = loc;
HITCBC 269   707 if(s.throws) 269   707 if(s.throws)
HITCBC 270   347 throw std::system_error(s.ec); 270   347 throw std::system_error(s.ec);
HITCBC 271   360 return s.ec; 271   360 return s.ec;
272   } 272   }
HITCBC 273   716 return {}; 273   716 return {};
274   } 274   }
275   275  
276   /** Signal a test failure and stop execution. 276   /** Signal a test failure and stop execution.
277   277  
278   Call this from the test function to indicate a failure 278   Call this from the test function to indicate a failure
279   condition. Both @ref armed and @ref inert return 279   condition. Both @ref armed and @ref inert return
280   a failed @ref result immediately. 280   a failed @ref result immediately.
281   281  
282   @par Example 282   @par Example
283   283  
284   @par !example example_1 284   @par !example example_1
285   285  
286   286  
287   @param loc The source location of the call site, 287   @param loc The source location of the call site,
288   captured automatically. 288   captured automatically.
289   */ 289   */
290   void 290   void
HITCBC 291   3 fail( 291   3 fail(
292   std::source_location loc = 292   std::source_location loc =
293   std::source_location::current()) noexcept 293   std::source_location::current()) noexcept
294   { 294   {
HITCBC 295   3 p_->loc = loc; 295   3 p_->loc = loc;
HITCBC 296   3 p_->stopped = true; 296   3 p_->stopped = true;
HITCBC 297   3 } 297   3 }
298   298  
299   /** Signal a test failure with an exception and stop execution. 299   /** Signal a test failure with an exception and stop execution.
300   300  
301   Call this from the test function to indicate a failure 301   Call this from the test function to indicate a failure
302   condition with an associated exception. Both @ref armed 302   condition with an associated exception. Both @ref armed
303   and @ref inert return a failed @ref result with 303   and @ref inert return a failed @ref result with
304   the captured exception pointer. 304   the captured exception pointer.
305   305  
306   @par Example 306   @par Example
307   307  
308   @par !example example_2 308   @par !example example_2
309   309  
310   310  
311   @param ep The exception pointer to capture. 311   @param ep The exception pointer to capture.
312   312  
313   @param loc The source location of the call site, 313   @param loc The source location of the call site,
314   captured automatically. 314   captured automatically.
315   */ 315   */
316   void 316   void
HITCBC 317   2 fail( 317   2 fail(
318   std::exception_ptr ep, 318   std::exception_ptr ep,
319   std::source_location loc = 319   std::source_location loc =
320   std::source_location::current()) noexcept 320   std::source_location::current()) noexcept
321   { 321   {
HITCBC 322   2 p_->ep = ep; 322   2 p_->ep = ep;
HITCBC 323   2 p_->loc = loc; 323   2 p_->loc = loc;
HITCBC 324   2 p_->stopped = true; 324   2 p_->stopped = true;
HITCBC 325   2 } 325   2 }
326   326  
327   private: 327   private:
328   /* Drive the two-phase armed loop, invoking `do_iter` once per round. 328   /* Drive the two-phase armed loop, invoking `do_iter` once per round.
329   329  
330   Phase 1 delivers injected failures as error codes; phase 2 as 330   Phase 1 delivers injected failures as error codes; phase 2 as
331   exceptions. Shared by the two coroutine `armed` overloads: each 331   exceptions. Shared by the two coroutine `armed` overloads: each
332   supplies a nullary `do_iter` that runs one iteration — via 332   supplies a nullary `do_iter` that runs one iteration — via
333   @ref run_blocking, or via a caller-supplied runner — so the round 333   @ref run_blocking, or via a caller-supplied runner — so the round
334   sequence and failure handling stay identical across them. 334   sequence and failure handling stay identical across them.
335   */ 335   */
336   template<class DoIter> 336   template<class DoIter>
337   result 337   result
HITCBC 338   134 run_phases(DoIter&& do_iter) 338   134 run_phases(DoIter&& do_iter)
339   { 339   {
HITCBC 340   134 result r; 340   134 result r;
341   341  
342   // Phase 1: error code mode 342   // Phase 1: error code mode
HITCBC 343   134 p_->throws = false; 343   134 p_->throws = false;
HITCBC 344   134 p_->inert = false; 344   134 p_->inert = false;
HITCBC 345   134 p_->n = (std::numeric_limits<std::size_t>::max)(); 345   134 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 346   581 while(*this) 346   581 while(*this)
347   { 347   {
348   try 348   try
349   { 349   {
HITCBC 350   448 do_iter(); 350   448 do_iter();
351   } 351   }
HITCBC 352   2 catch(...) 352   2 catch(...)
353   { 353   {
HITCBC 354   1 r.success = false; 354   1 r.success = false;
HITCBC 355   1 r.loc = p_->loc; 355   1 r.loc = p_->loc;
HITCBC 356   1 r.ep = p_->ep; 356   1 r.ep = p_->ep;
HITCBC 357   1 p_->inert = true; 357   1 p_->inert = true;
HITCBC 358   1 return r; 358   1 return r;
359   } 359   }
HITCBC 360   447 if(p_->stopped) 360   447 if(p_->stopped)
361   { 361   {
MISUBC 362   r.success = false; 362   r.success = false;
MISUBC 363   r.loc = p_->loc; 363   r.loc = p_->loc;
MISUBC 364   r.ep = p_->ep; 364   r.ep = p_->ep;
MISUBC 365   p_->inert = true; 365   p_->inert = true;
MISUBC 366   return r; 366   return r;
367   } 367   }
368   } 368   }
369   369  
370   // Phase 2: exception mode 370   // Phase 2: exception mode
HITCBC 371   133 p_->throws = true; 371   133 p_->throws = true;
HITCBC 372   133 p_->n = (std::numeric_limits<std::size_t>::max)(); 372   133 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 373   133 p_->i = 0; 373   133 p_->i = 0;
HITCBC 374   133 p_->triggered = false; 374   133 p_->triggered = false;
HITCBC 375   578 while(*this) 375   578 while(*this)
376   { 376   {
377   try 377   try
378   { 378   {
HITCBC 379   445 do_iter(); 379   445 do_iter();
380   } 380   }
HITCBC 381   624 catch(std::system_error const& ex) 381   624 catch(std::system_error const& ex)
382   { 382   {
HITCBC 383   312 if(ex.code() != p_->ec) 383   312 if(ex.code() != p_->ec)
384   { 384   {
MISUBC 385   r.success = false; 385   r.success = false;
MISUBC 386   r.loc = p_->loc; 386   r.loc = p_->loc;
MISUBC 387   r.ep = p_->ep; 387   r.ep = p_->ep;
MISUBC 388   p_->inert = true; 388   p_->inert = true;
MISUBC 389   return r; 389   return r;
390   } 390   }
391   } 391   }
MISUBC 392   catch(...) 392   catch(...)
393   { 393   {
MISUBC 394   r.success = false; 394   r.success = false;
MISUBC 395   r.loc = p_->loc; 395   r.loc = p_->loc;
MISUBC 396   r.ep = p_->ep; 396   r.ep = p_->ep;
MISUBC 397   p_->inert = true; 397   p_->inert = true;
MISUBC 398   return r; 398   return r;
399   } 399   }
HITCBC 400   445 if(p_->stopped) 400   445 if(p_->stopped)
401   { 401   {
MISUBC 402   r.success = false; 402   r.success = false;
MISUBC 403   r.loc = p_->loc; 403   r.loc = p_->loc;
MISUBC 404   r.ep = p_->ep; 404   r.ep = p_->ep;
MISUBC 405   p_->inert = true; 405   p_->inert = true;
MISUBC 406   return r; 406   return r;
407   } 407   }
408   } 408   }
HITCBC 409   133 p_->inert = true; 409   133 p_->inert = true;
HITCBC 410   133 return r; 410   133 return r;
MISUBC 411   } 411   }
412   412  
413   public: 413   public:
414   /** Run a test function with systematic failure injection. 414   /** Run a test function with systematic failure injection.
415   415  
416   Repeatedly invokes the provided function, failing at 416   Repeatedly invokes the provided function, failing at
417   successive points until the function completes without 417   successive points until the function completes without
418   encountering a failure. First runs the complete loop 418   encountering a failure. First runs the complete loop
419   using error codes, then runs using exceptions. 419   using error codes, then runs using exceptions.
420   420  
421   @par Example 421   @par Example
422   422  
423   @par !example example_2 423   @par !example example_2
424   424  
425   425  
426   @param fn The test function to invoke. It receives 426   @param fn The test function to invoke. It receives
427   a reference to the fuse and should call @ref maybe_fail 427   a reference to the fuse and should call @ref maybe_fail
428   at each potential failure point. 428   at each potential failure point.
429   429  
430   @return A @ref result indicating success or failure. 430   @return A @ref result indicating success or failure.
431   On failure, `result::loc` contains the source location 431   On failure, `result::loc` contains the source location
432   of the last @ref maybe_fail or @ref fail call. 432   of the last @ref maybe_fail or @ref fail call.
433   */ 433   */
434   template<class F> 434   template<class F>
435   result 435   result
HITCBC 436   26 armed(F&& fn) 436   26 armed(F&& fn)
437   { 437   {
HITCBC 438   26 result r; 438   26 result r;
439   439  
440   // Phase 1: error code mode 440   // Phase 1: error code mode
HITCBC 441   26 p_->throws = false; 441   26 p_->throws = false;
HITCBC 442   26 p_->inert = false; 442   26 p_->inert = false;
HITCBC 443   26 p_->n = (std::numeric_limits<std::size_t>::max)(); 443   26 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 444   92 while(*this) 444   92 while(*this)
445   { 445   {
446   try 446   try
447   { 447   {
HITCBC 448   72 fn(*this); 448   72 fn(*this);
449   } 449   }
HITCBC 450   6 catch(...) 450   6 catch(...)
451   { 451   {
HITCBC 452   3 r.success = false; 452   3 r.success = false;
HITCBC 453   3 r.loc = p_->loc; 453   3 r.loc = p_->loc;
HITCBC 454   3 r.ep = p_->ep; 454   3 r.ep = p_->ep;
HITCBC 455   3 p_->inert = true; 455   3 p_->inert = true;
HITCBC 456   3 return r; 456   3 return r;
457   } 457   }
HITCBC 458   69 if(p_->stopped) 458   69 if(p_->stopped)
459   { 459   {
HITCBC 460   3 r.success = false; 460   3 r.success = false;
HITCBC 461   3 r.loc = p_->loc; 461   3 r.loc = p_->loc;
HITCBC 462   3 r.ep = p_->ep; 462   3 r.ep = p_->ep;
HITCBC 463   3 p_->inert = true; 463   3 p_->inert = true;
HITCBC 464   3 return r; 464   3 return r;
465   } 465   }
466   } 466   }
467   467  
468   // Phase 2: exception mode 468   // Phase 2: exception mode
HITCBC 469   20 p_->throws = true; 469   20 p_->throws = true;
HITCBC 470   20 p_->n = (std::numeric_limits<std::size_t>::max)(); 470   20 p_->n = (std::numeric_limits<std::size_t>::max)();
HITCBC 471   20 p_->i = 0; 471   20 p_->i = 0;
HITCBC 472   20 p_->triggered = false; 472   20 p_->triggered = false;
HITCBC 473   75 while(*this) 473   75 while(*this)
474   { 474   {
475   try 475   try
476   { 476   {
HITCBC 477   55 fn(*this); 477   55 fn(*this);
478   } 478   }
HITCBC 479   70 catch(std::system_error const& ex) 479   70 catch(std::system_error const& ex)
480   { 480   {
HITCBC 481   35 if(ex.code() != p_->ec) 481   35 if(ex.code() != p_->ec)
482   { 482   {
MISUBC 483   r.success = false; 483   r.success = false;
MISUBC 484   r.loc = p_->loc; 484   r.loc = p_->loc;
MISUBC 485   r.ep = p_->ep; 485   r.ep = p_->ep;
MISUBC 486   p_->inert = true; 486   p_->inert = true;
MISUBC 487   return r; 487   return r;
488   } 488   }
489   } 489   }
MISUBC 490   catch(...) 490   catch(...)
491   { 491   {
MISUBC 492   r.success = false; 492   r.success = false;
MISUBC 493   r.loc = p_->loc; 493   r.loc = p_->loc;
MISUBC 494   r.ep = p_->ep; 494   r.ep = p_->ep;
MISUBC 495   p_->inert = true; 495   p_->inert = true;
MISUBC 496   return r; 496   return r;
497   } 497   }
HITCBC 498   55 if(p_->stopped) 498   55 if(p_->stopped)
499   { 499   {
MISUBC 500   r.success = false; 500   r.success = false;
MISUBC 501   r.loc = p_->loc; 501   r.loc = p_->loc;
MISUBC 502   r.ep = p_->ep; 502   r.ep = p_->ep;
MISUBC 503   p_->inert = true; 503   p_->inert = true;
MISUBC 504   return r; 504   return r;
505   } 505   }
506   } 506   }
HITCBC 507   20 p_->inert = true; 507   20 p_->inert = true;
HITCBC 508   20 return r; 508   20 return r;
MISUBC 509   } 509   }
510   510  
511   /** Run a coroutine test function with systematic failure injection. 511   /** Run a coroutine test function with systematic failure injection.
512   512  
513   Repeatedly invokes the provided coroutine function, failing at 513   Repeatedly invokes the provided coroutine function, failing at
514   successive points until the function completes without 514   successive points until the function completes without
515   encountering a failure. First runs the complete loop 515   encountering a failure. First runs the complete loop
516   using error codes, then runs using exceptions. 516   using error codes, then runs using exceptions.
517   517  
518   This overload handles lambdas that return an @ref IoRunnable 518   This overload handles lambdas that return an @ref IoRunnable
519   (such as `task<void>`), executing them synchronously via 519   (such as `task<void>`), executing them synchronously via
520   @ref run_blocking. 520   @ref run_blocking.
521   521  
522   @par Example 522   @par Example
523   523  
524   @par !example example_3 524   @par !example example_3
525   525  
526   526  
527   @param fn The coroutine test function to invoke. It receives 527   @param fn The coroutine test function to invoke. It receives
528   a reference to the fuse and should call @ref maybe_fail 528   a reference to the fuse and should call @ref maybe_fail
529   at each potential failure point. 529   at each potential failure point.
530   530  
531   @return A @ref result indicating success or failure. 531   @return A @ref result indicating success or failure.
532   On failure, `result::loc` contains the source location 532   On failure, `result::loc` contains the source location
533   of the last @ref maybe_fail or @ref fail call. 533   of the last @ref maybe_fail or @ref fail call.
534   */ 534   */
535   template<class F> 535   template<class F>
536   requires IoRunnable<std::invoke_result_t<F, fuse&>> 536   requires IoRunnable<std::invoke_result_t<F, fuse&>>
537   result 537   result
HITCBC 538   131 armed(F&& fn) 538   131 armed(F&& fn)
539   { 539   {
HITCBC 540   1445 return run_phases([&]{ run_blocking()(fn(*this)); }); 540   1445 return run_phases([&]{ run_blocking()(fn(*this)); });
541   } 541   }
542   542  
543   /** Run a coroutine test function on a caller-supplied runner. 543   /** Run a coroutine test function on a caller-supplied runner.
544   544  
545   Behaves like the @ref IoRunnable overload of @ref armed, but 545   Behaves like the @ref IoRunnable overload of @ref armed, but
546   instead of driving each iteration through @ref run_blocking, it 546   instead of driving each iteration through @ref run_blocking, it
547   hands the coroutine to `run_one`. This lets a caller run each 547   hands the coroutine to `run_one`. This lets a caller run each
548   iteration on any execution context it chooses. Operations built 548   iteration on any execution context it chooses. Operations built
549   on `corosio::timeout` or `corosio::delay` in particular require 549   on `corosio::timeout` or `corosio::delay` in particular require
550   an `io_context`, because they abort on a non-`io_context` 550   an `io_context`, because they abort on a non-`io_context`
551   executor. `fuse` never learns about the context; 551   executor. `fuse` never learns about the context;
552   the caller owns the drive loop. 552   the caller owns the drive loop.
553   553  
554   @par Runner contract 554   @par Runner contract
555   `run_one` is invoked once per round with the @ref IoRunnable 555   `run_one` is invoked once per round with the @ref IoRunnable
556   produced by `fn`. It must run that task to completion 556   produced by `fn`. It must run that task to completion
557   synchronously and *return* any exception the task raised as a 557   synchronously and *return* any exception the task raised as a
558   `std::exception_ptr` (null on success). It must not rethrow. 558   `std::exception_ptr` (null on success). It must not rethrow.
559   `armed` rethrows the returned pointer from its own synchronous 559   `armed` rethrows the returned pointer from its own synchronous
560   code, so the exception phase observes injected failures. An 560   code, so the exception phase observes injected failures. An
561   exception escaping a `run_async` completion handler would 561   exception escaping a `run_async` completion handler would
562   instead call `std::terminate`. Capture the exception in the error 562   instead call `std::terminate`. Capture the exception in the error
563   handler and return it once the run loop is done. 563   handler and return it once the run loop is done.
564   564  
565   @par Example 565   @par Example
566   @par !example example_1 566   @par !example example_1
567   567  
568   568  
569   @param run_one A callable invoked with each iteration's task; it 569   @param run_one A callable invoked with each iteration's task; it
570   runs the task to completion and returns any escaped exception 570   runs the task to completion and returns any escaped exception
571   (null on success) without rethrowing. 571   (null on success) without rethrowing.
572   572  
573   @param fn The coroutine test function to invoke. 573   @param fn The coroutine test function to invoke.
574   574  
575   @return A @ref result indicating success or failure. 575   @return A @ref result indicating success or failure.
576   */ 576   */
577   template<class Runner, class F> 577   template<class Runner, class F>
578   requires IoRunnable<std::invoke_result_t<F, fuse&>> 578   requires IoRunnable<std::invoke_result_t<F, fuse&>>
579   && std::same_as< 579   && std::same_as<
580   std::invoke_result_t<Runner&, std::invoke_result_t<F, fuse&>>, 580   std::invoke_result_t<Runner&, std::invoke_result_t<F, fuse&>>,
581   std::exception_ptr> 581   std::exception_ptr>
582   result 582   result
HITCBC 583   3 armed(Runner&& run_one, F&& fn) 583   3 armed(Runner&& run_one, F&& fn)
584   { 584   {
HITCBC 585   14 return run_phases([&]{ 585   14 return run_phases([&]{
HITCBC 586   23 if(auto ep = run_one(fn(*this))) 586   23 if(auto ep = run_one(fn(*this)))
HITCBC 587   12 std::rethrow_exception(ep); 587   12 std::rethrow_exception(ep);
HITCBC 588   6 }); 588   6 });
589   } 589   }
590   590  
591   /** Alias for @ref armed. 591   /** Alias for @ref armed.
592   592  
593   Allows the fuse to be invoked directly as a function 593   Allows the fuse to be invoked directly as a function
594   object for more concise syntax. 594   object for more concise syntax.
595   595  
596   @par Example 596   @par Example
597   597  
598   @par !example example 598   @par !example example
599   599  
600   600  
601   @param fn The test function to run under failure injection. 601   @param fn The test function to run under failure injection.
602   602  
603   @return The @ref result of the armed run. 603   @return The @ref result of the armed run.
604   604  
605   @see armed 605   @see armed
606   */ 606   */
607   template<class F> 607   template<class F>
608   result 608   result
HITCBC 609   15 operator()(F&& fn) 609   15 operator()(F&& fn)
610   { 610   {
HITCBC 611   15 return armed(std::forward<F>(fn)); 611   15 return armed(std::forward<F>(fn));
612   } 612   }
613   613  
614   /** Alias for @ref armed (coroutine overload). 614   /** Alias for @ref armed (coroutine overload).
615   615  
616   @param fn The test coroutine factory to run under failure injection. 616   @param fn The test coroutine factory to run under failure injection.
617   617  
618   @return The @ref result of the armed run. 618   @return The @ref result of the armed run.
619   619  
620   @see armed 620   @see armed
621   */ 621   */
622   template<class F> 622   template<class F>
623   requires IoRunnable<std::invoke_result_t<F, fuse&>> 623   requires IoRunnable<std::invoke_result_t<F, fuse&>>
624   result 624   result
625   operator()(F&& fn) 625   operator()(F&& fn)
626   { 626   {
627   return armed(std::forward<F>(fn)); 627   return armed(std::forward<F>(fn));
628   } 628   }
629   629  
630   /** Run a test function once without failure injection. 630   /** Run a test function once without failure injection.
631   631  
632   Invokes the provided function exactly once. Calls to 632   Invokes the provided function exactly once. Calls to
633   @ref maybe_fail always return an empty error code and 633   @ref maybe_fail always return an empty error code and
634   never throw. Only explicit calls to @ref fail can 634   never throw. Only explicit calls to @ref fail can
635   signal a test failure. 635   signal a test failure.
636   636  
637   This is useful for running tests where you want to 637   This is useful for running tests where you want to
638   manually control failures, or for quick single-run 638   manually control failures, or for quick single-run
639   tests without systematic error injection. 639   tests without systematic error injection.
640   640  
641   @par Example 641   @par Example
642   642  
643   @par !example example_1 643   @par !example example_1
644   644  
645   645  
646   @param fn The test function to invoke. It receives 646   @param fn The test function to invoke. It receives
647   a reference to the fuse. Calls to @ref maybe_fail 647   a reference to the fuse. Calls to @ref maybe_fail
648   always succeed. 648   always succeed.
649   649  
650   @return A @ref result indicating success or failure. 650   @return A @ref result indicating success or failure.
651   On failure, `result::loc` contains the source location 651   On failure, `result::loc` contains the source location
652   of the @ref fail call. 652   of the @ref fail call.
653   */ 653   */
654   template<class F> 654   template<class F>
655   result 655   result
HITCBC 656   9 inert(F&& fn) 656   9 inert(F&& fn)
657   { 657   {
HITCBC 658   9 result r; 658   9 result r;
HITCBC 659   9 p_->inert = true; 659   9 p_->inert = true;
660   try 660   try
661   { 661   {
HITCBC 662   9 fn(*this); 662   9 fn(*this);
663   } 663   }
HITCBC 664   2 catch(...) 664   2 catch(...)
665   { 665   {
HITCBC 666   1 r.success = false; 666   1 r.success = false;
HITCBC 667   1 r.loc = p_->loc; 667   1 r.loc = p_->loc;
HITCBC 668   1 r.ep = std::current_exception(); 668   1 r.ep = std::current_exception();
HITCBC 669   1 return r; 669   1 return r;
670   } 670   }
HITCBC 671   8 if(p_->stopped) 671   8 if(p_->stopped)
672   { 672   {
HITCBC 673   2 r.success = false; 673   2 r.success = false;
HITCBC 674   2 r.loc = p_->loc; 674   2 r.loc = p_->loc;
HITCBC 675   2 r.ep = p_->ep; 675   2 r.ep = p_->ep;
676   } 676   }
HITCBC 677   8 return r; 677   8 return r;
MISUBC 678   } 678   }
679   679  
680   /** Run a coroutine test function once without failure injection. 680   /** Run a coroutine test function once without failure injection.
681   681  
682   Invokes the provided coroutine function exactly once using 682   Invokes the provided coroutine function exactly once using
683   @ref run_blocking. Calls to @ref maybe_fail always return 683   @ref run_blocking. Calls to @ref maybe_fail always return
684   an empty error code and never throw. Only explicit calls 684   an empty error code and never throw. Only explicit calls
685   to @ref fail can signal a test failure. 685   to @ref fail can signal a test failure.
686   686  
687   @par Example 687   @par Example
688   688  
689   @par !example example_2 689   @par !example example_2
690   690  
691   691  
692   @param fn The coroutine test function to invoke. It receives 692   @param fn The coroutine test function to invoke. It receives
693   a reference to the fuse. Calls to @ref maybe_fail 693   a reference to the fuse. Calls to @ref maybe_fail
694   always succeed. 694   always succeed.
695   695  
696   @return A @ref result indicating success or failure. 696   @return A @ref result indicating success or failure.
697   On failure, `result::loc` contains the source location 697   On failure, `result::loc` contains the source location
698   of the @ref fail call. 698   of the @ref fail call.
699   */ 699   */
700   template<class F> 700   template<class F>
701   requires IoRunnable<std::invoke_result_t<F, fuse&>> 701   requires IoRunnable<std::invoke_result_t<F, fuse&>>
702   result 702   result
HITCBC 703   39 inert(F&& fn) 703   39 inert(F&& fn)
704   { 704   {
HITCBC 705   39 result r; 705   39 result r;
HITCBC 706   39 p_->inert = true; 706   39 p_->inert = true;
707   try 707   try
708   { 708   {
HITCBC 709   39 run_blocking()(fn(*this)); 709   39 run_blocking()(fn(*this));
710   } 710   }
MISUBC 711   catch(...) 711   catch(...)
712   { 712   {
MISUBC 713   r.success = false; 713   r.success = false;
MISUBC 714   r.loc = p_->loc; 714   r.loc = p_->loc;
MISUBC 715   r.ep = std::current_exception(); 715   r.ep = std::current_exception();
MISUBC 716   return r; 716   return r;
717   } 717   }
HITCBC 718   39 if(p_->stopped) 718   39 if(p_->stopped)
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;
723   } 723   }
HITCBC 724   39 return r; 724   39 return r;
MISUBC 725   } 725   }
726   }; 726   };
727   727  
728   } // test 728   } // test
729   } // capy 729   } // capy
730   } // boost 730   } // boost
731   731  
732   #endif 732   #endif