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 : /** A test utility for systematic error injection.
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 3154 : explicit operator bool() const noexcept
222 : {
223 3154 : auto& s = *p_;
224 3154 : if(s.n == (std::numeric_limits<std::size_t>::max)())
225 : {
226 : // First call: start round 0
227 683 : s.n = 0;
228 683 : return true;
229 : }
230 2471 : if(s.triggered)
231 : {
232 : // Previous round triggered, try next failure point
233 1795 : s.n++;
234 1795 : s.i = 0;
235 1795 : s.triggered = false;
236 1795 : return true;
237 : }
238 : // Test completed without trigger: success
239 676 : return false;
240 : }
241 :
242 : public:
243 : /** Result of a fuse operation.
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 @ref success.
280 42 : constexpr explicit operator bool() const noexcept
281 : {
282 42 : return success;
283 : }
284 : };
285 :
286 : /** Construct a fuse with a custom error code.
287 :
288 : @par Example
289 :
290 : @code
291 : auto custom_ec = make_error_code(
292 : std::errc::operation_canceled);
293 : fuse f(custom_ec);
294 :
295 : std::error_code captured_ec;
296 : auto r = f([&](fuse& f) {
297 : auto ec = f.maybe_fail();
298 : if(ec)
299 : {
300 : captured_ec = ec;
301 : return;
302 : }
303 : });
304 :
305 : assert(captured_ec == custom_ec);
306 : @endcode
307 :
308 : @param ec The error code to deliver at failure points.
309 : */
310 450 : explicit fuse(std::error_code ec)
311 450 : : p_(std::make_shared<state>())
312 : {
313 450 : p_->ec = ec;
314 450 : }
315 :
316 : /** Construct a fuse with the default error code.
317 :
318 : The default error code is `error::test_failure`.
319 :
320 : @par Example
321 :
322 : @code
323 : fuse f;
324 : std::error_code captured_ec;
325 :
326 : auto r = f([&](fuse& f) {
327 : auto ec = f.maybe_fail();
328 : if(ec)
329 : {
330 : captured_ec = ec;
331 : return;
332 : }
333 : });
334 :
335 : assert(captured_ec == error::test_failure);
336 : @endcode
337 : */
338 448 : fuse()
339 448 : : fuse(error::test_failure)
340 : {
341 448 : }
342 :
343 : /** Return an error or throw at the current failure point.
344 :
345 : When running under @ref armed, increments the internal
346 : counter. When the counter reaches the current failure
347 : point, returns the stored error code (or throws
348 : `std::system_error` in exception mode) and records
349 : the source location.
350 :
351 : When called outside of @ref armed or @ref inert (standalone
352 : usage), or when running under @ref inert, always returns
353 : an empty error code. This enables dependency injection
354 : where the fuse is a no-op in production code.
355 :
356 : @par Example
357 :
358 : @code
359 : fuse f;
360 : auto r = f([](fuse& f) {
361 : // Error code mode: returns the error
362 : auto ec = f.maybe_fail();
363 : if(ec)
364 : return;
365 :
366 : // Exception mode: throws system_error
367 : ec = f.maybe_fail();
368 : if(ec)
369 : return;
370 : });
371 : @endcode
372 :
373 : @par Standalone Usage
374 :
375 : @code
376 : fuse f;
377 : auto ec = f.maybe_fail(); // Always returns {} (no-op)
378 : @endcode
379 :
380 : @param loc The source location of the call site,
381 : captured automatically.
382 :
383 : @return The stored error code if at the failure point,
384 : otherwise an empty error code. In exception mode,
385 : throws instead of returning an error. When called
386 : outside @ref armed, or when running under @ref inert,
387 : always returns an empty error code.
388 :
389 : @throws std::system_error When in exception mode
390 : and at the failure point (not thrown outside @ref armed).
391 : */
392 : std::error_code
393 4873 : maybe_fail(
394 : std::source_location loc = std::source_location::current())
395 : {
396 4873 : auto& s = *p_;
397 4873 : if(s.inert)
398 236 : return {};
399 4637 : if(s.i < s.n)
400 3960 : ++s.i;
401 4637 : if(s.i == s.n)
402 : {
403 1873 : s.triggered = true;
404 1873 : s.loc = loc;
405 1873 : if(s.throws)
406 891 : throw std::system_error(s.ec);
407 982 : return s.ec;
408 : }
409 2764 : return {};
410 : }
411 :
412 : /** Signal a test failure and stop execution.
413 :
414 : Call this from the test function to indicate a failure
415 : condition. Both @ref armed and @ref inert will return
416 : a failed @ref result immediately.
417 :
418 : @par Example
419 :
420 : @code
421 : fuse f;
422 : auto r = f([](fuse& f) {
423 : auto ec = f.maybe_fail();
424 : if(ec)
425 : return;
426 :
427 : // Explicit failure when a condition is not met
428 : if(some_value != expected)
429 : {
430 : f.fail();
431 : return;
432 : }
433 : });
434 :
435 : if(!r)
436 : {
437 : std::cerr << "Test failed at "
438 : << r.loc.file_name() << ":"
439 : << r.loc.line() << "\n";
440 : }
441 : @endcode
442 :
443 : @param loc The source location of the call site,
444 : captured automatically.
445 : */
446 : void
447 3 : fail(
448 : std::source_location loc =
449 : std::source_location::current()) noexcept
450 : {
451 3 : p_->loc = loc;
452 3 : p_->stopped = true;
453 3 : }
454 :
455 : /** Signal a test failure with an exception and stop execution.
456 :
457 : Call this from the test function to indicate a failure
458 : condition with an associated exception. Both @ref armed
459 : and @ref inert will return a failed @ref result with
460 : the captured exception pointer.
461 :
462 : @par Example
463 :
464 : @code
465 : fuse f;
466 : auto r = f([](fuse& f) {
467 : try
468 : {
469 : do_something();
470 : }
471 : catch(...)
472 : {
473 : f.fail(std::current_exception());
474 : return;
475 : }
476 : });
477 :
478 : if(!r)
479 : {
480 : try
481 : {
482 : if(r.ep)
483 : std::rethrow_exception(r.ep);
484 : }
485 : catch(std::exception const& e)
486 : {
487 : std::cerr << "Exception: " << e.what() << "\n";
488 : }
489 : }
490 : @endcode
491 :
492 : @param ep The exception pointer to capture.
493 :
494 : @param loc The source location of the call site,
495 : captured automatically.
496 : */
497 : void
498 2 : fail(
499 : std::exception_ptr ep,
500 : std::source_location loc =
501 : std::source_location::current()) noexcept
502 : {
503 2 : p_->ep = ep;
504 2 : p_->loc = loc;
505 2 : p_->stopped = true;
506 2 : }
507 :
508 : private:
509 : /* Drive the two-phase armed loop, invoking `do_iter` once per round.
510 :
511 : Phase 1 delivers injected failures as error codes; phase 2 as
512 : exceptions. Shared by the two coroutine `armed` overloads: each
513 : supplies a nullary `do_iter` that runs one iteration — via
514 : @ref run_blocking, or via a caller-supplied runner — so the round
515 : sequence and failure handling stay identical across them.
516 : */
517 : template<class DoIter>
518 : result
519 313 : run_phases(DoIter&& do_iter)
520 : {
521 313 : result r;
522 :
523 : // Phase 1: error code mode
524 313 : p_->throws = false;
525 313 : p_->inert = false;
526 313 : p_->n = (std::numeric_limits<std::size_t>::max)();
527 1490 : while(*this)
528 : {
529 : try
530 : {
531 1178 : do_iter();
532 : }
533 2 : catch(...)
534 : {
535 1 : r.success = false;
536 1 : r.loc = p_->loc;
537 1 : r.ep = p_->ep;
538 1 : p_->inert = true;
539 1 : return r;
540 : }
541 1177 : if(p_->stopped)
542 : {
543 MIS 0 : r.success = false;
544 0 : r.loc = p_->loc;
545 0 : r.ep = p_->ep;
546 0 : p_->inert = true;
547 0 : return r;
548 : }
549 : }
550 :
551 : // Phase 2: exception mode
552 HIT 312 : p_->throws = true;
553 312 : p_->n = (std::numeric_limits<std::size_t>::max)();
554 312 : p_->i = 0;
555 312 : p_->triggered = false;
556 1487 : while(*this)
557 : {
558 : try
559 : {
560 1175 : do_iter();
561 : }
562 1726 : catch(std::system_error const& ex)
563 : {
564 863 : if(ex.code() != p_->ec)
565 : {
566 MIS 0 : r.success = false;
567 0 : r.loc = p_->loc;
568 0 : r.ep = p_->ep;
569 0 : p_->inert = true;
570 0 : return r;
571 : }
572 : }
573 0 : catch(...)
574 : {
575 0 : r.success = false;
576 0 : r.loc = p_->loc;
577 0 : r.ep = p_->ep;
578 0 : p_->inert = true;
579 0 : return r;
580 : }
581 HIT 1175 : if(p_->stopped)
582 : {
583 MIS 0 : r.success = false;
584 0 : r.loc = p_->loc;
585 0 : r.ep = p_->ep;
586 0 : p_->inert = true;
587 0 : return r;
588 : }
589 : }
590 HIT 312 : p_->inert = true;
591 312 : return r;
592 MIS 0 : }
593 :
594 : public:
595 : /** Run a test function with systematic failure injection.
596 :
597 : Repeatedly invokes the provided function, failing at
598 : successive points until the function completes without
599 : encountering a failure. First runs the complete loop
600 : using error codes, then runs using exceptions.
601 :
602 : @par Example
603 :
604 : @code
605 : fuse f;
606 : auto r = f.armed([](fuse& f) {
607 : auto ec = f.maybe_fail();
608 : if(ec)
609 : return;
610 :
611 : ec = f.maybe_fail();
612 : if(ec)
613 : return;
614 : });
615 :
616 : if(!r)
617 : {
618 : std::cerr << "Failure at "
619 : << r.loc.file_name() << ":"
620 : << r.loc.line() << "\n";
621 : }
622 : @endcode
623 :
624 : @param fn The test function to invoke. It receives
625 : a reference to the fuse and should call @ref maybe_fail
626 : at each potential failure point.
627 :
628 : @return A @ref result indicating success or failure.
629 : On failure, `result::loc` contains the source location
630 : of the last @ref maybe_fail or @ref fail call.
631 : */
632 : template<class F>
633 : result
634 HIT 32 : armed(F&& fn)
635 : {
636 32 : result r;
637 :
638 : // Phase 1: error code mode
639 32 : p_->throws = false;
640 32 : p_->inert = false;
641 32 : p_->n = (std::numeric_limits<std::size_t>::max)();
642 97 : while(*this)
643 : {
644 : try
645 : {
646 71 : fn(*this);
647 : }
648 6 : catch(...)
649 : {
650 3 : r.success = false;
651 3 : r.loc = p_->loc;
652 3 : r.ep = p_->ep;
653 3 : p_->inert = true;
654 3 : return r;
655 : }
656 68 : if(p_->stopped)
657 : {
658 3 : r.success = false;
659 3 : r.loc = p_->loc;
660 3 : r.ep = p_->ep;
661 3 : p_->inert = true;
662 3 : return r;
663 : }
664 : }
665 :
666 : // Phase 2: exception mode
667 26 : p_->throws = true;
668 26 : p_->n = (std::numeric_limits<std::size_t>::max)();
669 26 : p_->i = 0;
670 26 : p_->triggered = false;
671 80 : while(*this)
672 : {
673 : try
674 : {
675 54 : fn(*this);
676 : }
677 56 : catch(std::system_error const& ex)
678 : {
679 28 : if(ex.code() != p_->ec)
680 : {
681 MIS 0 : r.success = false;
682 0 : r.loc = p_->loc;
683 0 : r.ep = p_->ep;
684 0 : p_->inert = true;
685 0 : return r;
686 : }
687 : }
688 0 : catch(...)
689 : {
690 0 : r.success = false;
691 0 : r.loc = p_->loc;
692 0 : r.ep = p_->ep;
693 0 : p_->inert = true;
694 0 : return r;
695 : }
696 HIT 54 : if(p_->stopped)
697 : {
698 MIS 0 : r.success = false;
699 0 : r.loc = p_->loc;
700 0 : r.ep = p_->ep;
701 0 : p_->inert = true;
702 0 : return r;
703 : }
704 : }
705 HIT 26 : p_->inert = true;
706 26 : return r;
707 MIS 0 : }
708 :
709 : /** Run a coroutine test function with systematic failure injection.
710 :
711 : Repeatedly invokes the provided coroutine function, failing at
712 : successive points until the function completes without
713 : encountering a failure. First runs the complete loop
714 : using error codes, then runs using exceptions.
715 :
716 : This overload handles lambdas that return an @ref IoRunnable
717 : (such as `task<void>`), executing them synchronously via
718 : @ref run_blocking.
719 :
720 : @par Example
721 :
722 : @code
723 : fuse f;
724 : auto r = f.armed([&](fuse&) -> task<void> {
725 : auto ec = f.maybe_fail();
726 : if(ec)
727 : co_return;
728 :
729 : ec = f.maybe_fail();
730 : if(ec)
731 : co_return;
732 : });
733 :
734 : if(!r)
735 : {
736 : std::cerr << "Failure at "
737 : << r.loc.file_name() << ":"
738 : << r.loc.line() << "\n";
739 : }
740 : @endcode
741 :
742 : @param fn The coroutine test function to invoke. It receives
743 : a reference to the fuse and should call @ref maybe_fail
744 : at each potential failure point.
745 :
746 : @return A @ref result indicating success or failure.
747 : On failure, `result::loc` contains the source location
748 : of the last @ref maybe_fail or @ref fail call.
749 : */
750 : template<class F>
751 : requires IoRunnable<std::invoke_result_t<F, fuse&>>
752 : result
753 HIT 310 : armed(F&& fn)
754 : {
755 3814 : return run_phases([&]{ run_blocking()(fn(*this)); });
756 : }
757 :
758 : /** Run a coroutine test function on a caller-supplied runner.
759 :
760 : Behaves like the @ref IoRunnable overload of @ref armed, but
761 : instead of driving each iteration through @ref run_blocking, it
762 : hands the coroutine to `run_one`. This lets a caller run each
763 : iteration on any execution context it chooses — in particular an
764 : `io_context`, which operations built on `corosio::timeout` or
765 : `corosio::delay` require, since those abort on a
766 : non-`io_context` executor. `fuse` never learns about the context;
767 : the caller owns the drive loop.
768 :
769 : @par Runner contract
770 : `run_one` is invoked once per round with the @ref IoRunnable
771 : produced by `fn`. It must run that task to completion
772 : synchronously and *return* any exception the task raised as a
773 : `std::exception_ptr` (null on success). It must not rethrow:
774 : `armed` rethrows the returned pointer from its own synchronous
775 : code so the exception phase observes injected failures, whereas
776 : an exception escaping a `run_async` completion handler would call
777 : `std::terminate`. Capture the exception in the error handler and
778 : return it once the run loop is done.
779 :
780 : @par Example
781 : @code
782 : // Drive each iteration on a fresh io_context.
783 : auto io_runner = [](capy::task<> t) -> std::exception_ptr
784 : {
785 : corosio::io_context ioc;
786 : std::exception_ptr ep;
787 : capy::run_async(ioc.get_executor(),
788 : [](auto&&...){},
789 : [&ep](std::exception_ptr e){ ep = e; }
790 : )(std::move(t));
791 : ioc.run();
792 : return ep;
793 : };
794 : auto r = f.armed(io_runner,
795 : [&](capy::test::fuse&) -> capy::task<>
796 : {
797 : co_await corosio::timeout(some_op(), 5s);
798 : });
799 : @endcode
800 :
801 : @param run_one A callable invoked with each iteration's task; it
802 : runs the task to completion and returns any escaped exception
803 : (null on success) without rethrowing.
804 :
805 : @param fn The coroutine test function to invoke.
806 :
807 : @return A @ref result indicating success or failure.
808 : */
809 : template<class Runner, class F>
810 : requires IoRunnable<std::invoke_result_t<F, fuse&>>
811 : && std::same_as<
812 : std::invoke_result_t<Runner&, std::invoke_result_t<F, fuse&>>,
813 : std::exception_ptr>
814 : result
815 3 : armed(Runner&& run_one, F&& fn)
816 : {
817 14 : return run_phases([&]{
818 23 : if(auto ep = run_one(fn(*this)))
819 12 : std::rethrow_exception(ep);
820 6 : });
821 : }
822 :
823 : /** Alias for @ref armed.
824 :
825 : Allows the fuse to be invoked directly as a function
826 : object for more concise syntax.
827 :
828 : @par Example
829 :
830 : @code
831 : // These are equivalent:
832 : fuse f;
833 : auto r1 = f.armed([](fuse& f) { ... });
834 : auto r2 = f([](fuse& f) { ... });
835 :
836 : // Inline usage:
837 : auto r3 = fuse()([](fuse& f) {
838 : auto ec = f.maybe_fail();
839 : if(ec)
840 : return;
841 : });
842 : @endcode
843 :
844 : @see armed
845 : */
846 : template<class F>
847 : result
848 15 : operator()(F&& fn)
849 : {
850 15 : return armed(std::forward<F>(fn));
851 : }
852 :
853 : /** Alias for @ref armed (coroutine overload).
854 :
855 : @see armed
856 : */
857 : template<class F>
858 : requires IoRunnable<std::invoke_result_t<F, fuse&>>
859 : result
860 : operator()(F&& fn)
861 : {
862 : return armed(std::forward<F>(fn));
863 : }
864 :
865 : /** Run a test function once without failure injection.
866 :
867 : Invokes the provided function exactly once. Calls to
868 : @ref maybe_fail always return an empty error code and
869 : never throw. Only explicit calls to @ref fail can
870 : signal a test failure.
871 :
872 : This is useful for running tests where you want to
873 : manually control failures, or for quick single-run
874 : tests without systematic error injection.
875 :
876 : @par Example
877 :
878 : @code
879 : fuse f;
880 : auto r = f.inert([](fuse& f) {
881 : auto ec = f.maybe_fail(); // Always succeeds
882 : assert(!ec);
883 :
884 : // Only way to signal failure:
885 : if(some_condition)
886 : {
887 : f.fail();
888 : return;
889 : }
890 : });
891 :
892 : if(!r)
893 : {
894 : std::cerr << "Test failed at "
895 : << r.loc.file_name() << ":"
896 : << r.loc.line() << "\n";
897 : }
898 : @endcode
899 :
900 : @param fn The test function to invoke. It receives
901 : a reference to the fuse. Calls to @ref maybe_fail
902 : will always succeed.
903 :
904 : @return A @ref result indicating success or failure.
905 : On failure, `result::loc` contains the source location
906 : of the @ref fail call.
907 : */
908 : template<class F>
909 : result
910 8 : inert(F&& fn)
911 : {
912 8 : result r;
913 8 : p_->inert = true;
914 : try
915 : {
916 8 : fn(*this);
917 : }
918 2 : catch(...)
919 : {
920 1 : r.success = false;
921 1 : r.loc = p_->loc;
922 1 : r.ep = std::current_exception();
923 1 : return r;
924 : }
925 7 : if(p_->stopped)
926 : {
927 2 : r.success = false;
928 2 : r.loc = p_->loc;
929 2 : r.ep = p_->ep;
930 : }
931 7 : return r;
932 MIS 0 : }
933 :
934 : /** Run a coroutine test function once without failure injection.
935 :
936 : Invokes the provided coroutine function exactly once using
937 : @ref run_blocking. Calls to @ref maybe_fail always return
938 : an empty error code and never throw. Only explicit calls
939 : to @ref fail can signal a test failure.
940 :
941 : @par Example
942 :
943 : @code
944 : fuse f;
945 : auto r = f.inert([](fuse& f) -> task<void> {
946 : auto ec = f.maybe_fail(); // Always succeeds
947 : assert(!ec);
948 :
949 : // Only way to signal failure:
950 : if(some_condition)
951 : {
952 : f.fail();
953 : co_return;
954 : }
955 : });
956 :
957 : if(!r)
958 : {
959 : std::cerr << "Test failed at "
960 : << r.loc.file_name() << ":"
961 : << r.loc.line() << "\n";
962 : }
963 : @endcode
964 :
965 : @param fn The coroutine test function to invoke. It receives
966 : a reference to the fuse. Calls to @ref maybe_fail
967 : will always succeed.
968 :
969 : @return A @ref result indicating success or failure.
970 : On failure, `result::loc` contains the source location
971 : of the @ref fail call.
972 : */
973 : template<class F>
974 : requires IoRunnable<std::invoke_result_t<F, fuse&>>
975 : result
976 HIT 29 : inert(F&& fn)
977 : {
978 29 : result r;
979 29 : p_->inert = true;
980 : try
981 : {
982 29 : run_blocking()(fn(*this));
983 : }
984 MIS 0 : catch(...)
985 : {
986 0 : r.success = false;
987 0 : r.loc = p_->loc;
988 0 : r.ep = std::current_exception();
989 0 : return r;
990 : }
991 HIT 29 : if(p_->stopped)
992 : {
993 MIS 0 : r.success = false;
994 0 : r.loc = p_->loc;
995 0 : r.ep = p_->ep;
996 : }
997 HIT 29 : return r;
998 MIS 0 : }
999 : };
1000 :
1001 : } // test
1002 : } // capy
1003 : } // boost
1004 :
1005 : #endif
|