diff --git a/include/boost/beast2/impl/read.hpp b/include/boost/beast2/impl/read.hpp index 403a430..3fc4f21 100644 --- a/include/boost/beast2/impl/read.hpp +++ b/include/boost/beast2/impl/read.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Mohammad Nejati // // 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) @@ -62,7 +63,15 @@ class read_until_op { pr_.parse(ec); if(ec == http_proto::condition::need_more_input) + { + // specific to http_io::async_read_some + if(total_bytes_ != 0 && condition_(pr_)) + { + ec = {}; + goto upcall; + } break; + } if(ec.failed() || condition_(pr_)) { if(total_bytes_ == 0) diff --git a/include/boost/beast2/read.hpp b/include/boost/beast2/read.hpp index e02c624..aece67c 100644 --- a/include/boost/beast2/read.hpp +++ b/include/boost/beast2/read.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Mohammad Nejati // // 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) diff --git a/include/boost/beast2/test/detail/service_base.hpp b/include/boost/beast2/test/detail/service_base.hpp new file mode 100644 index 0000000..b82cb82 --- /dev/null +++ b/include/boost/beast2/test/detail/service_base.hpp @@ -0,0 +1,40 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/cppaliance/beast2 +// + +#ifndef BOOST_BEAST2_TEST_DETAIL_SERVICE_BASE_HPP +#define BOOST_BEAST2_TEST_DETAIL_SERVICE_BASE_HPP + +#include + +namespace boost { +namespace beast2 { +namespace test { +namespace detail { + +template +struct service_base : asio::execution_context::service +{ + static asio::execution_context::id const id; + + explicit + service_base(asio::execution_context& ctx) + : asio::execution_context::service(ctx) + { + } +}; + +template +asio::execution_context::id const service_base::id; + +} // detail +} // test +} // beast2 +} // boost + +#endif diff --git a/include/boost/beast2/test/detail/stream_state.hpp b/include/boost/beast2/test/detail/stream_state.hpp new file mode 100644 index 0000000..41fc684 --- /dev/null +++ b/include/boost/beast2/test/detail/stream_state.hpp @@ -0,0 +1,260 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2020 Richard Hodges (hodges.r@gmail.com) +// +// 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/cppaliance/beast2 +// + +#ifndef BOOST_BEAST2_TEST_DETAIL_STREAM_STATE_HPP +#define BOOST_BEAST2_TEST_DETAIL_STREAM_STATE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace boost { +namespace beast2 { +namespace test { +namespace detail { + +struct stream_state; + +struct stream_service_impl +{ + std::mutex m_; + std::vector v_; + + void + remove(stream_state& impl); +}; + +//------------------------------------------------------------------------------ + +class stream_service + : public beast2::test::detail::service_base +{ + boost::shared_ptr sp_; + + void + shutdown() override; + +public: + explicit + stream_service(asio::execution_context& ctx); + + static + auto + make_impl( + asio::any_io_executor exec, + test::fail_count* fc) -> + boost::shared_ptr; +}; + +//------------------------------------------------------------------------------ + +struct stream_read_op_base +{ + virtual ~stream_read_op_base() = default; + virtual void operator()(system::error_code ec) = 0; +}; + +//------------------------------------------------------------------------------ + +enum class stream_status +{ + ok, + eof, +}; + +//------------------------------------------------------------------------------ + +struct stream_state +{ + asio::any_io_executor exec; + boost::weak_ptr wp; + std::mutex m; + std::string storage; + buffers::string_buffer b; + std::condition_variable cv; + std::unique_ptr op; + stream_status code = stream_status::ok; + fail_count* fc = nullptr; + std::size_t nread = 0; + std::size_t nread_bytes = 0; + std::size_t nwrite = 0; + std::size_t nwrite_bytes = 0; + std::size_t read_max = + (std::numeric_limits::max)(); + std::size_t write_max = + (std::numeric_limits::max)(); + + stream_state( + asio::any_io_executor exec_, + boost::weak_ptr wp_, + fail_count* fc_); + + stream_state(stream_state&&) = delete; + + ~stream_state(); + + void + remove() noexcept; + + void + notify_read(); + + void + cancel_read(); +}; + +//------------------------------------------------------------------------------ + +inline +stream_service:: +stream_service(asio::execution_context& ctx) + : beast2::test::detail::service_base(ctx) + , sp_(boost::make_shared()) +{ +} + +inline +void +stream_service:: +shutdown() +{ + std::vector> v; + std::lock_guard g1(sp_->m_); + v.reserve(sp_->v_.size()); + for(auto p : sp_->v_) + { + std::lock_guard g2(p->m); + v.emplace_back(std::move(p->op)); + p->code = detail::stream_status::eof; + } +} + +inline +auto +stream_service:: +make_impl( + asio::any_io_executor exec, + test::fail_count* fc) -> + boost::shared_ptr +{ +#if defined(BOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT) + auto& ctx = exec.context(); +#else + auto& ctx = asio::query( + exec, + asio::execution::context); +#endif + auto& svc = asio::use_service(ctx); + auto sp = boost::make_shared(exec, svc.sp_, fc); + std::lock_guard g(svc.sp_->m_); + svc.sp_->v_.push_back(sp.get()); + return sp; +} + +//------------------------------------------------------------------------------ + +inline +void +stream_service_impl:: +remove(stream_state& impl) +{ + std::lock_guard g(m_); + *std::find( + v_.begin(), v_.end(), + &impl) = std::move(v_.back()); + v_.pop_back(); +} + +//------------------------------------------------------------------------------ + +inline +stream_state:: +stream_state( + asio::any_io_executor exec_, + boost::weak_ptr wp_, + fail_count* fc_) + : exec(std::move(exec_)) + , wp(std::move(wp_)) + , b(&storage) + , fc(fc_) +{ +} + +inline +stream_state:: +~stream_state() +{ + // cancel outstanding read + if(op != nullptr) + (*op)(asio::error::operation_aborted); +} + +inline +void +stream_state:: +remove() noexcept +{ + auto sp = wp.lock(); + + // If this goes off, it means the lifetime of a test::stream object + // extended beyond the lifetime of the associated execution context. + BOOST_ASSERT(sp); + + sp->remove(*this); +} + +inline +void +stream_state:: +notify_read() +{ + if(op) + { + auto op_ = std::move(op); + op_->operator()(system::error_code{}); + } + else + { + cv.notify_all(); + } +} + +inline +void +stream_state:: +cancel_read() +{ + std::unique_ptr p; + { + std::lock_guard lock(m); + code = stream_status::eof; + p = std::move(op); + } + if(p != nullptr) + (*p)(asio::error::operation_aborted); +} + +} // detail +} // test +} // beast2 +} // boost + +#endif diff --git a/include/boost/beast2/test/error.hpp b/include/boost/beast2/test/error.hpp new file mode 100644 index 0000000..64a62fa --- /dev/null +++ b/include/boost/beast2/test/error.hpp @@ -0,0 +1,34 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/cppaliance/beast2 +// + +#ifndef BOOST_BEAST2_TEST_ERROR_HPP +#define BOOST_BEAST2_TEST_ERROR_HPP + +namespace boost { +namespace beast2 { +namespace test { + +/// Error codes returned from unit testing algorithms +enum class error +{ + /** The test stream generated a simulated testing error + + This error is returned by a @ref fail_count object + when it generates a simulated error. + */ + test_failure = 1 +}; + +} // test +} // beast2 +} // boost + +#include + +#endif diff --git a/include/boost/beast2/test/fail_count.hpp b/include/boost/beast2/test/fail_count.hpp new file mode 100644 index 0000000..5e98433 --- /dev/null +++ b/include/boost/beast2/test/fail_count.hpp @@ -0,0 +1,88 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/cppaliance/beast2 +// + +#ifndef BOOST_BEAST2_TEST_FAIL_COUNT_HPP +#define BOOST_BEAST2_TEST_FAIL_COUNT_HPP + +#include +#include + +#include +#include + +#include + +namespace boost { +namespace beast2 { +namespace test { + +/** A countdown to simulated failure + + On the Nth operation, the class will fail with the specified + error code, or the default error code of @ref error::test_failure. + + Instances of this class may be used to build objects which + are specifically designed to aid in writing unit tests, for + interfaces which can throw exceptions or return `error_code` + values representing failure. +*/ +class fail_count +{ + std::size_t n_; + std::size_t i_ = 0; + system::error_code ec_; + +public: + fail_count(fail_count&&) = default; + + /** Construct a counter + + @param n The 0-based index of the operation to fail on or after + @param ev An optional error code to use when generating a simulated failure + */ + explicit + fail_count( + std::size_t n, + system::error_code ev = error::test_failure) + : n_(n) + , ec_(ev) + { + } + + /// Throw an exception on the Nth failure + void + fail() + { + if(i_ < n_) + ++i_; + if(i_ == n_) + BOOST_THROW_EXCEPTION(system::system_error{ec_}); + } + + /// Set an error code on the Nth failure + bool + fail(system::error_code& ec) + { + if(i_ < n_) + ++i_; + if(i_ == n_) + { + ec = ec_; + return true; + } + ec = {}; + return false; + } +}; + +} // test +} // beast2 +} // boost + +#endif diff --git a/include/boost/beast2/test/impl/error.hpp b/include/boost/beast2/test/impl/error.hpp new file mode 100644 index 0000000..4a41066 --- /dev/null +++ b/include/boost/beast2/test/impl/error.hpp @@ -0,0 +1,76 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/cppaliance/beast2 +// + +#ifndef BOOST_BEAST2_TEST_IMPL_ERROR_HPP +#define BOOST_BEAST2_TEST_IMPL_ERROR_HPP + +#include +#include + +namespace boost { +namespace system { +template<> +struct is_error_code_enum< + boost::beast2::test::error> + : std::true_type +{ +}; +} // system +} // boost + +namespace boost { +namespace beast2 { +namespace test { + +namespace detail { + +class error_cat_type : + public system::error_category +{ +public: + const char* + name() const noexcept override + { + return "boost.beast2.test"; + } + + char const* + message(int ev, char*, std::size_t) const noexcept override + { + switch(static_cast(ev)) + { + default: + case error::test_failure: return + "An automatic unit test failure occurred"; + } + } + + std::string + message(int ev) const override + { + return message(ev, nullptr, 0); + } +}; + +} // detail + +inline +system::error_code +make_error_code(error e) noexcept +{ + static detail::error_cat_type const cat{}; + return {static_cast< + std::underlying_type::type>(e), cat }; +} + +} // test +} // beast2 +} // boost + +#endif diff --git a/include/boost/beast2/test/impl/stream.hpp b/include/boost/beast2/test/impl/stream.hpp new file mode 100644 index 0000000..dcef188 --- /dev/null +++ b/include/boost/beast2/test/impl/stream.hpp @@ -0,0 +1,547 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/cppaliance/beast2 +// + +#ifndef BOOST_BEAST2_TEST_IMPL_STREAM_HPP +#define BOOST_BEAST2_TEST_IMPL_STREAM_HPP + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace boost { +namespace beast2 { +namespace test { + +namespace detail +{ +template +struct extract_executor_op +{ + To operator()(asio::any_io_executor& ex) const + { + assert(ex.template target()); + return *ex.template target(); + } +}; + +template<> +struct extract_executor_op +{ + asio::any_io_executor operator()(asio::any_io_executor& ex) const + { + return ex; + } +}; +} // detail + +template +template +class basic_stream::read_op : public detail::stream_read_op_base +{ + struct lambda + { + Handler h_; + boost::weak_ptr wp_; + Buffers b_; + asio::executor_work_guard< + asio::associated_executor_t> wg2_; + lambda(lambda&&) = default; + lambda(lambda const&) = default; + + template + lambda( + Handler_&& h, + boost::shared_ptr const& s, + Buffers const& b) + : h_(std::forward(h)) + , wp_(s) + , b_(b) + , wg2_(asio::get_associated_executor(h_, s->exec)) + { + } + + using allocator_type = asio::associated_allocator_t; + + allocator_type get_allocator() const noexcept + { + return asio::get_associated_allocator(h_); + } + + using cancellation_slot_type = + asio::associated_cancellation_slot_t; + + cancellation_slot_type + get_cancellation_slot() const noexcept + { + return asio::get_associated_cancellation_slot(h_, + asio::cancellation_slot()); + } + + void + operator()(system::error_code ec) + { + std::size_t bytes_transferred = 0; + auto sp = wp_.lock(); + if(! sp) + { + ec = asio::error::operation_aborted; + } + if(! ec) + { + std::lock_guard lock(sp->m); + BOOST_ASSERT(! sp->op); + if(sp->b.size() > 0) + { + bytes_transferred = + buffers::copy( + b_, sp->b.data(), sp->read_max); + sp->b.consume(bytes_transferred); + sp->nread_bytes += bytes_transferred; + } + else if (buffers::size(b_) > 0) + { + ec = asio::error::eof; + } + } + + asio::dispatch(wg2_.get_executor(), + asio::append(std::move(h_), ec, bytes_transferred)); + wg2_.reset(); + } + }; + + lambda fn_; + asio::executor_work_guard wg1_; + +public: + template + read_op( + Handler_&& h, + boost::shared_ptr const& s, + Buffers const& b) + : fn_(std::forward(h), s, b) + , wg1_(s->exec) + { + } + + void + operator()(system::error_code ec) override + { + asio::post(wg1_.get_executor(), asio::append(std::move(fn_), ec)); + wg1_.reset(); + } +}; + +template +struct basic_stream::run_read_op +{ + boost::shared_ptr const& in; + + using executor_type = typename basic_stream::executor_type; + + executor_type + get_executor() const noexcept + { + return detail::extract_executor_op()(in->exec); + } + + template< + class ReadHandler, + class MutableBufferSequence> + void + operator()( + ReadHandler&& h, + MutableBufferSequence const& buffers) + { + // If you get an error on the following line it means + // that your handler does not meet the documented type + // requirements for the handler. + + initiate_read( + in, + std::unique_ptr{ + new read_op< + typename std::decay::type, + MutableBufferSequence>( + std::move(h), + in, + buffers)}, + buffers::size(buffers)); + } +}; + +template +struct basic_stream::run_write_op +{ + boost::shared_ptr const& in_; + + using executor_type = typename basic_stream::executor_type; + + executor_type + get_executor() const noexcept + { + return detail::extract_executor_op()(in_->exec); + } + + template< + class WriteHandler, + class ConstBufferSequence> + void + operator()( + WriteHandler&& h, + boost::weak_ptr out_, + ConstBufferSequence const& buffers) + { + // If you get an error on the following line it means + // that your handler does not meet the documented type + // requirements for the handler. + + ++in_->nwrite; + auto const upcall = [&](system::error_code ec, std::size_t n) + { + asio::post(in_->exec, asio::append(std::move(h), ec, n)); + }; + + // test failure + system::error_code ec; + std::size_t n = 0; + if(in_->fc && in_->fc->fail(ec)) + return upcall(ec, n); + + // A request to write 0 bytes to a stream is a no-op. + if(buffers::size(buffers) == 0) + return upcall(ec, n); + + // connection closed + auto out = out_.lock(); + if(! out) + return upcall(asio::error::connection_reset, n); + + // copy buffers + n = std::min( + buffers::size(buffers), in_->write_max); + { + std::lock_guard lock(out->m); + n = buffers::copy(out->b.prepare(n), buffers); + out->b.commit(n); + out->nwrite_bytes += n; + out->notify_read(); + } + BOOST_ASSERT(! ec); + upcall(ec, n); + } +}; + +//------------------------------------------------------------------------------ + +template +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(ReadHandler, void(system::error_code, std::size_t)) +basic_stream:: +async_read_some( + MutableBufferSequence const& buffers, + ReadHandler&& handler) +{ + static_assert(asio::is_mutable_buffer_sequence< + MutableBufferSequence>::value, + "MutableBufferSequence type requirements not met"); + + return asio::async_initiate< + ReadHandler, + void(system::error_code, std::size_t)>( + run_read_op{in_}, + handler, + buffers); +} + +template +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(WriteHandler, void(system::error_code, std::size_t)) +basic_stream:: +async_write_some( + ConstBufferSequence const& buffers, + WriteHandler&& handler) +{ + static_assert(asio::is_const_buffer_sequence< + ConstBufferSequence>::value, + "ConstBufferSequence type requirements not met"); + + return asio::async_initiate< + WriteHandler, + void(system::error_code, std::size_t)>( + run_write_op{in_}, + handler, + out_, + buffers); +} + +//------------------------------------------------------------------------------ + +template +basic_stream +connect(stream& to, Arg1&& arg1, ArgN&&... argn) +{ + stream from{ + std::forward(arg1), + std::forward(argn)...}; + from.connect(to); + return from; +} + +template +auto basic_stream::get_executor() noexcept -> executor_type +{ + return detail::extract_executor_op()(in_->exec); +} + + +//------------------------------------------------------------------------------ + +template +void basic_stream::initiate_read( + boost::shared_ptr const& in_, + std::unique_ptr&& op, + std::size_t buf_size) +{ + std::unique_lock lock(in_->m); + + ++in_->nread; + if(in_->op != nullptr) + BOOST_THROW_EXCEPTION( + std::logic_error{"in_->op != nullptr"}); + + // test failure + system::error_code ec; + if(in_->fc && in_->fc->fail(ec)) + { + lock.unlock(); + (*op)(ec); + return; + } + + // A request to read 0 bytes from a stream is a no-op. + if(buf_size == 0 || buffers::size(in_->b.data()) > 0) + { + lock.unlock(); + (*op)(ec); + return; + } + + // deliver error + if(in_->code != detail::stream_status::ok) + { + lock.unlock(); + (*op)(asio::error::eof); + return; + } + + // complete when bytes available or closed + in_->op = std::move(op); +} + +//------------------------------------------------------------------------------ + +template +basic_stream:: +~basic_stream() +{ + close(); + in_->remove(); +} + +template +basic_stream:: +basic_stream(basic_stream&& other) +{ + auto in = detail::stream_service::make_impl( + other.in_->exec, other.in_->fc); + in_ = std::move(other.in_); + out_ = std::move(other.out_); + other.in_ = in; +} + + +template +basic_stream& +basic_stream:: +operator=(basic_stream&& other) +{ + close(); + auto in = detail::stream_service::make_impl( + other.in_->exec, other.in_->fc); + in_->remove(); + in_ = std::move(other.in_); + out_ = std::move(other.out_); + other.in_ = in; + return *this; +} + +//------------------------------------------------------------------------------ + +template +basic_stream:: +basic_stream(executor_type exec) + : in_(detail::stream_service::make_impl(std::move(exec), nullptr)) +{ +} + +template +basic_stream:: +basic_stream( + asio::io_context& ioc, + fail_count& fc) + : in_(detail::stream_service::make_impl(ioc.get_executor(), &fc)) +{ +} + +template +basic_stream:: +basic_stream( + asio::io_context& ioc, + core::string_view s) + : in_(detail::stream_service::make_impl(ioc.get_executor(), nullptr)) +{ + in_->b.commit(buffers::copy( + in_->b.prepare(s.size()), + buffers::const_buffer(s.data(), s.size()))); +} + +template +basic_stream:: +basic_stream( + asio::io_context& ioc, + fail_count& fc, + core::string_view s) + : in_(detail::stream_service::make_impl(ioc.get_executor(), &fc)) +{ + in_->b.commit(buffers::copy( + in_->b.prepare(s.size()), + buffers::const_buffer(s.data(), s.size()))); +} + +template +void +basic_stream:: +connect(basic_stream& remote) +{ + BOOST_ASSERT(! out_.lock()); + BOOST_ASSERT(! remote.out_.lock()); + std::lock(in_->m, remote.in_->m); + std::lock_guard guard1{in_->m, std::adopt_lock}; + std::lock_guard guard2{remote.in_->m, std::adopt_lock}; + out_ = remote.in_; + remote.out_ = in_; + in_->code = detail::stream_status::ok; + remote.in_->code = detail::stream_status::ok; +} + +template +core::string_view +basic_stream:: +str() const +{ + auto const bs = in_->b.data(); + if(buffers::size(bs) == 0) + return {}; + buffers::const_buffer const b = *asio::buffer_sequence_begin(bs); + return {static_cast(b.data()), b.size()}; +} + +template +void +basic_stream:: +append(core::string_view s) +{ + std::lock_guard lock{in_->m}; + in_->b.commit(buffers::copy( + in_->b.prepare(s.size()), + buffers::const_buffer(s.data(), s.size()))); +} + +template +void +basic_stream:: +clear() +{ + std::lock_guard lock{in_->m}; + in_->b.consume(in_->b.size()); +} + +template +void +basic_stream:: +close() +{ + in_->cancel_read(); + + // disconnect + { + auto out = out_.lock(); + out_.reset(); + + // notify peer + if(out) + { + std::lock_guard lock(out->m); + if(out->code == detail::stream_status::ok) + { + out->code = detail::stream_status::eof; + out->notify_read(); + } + } + } +} + +template +void +basic_stream:: +close_remote() +{ + std::lock_guard lock{in_->m}; + if(in_->code == detail::stream_status::ok) + { + in_->code = detail::stream_status::eof; + in_->notify_read(); + } +} + +//------------------------------------------------------------------------------ + +template +basic_stream +connect(basic_stream& to) +{ + basic_stream from(to.get_executor()); + from.connect(to); + return from; +} + +template +void +connect(basic_stream& s1, basic_stream& s2) +{ + s1.connect(s2); +} + +} // test +} // beast2 +} // boost + +#endif diff --git a/include/boost/beast2/test/stream.hpp b/include/boost/beast2/test/stream.hpp new file mode 100644 index 0000000..a5b636e --- /dev/null +++ b/include/boost/beast2/test/stream.hpp @@ -0,0 +1,502 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/cppaliance/beast2 +// + +#ifndef BOOST_BEAST2_TEST_STREAM_HPP +#define BOOST_BEAST2_TEST_STREAM_HPP + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// Forward declarations +namespace boost { +namespace asio { +namespace ssl { +template class stream; +} // ssl +} // asio +} // boost + +namespace boost { +namespace beast2 { +namespace test { + +/** A two-way socket useful for unit testing + + An instance of this class simulates a traditional socket, + while also providing features useful for unit testing. + Each endpoint maintains an independent buffer called + the input area. Writes from one endpoint append data + to the peer's pending input area. When an endpoint performs + a read and data is present in the input area, the data is + delivered to the blocking or asynchronous operation. Otherwise + the operation is blocked or deferred until data is made + available, or until the endpoints become disconnected. + + These streams may be used anywhere an algorithm accepts a + reference to a synchronous or asynchronous read or write + stream. It is possible to use a test stream in a call to + `asio::read_until`, or in a call to + @ref boost::beast2::http::async_write for example. + + As with Boost.Asio I/O objects, a @ref stream constructs + with a reference to the `asio::io_context` to use for + handling asynchronous I/O. For asynchronous operations, the + stream follows the same rules as a traditional asio socket + with respect to how completion handlers for asynchronous + operations are performed. + + To facilitate testing, these streams support some additional + features: + + @li The input area, represented by a @ref beast2::basic_flat_buffer, + may be directly accessed by the caller to inspect the contents + before or after the remote endpoint writes data. This allows + a unit test to verify that the received data matches. + + @li Data may be manually appended to the input area. This data + will delivered in the next call to + @ref stream::read_some or @ref stream::async_read_some. + This allows predefined test vectors to be set up for testing + read algorithms. + + @li The stream may be constructed with a fail count. The + stream will eventually fail with a predefined error after a + certain number of operations, where the number of operations + is controlled by the test. When a test loops over a range of + operation counts, it is possible to exercise every possible + point of failure in the algorithm being tested. When used + correctly the technique allows the tests to reach a high + percentage of code coverage. + + @par Thread Safety + @e Distinct @e objects: Safe.@n + @e Shared @e objects: Unsafe. + The application must also ensure that all asynchronous + operations are performed within the same implicit or explicit strand. + + @par Concepts + @li SyncReadStream + @li SyncWriteStream + @li AsyncReadStream + @li AsyncWriteStream +*/ +template +class basic_stream; + +template +class basic_stream +{ +public: + /// The type of the executor associated with the object. + using executor_type = + Executor; + + /// Rebinds the socket type to another executor. + template + struct rebind_executor + { + /// The socket type when rebound to the specified executor. + typedef basic_stream other; + }; + +private: + template + friend class basic_stream; + + boost::shared_ptr in_; + boost::weak_ptr out_; + + template + class read_op; + + struct run_read_op; + struct run_write_op; + + static + void + initiate_read( + boost::shared_ptr const& in, + std::unique_ptr&& op, + std::size_t buf_size); + +#if ! BOOST_BEAST2_DOXYGEN + // boost::asio::ssl::stream needs these + // DEPRECATED + template + friend class boost::asio::ssl::stream; + // DEPRECATED + using lowest_layer_type = basic_stream; + // DEPRECATED + lowest_layer_type& + lowest_layer() noexcept + { + return *this; + } + // DEPRECATED + lowest_layer_type const& + lowest_layer() const noexcept + { + return *this; + } +#endif + +public: + using buffer_type = buffers::string_buffer; + + /** Destructor + + If an asynchronous read operation is pending, it will + simply be discarded with no notification to the completion + handler. + + If a connection is established while the stream is destroyed, + the peer will see the error `asio::error::connection_reset` + when performing any reads or writes. + */ + ~basic_stream(); + + /** Move Constructor + + Moving the stream while asynchronous operations are pending + results in undefined behavior. + */ + basic_stream(basic_stream&& other); + + /** Move Constructor + + Moving the stream while asynchronous operations are pending + results in undefined behavior. + */ + template + basic_stream(basic_stream&& other) + : in_(std::move(other.in_)) + , out_(std::move(other.out_)) + { + BOOST_ASSERT(in_->exec.template target() != nullptr); + in_->exec = executor_type(*in_->exec.template target()); + } + + /** Move Assignment + + Moving the stream while asynchronous operations are pending + results in undefined behavior. + */ + basic_stream& + operator=(basic_stream&& other); + + template + basic_stream& + operator==(basic_stream&& other); + + /** Construct a stream + + The stream will be created in a disconnected state. + + @param context The `io_context` object that the stream will use to + dispatch handlers for any asynchronous operations. + */ + template ::value>::type> + explicit + basic_stream(ExecutionContext& context) + : basic_stream(context.get_executor()) + { + } + + /** Construct a stream + + The stream will be created in a disconnected state. + + @param exec The `executor` object that the stream will use to + dispatch handlers for any asynchronous operations. + */ + explicit + basic_stream(executor_type exec); + + /** Construct a stream + + The stream will be created in a disconnected state. + + @param ioc The `io_context` object that the stream will use to + dispatch handlers for any asynchronous operations. + + @param fc The @ref fail_count to associate with the stream. + Each I/O operation performed on the stream will increment the + fail count. When the fail count reaches its internal limit, + a simulated failure error will be raised. + */ + basic_stream( + asio::io_context& ioc, + fail_count& fc); + + /** Construct a stream + + The stream will be created in a disconnected state. + + @param ioc The `io_context` object that the stream will use to + dispatch handlers for any asynchronous operations. + + @param s A string which will be appended to the input area, not + including the null terminator. + */ + basic_stream( + asio::io_context& ioc, + core::string_view s); + + /** Construct a stream + + The stream will be created in a disconnected state. + + @param ioc The `io_context` object that the stream will use to + dispatch handlers for any asynchronous operations. + + @param fc The @ref fail_count to associate with the stream. + Each I/O operation performed on the stream will increment the + fail count. When the fail count reaches its internal limit, + a simulated failure error will be raised. + + @param s A string which will be appended to the input area, not + including the null terminator. + */ + basic_stream( + asio::io_context& ioc, + fail_count& fc, + core::string_view s); + + /// Establish a connection + void + connect(basic_stream& remote); + + /// Return the executor associated with the object. + executor_type + get_executor() noexcept; + + /// Set the maximum number of bytes returned by read_some + void + read_size(std::size_t n) noexcept + { + in_->read_max = n; + } + + /// Set the maximum number of bytes returned by write_some + void + write_size(std::size_t n) noexcept + { + in_->write_max = n; + } + + /// Direct input buffer access + buffer_type& + buffer() noexcept + { + return in_->b; + } + + /// Returns a string view representing the pending input data + core::string_view + str() const; + + /// Appends a string to the pending input data + void + append(core::string_view s); + + /// Clear the pending input area + void + clear(); + + /// Return the number of reads + std::size_t + nread() const noexcept + { + return in_->nread; + } + + /// Return the number of bytes read + std::size_t + nread_bytes() const noexcept + { + return in_->nread_bytes; + } + + /// Return the number of writes + std::size_t + nwrite() const noexcept + { + return in_->nwrite; + } + + /// Return the number of bytes written + std::size_t + nwrite_bytes() const noexcept + { + return in_->nwrite_bytes; + } + + /** Close the stream. + + The other end of the connection will see + `error::eof` after reading all the remaining data. + */ + void + close(); + + /** Close the other end of the stream. + + This end of the connection will see + `error::eof` after reading all the remaining data. + */ + void + close_remote(); + + /** Start an asynchronous read. + + This function is used to asynchronously read one or more bytes of data from + the stream. The function call always returns immediately. + + @param buffers The buffers into which the data will be read. Although the + buffers object may be copied as necessary, ownership of the underlying + buffers is retained by the caller, which must guarantee that they remain + valid until the handler is called. + + @param handler The completion handler to invoke when the operation + completes. The implementation takes ownership of the handler by + performing a decay-copy. The equivalent function signature of + the handler must be: + @code + void handler( + system::error_code const& ec, // Result of operation. + std::size_t bytes_transferred // Number of bytes read. + ); + @endcode + If the handler has an associated immediate executor, + an immediate completion will be dispatched to it. + Otherwise, the handler will not be invoked from within + this function. Invocation of the handler will be performed + by dispatching to the immediate executor. If no + immediate executor is specified, this is equivalent + to using `asio::post`. + @note The `async_read_some` operation may not read all of the requested number of + bytes. Consider using the function `asio::async_read` if you need + to ensure that the requested amount of data is read before the asynchronous + operation completes. + */ + template< + class MutableBufferSequence, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(system::error_code, std::size_t)) ReadHandler + BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)> + BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(ReadHandler, void(system::error_code, std::size_t)) + async_read_some( + MutableBufferSequence const& buffers, + ReadHandler&& handler BOOST_ASIO_DEFAULT_COMPLETION_TOKEN(executor_type)); + + /** Start an asynchronous write. + + This function is used to asynchronously write one or more bytes of data to + the stream. The function call always returns immediately. + + @param buffers The data to be written to the stream. Although the buffers + object may be copied as necessary, ownership of the underlying buffers is + retained by the caller, which must guarantee that they remain valid until + the handler is called. + + @param handler The completion handler to invoke when the operation + completes. The implementation takes ownership of the handler by + performing a decay-copy. The equivalent function signature of + the handler must be: + @code + void handler( + system::error_code const& ec, // Result of operation. + std::size_t bytes_transferred // Number of bytes written. + ); + @endcode + If the handler has an associated immediate executor, + an immediate completion will be dispatched to it. + Otherwise, the handler will not be invoked from within + this function. Invocation of the handler will be performed + by dispatching to the immediate executor. If no + immediate executor is specified, this is equivalent + to using `asio::post`. + @note The `async_write_some` operation may not transmit all of the data to + the peer. Consider using the function `asio::async_write` if you need + to ensure that all data is written before the asynchronous operation completes. + */ + template< + class ConstBufferSequence, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(system::error_code, std::size_t)) WriteHandler + BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)> + BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(WriteHandler, void(system::error_code, std::size_t)) + async_write_some( + ConstBufferSequence const& buffers, + WriteHandler&& handler BOOST_ASIO_DEFAULT_COMPLETION_TOKEN(executor_type) + ); +}; + +#if ! BOOST_BEAST2_DOXYGEN +template +void +beast2_close_socket(basic_stream& s) +{ + s.close(); +} +#endif + +#if BOOST_BEAST2_DOXYGEN +/** Return a new stream connected to the given stream + + @param to The stream to connect to. + + @param args Optional arguments forwarded to the new stream's constructor. + + @return The new, connected stream. +*/ +template +template +basic_stream +connect(basic_stream& to, Args&&... args); + +#else +template +basic_stream +connect(basic_stream& to); + +template +void +connect(basic_stream& s1, basic_stream& s2); + +template +basic_stream +connect(basic_stream& to, Arg1&& arg1, ArgN&&... argn); +#endif + +using stream = basic_stream<>; + +} // test +} // beast2 +} // boost + +#include + +#endif diff --git a/include/boost/beast2/test/tcp.hpp b/include/boost/beast2/test/tcp.hpp new file mode 100644 index 0000000..6c1a1d9 --- /dev/null +++ b/include/boost/beast2/test/tcp.hpp @@ -0,0 +1,57 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/cppaliance/beast2 +// + +#ifndef BOOST_BEAST2_TEST_TCP_HPP +#define BOOST_BEAST2_TEST_TCP_HPP + +#include + +#include +#include + +namespace boost { +namespace beast2 { +namespace test { + +/** Connect two TCP sockets together. +*/ +template +bool +connect( + asio::io_context& ioc, + asio::basic_stream_socket& s1, + asio::basic_stream_socket& s2) + +{ + asio::basic_socket_acceptor< + asio::ip::tcp, Executor> a(s1.get_executor()); + auto ep = asio::ip::tcp::endpoint( + asio::ip::make_address_v4("127.0.0.1"), 0); + a.open(ep.protocol()); + a.set_option( + asio::socket_base::reuse_address(true)); + a.bind(ep); + a.listen(0); + ep = a.local_endpoint(); + a.async_accept(s2, asio::detached); + s1.async_connect(ep, asio::detached); + ioc.run(); + ioc.restart(); + if(! s1.remote_endpoint() == s2.local_endpoint()) + return false; + if(! s2.remote_endpoint() == s1.local_endpoint()) + return false; + return true; +} + +} // test +} // beast2 +} // boost + +#endif diff --git a/test/unit/read.cpp b/test/unit/read.cpp index 29ee114..a7e523c 100644 --- a/test/unit/read.cpp +++ b/test/unit/read.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Mohammad Nejati // // 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) @@ -10,99 +11,201 @@ // Test that header file is self-contained. #include +#include +#include #include -#include +#include +#include +#include -#include "test_suite.hpp" +#include "test_helpers.hpp" namespace boost { namespace beast2 { -#if 0 - -auto read_some( Stream&, parser& ); -auto read_some( Stream&, parser&, DynamicBuffer& ); -auto read( Stream&, parser& ); -auto read( Stream&, parser&, DynamicBuffer& ); - - - //-------------------------------------------- - - read( s, p ); // read message - - p.header(); // header - p.body(); // decoded body - - //-------------------------------------------- - - read_some( s, p ); // read header - if( ! p.is_complete() ) - read( s, p ); // read body - - p.header(); // header - p.body(); // decoded body - - //-------------------------------------------- - - read_some( s, p ); // read header - read( s, p, b ); // read body into b - - p.header(); // header - b; // decoded body - - //-------------------------------------------- - - read_some( s, p, b ); // read header, some body - if( ! p.is_complete() ) - read( s, p, b ); // read body into b - else - // (avoid immediate completion) - - p.header(); // header - b; // decoded body - - //-------------------------------------------- - - read_some( s, p ); // read header - if( ! p.is_complete() ) - read( s, p, b ); // read body into b - else if( ! p.body().empty() ) - p.append_body( b ); // not an I/O - - p.header(); // header - b; // decoded body - - //-------------------------------------------- - - read( s, p, ec ); // read header, some body - if( ec == error::buffer_full ) - ec = {}; - if( ! ec.failed() ) - { - process( p,body() ); - p.discard_body(); - } - -#endif - class read_test { + core::string_view const msg = + "HTTP/1.1 200 OK\r\n" + "Content-Length: 3\r\n" + "\r\n" + "abc"; public: void - testRead() + testAsyncReadSome() { boost::asio::io_context ioc; - boost::asio::post( - ioc.get_executor(), - [] + rts::context rts_ctx; + http_proto::install_parser_service(rts_ctx, {}); + + // async_read_some completes when the parser reads + // the header section of the message. + { + test::stream ts(ioc, msg); + http_proto::response_parser pr(rts_ctx); + pr.reset(); + pr.start(); + + // limit async_read_some for better coverage + ts.read_size(1); + + // header + async_read_some( + ts, + pr, + [&](system::error_code ec, std::size_t n) + { + BOOST_TEST(! ec.failed()); + BOOST_TEST_EQ(n, msg.size() - 3); // minus body + }); + test::run(ioc); + BOOST_TEST(pr.got_header()); + BOOST_TEST(! pr.is_complete()); + + // body + for(auto i = 0; i < 3; i++) { - }); + async_read_some( + ts, + pr, + [&](system::error_code ec, std::size_t n) + { + BOOST_TEST(! ec.failed()); + BOOST_TEST_EQ(n, 1); // because of ts.read_size(1) + }); + BOOST_TEST_EQ(test::run(ioc), 1); + } + BOOST_TEST(pr.is_complete()); + BOOST_TEST(pr.body() == "abc"); + } + + // async_read_some reports stream errors + { + test::fail_count fc(11, asio::error::network_down); + test::stream ts(ioc, fc, msg); + http_proto::response_parser pr(rts_ctx); + pr.reset(); + pr.start(); + + // limit async_read_some for better coverage + ts.read_size(1); + + bool invoked = false; + async_read_some( + ts, + pr, + [&](system::error_code ec, std::size_t n) + { + invoked = true; + BOOST_TEST_EQ(ec, asio::error::network_down); + BOOST_TEST_EQ(n, 10); + }); + BOOST_TEST_EQ(test::run(ioc), 11); + BOOST_TEST(invoked); + } + + // async_read_some reports parser errors + { + test::stream ts(ioc, msg); + http_proto::response_parser pr(rts_ctx); + pr.reset(); + pr.start(); + + // read header + async_read_some(ts, pr, test::success_handler()); + test::run(ioc); + + // read body + pr.set_body_limit(2); + async_read_some( + ts, + pr, + test::fail_handler(http_proto::error::body_too_large)); + test::run(ioc); + } + } + + void + testAsyncReadHeader() + { + // currently, async_read_header and + // async_read_some are identical + } + + void + testAsyncRead() + { + boost::asio::io_context ioc; + rts::context rts_ctx; + http_proto::install_parser_service(rts_ctx, {}); + + // async_read completes when the parser reads + // the entire message. + { + test::stream ts(ioc, msg); + http_proto::response_parser pr(rts_ctx); + pr.reset(); + pr.start(); + + // limit async_read_some for better coverage + ts.read_size(1); + + async_read( + ts, + pr, + [&](system::error_code ec, std::size_t n) + { + BOOST_TEST(! ec.failed()); + BOOST_TEST_EQ(n, msg.size()); + }); + + test::run(ioc); + + BOOST_TEST_EQ(ts.nread(), msg.size()); // because of ts.read_size(1) + BOOST_TEST(pr.is_complete()); + BOOST_TEST(pr.body() == "abc"); + } + + // async_read completes immediatly when + // parser contains enough data + { + asio::post( + ioc, + [&]() + { + test::stream ts(ioc); + http_proto::response_parser pr(rts_ctx); + pr.reset(); + pr.start(); + + pr.commit( + buffers::copy( + pr.prepare(), + buffers::const_buffer( + msg.data(), + msg.size()))); + + async_read( + ts, + pr, + asio::bind_immediate_executor( + ioc.get_executor(), + test::success_handler())); + + BOOST_TEST_EQ(ts.nread(), 0); + BOOST_TEST(pr.is_complete()); + BOOST_TEST(pr.body() == "abc"); + }); + BOOST_TEST_EQ(test::run(ioc), 1); + } } void run() { - testRead(); + testAsyncReadSome(); + testAsyncReadHeader(); + testAsyncRead(); } }; diff --git a/test/unit/test_helpers.hpp b/test/unit/test_helpers.hpp new file mode 100644 index 0000000..4dc1c05 --- /dev/null +++ b/test/unit/test_helpers.hpp @@ -0,0 +1,199 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/beast2 +// + +#ifndef BOOST_BEAST2_TEST_TEST_HELPERS_HPP +#define BOOST_BEAST2_TEST_TEST_HELPERS_HPP + +#include +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace beast2 { +namespace test { + +/** A CompletionHandler used for testing. + + This completion handler is used by tests to ensure correctness + of behavior. It is designed as a single type to reduce template + instantiations, with configurable settings through constructor + arguments. Typically this type will be used in type lists and + not instantiated directly; instances of this class are returned + by the helper functions listed below. + + @see success_handler, @ref fail_handler, @ref any_handler +*/ +class handler +{ + boost::optional ec_; + bool pass_ = false; + boost::source_location loc_{BOOST_CURRENT_LOCATION}; + + public: + handler( + boost::source_location loc = BOOST_CURRENT_LOCATION) + : loc_(loc) + { + } + + explicit + handler( + system::error_code ec, + boost::source_location loc = BOOST_CURRENT_LOCATION) + : ec_(ec) + , loc_(loc) + { + } + + explicit + handler( + boost::none_t, + boost::source_location loc = BOOST_CURRENT_LOCATION) + : loc_(loc) + { + } + + handler(handler&& other) + : ec_(other.ec_) + , pass_(boost::exchange(other.pass_, true)) + , loc_(other.loc_) + + { + } + + ~handler() + { + test_suite::any_runner::instance().test( + pass_, + "handler never invoked", + "", + loc_.file_name(), + loc_.line()); + } + + template + void + operator()(system::error_code ec, Args&&...) + { + test_suite::any_runner::instance().test( + !pass_, + "handler invoked multiple times", + "", + loc_.file_name(), + loc_.line()); + + test_suite::any_runner::instance().test( + !ec_.has_value() || ec == *ec_, + ec.message().c_str(), + "", + loc_.file_name(), + loc_.line()); + pass_ = true; + } +}; + +/** Return a test CompletionHandler which requires success. + + The returned handler can be invoked with any signature whose + first parameter is an `system::error_code`. The handler fails the test + if: + + @li The handler is destroyed without being invoked, or + + @li The handler is invoked with a non-successful error code. +*/ +inline +handler +success_handler(boost::source_location loc = BOOST_CURRENT_LOCATION) noexcept +{ + return handler(system::error_code{}, loc); +} + +/** Return a test CompletionHandler which requires invocation. + + The returned handler can be invoked with any signature. + The handler fails the test if: + + @li The handler is destroyed without being invoked. +*/ +inline +handler +any_handler(boost::source_location loc = BOOST_CURRENT_LOCATION) noexcept +{ + return handler(boost::none, loc); +} + +/** Return a test CompletionHandler which requires a specific error code. + + This handler can be invoked with any signature whose first + parameter is an `system::error_code`. The handler fails the test if: + + @li The handler is destroyed without being invoked. + + @li The handler is invoked with an error code different from + what is specified. + + @param ec The error code to specify. +*/ +inline +handler +fail_handler(system::error_code ec,boost::source_location loc = BOOST_CURRENT_LOCATION) noexcept +{ + return handler(ec, loc); +} + +/** Run an I/O context. + + This function runs and dispatches handlers on the specified + I/O context, until one of the following conditions is true: + + @li The I/O context runs out of work. + + @param ioc The I/O context to run +*/ +inline +std::size_t +run(asio::io_context& ioc) +{ + std::size_t n = ioc.run(); + ioc.restart(); + return n; +} + +/** Run an I/O context for a certain amount of time. + + This function runs and dispatches handlers on the specified + I/O context, until one of the following conditions is true: + + @li The I/O context runs out of work. + + @li No completions occur and the specified amount of time has elapsed. + + @param ioc The I/O context to run + + @param elapsed The maximum amount of time to run for. +*/ +template +std::size_t +run_for( + asio::io_context& ioc, + std::chrono::duration elapsed) +{ + std::size_t n = ioc.run_for(elapsed); + ioc.restart(); + return n; +} + +} // test +} // beast2 +} // boost + +#endif