diff --git a/include/boost/capy/io/any_buffer_sink.hpp b/include/boost/capy/io/any_buffer_sink.hpp index dc92cf4fe..200277b07 100644 --- a/include/boost/capy/io/any_buffer_sink.hpp +++ b/include/boost/capy/io/any_buffer_sink.hpp @@ -773,7 +773,10 @@ any_buffer_sink::any_buffer_sink(S s) bool committed = false; ~guard() { if(!committed && self->storage_) { - self->vt_->destroy(self->sink_); + // sink_ is null if the sink move-ctor threw before + // the placement-new assigned it. + if(self->sink_) + self->vt_->destroy(self->sink_); ::operator delete(self->storage_); self->storage_ = nullptr; self->sink_ = nullptr; diff --git a/include/boost/capy/io/any_buffer_source.hpp b/include/boost/capy/io/any_buffer_source.hpp index bdce6e542..68d7340cc 100644 --- a/include/boost/capy/io/any_buffer_source.hpp +++ b/include/boost/capy/io/any_buffer_source.hpp @@ -581,7 +581,10 @@ any_buffer_source::any_buffer_source(S s) bool committed = false; ~guard() { if(!committed && self->storage_) { - self->vt_->destroy(self->source_); + // source_ is null if the source move-ctor threw before + // the placement-new assigned it. + if(self->source_) + self->vt_->destroy(self->source_); ::operator delete(self->storage_); self->storage_ = nullptr; self->source_ = nullptr; diff --git a/include/boost/capy/io/any_read_source.hpp b/include/boost/capy/io/any_read_source.hpp index 5f441aa54..e798dc934 100644 --- a/include/boost/capy/io/any_read_source.hpp +++ b/include/boost/capy/io/any_read_source.hpp @@ -439,7 +439,10 @@ any_read_source::any_read_source(S s) bool committed = false; ~guard() { if(!committed && self->storage_) { - self->vt_->destroy(self->source_); + // source_ is null if the source move-ctor threw before + // the placement-new assigned it. + if(self->source_) + self->vt_->destroy(self->source_); ::operator delete(self->storage_); self->storage_ = nullptr; self->source_ = nullptr; diff --git a/include/boost/capy/io/any_read_stream.hpp b/include/boost/capy/io/any_read_stream.hpp index ddbcc3b23..ea2320c8b 100644 --- a/include/boost/capy/io/any_read_stream.hpp +++ b/include/boost/capy/io/any_read_stream.hpp @@ -345,7 +345,10 @@ any_read_stream::any_read_stream(S s) bool committed = false; ~guard() { if(!committed && self->storage_) { - self->vt_->destroy(self->stream_); + // stream_ is null if the stream move-ctor threw before + // the placement-new assigned it. + if(self->stream_) + self->vt_->destroy(self->stream_); ::operator delete(self->storage_); self->storage_ = nullptr; self->stream_ = nullptr; diff --git a/include/boost/capy/io/any_write_sink.hpp b/include/boost/capy/io/any_write_sink.hpp index 1cdfd43dd..d3a9d784f 100644 --- a/include/boost/capy/io/any_write_sink.hpp +++ b/include/boost/capy/io/any_write_sink.hpp @@ -576,7 +576,10 @@ any_write_sink::any_write_sink(S s) bool committed = false; ~guard() { if(!committed && self->storage_) { - self->vt_->destroy(self->sink_); + // sink_ is null if the sink move-ctor threw before + // the placement-new assigned it. + if(self->sink_) + self->vt_->destroy(self->sink_); ::operator delete(self->storage_); self->storage_ = nullptr; self->sink_ = nullptr; diff --git a/include/boost/capy/io/any_write_stream.hpp b/include/boost/capy/io/any_write_stream.hpp index 40088127b..e45c6ce3d 100644 --- a/include/boost/capy/io/any_write_stream.hpp +++ b/include/boost/capy/io/any_write_stream.hpp @@ -346,7 +346,10 @@ any_write_stream::any_write_stream(S s) bool committed = false; ~guard() { if(!committed && self->storage_) { - self->vt_->destroy(self->stream_); + // stream_ is null if the stream move-ctor threw before + // the placement-new assigned it. + if(self->stream_) + self->vt_->destroy(self->stream_); ::operator delete(self->storage_); self->storage_ = nullptr; self->stream_ = nullptr; diff --git a/test/unit/buffers/buffer.cpp b/test/unit/buffers/buffer.cpp index dfda2e296..3c7feb454 100644 --- a/test/unit/buffers/buffer.cpp +++ b/test/unit/buffers/buffer.cpp @@ -389,6 +389,17 @@ struct buffer_test std::span bs(&b[0], 3); test::check_sequence(bs, "123456789"); } + + // operator+= advances and clamps to size() + { + char const* p = "12345"; + const_buffer b(p, 5); + b += 2; + BOOST_TEST_EQ(b.data(), p + 2); + BOOST_TEST_EQ(b.size(), 3); + b += 100; // over-advance is clamped + BOOST_TEST_EQ(b.size(), 0); + } } void testMutableBuffer() @@ -442,6 +453,17 @@ struct buffer_test std::span bs(&b[0], 3); test::check_sequence(bs, "123456789"); } + + // operator+= advances and clamps to size() + { + char p[6] = "12345"; + mutable_buffer b(p, 5); + b += 2; + BOOST_TEST_EQ(b.data(), p + 2); + BOOST_TEST_EQ(b.size(), 3); + b += 100; // over-advance is clamped + BOOST_TEST_EQ(b.size(), 0); + } } void testSize() diff --git a/test/unit/concept/decomposes_to.cpp b/test/unit/concept/decomposes_to.cpp index 2fb1cb9a3..150e44ed1 100644 --- a/test/unit/concept/decomposes_to.cpp +++ b/test/unit/concept/decomposes_to.cpp @@ -19,6 +19,8 @@ #include #include +#include "test_suite.hpp" + namespace boost { namespace capy { @@ -283,5 +285,48 @@ struct bad_aw }; #endif +// A type whose awaiter is obtained via a free operator co_await. +struct mock_free_co_await { }; + +inline mock_int_awaitable +operator co_await(mock_free_co_await) noexcept +{ + return {}; +} + +// The static_asserts above cover decomposes_to at compile time; this +// suite drives get_awaiter at runtime, which otherwise appears only in +// the unevaluated operand of awaitable_return_t. +class decomposes_to_test +{ +public: + void + run() + { + // No operator co_await: get_awaiter returns the value itself. + { + mock_int_awaitable a; + auto aw = detail::get_awaiter(a); + BOOST_TEST(aw.await_ready()); + } + + // Member operator co_await. + { + mock_with_co_await_op a; + auto aw = detail::get_awaiter(a); + BOOST_TEST(aw.await_ready()); + } + + // Free operator co_await. + { + mock_free_co_await a; + auto aw = detail::get_awaiter(a); + BOOST_TEST(aw.await_ready()); + } + } +}; + +TEST_SUITE(decomposes_to_test, "boost.capy.concept.decomposes_to"); + } // namespace capy } // namespace boost diff --git a/test/unit/cond.cpp b/test/unit/cond.cpp index 7602f1028..7dfd4c2e4 100644 --- a/test/unit/cond.cpp +++ b/test/unit/cond.cpp @@ -76,6 +76,28 @@ class cond_test auto ec = make_error_code(error::eof); BOOST_TEST(!(ec == cond::not_found)); } + + // Remaining messages, including the default branch. + auto const ecnd = make_error_condition(cond::eof); + auto const& cat = ecnd.category(); + BOOST_TEST( + cat.message(static_cast(cond::stream_truncated)) == + "stream truncated"); + BOOST_TEST( + cat.message(static_cast(cond::timeout)) == + "operation timed out"); + BOOST_TEST(cat.message(9999) == "unknown"); + + // Equivalence: stream_truncated and timeout. + BOOST_TEST(make_error_code(error::stream_truncated) == + cond::stream_truncated); + BOOST_TEST(make_error_code(error::timeout) == cond::timeout); + + // Out-of-range condition is equivalent to nothing. + { + auto ec = make_error_code(error::eof); + BOOST_TEST(! cat.equivalent(ec, 9999)); + } } }; diff --git a/test/unit/detail/await_suspend_helper.cpp b/test/unit/detail/await_suspend_helper.cpp new file mode 100644 index 000000000..2c3583484 --- /dev/null +++ b/test/unit/detail/await_suspend_helper.cpp @@ -0,0 +1,87 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that header file is self-contained. +#include + +#include + +#include + +#include "test_suite.hpp" + +namespace boost { +namespace capy { +namespace detail { + +class await_suspend_helper_test +{ + // await_suspend returning void: caller suspends unconditionally. + struct void_awaitable + { + bool suspended = false; + void await_suspend(std::coroutine_handle<>, io_env const*) + { + suspended = true; + } + }; + + // await_suspend returning bool: true suspends, false resumes. + struct bool_awaitable + { + bool value; + bool await_suspend(std::coroutine_handle<>, io_env const*) + { + return value; + } + }; + + // await_suspend returning a handle: symmetric transfer to it. + struct handle_awaitable + { + std::coroutine_handle<> next; + std::coroutine_handle<> + await_suspend(std::coroutine_handle<>, io_env const*) + { + return next; + } + }; + +public: + void + run() + { + auto const h = std::noop_coroutine(); + + // void -> noop_coroutine, and the awaitable was invoked. + void_awaitable va; + BOOST_TEST(call_await_suspend(&va, h, nullptr) == h); + BOOST_TEST(va.suspended); + + // bool true -> noop_coroutine (stay suspended). + bool_awaitable bt{true}; + BOOST_TEST(call_await_suspend(&bt, h, nullptr) == h); + + // bool false -> the original handle (resume). + bool_awaitable bf{false}; + BOOST_TEST(call_await_suspend(&bf, h, nullptr) == h); + + // handle -> the returned handle. + handle_awaitable ha{h}; + BOOST_TEST(call_await_suspend(&ha, h, nullptr) == h); + } +}; + +TEST_SUITE( + await_suspend_helper_test, + "boost.capy.detail.await_suspend_helper"); + +} // detail +} // capy +} // boost diff --git a/test/unit/detail/except.cpp b/test/unit/detail/except.cpp new file mode 100644 index 000000000..66919b7b5 --- /dev/null +++ b/test/unit/detail/except.cpp @@ -0,0 +1,97 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace capy { +namespace detail { + +class except_test +{ + // Run the thrower and return the message carried by the exception. + template + std::string + catch_message(F&& f) + { + try + { + f(); + } + catch(Ex const& e) + { + return e.what(); + } + return {}; + } + +public: + // The throw_* helpers are [[noreturn]], so MSVC flags the code + // after each BOOST_TEST_THROWS expression as unreachable. +#if defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable : 4702) // unreachable code after throw +#endif + void + run() + { + BOOST_TEST_THROWS(throw_bad_typeid(), std::bad_typeid); + BOOST_TEST_THROWS(throw_bad_alloc(), std::bad_alloc); + + BOOST_TEST_THROWS(throw_invalid_argument(), std::invalid_argument); + BOOST_TEST_THROWS( + throw_invalid_argument("bad"), std::invalid_argument); + BOOST_TEST( + catch_message( + [] { throw_invalid_argument("bad"); }) == "bad"); + + BOOST_TEST_THROWS(throw_length_error(), std::length_error); + BOOST_TEST_THROWS(throw_length_error("too long"), std::length_error); + BOOST_TEST( + catch_message( + [] { throw_length_error("too long"); }) == "too long"); + + BOOST_TEST_THROWS(throw_logic_error(), std::logic_error); + BOOST_TEST_THROWS(throw_out_of_range(), std::out_of_range); + + BOOST_TEST_THROWS(throw_runtime_error("boom"), std::runtime_error); + BOOST_TEST( + catch_message( + [] { throw_runtime_error("boom"); }) == "boom"); + + auto const ec = std::make_error_code(std::errc::invalid_argument); + BOOST_TEST_THROWS(throw_system_error(ec), std::system_error); + try + { + throw_system_error(ec); + } + catch(std::system_error const& e) + { + BOOST_TEST(e.code() == ec); + } + } +#if defined(_MSC_VER) +#pragma warning(pop) +#endif +}; + +TEST_SUITE(except_test, "boost.capy.detail.except"); + +} // detail +} // capy +} // boost diff --git a/test/unit/detail/frame_memory_resource.cpp b/test/unit/detail/frame_memory_resource.cpp new file mode 100644 index 000000000..372a5b796 --- /dev/null +++ b/test/unit/detail/frame_memory_resource.cpp @@ -0,0 +1,51 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that header file is self-contained. +#include + +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace capy { +namespace detail { + +class frame_memory_resource_test +{ +public: + void + run() + { + frame_memory_resource> fmr{ + std::allocator{}}; + + std::pmr::memory_resource* mr = fmr.get(); + BOOST_TEST(mr == &fmr); + + // do_allocate / do_deallocate round trip through the resource. + void* p = mr->allocate(64, alignof(std::max_align_t)); + BOOST_TEST(p != nullptr); + mr->deallocate(p, 64, alignof(std::max_align_t)); + + // do_is_equal: identity only. + frame_memory_resource> other{ + std::allocator{}}; + BOOST_TEST(mr->is_equal(*mr)); + BOOST_TEST(! mr->is_equal(*other.get())); + } +}; + +TEST_SUITE(frame_memory_resource_test, "boost.capy.detail.frame_memory_resource"); + +} // detail +} // capy +} // boost diff --git a/test/unit/detail/run_callbacks.cpp b/test/unit/detail/run_callbacks.cpp new file mode 100644 index 000000000..3b3d78f4e --- /dev/null +++ b/test/unit/detail/run_callbacks.cpp @@ -0,0 +1,140 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that header file is self-contained. +#include + +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace capy { +namespace detail { + +class run_callbacks_test +{ +public: + void + test_default_handler() + { + default_handler h; + h(42); // value overload, discarded + h(); // void overload + + std::exception_ptr null; + h(null); // null: no rethrow + + bool rethrown = false; + std::exception_ptr ep = + std::make_exception_ptr(std::runtime_error("x")); + try + { + h(ep); + } + catch(std::runtime_error const&) + { + rethrown = true; + } + BOOST_TEST(rethrown); + } + + void + test_handler_pair() + { + int value = 0; + bool voided = false; + std::exception_ptr captured; + + auto vh = [&](auto&& v) noexcept { value = static_cast(v); }; + auto eh = [&](std::exception_ptr ep) noexcept { captured = ep; }; + handler_pair hp{vh, eh}; + + hp(7); + BOOST_TEST(value == 7); + + auto vh2 = [&](auto&&...) noexcept { voided = true; }; + handler_pair hp2{vh2, eh}; + hp2(); + BOOST_TEST(voided); + + auto ep = std::make_exception_ptr(std::runtime_error("e")); + hp2(ep); + BOOST_TEST(captured == ep); + } + + // A value-only handler: invocable with int and with no args, but not + // with exception_ptr, so handler_pair's if-constexpr takes the rethrow + // branch. A generic lambda cannot be used here: the invocable trait + // would hard-instantiate its body with exception_ptr. + struct value_handler + { + int* value; + int* calls; + void operator()(int v) noexcept { ++*calls; *value = v; } + void operator()() noexcept { ++*calls; } + }; + + void + test_handler_pair_default() + { + // H1 not invocable with exception_ptr: the exception path rethrows. + int value = 0; + int calls = 0; + handler_pair hp{ + value_handler{&value, &calls}}; + + hp(5); + BOOST_TEST(value == 5); + hp(); // void overload + BOOST_TEST(calls == 2); + + bool rethrown = false; + std::exception_ptr ep = + std::make_exception_ptr(std::runtime_error("boom")); + try + { + hp(ep); + } + catch(std::runtime_error const&) + { + rethrown = true; + } + BOOST_TEST(rethrown); + } + + void + test_handler_pair_default_invocable() + { + // H1 invocable with exception_ptr: the exception is forwarded to it. + std::exception_ptr captured; + auto h = [&](std::exception_ptr ep) noexcept { captured = ep; }; + handler_pair hp{h}; + + auto ep = std::make_exception_ptr(std::runtime_error("fwd")); + hp(ep); + BOOST_TEST(captured == ep); + } + + void + run() + { + test_default_handler(); + test_handler_pair(); + test_handler_pair_default(); + test_handler_pair_default_invocable(); + } +}; + +TEST_SUITE(run_callbacks_test, "boost.capy.detail.run_callbacks"); + +} // detail +} // capy +} // boost diff --git a/test/unit/error.cpp b/test/unit/error.cpp index 79755c835..13e18e052 100644 --- a/test/unit/error.cpp +++ b/test/unit/error.cpp @@ -9,3 +9,46 @@ // Test that header file is self-contained. #include + +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace capy { + +class error_category_test +{ +public: + void + run() + { + auto const ec = make_error_code(error::eof); + auto const& cat = ec.category(); + + BOOST_TEST(std::string(cat.name()) == "boost.capy"); + + BOOST_TEST(cat.message(static_cast(error::eof)) == "eof"); + BOOST_TEST( + cat.message(static_cast(error::canceled)) == + "operation canceled"); + BOOST_TEST( + cat.message(static_cast(error::test_failure)) == + "test failure"); + BOOST_TEST( + cat.message(static_cast(error::stream_truncated)) == + "stream truncated"); + BOOST_TEST( + cat.message(static_cast(error::not_found)) == "not found"); + BOOST_TEST(cat.message(static_cast(error::timeout)) == "timeout"); + + // Out-of-range value hits the default branch. + BOOST_TEST(cat.message(9999) == "unknown"); + } +}; + +TEST_SUITE(error_category_test, "boost.capy.error"); + +} // capy +} // boost diff --git a/test/unit/ex/any_executor.cpp b/test/unit/ex/any_executor.cpp index 4c059fc36..af5605284 100644 --- a/test/unit/ex/any_executor.cpp +++ b/test/unit/ex/any_executor.cpp @@ -131,6 +131,33 @@ make_counter_coro(std::atomic& counter) }(&counter); } +// Executor whose work-tracking hooks are observable, so tests can +// confirm on_work_started/on_work_finished forward through the wrapper. +struct counting_context : execution_context +{ + int work = 0; +}; + +struct counting_executor +{ + counting_context* ctx_ = nullptr; + + counting_executor() = default; + explicit counting_executor(counting_context& ctx) noexcept : ctx_(&ctx) {} + + bool operator==(counting_executor const& other) const noexcept + { + return ctx_ == other.ctx_; + } + execution_context& context() const noexcept { return *ctx_; } + void on_work_started() const noexcept { ++ctx_->work; } + void on_work_finished() const noexcept { --ctx_->work; } + std::coroutine_handle<> dispatch(continuation& c) const { return c.h; } + void post(continuation&) const { } +}; + +static_assert(Executor); + } // namespace struct any_executor_test @@ -213,6 +240,22 @@ struct any_executor_test BOOST_TEST(!(ex1 == ex4)); // Non-empty vs empty } + void + testWorkTracking() + { + // on_work_started/on_work_finished forward through the + // type-erased impl to the wrapped executor. + counting_context ctx; + counting_executor under(ctx); + any_executor ex(under); + + BOOST_TEST_EQ(ctx.work, 0); + ex.on_work_started(); + BOOST_TEST_EQ(ctx.work, 1); + ex.on_work_finished(); + BOOST_TEST_EQ(ctx.work, 0); + } + void testTargetType() { @@ -357,6 +400,7 @@ struct any_executor_test testCopy(); testMove(); testEquality(); + testWorkTracking(); testTargetType(); testContext(); testDispatch(); diff --git a/test/unit/ex/executor_ref.cpp b/test/unit/ex/executor_ref.cpp index f01360ac3..7880d1826 100644 --- a/test/unit/ex/executor_ref.cpp +++ b/test/unit/ex/executor_ref.cpp @@ -128,6 +128,33 @@ make_counter_coro(std::atomic& counter) }(&counter); } +// Executor whose work-tracking hooks are observable, so tests can +// confirm on_work_started/on_work_finished forward through the wrapper. +struct counting_context : execution_context +{ + int work = 0; +}; + +struct counting_executor +{ + counting_context* ctx_ = nullptr; + + counting_executor() = default; + explicit counting_executor(counting_context& ctx) noexcept : ctx_(&ctx) {} + + bool operator==(counting_executor const& other) const noexcept + { + return ctx_ == other.ctx_; + } + execution_context& context() const noexcept { return *ctx_; } + void on_work_started() const noexcept { ++ctx_->work; } + void on_work_finished() const noexcept { --ctx_->work; } + std::coroutine_handle<> dispatch(continuation& c) const { return c.h; } + void post(continuation&) const { } +}; + +static_assert(Executor); + } // namespace struct executor_ref_test @@ -181,6 +208,29 @@ struct executor_ref_test BOOST_TEST(ex1 == ex2); BOOST_TEST(!(ex1 == ex3)); + + // Different executor types compare unequal via the vtable + // mismatch path, without invoking the per-type equals thunk. + test::blocking_context bctx; + auto ie = bctx.get_executor(); + executor_ref ex4(ie); + BOOST_TEST(!(ex1 == ex4)); + } + + void + testWorkTracking() + { + // on_work_started/on_work_finished forward through the vtable + // thunks to the wrapped executor. + counting_context ctx; + counting_executor under(ctx); + executor_ref ex(under); + + BOOST_TEST_EQ(ctx.work, 0); + ex.on_work_started(); + BOOST_TEST_EQ(ctx.work, 1); + ex.on_work_finished(); + BOOST_TEST_EQ(ctx.work, 0); } void @@ -277,6 +327,12 @@ struct executor_ref_test BOOST_TEST_EQ( ex2.target(), nullptr); + + // Wrong type through the const overload returns nullptr. + executor_ref const& cex2 = ex2; + BOOST_TEST_EQ( + cex2.target(), + nullptr); } void @@ -285,6 +341,7 @@ struct executor_ref_test testConstruct(); testCopy(); testEquality(); + testWorkTracking(); testDispatch(); testPost(); testMultiplePost(); diff --git a/test/unit/ex/run_async.cpp b/test/unit/ex/run_async.cpp index 8816b0362..ac91aa8ed 100644 --- a/test/unit/ex/run_async.cpp +++ b/test/unit/ex/run_async.cpp @@ -17,6 +17,8 @@ #include "test_helpers.hpp" #include +#include +#include #include #include #include @@ -193,7 +195,23 @@ struct run_async_test int result = 0; run_async(d, [&](int v) { result = v; })(returns_int()); - + + BOOST_TEST_EQ(result, 42); + BOOST_TEST_EQ(dispatch_count, 1); + } + + void + testValueAllocator() + { + // The value-type allocator overload wraps the allocator in a + // frame_memory_resource and uses the allocating trampoline. + int dispatch_count = 0; + sync_executor d(dispatch_count); + int result = 0; + + run_async(d, std::allocator{}, + [&](int v) { result = v; })(returns_int()); + BOOST_TEST_EQ(result, 42); BOOST_TEST_EQ(dispatch_count, 1); } @@ -669,6 +687,7 @@ struct run_async_test { // Basic Functionality testNoHandlers(); + testValueAllocator(); testResultHandler(); testVoidTaskResultHandler(); testDualHandlers(); diff --git a/test/unit/io/any_buffer_sink.cpp b/test/unit/io/any_buffer_sink.cpp index 15f8b1406..2e29e2fd7 100644 --- a/test/unit/io/any_buffer_sink.cpp +++ b/test/unit/io/any_buffer_sink.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -277,6 +278,72 @@ static_assert(!WriteSink); //---------------------------------------------------------- +// Suspends, then resumes from await_suspend, to exercise the +// type-erased await_suspend forwarding the always-ready mocks skip. +struct resuming_eof_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result<> await_resume() { return {}; } +}; + +struct resuming_size_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result await_resume() { return {{}, 1}; } +}; + +// Satisfies BufferSink + WriteSink with suspending operations. +struct resuming_buffer_sink +{ + char buf_[8] = {}; + std::span prepare(std::span dest) + { + if(dest.empty()) + return {}; + dest[0] = make_buffer(buf_, sizeof(buf_)); + return dest.first(1); + } + resuming_eof_awaitable commit(std::size_t) { return {}; } + resuming_eof_awaitable commit_eof(std::size_t) { return {}; } + template + resuming_size_awaitable write_some(CB) { return {}; } + template + resuming_size_awaitable write(CB) { return {}; } + template + resuming_size_awaitable write_eof(CB) { return {}; } + resuming_eof_awaitable write_eof() { return {}; } +}; + +// Move constructor throws so owning construction fails after storage +// is allocated but before the sink is constructed. +struct throwing_move_buffer_sink +{ + int* destroyed_; + explicit throwing_move_buffer_sink(int* d) : destroyed_(d) {} + throwing_move_buffer_sink(throwing_move_buffer_sink&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_buffer_sink() { if(destroyed_) ++(*destroyed_); } + std::span prepare(std::span dest) + { return dest.empty() ? std::span{} : dest.first(0); } + resuming_eof_awaitable commit(std::size_t) { return {}; } + resuming_eof_awaitable commit_eof(std::size_t) { return {}; } + template + resuming_size_awaitable write_some(CB) { return {}; } + template + resuming_size_awaitable write(CB) { return {}; } + template + resuming_size_awaitable write_eof(CB) { return {}; } + resuming_eof_awaitable write_eof() { return {}; } +}; + +//---------------------------------------------------------- + class any_buffer_sink_test { public: @@ -1269,11 +1336,68 @@ class any_buffer_sink_test BOOST_TEST(r.success); } + void + testConstructThrows() + { + // Owning construction whose sink move-ctor throws must not run + // the sink destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_buffer_sink(throwing_move_buffer_sink{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + + void + testSuspends() + { + // Drive every operation through an awaitable that suspends and + // then resumes, covering the type-erased await_suspend paths. + resuming_buffer_sink sink; + any_buffer_sink abs(&sink); + + auto coro = [&]() -> task { + mutable_buffer arr[detail::max_iovec_]; + abs.prepare(arr); + + auto [ec1] = co_await abs.commit(1); + if(ec1) + co_return 0; + + char const data[] = "x"; + auto [ec2, n2] = co_await abs.write_some(const_buffer(data, 1)); + if(ec2) + co_return 0; + auto [ec3, n3] = co_await abs.write(const_buffer(data, 1)); + if(ec3) + co_return 0; + auto [ec4, n4] = co_await abs.write_eof(const_buffer(data, 1)); + if(ec4) + co_return 0; + + abs.prepare(arr); + auto [ec5] = co_await abs.commit_eof(1); + if(ec5) + co_return 0; + auto [ec6] = co_await abs.write_eof(); + if(ec6) + co_return 0; + + co_return n2 + n3 + n4; + }; + + std::size_t result{}; + test::run_blocking([&](std::size_t v) { result = v; })(coro()); + BOOST_TEST_EQ(result, 3u); + } + void run() { testConstruct(); testConstructOwning(); + testConstructThrows(); + testSuspends(); testMove(); testMoveAssignOverExisting(); testPrepareCommit(); diff --git a/test/unit/io/any_buffer_source.cpp b/test/unit/io/any_buffer_source.cpp index d48f8c29d..91b4696d4 100644 --- a/test/unit/io/any_buffer_source.cpp +++ b/test/unit/io/any_buffer_source.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "test/unit/test_helpers.hpp" @@ -202,6 +203,66 @@ class buffer_read_source } }; +// Suspends, then resumes from await_suspend, to exercise the +// type-erased await_suspend forwarding the always-ready mock skips. +struct resuming_pull_awaitable +{ + std::span dest_; + char const* data_; + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result> + await_resume() + { + if(dest_.empty()) + return {{}, {}}; + dest_[0] = make_buffer(data_, 3); + return {{}, dest_.first(1)}; + } +}; + +struct resuming_io_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result await_resume() { return {{}, 3}; } +}; + +// Satisfies BufferSource + ReadSource with suspending operations. +struct resuming_buffer_source +{ + char data_[4] = "abc"; + resuming_pull_awaitable pull(std::span dest) + { return {dest, data_}; } + void consume(std::size_t) noexcept {} + template + resuming_io_awaitable read_some(MB) { return {}; } + template + resuming_io_awaitable read(MB) { return {}; } +}; + +// Move constructor throws so owning construction fails after storage +// is allocated but before the source is constructed. +struct throwing_move_buffer_source +{ + int* destroyed_; + explicit throwing_move_buffer_source(int* d) : destroyed_(d) {} + throwing_move_buffer_source(throwing_move_buffer_source&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_buffer_source() { if(destroyed_) ++(*destroyed_); } + resuming_pull_awaitable pull(std::span dest) + { return {dest, nullptr}; } + void consume(std::size_t) noexcept {} + template + resuming_io_awaitable read_some(MB) { return {}; } + template + resuming_io_awaitable read(MB) { return {}; } +}; + // Verify concepts at compile time static_assert(BufferSource); static_assert(ReadSource); @@ -291,6 +352,63 @@ class any_buffer_source_test BOOST_TEST(!abs2.has_value()); } + void + testMoveAssignOwning() + { + // Move-assign over an owning wrapper to exercise the storage_ + // teardown branch in operator=. + any_buffer_source a(buffer_read_source{test::fuse{}}); + any_buffer_source b(buffer_read_source{test::fuse{}}); + BOOST_TEST(a.has_value()); + + a = std::move(b); + BOOST_TEST(a.has_value()); + BOOST_TEST(!b.has_value()); + } + + void + testConstructThrows() + { + // Owning construction whose source move-ctor throws must not + // run the source destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_buffer_source(throwing_move_buffer_source{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + + void + testSuspends() + { + // Drive pull/read/read_some whose awaitables suspend then + // resume, covering the type-erased await_suspend forwarding. + resuming_buffer_source src; + any_buffer_source abs(&src); + + auto coro = [&]() -> task { + const_buffer arr[detail::max_iovec_]; + auto [ec1, bufs] = co_await abs.pull(arr); + if(ec1) + co_return 0; + + char buf[3] = {}; + auto [ec2, n2] = co_await abs.read(make_buffer(buf, 3)); + if(ec2) + co_return 0; + + auto [ec3, n3] = co_await abs.read_some(make_buffer(buf, 3)); + if(ec3) + co_return 0; + + co_return bufs.size() + n2 + n3; + }; + + std::size_t result{}; + test::run_blocking([&](std::size_t v) { result = v; })(coro()); + BOOST_TEST_EQ(result, 7u); + } + void testPull() { @@ -787,6 +905,9 @@ class any_buffer_source_test testConstruct(); testMove(); testMoveNative(); + testMoveAssignOwning(); + testConstructThrows(); + testSuspends(); testPull(); testConsume(); testPullWithoutConsume(); diff --git a/test/unit/io/any_read_source.cpp b/test/unit/io/any_read_source.cpp index f25c84b32..b178edb47 100644 --- a/test/unit/io/any_read_source.cpp +++ b/test/unit/io/any_read_source.cpp @@ -60,6 +60,44 @@ struct pending_read_source { return pending_source_awaitable{counter_}; } }; +// Reports not-ready, then resumes from await_suspend, exercising the +// type-erased await_suspend forwarding the always-ready mocks skip. +struct resuming_source_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result await_resume() { return {{}, 5}; } +}; + +struct resuming_read_source +{ + resuming_source_awaitable read_some( + MutableBufferSequence auto) + { return {}; } + resuming_source_awaitable read( + MutableBufferSequence auto) + { return {}; } +}; + +// Move constructor throws so owning construction fails after storage +// is allocated but before the source is constructed. +struct throwing_move_read_source +{ + int* destroyed_; + explicit throwing_move_read_source(int* d) : destroyed_(d) {} + throwing_move_read_source(throwing_move_read_source&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_read_source() { if(destroyed_) ++(*destroyed_); } + resuming_source_awaitable read_some( + MutableBufferSequence auto) + { return {}; } + resuming_source_awaitable read( + MutableBufferSequence auto) + { return {}; } +}; + class any_read_source_test { public: @@ -145,6 +183,55 @@ class any_read_source_test BOOST_TEST(!ars2.has_value()); } + void + testMoveAssignOwning() + { + // Move-assign over an owning wrapper to exercise the storage_ + // teardown branch in operator=. + test::fuse f1; + test::fuse f2; + any_read_source a(test::read_source{f1}); + any_read_source b(test::read_source{f2}); + BOOST_TEST(a.has_value()); + + a = std::move(b); + BOOST_TEST(a.has_value()); + BOOST_TEST(!b.has_value()); + } + + void + testConstructThrows() + { + // Owning construction whose source move-ctor throws must not + // run the source destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_read_source(throwing_move_read_source{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + + void + testReadSuspends() + { + // Drive a read whose awaitable suspends and then resumes, + // covering the type-erased await_suspend forwarding. + resuming_read_source rs; + any_read_source ars(&rs); + + auto coro = [&]() -> task { + char buf[1]; + auto [ec, n] = co_await ars.read(make_buffer(buf, 1)); + if(ec) + co_return 0; + co_return n; + }; + + std::size_t result{}; + test::run_blocking([&](std::size_t v) { result = v; })(coro()); + BOOST_TEST_EQ(result, 5u); + } + void testSelfAssign() { @@ -724,6 +811,9 @@ class any_read_source_test testConstructOwning(); testMove(); testMoveAssignNonEmpty(); + testMoveAssignOwning(); + testConstructThrows(); + testReadSuspends(); testSelfAssign(); testReadSome(); testReadSomePartial(); diff --git a/test/unit/io/any_read_stream.cpp b/test/unit/io/any_read_stream.cpp index 7920fe77a..bb8705a1e 100644 --- a/test/unit/io/any_read_stream.cpp +++ b/test/unit/io/any_read_stream.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "test/unit/test_helpers.hpp" @@ -55,6 +56,40 @@ struct pending_read_stream { return pending_read_awaitable{counter_}; } }; +// Reports not-ready, then resumes the awaiting coroutine from +// await_suspend. This exercises the type-erased await_suspend +// thunk, which the always-ready test mocks never reach. +struct resuming_read_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result await_resume() { return {{}, 7}; } +}; + +struct resuming_read_stream +{ + resuming_read_awaitable read_some( + MutableBufferSequence auto) + { return {}; } +}; + +// Move constructor throws so owning construction fails after storage +// is allocated but before the stream is constructed. The destructor +// touches a member, so destroying a null instance would fault. +struct throwing_move_read_stream +{ + int* destroyed_; + explicit throwing_move_read_stream(int* d) : destroyed_(d) {} + throwing_move_read_stream(throwing_move_read_stream&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_read_stream() { if(destroyed_) ++(*destroyed_); } + resuming_read_awaitable read_some( + MutableBufferSequence auto) + { return {}; } +}; + class any_read_stream_test { public: @@ -495,6 +530,55 @@ class any_read_stream_test } } + void + testMoveAssignOwning() + { + // Move-assign over an owning wrapper to exercise the + // storage_ teardown branch in operator=. + test::fuse f1; + test::fuse f2; + any_read_stream a(test::read_stream{f1}); + any_read_stream b(test::read_stream{f2}); + BOOST_TEST(a.has_value()); + + a = std::move(b); + BOOST_TEST(a.has_value()); + BOOST_TEST(!b.has_value()); + } + + void + testConstructThrows() + { + // Owning construction whose stream move-ctor throws must not + // run the stream destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_read_stream(throwing_move_read_stream{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + + void + testReadSomeSuspends() + { + // Drive a read whose awaitable suspends and then resumes, + // covering the type-erased await_suspend forwarding. + resuming_read_stream rs; + any_read_stream ars(&rs); + + auto coro = [&]() -> task { + char buf[1]; + auto [ec, n] = co_await ars.read_some(mutable_buffer(buf, 1)); + if(ec) + co_return 0; + co_return n; + }; + + std::size_t result{}; + test::run_blocking([&](std::size_t v) { result = v; })(coro()); + BOOST_TEST_EQ(result, 7u); + } + void run() { @@ -515,6 +599,9 @@ class any_read_stream_test testReadSomeManyBuffers(); testDestroyWithActiveAwaitable(); testMoveAssignWithActiveAwaitable(); + testMoveAssignOwning(); + testConstructThrows(); + testReadSomeSuspends(); } }; diff --git a/test/unit/io/any_stream.cpp b/test/unit/io/any_stream.cpp index b92483d19..56b84e1a7 100644 --- a/test/unit/io/any_stream.cpp +++ b/test/unit/io/any_stream.cpp @@ -182,6 +182,22 @@ class any_stream_test BOOST_TEST(!as2.has_value()); } + void + testMoveAssignOwning() + { + // Move-assign over an owning wrapper to exercise the storage_ + // teardown branch in operator=. + test::fuse f1; + test::fuse f2; + any_stream a(mock_stream{f1}); + any_stream b(mock_stream{f2}); + BOOST_TEST(a.has_value()); + + a = std::move(b); + BOOST_TEST(a.has_value()); + BOOST_TEST(!b.has_value()); + } + void testImplicitConversion() { @@ -279,6 +295,7 @@ class any_stream_test { testConstruct(); testMove(); + testMoveAssignOwning(); testImplicitConversion(); testReadWrite(); testReadViaBase(); diff --git a/test/unit/io/any_write_sink.cpp b/test/unit/io/any_write_sink.cpp index 0850a9978..6d3b0852b 100644 --- a/test/unit/io/any_write_sink.cpp +++ b/test/unit/io/any_write_sink.cpp @@ -79,6 +79,55 @@ struct pending_write_sink { return pending_sink_eof_awaitable{counter_}; } }; +// Suspends, then resumes from await_suspend, to exercise the +// type-erased await_suspend forwarding the always-ready mocks skip. +struct resuming_sink_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result await_resume() { return {{}, 1}; } +}; + +struct resuming_sink_eof_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result<> await_resume() { return {}; } +}; + +struct resuming_write_sink +{ + resuming_sink_awaitable write_some(ConstBufferSequence auto) + { return {}; } + resuming_sink_awaitable write(ConstBufferSequence auto) + { return {}; } + resuming_sink_awaitable write_eof(ConstBufferSequence auto) + { return {}; } + resuming_sink_eof_awaitable write_eof() { return {}; } +}; + +// Move constructor throws so owning construction fails after storage +// is allocated but before the sink is constructed. +struct throwing_move_write_sink +{ + int* destroyed_; + explicit throwing_move_write_sink(int* d) : destroyed_(d) {} + throwing_move_write_sink(throwing_move_write_sink&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_write_sink() { if(destroyed_) ++(*destroyed_); } + resuming_sink_awaitable write_some(ConstBufferSequence auto) + { return {}; } + resuming_sink_awaitable write(ConstBufferSequence auto) + { return {}; } + resuming_sink_awaitable write_eof(ConstBufferSequence auto) + { return {}; } + resuming_sink_eof_awaitable write_eof() { return {}; } +}; + class any_write_sink_test { public: @@ -634,6 +683,87 @@ class any_write_sink_test } } + void + testMoveAssignWithActiveEofAwaitable() + { + // Move-assign while an eof awaitable is active exercises the + // active_eof_ops_ destroy branch in operator=. + int destroyed = 0; + pending_write_sink ps{&destroyed}; + { + any_write_sink aws(&ps); + auto aw = aws.write_eof(); + BOOST_TEST(!aw.await_ready()); + + test::blocking_context bctx; + auto ex = bctx.get_executor(); + io_env env{executor_ref(ex), {}}; + aw.await_suspend(std::noop_coroutine(), &env); + + any_write_sink empty; + aws = std::move(empty); + BOOST_TEST_EQ(destroyed, 1); + } + } + + void + testMoveAssignOwning() + { + // Move-assign over an owning wrapper to exercise the storage_ + // teardown branch in operator=. + test::fuse f1; + test::fuse f2; + any_write_sink a(test::write_sink{f1}); + any_write_sink b(test::write_sink{f2}); + BOOST_TEST(a.has_value()); + + a = std::move(b); + BOOST_TEST(a.has_value()); + BOOST_TEST(!b.has_value()); + } + + void + testConstructThrows() + { + // Owning construction whose sink move-ctor throws must not run + // the sink destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_write_sink(throwing_move_write_sink{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + + void + testSuspends() + { + // Drive write/write_some/write_eof whose awaitables suspend + // then resume, covering the type-erased await_suspend paths. + resuming_write_sink sink; + any_write_sink aws(&sink); + + auto coro = [&]() -> task { + char const data[] = "x"; + auto [ec1, n1] = co_await aws.write_some(const_buffer(data, 1)); + if(ec1) + co_return 0; + auto [ec2, n2] = co_await aws.write(const_buffer(data, 1)); + if(ec2) + co_return 0; + auto [ec3, n3] = co_await aws.write_eof(const_buffer(data, 1)); + if(ec3) + co_return 0; + auto [ec4] = co_await aws.write_eof(); + if(ec4) + co_return 0; + co_return n1 + n2 + n3; + }; + + std::size_t result{}; + test::run_blocking([&](std::size_t v) { result = v; })(coro()); + BOOST_TEST_EQ(result, 3u); + } + void run() { @@ -661,6 +791,10 @@ class any_write_sink_test testDestroyWithActiveWriteAwaitable(); testDestroyWithActiveEofAwaitable(); testMoveAssignWithActiveAwaitable(); + testMoveAssignWithActiveEofAwaitable(); + testMoveAssignOwning(); + testConstructThrows(); + testSuspends(); } }; diff --git a/test/unit/io/any_write_stream.cpp b/test/unit/io/any_write_stream.cpp index aa1d1e171..ae298094a 100644 --- a/test/unit/io/any_write_stream.cpp +++ b/test/unit/io/any_write_stream.cpp @@ -57,6 +57,20 @@ struct pending_write_stream { return pending_write_awaitable{counter_}; } }; +// Move constructor throws so owning construction fails after storage +// is allocated but before the stream is constructed. +struct throwing_move_write_stream +{ + int* destroyed_; + explicit throwing_move_write_stream(int* d) : destroyed_(d) {} + throwing_move_write_stream(throwing_move_write_stream&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_write_stream() { if(destroyed_) ++(*destroyed_); } + pending_write_awaitable write_some( + ConstBufferSequence auto) + { return pending_write_awaitable{nullptr}; } +}; + class any_write_stream_test { public: @@ -124,6 +138,34 @@ class any_write_stream_test } } + void + testMoveAssignOwning() + { + // Move-assign over an owning wrapper to exercise the storage_ + // teardown branch in operator=. + test::fuse f1; + test::fuse f2; + any_write_stream a(test::write_stream{f1}); + any_write_stream b(test::write_stream{f2}); + BOOST_TEST(a.has_value()); + + a = std::move(b); + BOOST_TEST(a.has_value()); + BOOST_TEST(!b.has_value()); + } + + void + testConstructThrows() + { + // Owning construction whose stream move-ctor throws must not + // run the stream destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_write_stream(throwing_move_write_stream{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + void testWriteSome() { @@ -456,6 +498,8 @@ class any_write_stream_test testTrichotomyError(); testDestroyWithActiveAwaitable(); testMoveAssignWithActiveAwaitable(); + testMoveAssignOwning(); + testConstructThrows(); } }; diff --git a/test/unit/read_until.cpp b/test/unit/read_until.cpp index 6044a04d3..56e87c6a9 100644 --- a/test/unit/read_until.cpp +++ b/test/unit/read_until.cpp @@ -19,7 +19,9 @@ #include "test_suite.hpp" #include +#include #include +#include namespace boost { namespace capy { @@ -202,6 +204,24 @@ struct read_until_test BOOST_TEST_EQ(n, 12u); // "helloENDMARK" })); + + // Lvalue (non-owning) buffer with a chunked read: the + // delimiter is not present up front, so the inner coroutine + // runs and dereferences the pointer-stored buffer. + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::read_stream rs(f, 3); // max 3 bytes per read + rs.provide("hello\r\nworld"); + + std::string data; + string_dynamic_buffer db(&data); // lvalue: pointer storage + auto [ec, n] = co_await read_until(rs, db, "\r\n"); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 7u); + BOOST_TEST_EQ(data.substr(0, n), "hello\r\n"); + })); } //---------------------------------------------------------- @@ -433,6 +453,30 @@ struct read_until_test //---------------------------------------------------------- + void + testMatchHelpers() + { + // A multi-buffer sequence forces linearize_buffers and the + // multi-buffer branch of search_buffer_for_match, which the + // contiguous string_dynamic_buffer never reaches. + const_buffer bufs[2] = { + const_buffer("hel", 3), + const_buffer("lo\r\nx", 5) + }; + std::span seq(bufs, 2); + + BOOST_TEST_EQ(detail::linearize_buffers(seq), "hello\r\nx"); + + match_delim m{"\r\n"}; + BOOST_TEST_EQ(detail::search_buffer_for_match(seq, m), 7u); + + // On a miss, a multi-character delimiter records an overlap + // hint of delim.size() - 1. + std::size_t hint = 999; + BOOST_TEST(m("partial te", &hint) == std::string_view::npos); + BOOST_TEST_EQ(hint, 1u); + } + void run() { @@ -442,6 +486,7 @@ struct read_until_test testErrorConditions(); testPrefilledBuffer(); testMatchCondition(); + testMatchHelpers(); } }; diff --git a/test/unit/test_helpers.hpp b/test/unit/test_helpers.hpp index 08f421f5c..a0f102b6d 100644 --- a/test/unit/test_helpers.hpp +++ b/test/unit/test_helpers.hpp @@ -345,6 +345,22 @@ throw_test_exception(char const* msg) throw test_exception(msg); } +// Throw test_exception in a way the optimizer cannot prove always +// happens. Used by throwing move-constructors in the type-erasure +// tests: an unconditional throw lets the optimizer prove construction +// never completes, which flags the code after the placement-new (and +// after the BOOST_TEST_THROWS expression) as unreachable (MSVC /O2 +// C4702). Gating the throw on a volatile read forces a re-read the +// optimizer cannot fold, so the call is not provably non-returning, +// while at runtime the throw always fires. +inline void +throw_test_exception_opaque(char const* msg) +{ + volatile bool always = true; + if(always) + throw test_exception(msg); +} + //---------------------------------------------------------- // Common Test Task Helpers //----------------------------------------------------------