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 : @code
76 : fuse()([](fuse& f) {
77 : auto ec = f.maybe_fail();
78 : if(ec)
79 : return;
80 :
81 : ec = f.maybe_fail();
82 : if(ec)
83 : return;
84 : });
85 : @endcode
86 :
87 : @par Named Fuse with armed()
88 :
89 : @code
90 : fuse f;
91 : MyObject obj(f);
92 : auto r = f.armed([&](fuse&) {
93 : obj.do_something();
94 : });
95 : @endcode
96 :
97 : @par Using inert() for Single-Run Tests
98 :
99 : @code
100 : fuse f;
101 : auto r = f.inert([](fuse& f) {
102 : auto ec = f.maybe_fail(); // Always succeeds
103 : if(some_condition)
104 : f.fail(); // Only way to signal failure
105 : });
106 : @endcode
107 :
108 : @par Dependency Injection (Standalone Usage)
109 :
110 : A default-constructed fuse is a no-op when used outside
111 : of @ref armed or @ref inert. This enables passing a fuse
112 : to classes for dependency injection without affecting
113 : normal operation.
114 :
115 : @code
116 : class MyService
117 : {
118 : fuse& f_;
119 : public:
120 : explicit MyService(fuse& f) : f_(f) {}
121 :
122 : std::error_code do_work()
123 : {
124 : auto ec = f_.maybe_fail(); // No-op outside armed/inert
125 : if(ec)
126 : return ec;
127 : // ... actual work ...
128 : return {};
129 : }
130 : };
131 :
132 : // Production usage - fuse is no-op
133 : fuse f;
134 : MyService svc(f);
135 : svc.do_work(); // maybe_fail() returns {} always
136 :
137 : // Test usage - failures are injected
138 : auto r = f.armed([&](fuse&) {
139 : svc.do_work(); // maybe_fail() triggers failures
140 : });
141 : @endcode
142 :
143 : @par Custom Error Code
144 :
145 : @code
146 : auto custom_ec = make_error_code(
147 : std::errc::operation_canceled);
148 : fuse f(custom_ec);
149 : auto r = f.armed([](fuse& f) {
150 : auto ec = f.maybe_fail();
151 : if(ec)
152 : return;
153 : });
154 : @endcode
155 :
156 : @par Checking the Result
157 :
158 : @code
159 : fuse f;
160 : auto r = f([](fuse& f) {
161 : auto ec = f.maybe_fail();
162 : if(ec)
163 : return;
164 : });
165 :
166 : if(!r)
167 : {
168 : std::cerr << "Failure at "
169 : << r.loc.file_name() << ":"
170 : << r.loc.line() << "\n";
171 : }
172 : @endcode
173 :
174 : @par Test Framework Integration
175 :
176 : @code
177 : fuse f;
178 : auto r = f([](fuse& f) {
179 : auto ec = f.maybe_fail();
180 : if(ec)
181 : return;
182 : });
183 :
184 : // Boost.Test
185 : BOOST_TEST(r.success);
186 : if(!r)
187 : BOOST_TEST_MESSAGE("Failed at " << r.loc.file_name()
188 : << ":" << r.loc.line());
189 :
190 : // Catch2
191 : REQUIRE(r.success);
192 : if(!r)
193 : INFO("Failed at " << r.loc.file_name()
194 : << ":" << r.loc.line());
195 : @endcode
196 : */
197 : class fuse
198 : {
199 : struct state
200 : {
201 : std::size_t n = (std::numeric_limits<std::size_t>::max)();
202 : std::size_t i = 0;
203 : bool triggered = false;
204 : bool throws = false;
205 : bool stopped = false;
206 : bool inert = true;
207 : std::error_code ec;
208 : std::source_location loc;
209 : std::exception_ptr ep;
210 : };
211 :
212 : std::shared_ptr<state> p_;
213 :
214 : /** Return true if testing should continue.
215 :
216 : On the first call, initializes the failure point to 0.
217 : After a triggered failure, increments the failure point
218 : and resets for the next iteration. Returns false when
219 : the test completes without triggering a failure.
220 : */
221 HIT 1326 : explicit operator bool() const noexcept
222 : {
223 1326 : auto& s = *p_;
224 1326 : if(s.n == (std::numeric_limits<std::size_t>::max)())
225 : {
226 : // First call: start round 0
227 313 : s.n = 0;
228 313 : return true;
229 : }
230 1013 : if(s.triggered)
231 : {
232 : // Previous round triggered, try next failure point
233 707 : s.n++;
234 707 : s.i = 0;
235 707 : s.triggered = false;
236 707 : return true;
237 : }
238 : // Test completed without trigger: success
239 306 : return false;
240 : }
241 :
242 : public:
243 : /** Converts to `bool`, reporting success, and carries the failure point on failure.
244 :
245 : Contains the outcome of @ref armed or @ref inert
246 : and, on failure, the source location of the failing
247 : point. Converts to `bool` for convenient success
248 : checking.
249 :
250 : @par Example
251 :
252 : @code
253 : fuse f;
254 : auto r = f([](fuse& f) {
255 : auto ec = f.maybe_fail();
256 : if(ec)
257 : return;
258 : });
259 :
260 : if(!r)
261 : {
262 : std::cerr << "Failure at "
263 : << r.loc.file_name() << ":"
264 : << r.loc.line() << "\n";
265 : }
266 : @endcode
267 : */
268 : struct result
269 : {
270 : /// Source location of the failing point, set only on failure.
271 : std::source_location loc = {};
272 :
273 : /// Exception captured by @ref fail, or null if none.
274 : std::exception_ptr ep = nullptr;
275 :
276 : /// True if the test completed without a failure.
277 : bool success = true;
278 :
279 : /** Return whether the test completed without a failure.
280 :
281 : @return @ref success.
282 : */
283 42 : constexpr explicit operator bool() const noexcept
284 : {
285 42 : return success;
286 : }
287 : };
288 :
289 : /** Construct a fuse with a custom error code.
290 :
291 : @par Example
292 :
293 : @code
294 : auto custom_ec = make_error_code(
295 : std::errc::operation_canceled);
296 : fuse f(custom_ec);
297 :
298 : std::error_code captured_ec;
299 : auto r = f([&](fuse& f) {
300 : auto ec = f.maybe_fail();
301 : if(ec)
302 : {
303 : captured_ec = ec;
304 : return;
305 : }
306 : });
307 :
308 : assert(captured_ec == custom_ec);
309 : @endcode
310 :
311 : @param ec The error code to deliver at failure points.
312 : */
313 274 : explicit fuse(std::error_code ec)
314 274 : : p_(std::make_shared<state>())
315 : {
316 274 : p_->ec = ec;
317 274 : }
318 :
319 : /** Construct a fuse with the default error code.
320 :
321 : The default error code is `error::test_failure`.
322 :
323 : @par Example
324 :
325 : @code
326 : fuse f;
327 : std::error_code captured_ec;
328 :
329 : auto r = f([&](fuse& f) {
330 : auto ec = f.maybe_fail();
331 : if(ec)
332 : {
333 : captured_ec = ec;
334 : return;
335 : }
336 : });
337 :
338 : assert(captured_ec == error::test_failure);
339 : @endcode
340 : */
341 271 : fuse()
342 271 : : fuse(error::test_failure)
343 : {
344 271 : }
345 :
346 : /** Return an error or throw at the current failure point.
347 :
348 : When running under @ref armed, increments the internal
349 : counter. When the counter reaches the current failure
350 : point, returns the stored error code (or throws
351 : `std::system_error` in exception mode) and records
352 : the source location.
353 :
354 : When called outside of @ref armed or @ref inert (standalone
355 : usage), or when running under @ref inert, always returns
356 : an empty error code. This enables dependency injection
357 : where the fuse is a no-op in production code.
358 :
359 : @par Example
360 :
361 : @code
362 : fuse f;
363 : auto r = f([](fuse& f) {
364 : // Error code mode: returns the error
365 : auto ec = f.maybe_fail();
366 : if(ec)
367 : return;
368 :
369 : // Exception mode: throws system_error
370 : ec = f.maybe_fail();
371 : if(ec)
372 : return;
373 : });
374 : @endcode
375 :
376 : @par Standalone Usage
377 :
378 : @code
379 : fuse f;
380 : auto ec = f.maybe_fail(); // Always returns {} (no-op)
381 : @endcode
382 :
383 : @param loc The source location of the call site,
384 : captured automatically.
385 :
386 : @return The stored error code if at the failure point,
387 : otherwise an empty error code. In exception mode,
388 : throws instead of returning an error. When called
389 : outside @ref armed, or when running under @ref inert,
390 : always returns an empty error code.
391 :
392 : @throws std::system_error When in exception mode
393 : and at the failure point (not thrown outside @ref armed).
394 : */
395 : std::error_code
396 1746 : maybe_fail(
397 : std::source_location loc = std::source_location::current())
398 : {
399 1746 : auto& s = *p_;
400 1746 : if(s.inert)
401 323 : return {};
402 1423 : if(s.i < s.n)
403 1152 : ++s.i;
404 1423 : if(s.i == s.n)
405 : {
406 707 : s.triggered = true;
407 707 : s.loc = loc;
408 707 : if(s.throws)
409 347 : throw std::system_error(s.ec);
410 360 : return s.ec;
411 : }
412 716 : return {};
413 : }
414 :
415 : /** Signal a test failure and stop execution.
416 :
417 : Call this from the test function to indicate a failure
418 : condition. Both @ref armed and @ref inert return
419 : a failed @ref result immediately.
420 :
421 : @par Example
422 :
423 : @code
424 : fuse f;
425 : auto r = f([](fuse& f) {
426 : auto ec = f.maybe_fail();
427 : if(ec)
428 : return;
429 :
430 : // Explicit failure when a condition is not met
431 : if(some_value != expected)
432 : {
433 : f.fail();
434 : return;
435 : }
436 : });
437 :
438 : if(!r)
439 : {
440 : std::cerr << "Test failed at "
441 : << r.loc.file_name() << ":"
442 : << r.loc.line() << "\n";
443 : }
444 : @endcode
445 :
446 : @param loc The source location of the call site,
447 : captured automatically.
448 : */
449 : void
450 3 : fail(
451 : std::source_location loc =
452 : std::source_location::current()) noexcept
453 : {
454 3 : p_->loc = loc;
455 3 : p_->stopped = true;
456 3 : }
457 :
458 : /** Signal a test failure with an exception and stop execution.
459 :
460 : Call this from the test function to indicate a failure
461 : condition with an associated exception. Both @ref armed
462 : and @ref inert return a failed @ref result with
463 : the captured exception pointer.
464 :
465 : @par Example
466 :
467 : @code
468 : fuse f;
469 : auto r = f([](fuse& f) {
470 : try
471 : {
472 : do_something();
473 : }
474 : catch(...)
475 : {
476 : f.fail(std::current_exception());
477 : return;
478 : }
479 : });
480 :
481 : if(!r)
482 : {
483 : try
484 : {
485 : if(r.ep)
486 : std::rethrow_exception(r.ep);
487 : }
488 : catch(std::exception const& e)
489 : {
490 : std::cerr << "Exception: " << e.what() << "\n";
491 : }
492 : }
493 : @endcode
494 :
495 : @param ep The exception pointer to capture.
496 :
497 : @param loc The source location of the call site,
498 : captured automatically.
499 : */
500 : void
501 2 : fail(
502 : std::exception_ptr ep,
503 : std::source_location loc =
504 : std::source_location::current()) noexcept
505 : {
506 2 : p_->ep = ep;
507 2 : p_->loc = loc;
508 2 : p_->stopped = true;
509 2 : }
510 :
511 : private:
512 : /* Drive the two-phase armed loop, invoking `do_iter` once per round.
513 :
514 : Phase 1 delivers injected failures as error codes; phase 2 as
515 : exceptions. Shared by the two coroutine `armed` overloads: each
516 : supplies a nullary `do_iter` that runs one iteration — via
517 : @ref run_blocking, or via a caller-supplied runner — so the round
518 : sequence and failure handling stay identical across them.
519 : */
520 : template<class DoIter>
521 : result
522 134 : run_phases(DoIter&& do_iter)
523 : {
524 134 : result r;
525 :
526 : // Phase 1: error code mode
527 134 : p_->throws = false;
528 134 : p_->inert = false;
529 134 : p_->n = (std::numeric_limits<std::size_t>::max)();
530 581 : while(*this)
531 : {
532 : try
533 : {
534 448 : do_iter();
535 : }
536 2 : catch(...)
537 : {
538 1 : r.success = false;
539 1 : r.loc = p_->loc;
540 1 : r.ep = p_->ep;
541 1 : p_->inert = true;
542 1 : return r;
543 : }
544 447 : if(p_->stopped)
545 : {
546 MIS 0 : r.success = false;
547 0 : r.loc = p_->loc;
548 0 : r.ep = p_->ep;
549 0 : p_->inert = true;
550 0 : return r;
551 : }
552 : }
553 :
554 : // Phase 2: exception mode
555 HIT 133 : p_->throws = true;
556 133 : p_->n = (std::numeric_limits<std::size_t>::max)();
557 133 : p_->i = 0;
558 133 : p_->triggered = false;
559 578 : while(*this)
560 : {
561 : try
562 : {
563 445 : do_iter();
564 : }
565 624 : catch(std::system_error const& ex)
566 : {
567 312 : if(ex.code() != p_->ec)
568 : {
569 MIS 0 : r.success = false;
570 0 : r.loc = p_->loc;
571 0 : r.ep = p_->ep;
572 0 : p_->inert = true;
573 0 : return r;
574 : }
575 : }
576 0 : catch(...)
577 : {
578 0 : r.success = false;
579 0 : r.loc = p_->loc;
580 0 : r.ep = p_->ep;
581 0 : p_->inert = true;
582 0 : return r;
583 : }
584 HIT 445 : if(p_->stopped)
585 : {
586 MIS 0 : r.success = false;
587 0 : r.loc = p_->loc;
588 0 : r.ep = p_->ep;
589 0 : p_->inert = true;
590 0 : return r;
591 : }
592 : }
593 HIT 133 : p_->inert = true;
594 133 : return r;
595 MIS 0 : }
596 :
597 : public:
598 : /** Run a test function with systematic failure injection.
599 :
600 : Repeatedly invokes the provided function, failing at
601 : successive points until the function completes without
602 : encountering a failure. First runs the complete loop
603 : using error codes, then runs using exceptions.
604 :
605 : @par Example
606 :
607 : @code
608 : fuse f;
609 : auto r = f.armed([](fuse& f) {
610 : auto ec = f.maybe_fail();
611 : if(ec)
612 : return;
613 :
614 : ec = f.maybe_fail();
615 : if(ec)
616 : return;
617 : });
618 :
619 : if(!r)
620 : {
621 : std::cerr << "Failure at "
622 : << r.loc.file_name() << ":"
623 : << r.loc.line() << "\n";
624 : }
625 : @endcode
626 :
627 : @param fn The test function to invoke. It receives
628 : a reference to the fuse and should call @ref maybe_fail
629 : at each potential failure point.
630 :
631 : @return A @ref result indicating success or failure.
632 : On failure, `result::loc` contains the source location
633 : of the last @ref maybe_fail or @ref fail call.
634 : */
635 : template<class F>
636 : result
637 HIT 26 : armed(F&& fn)
638 : {
639 26 : result r;
640 :
641 : // Phase 1: error code mode
642 26 : p_->throws = false;
643 26 : p_->inert = false;
644 26 : p_->n = (std::numeric_limits<std::size_t>::max)();
645 92 : while(*this)
646 : {
647 : try
648 : {
649 72 : fn(*this);
650 : }
651 6 : catch(...)
652 : {
653 3 : r.success = false;
654 3 : r.loc = p_->loc;
655 3 : r.ep = p_->ep;
656 3 : p_->inert = true;
657 3 : return r;
658 : }
659 69 : if(p_->stopped)
660 : {
661 3 : r.success = false;
662 3 : r.loc = p_->loc;
663 3 : r.ep = p_->ep;
664 3 : p_->inert = true;
665 3 : return r;
666 : }
667 : }
668 :
669 : // Phase 2: exception mode
670 20 : p_->throws = true;
671 20 : p_->n = (std::numeric_limits<std::size_t>::max)();
672 20 : p_->i = 0;
673 20 : p_->triggered = false;
674 75 : while(*this)
675 : {
676 : try
677 : {
678 55 : fn(*this);
679 : }
680 70 : catch(std::system_error const& ex)
681 : {
682 35 : if(ex.code() != p_->ec)
683 : {
684 MIS 0 : r.success = false;
685 0 : r.loc = p_->loc;
686 0 : r.ep = p_->ep;
687 0 : p_->inert = true;
688 0 : return r;
689 : }
690 : }
691 0 : catch(...)
692 : {
693 0 : r.success = false;
694 0 : r.loc = p_->loc;
695 0 : r.ep = p_->ep;
696 0 : p_->inert = true;
697 0 : return r;
698 : }
699 HIT 55 : if(p_->stopped)
700 : {
701 MIS 0 : r.success = false;
702 0 : r.loc = p_->loc;
703 0 : r.ep = p_->ep;
704 0 : p_->inert = true;
705 0 : return r;
706 : }
707 : }
708 HIT 20 : p_->inert = true;
709 20 : return r;
710 MIS 0 : }
711 :
712 : /** Run a coroutine test function with systematic failure injection.
713 :
714 : Repeatedly invokes the provided coroutine function, failing at
715 : successive points until the function completes without
716 : encountering a failure. First runs the complete loop
717 : using error codes, then runs using exceptions.
718 :
719 : This overload handles lambdas that return an @ref IoRunnable
720 : (such as `task<void>`), executing them synchronously via
721 : @ref run_blocking.
722 :
723 : @par Example
724 :
725 : @code
726 : fuse f;
727 : auto r = f.armed([&](fuse&) -> task<void> {
728 : auto ec = f.maybe_fail();
729 : if(ec)
730 : co_return;
731 :
732 : ec = f.maybe_fail();
733 : if(ec)
734 : co_return;
735 : });
736 :
737 : if(!r)
738 : {
739 : std::cerr << "Failure at "
740 : << r.loc.file_name() << ":"
741 : << r.loc.line() << "\n";
742 : }
743 : @endcode
744 :
745 : @param fn The coroutine test function to invoke. It receives
746 : a reference to the fuse and should call @ref maybe_fail
747 : at each potential failure point.
748 :
749 : @return A @ref result indicating success or failure.
750 : On failure, `result::loc` contains the source location
751 : of the last @ref maybe_fail or @ref fail call.
752 : */
753 : template<class F>
754 : requires IoRunnable<std::invoke_result_t<F, fuse&>>
755 : result
756 HIT 131 : armed(F&& fn)
757 : {
758 1445 : return run_phases([&]{ run_blocking()(fn(*this)); });
759 : }
760 :
761 : /** Run a coroutine test function on a caller-supplied runner.
762 :
763 : Behaves like the @ref IoRunnable overload of @ref armed, but
764 : instead of driving each iteration through @ref run_blocking, it
765 : hands the coroutine to `run_one`. This lets a caller run each
766 : iteration on any execution context it chooses. Operations built
767 : on `corosio::timeout` or `corosio::delay` in particular require
768 : an `io_context`, because they abort on a non-`io_context`
769 : executor. `fuse` never learns about the context;
770 : the caller owns the drive loop.
771 :
772 : @par Runner contract
773 : `run_one` is invoked once per round with the @ref IoRunnable
774 : produced by `fn`. It must run that task to completion
775 : synchronously and *return* any exception the task raised as a
776 : `std::exception_ptr` (null on success). It must not rethrow.
777 : `armed` rethrows the returned pointer from its own synchronous
778 : code, so the exception phase observes injected failures. An
779 : exception escaping a `run_async` completion handler would
780 : instead call `std::terminate`. Capture the exception in the error
781 : handler and return it once the run loop is done.
782 :
783 : @par Example
784 : @code
785 : // Drive each iteration on a fresh io_context.
786 : auto io_runner = [](capy::task<> t) -> std::exception_ptr
787 : {
788 : corosio::io_context ioc;
789 : std::exception_ptr ep;
790 : capy::run_async(ioc.get_executor(),
791 : [](auto&&...){},
792 : [&ep](std::exception_ptr e){ ep = e; }
793 : )(std::move(t));
794 : ioc.run();
795 : return ep;
796 : };
797 : auto r = f.armed(io_runner,
798 : [&](capy::test::fuse&) -> capy::task<>
799 : {
800 : co_await corosio::timeout(some_op(), 5s);
801 : });
802 : @endcode
803 :
804 : @param run_one A callable invoked with each iteration's task; it
805 : runs the task to completion and returns any escaped exception
806 : (null on success) without rethrowing.
807 :
808 : @param fn The coroutine test function to invoke.
809 :
810 : @return A @ref result indicating success or failure.
811 : */
812 : template<class Runner, class F>
813 : requires IoRunnable<std::invoke_result_t<F, fuse&>>
814 : && std::same_as<
815 : std::invoke_result_t<Runner&, std::invoke_result_t<F, fuse&>>,
816 : std::exception_ptr>
817 : result
818 3 : armed(Runner&& run_one, F&& fn)
819 : {
820 14 : return run_phases([&]{
821 23 : if(auto ep = run_one(fn(*this)))
822 12 : std::rethrow_exception(ep);
823 6 : });
824 : }
825 :
826 : /** Alias for @ref armed.
827 :
828 : Allows the fuse to be invoked directly as a function
829 : object for more concise syntax.
830 :
831 : @par Example
832 :
833 : @code
834 : // These are equivalent:
835 : fuse f;
836 : auto r1 = f.armed([](fuse& f) { ... });
837 : auto r2 = f([](fuse& f) { ... });
838 :
839 : // Inline usage:
840 : auto r3 = fuse()([](fuse& f) {
841 : auto ec = f.maybe_fail();
842 : if(ec)
843 : return;
844 : });
845 : @endcode
846 :
847 : @param fn The test function to run under failure injection.
848 :
849 : @return The @ref result of the armed run.
850 :
851 : @see armed
852 : */
853 : template<class F>
854 : result
855 15 : operator()(F&& fn)
856 : {
857 15 : return armed(std::forward<F>(fn));
858 : }
859 :
860 : /** Alias for @ref armed (coroutine overload).
861 :
862 : @param fn The test coroutine factory to run under failure injection.
863 :
864 : @return The @ref result of the armed run.
865 :
866 : @see armed
867 : */
868 : template<class F>
869 : requires IoRunnable<std::invoke_result_t<F, fuse&>>
870 : result
871 : operator()(F&& fn)
872 : {
873 : return armed(std::forward<F>(fn));
874 : }
875 :
876 : /** Run a test function once without failure injection.
877 :
878 : Invokes the provided function exactly once. Calls to
879 : @ref maybe_fail always return an empty error code and
880 : never throw. Only explicit calls to @ref fail can
881 : signal a test failure.
882 :
883 : This is useful for running tests where you want to
884 : manually control failures, or for quick single-run
885 : tests without systematic error injection.
886 :
887 : @par Example
888 :
889 : @code
890 : fuse f;
891 : auto r = f.inert([](fuse& f) {
892 : auto ec = f.maybe_fail(); // Always succeeds
893 : assert(!ec);
894 :
895 : // Only way to signal failure:
896 : if(some_condition)
897 : {
898 : f.fail();
899 : return;
900 : }
901 : });
902 :
903 : if(!r)
904 : {
905 : std::cerr << "Test failed at "
906 : << r.loc.file_name() << ":"
907 : << r.loc.line() << "\n";
908 : }
909 : @endcode
910 :
911 : @param fn The test function to invoke. It receives
912 : a reference to the fuse. Calls to @ref maybe_fail
913 : always succeed.
914 :
915 : @return A @ref result indicating success or failure.
916 : On failure, `result::loc` contains the source location
917 : of the @ref fail call.
918 : */
919 : template<class F>
920 : result
921 9 : inert(F&& fn)
922 : {
923 9 : result r;
924 9 : p_->inert = true;
925 : try
926 : {
927 9 : fn(*this);
928 : }
929 2 : catch(...)
930 : {
931 1 : r.success = false;
932 1 : r.loc = p_->loc;
933 1 : r.ep = std::current_exception();
934 1 : return r;
935 : }
936 8 : if(p_->stopped)
937 : {
938 2 : r.success = false;
939 2 : r.loc = p_->loc;
940 2 : r.ep = p_->ep;
941 : }
942 8 : return r;
943 MIS 0 : }
944 :
945 : /** Run a coroutine test function once without failure injection.
946 :
947 : Invokes the provided coroutine function exactly once using
948 : @ref run_blocking. Calls to @ref maybe_fail always return
949 : an empty error code and never throw. Only explicit calls
950 : to @ref fail can signal a test failure.
951 :
952 : @par Example
953 :
954 : @code
955 : fuse f;
956 : auto r = f.inert([](fuse& f) -> task<void> {
957 : auto ec = f.maybe_fail(); // Always succeeds
958 : assert(!ec);
959 :
960 : // Only way to signal failure:
961 : if(some_condition)
962 : {
963 : f.fail();
964 : co_return;
965 : }
966 : });
967 :
968 : if(!r)
969 : {
970 : std::cerr << "Test failed at "
971 : << r.loc.file_name() << ":"
972 : << r.loc.line() << "\n";
973 : }
974 : @endcode
975 :
976 : @param fn The coroutine test function to invoke. It receives
977 : a reference to the fuse. Calls to @ref maybe_fail
978 : always succeed.
979 :
980 : @return A @ref result indicating success or failure.
981 : On failure, `result::loc` contains the source location
982 : of the @ref fail call.
983 : */
984 : template<class F>
985 : requires IoRunnable<std::invoke_result_t<F, fuse&>>
986 : result
987 HIT 39 : inert(F&& fn)
988 : {
989 39 : result r;
990 39 : p_->inert = true;
991 : try
992 : {
993 39 : run_blocking()(fn(*this));
994 : }
995 MIS 0 : catch(...)
996 : {
997 0 : r.success = false;
998 0 : r.loc = p_->loc;
999 0 : r.ep = std::current_exception();
1000 0 : return r;
1001 : }
1002 HIT 39 : if(p_->stopped)
1003 : {
1004 MIS 0 : r.success = false;
1005 0 : r.loc = p_->loc;
1006 0 : r.ep = p_->ep;
1007 : }
1008 HIT 39 : return r;
1009 MIS 0 : }
1010 : };
1011 :
1012 : } // test
1013 : } // capy
1014 : } // boost
1015 :
1016 : #endif
|