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