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