diff options
author | DongHun Kwak <dh0128.kwak@samsung.com> | 2019-12-05 15:22:41 +0900 |
---|---|---|
committer | DongHun Kwak <dh0128.kwak@samsung.com> | 2019-12-05 15:22:41 +0900 |
commit | 3c1df2168531ad5580076ae08d529054689aeedd (patch) | |
tree | 941aff6f86393eecacddfec252a8508c7e8351c9 /boost/beast/_experimental | |
parent | d6a306e745acfee00e81ccaf3324a2a03516db41 (diff) | |
download | boost-3c1df2168531ad5580076ae08d529054689aeedd.tar.gz boost-3c1df2168531ad5580076ae08d529054689aeedd.tar.bz2 boost-3c1df2168531ad5580076ae08d529054689aeedd.zip |
Imported Upstream version 1.70.0upstream/1.70.0
Diffstat (limited to 'boost/beast/_experimental')
26 files changed, 5195 insertions, 0 deletions
diff --git a/boost/beast/_experimental/http/icy_stream.hpp b/boost/beast/_experimental/http/icy_stream.hpp new file mode 100644 index 0000000000..5d075fffed --- /dev/null +++ b/boost/beast/_experimental/http/icy_stream.hpp @@ -0,0 +1,319 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_HTTP_ICY_STREAM_HPP +#define BOOST_BEAST_HTTP_ICY_STREAM_HPP + +#include <boost/beast/core/detail/config.hpp> +#include <boost/beast/core/error.hpp> +#include <boost/asio/async_result.hpp> +#include <boost/asio/buffer.hpp> +#include <boost/logic/tribool.hpp> +#include <type_traits> + +namespace boost { +namespace beast { +namespace http { + +/** Stream wrapper to process Shoutcast HTTP responses + + This wrapper replaces the word "ICY" in the first + HTTP response received on the connection, with "HTTP/1.1". + This allows the Beast parser to be used with Shoutcast + servers, which send a non-standard HTTP message as the + response. + + For asynchronous operations, the application must ensure + that they are are all performed within the same implicit + or explicit strand. + + @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 Example + To use the @ref icy_stream template with an @ref tcp_stream + you would write: + @code + http::icy_stream<tcp_stream> is(ioc); + @endcode + + @tparam NextLayer The type representing the next layer, to which + data will be read and written during operations. For synchronous + operations, the type must support the <em>SyncStream</em> concept. + For asynchronous operations, the type must support the + <em>AsyncStream</em> concept. + + @note A stream object must not be moved or destroyed while there + are pending asynchronous operations associated with it. + + @par Concepts + <em>AsyncStream</em>, <em>SyncStream</em> +*/ +template<class NextLayer> +class icy_stream +{ + NextLayer stream_; + char buf_[8]; + unsigned char n_ = 0; + bool detect_ = true; + + struct ops; + + static + net::const_buffer + version() + { + return {"HTTP/1.1", 8}; + } + +public: + /// The type of the next layer. + using next_layer_type = + typename std::remove_reference<NextLayer>::type; + + /// The type of the executor associated with the object. + using executor_type = typename next_layer_type::executor_type; + + icy_stream(icy_stream&&) = default; + icy_stream(icy_stream const&) = default; + icy_stream& operator=(icy_stream&&) = default; + icy_stream& operator=(icy_stream const&) = default; + + /** Destructor + + The treatment of pending operations will be the same as that + of the next layer. + */ + ~icy_stream() = default; + + /** Constructor + + Arguments, if any, are forwarded to the next layer's constructor. + */ + template<class... Args> + explicit + icy_stream(Args&&... args); + + //-------------------------------------------------------------------------- + + /** Get the executor associated with the object. + + This function may be used to obtain the executor object that the + stream uses to dispatch handlers for asynchronous operations. + + @return A copy of the executor that stream will use to dispatch handlers. + */ + executor_type + get_executor() noexcept + { + return stream_.get_executor(); + } + + /** Get a reference to the next layer + + This function returns a reference to the next layer + in a stack of stream layers. + + @return A reference to the next layer in the stack of + stream layers. + */ + next_layer_type& + next_layer() + { + return stream_; + } + + /** Get a reference to the next layer + + This function returns a reference to the next layer in a + stack of stream layers. + + @return A reference to the next layer in the stack of + stream layers. + */ + next_layer_type const& + next_layer() const + { + return stream_; + } + + //-------------------------------------------------------------------------- + + /** Read some data from the stream. + + This function is used to read data from the stream. The function call will + block until one or more bytes of data has been read successfully, or until + an error occurs. + + @param buffers The buffers into which the data will be read. + + @returns The number of bytes read. + + @throws system_error Thrown on failure. + + @note The `read_some` operation may not read all of the requested number of + bytes. Consider using the function `net::read` if you need to ensure + that the requested amount of data is read before the blocking operation + completes. + */ + template<class MutableBufferSequence> + std::size_t + read_some(MutableBufferSequence const& buffers); + + /** Read some data from the stream. + + This function is used to read data from the stream. The function call will + block until one or more bytes of data has been read successfully, or until + an error occurs. + + @param buffers The buffers into which the data will be read. + + @param ec Set to indicate what error occurred, if any. + + @returns The number of bytes read. + + @note The `read_some` operation may not read all of the requested number of + bytes. Consider using the function `net::read` if you need to ensure + that the requested amount of data is read before the blocking operation + completes. + */ + template<class MutableBufferSequence> + std::size_t + read_some( + MutableBufferSequence const& buffers, + error_code& ec); + + /** 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( + const boost::system::error_code& error, // Result of operation. + std::size_t bytes_transferred // Number of bytes read. + ); + @endcode + Regardless of whether the asynchronous operation completes + immediately or not, the handler will not be invoked from within + this function. Invocation of the handler will be performed in a + manner equivalent to using `net::post`. + + @note The `async_read_some` operation may not read all of the requested number of + bytes. Consider using the function `net::async_read` if you need + to ensure that the requested amount of data is read before the asynchronous + operation completes. + */ + template< + class MutableBufferSequence, + class ReadHandler> + BOOST_BEAST_ASYNC_RESULT2(ReadHandler) + async_read_some( + MutableBufferSequence const& buffers, + ReadHandler&& handler); + + /** Write some data to the stream. + + This function is used to write data on the stream. The function call will + block until one or more bytes of data has been written successfully, or + until an error occurs. + + @param buffers The data to be written. + + @returns The number of bytes written. + + @throws system_error Thrown on failure. + + @note The `write_some` operation may not transmit all of the data to the + peer. Consider using the function `net::write` if you need to + ensure that all data is written before the blocking operation completes. + */ + template<class ConstBufferSequence> + std::size_t + write_some(ConstBufferSequence const& buffers); + + /** Write some data to the stream. + + This function is used to write data on the stream. The function call will + block until one or more bytes of data has been written successfully, or + until an error occurs. + + @param buffers The data to be written. + + @param ec Set to indicate what error occurred, if any. + + @returns The number of bytes written. + + @note The `write_some` operation may not transmit all of the data to the + peer. Consider using the function `net::write` if you need to + ensure that all data is written before the blocking operation completes. + */ + template<class ConstBufferSequence> + std::size_t + write_some( + ConstBufferSequence const& buffers, + error_code& ec); + + /** 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( + error_code const& error, // Result of operation. + std::size_t bytes_transferred // Number of bytes written. + ); + @endcode + Regardless of whether the asynchronous operation completes + immediately or not, the handler will not be invoked from within + this function. Invocation of the handler will be performed in a + manner equivalent to using `net::post`. + + @note The `async_write_some` operation may not transmit all of the data to + the peer. Consider using the function `net::async_write` if you need + to ensure that all data is written before the asynchronous operation completes. + */ + template< + class ConstBufferSequence, + class WriteHandler> + BOOST_BEAST_ASYNC_RESULT2(WriteHandler) + async_write_some( + ConstBufferSequence const& buffers, + WriteHandler&& handler); +}; + +} // http +} // beast +} // boost + +#include <boost/beast/_experimental/http/impl/icy_stream.hpp> + +#endif diff --git a/boost/beast/_experimental/http/impl/icy_stream.hpp b/boost/beast/_experimental/http/impl/icy_stream.hpp new file mode 100644 index 0000000000..f7329180ee --- /dev/null +++ b/boost/beast/_experimental/http/impl/icy_stream.hpp @@ -0,0 +1,334 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_CORE_IMPL_ICY_STREAM_HPP +#define BOOST_BEAST_CORE_IMPL_ICY_STREAM_HPP + +#include <boost/beast/core/async_base.hpp> +#include <boost/beast/core/buffer_traits.hpp> +#include <boost/beast/core/error.hpp> +#include <boost/beast/core/stream_traits.hpp> +#include <boost/beast/core/detail/is_invocable.hpp> +#include <boost/asio/coroutine.hpp> +#include <boost/assert.hpp> +#include <boost/throw_exception.hpp> +#include <cstring> +#include <memory> +#include <utility> + +namespace boost { +namespace beast { +namespace http { + +namespace detail { + +template<class ConstBufferSequence> +boost::tribool +is_icy(ConstBufferSequence const& buffers) +{ + char buf[3]; + auto const n = net::buffer_copy( + net::mutable_buffer(buf, 3), + buffers); + if(n >= 1 && buf[0] != 'I') + return false; + if(n >= 2 && buf[1] != 'C') + return false; + if(n >= 3 && buf[2] != 'Y') + return false; + if(n < 3) + return boost::indeterminate; + return true; +} + +} // detail + +template<class NextLayer> +struct icy_stream<NextLayer>::ops +{ + +template<class Buffers, class Handler> +class read_op + : public beast::async_base<Handler, + beast::executor_type<icy_stream>> + , public net::coroutine +{ + icy_stream& s_; + Buffers b_; + std::size_t n_ = 0; + error_code ec_; + bool match_ = false; + +public: + template<class Handler_> + read_op( + Handler_&& h, + icy_stream& s, + Buffers const& b) + : async_base<Handler, + beast::executor_type<icy_stream>>( + std::forward<Handler_>(h), s.get_executor()) + , s_(s) + , b_(b) + { + (*this)({}, 0, false); + } + + void + operator()( + error_code ec, + std::size_t bytes_transferred, + bool cont = true) + { + BOOST_ASIO_CORO_REENTER(*this) + { + if(s_.detect_) + { + BOOST_ASSERT(s_.n_ == 0); + for(;;) + { + // Try to read the first three characters + BOOST_ASIO_CORO_YIELD + s_.next_layer().async_read_some( + net::mutable_buffer( + s_.buf_ + s_.n_, 3 - s_.n_), + std::move(*this)); + s_.n_ += static_cast<char>(bytes_transferred); + if(ec) + goto upcall; + auto result = detail::is_icy( + net::const_buffer(s_.buf_, s_.n_)); + if(boost::indeterminate(result)) + continue; + if(result) + s_.n_ = static_cast<char>(net::buffer_copy( + net::buffer(s_.buf_, sizeof(s_.buf_)), + icy_stream::version())); + break; + } + s_.detect_ = false; + } + if(s_.n_ > 0) + { + bytes_transferred = net::buffer_copy( + b_, net::const_buffer(s_.buf_, s_.n_)); + s_.n_ -= static_cast<char>(bytes_transferred); + std::memmove( + s_.buf_, + s_.buf_ + bytes_transferred, + sizeof(s_.buf_) - bytes_transferred); + } + else + { + BOOST_ASIO_CORO_YIELD + s_.next_layer().async_read_some( + b_, std::move(*this)); + } + upcall: + if(! cont) + { + ec_ = ec; + n_ = bytes_transferred; + BOOST_ASIO_CORO_YIELD + s_.next_layer().async_read_some( + net::mutable_buffer{}, + std::move(*this)); + ec = ec_; + bytes_transferred = n_; + } + this->complete_now(ec, bytes_transferred); + } + } +}; + +struct run_read_op +{ + template<class ReadHandler, class Buffers> + void + operator()( + ReadHandler&& h, + icy_stream* s, + Buffers const& b) + { + // If you get an error on the following line it means + // that your handler does not meet the documented type + // requirements for the handler. + + static_assert( + beast::detail::is_invocable<ReadHandler, + void(error_code, std::size_t)>::value, + "ReadHandler type requirements not met"); + + read_op< + Buffers, + typename std::decay<ReadHandler>::type>( + std::forward<ReadHandler>(h), *s, b); + } +}; + +}; + +//------------------------------------------------------------------------------ + +template<class NextLayer> +template<class... Args> +icy_stream<NextLayer>:: +icy_stream(Args&&... args) + : stream_(std::forward<Args>(args)...) +{ + std::memset(buf_, 0, sizeof(buf_)); +} + +template<class NextLayer> +template<class MutableBufferSequence> +std::size_t +icy_stream<NextLayer>:: +read_some(MutableBufferSequence const& buffers) +{ + static_assert(is_sync_read_stream<next_layer_type>::value, + "SyncReadStream type requirements not met"); + static_assert(net::is_mutable_buffer_sequence< + MutableBufferSequence>::value, + "MutableBufferSequence type requirements not met"); + error_code ec; + auto n = read_some(buffers, ec); + if(ec) + BOOST_THROW_EXCEPTION(system_error{ec}); + return n; +} + +template<class NextLayer> +template<class MutableBufferSequence> +std::size_t +icy_stream<NextLayer>:: +read_some(MutableBufferSequence const& buffers, error_code& ec) +{ + static_assert(is_sync_read_stream<next_layer_type>::value, + "SyncReadStream type requirements not met"); + static_assert(net::is_mutable_buffer_sequence< + MutableBufferSequence>::value, + "MutableBufferSequence type requirements not met"); + std::size_t bytes_transferred; + if(detect_) + { + BOOST_ASSERT(n_ == 0); + for(;;) + { + // Try to read the first three characters + bytes_transferred = next_layer().read_some( + net::mutable_buffer(buf_ + n_, 3 - n_), ec); + n_ += static_cast<char>(bytes_transferred); + if(ec) + return 0; + auto result = detail::is_icy( + net::const_buffer(buf_, n_)); + if(boost::indeterminate(result)) + continue; + if(result) + n_ = static_cast<char>(net::buffer_copy( + net::buffer(buf_, sizeof(buf_)), + icy_stream::version())); + break; + } + detect_ = false; + } + if(n_ > 0) + { + bytes_transferred = net::buffer_copy( + buffers, net::const_buffer(buf_, n_)); + n_ -= static_cast<char>(bytes_transferred); + std::memmove( + buf_, + buf_ + bytes_transferred, + sizeof(buf_) - bytes_transferred); + } + else + { + bytes_transferred = + next_layer().read_some(buffers, ec); + } + return bytes_transferred; +} + +template<class NextLayer> +template< + class MutableBufferSequence, + class ReadHandler> +BOOST_BEAST_ASYNC_RESULT2(ReadHandler) +icy_stream<NextLayer>:: +async_read_some( + MutableBufferSequence const& buffers, + ReadHandler&& handler) +{ + static_assert(is_async_read_stream<next_layer_type>::value, + "AsyncReadStream type requirements not met"); + static_assert(net::is_mutable_buffer_sequence< + MutableBufferSequence >::value, + "MutableBufferSequence type requirements not met"); + return net::async_initiate< + ReadHandler, + void(error_code, std::size_t)>( + typename ops::run_read_op{}, + handler, + this, + buffers); +} + +template<class NextLayer> +template<class MutableBufferSequence> +std::size_t +icy_stream<NextLayer>:: +write_some(MutableBufferSequence const& buffers) +{ + static_assert(is_sync_write_stream<next_layer_type>::value, + "SyncWriteStream type requirements not met"); + static_assert(net::is_const_buffer_sequence< + MutableBufferSequence>::value, + "MutableBufferSequence type requirements not met"); + return stream_.write_some(buffers); +} + +template<class NextLayer> +template<class MutableBufferSequence> +std::size_t +icy_stream<NextLayer>:: +write_some(MutableBufferSequence const& buffers, error_code& ec) +{ + static_assert(is_sync_write_stream<next_layer_type>::value, + "SyncWriteStream type requirements not met"); + static_assert(net::is_const_buffer_sequence< + MutableBufferSequence>::value, + "MutableBufferSequence type requirements not met"); + return stream_.write_some(buffers, ec); +} + +template<class NextLayer> +template< + class MutableBufferSequence, + class WriteHandler> +BOOST_BEAST_ASYNC_RESULT2(WriteHandler) +icy_stream<NextLayer>:: +async_write_some( + MutableBufferSequence const& buffers, + WriteHandler&& handler) +{ + static_assert(is_async_write_stream<next_layer_type>::value, + "AsyncWriteStream type requirements not met"); + static_assert(net::is_const_buffer_sequence< + MutableBufferSequence>::value, + "MutableBufferSequence type requirements not met"); + return stream_.async_write_some( + buffers, std::forward<WriteHandler>(handler)); +} + +} // http +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/test/error.hpp b/boost/beast/_experimental/test/error.hpp new file mode 100644 index 0000000000..e857999aae --- /dev/null +++ b/boost/beast/_experimental/test/error.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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_ERROR_HPP +#define BOOST_BEAST_TEST_ERROR_HPP + +#include <boost/beast/core/detail/config.hpp> +#include <boost/beast/core/error.hpp> + +namespace boost { +namespace beast { +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 +} // beast +} // boost + +#include <boost/beast/_experimental/test/impl/error.hpp> +#ifdef BOOST_BEAST_HEADER_ONLY +#include <boost/beast/_experimental/test/impl/error.ipp> +#endif + +#endif diff --git a/boost/beast/_experimental/test/fail_count.hpp b/boost/beast/_experimental/test/fail_count.hpp new file mode 100644 index 0000000000..f56831919d --- /dev/null +++ b/boost/beast/_experimental/test/fail_count.hpp @@ -0,0 +1,70 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_FAIL_COUNT_HPP +#define BOOST_BEAST_TEST_FAIL_COUNT_HPP + +#include <boost/beast/core/detail/config.hpp> +#include <boost/beast/_experimental/test/error.hpp> +#include <cstdlib> + +namespace boost { +namespace beast { +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; + 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 + */ + BOOST_BEAST_DECL + explicit + fail_count( + std::size_t n, + error_code ev = error::test_failure); + + /// Throw an exception on the Nth failure + BOOST_BEAST_DECL + void + fail(); + + /// Set an error code on the Nth failure + BOOST_BEAST_DECL + bool + fail(error_code& ec); +}; + +} // test +} // beast +} // boost + +#ifdef BOOST_BEAST_HEADER_ONLY +#include <boost/beast/_experimental/test/impl/fail_count.ipp> +#endif + +#endif diff --git a/boost/beast/_experimental/test/handler.hpp b/boost/beast/_experimental/test/handler.hpp new file mode 100644 index 0000000000..5998826563 --- /dev/null +++ b/boost/beast/_experimental/test/handler.hpp @@ -0,0 +1,188 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_HANDLER_HPP +#define BOOST_BEAST_TEST_HANDLER_HPP + +#include <boost/beast/_experimental/unit_test/suite.hpp> +#include <boost/beast/core/error.hpp> +#include <boost/asio/io_context.hpp> +#include <boost/core/exchange.hpp> +#include <boost/optional.hpp> + +namespace boost { +namespace beast { +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<error_code> ec_; + bool pass_ = false; + +public: + handler() = default; + + explicit + handler(error_code ec) + : ec_(ec) + { + } + + explicit + handler(boost::none_t) + { + } + + handler(handler&& other) + : ec_(other.ec_) + , pass_(boost::exchange(other.pass_, true)) + { + } + + ~handler() + { + BEAST_EXPECT(pass_); + } + + template<class... Args> + void + operator()(error_code ec, Args&&...) + { + BEAST_EXPECT(! pass_); // can't call twice + BEAST_EXPECTS(! ec_ || ec == *ec_, + ec.message()); + pass_ = true; + } + + void + operator()() + { + BEAST_EXPECT(! pass_); // can't call twice + BEAST_EXPECT(! ec_); + pass_ = true; + } + + template<class Arg0, class... Args, + class = typename std::enable_if< + ! std::is_convertible<Arg0, error_code>::value>::type> + void + operator()(Arg0&&, Args&&...) + { + BEAST_EXPECT(! pass_); // can't call twice + BEAST_EXPECT(! ec_); + pass_ = true; + } +}; + +/** Return a test CompletionHandler which requires success. + + The returned handler can be invoked with any signature whose + first parameter is an `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() noexcept +{ + return handler(error_code{}); +} + +/** 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() noexcept +{ + return handler(boost::none); +} + +/** Return a test CompletionHandler which requires a specific error code. + + This handler can be invoked with any signature whose first + parameter is an `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(error_code ec) noexcept +{ + return handler(ec); +} + +/** 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 +void +run(net::io_context& ioc) +{ + ioc.run(); + ioc.restart(); +} + +/** 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<class Rep, class Period> +void +run_for( + net::io_context& ioc, + std::chrono::duration<Rep, Period> elapsed) +{ + ioc.run_for(elapsed); + ioc.restart(); +} + +} // test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/test/impl/error.hpp b/boost/beast/_experimental/test/impl/error.hpp new file mode 100644 index 0000000000..e9c847954a --- /dev/null +++ b/boost/beast/_experimental/test/impl/error.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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_IMPL_ERROR_HPP +#define BOOST_BEAST_TEST_IMPL_ERROR_HPP + +#include <boost/beast/core/error.hpp> +#include <boost/beast/core/string.hpp> +#include <type_traits> + +namespace boost { +namespace system { +template<> +struct is_error_code_enum< + boost::beast::test::error> + : std::true_type +{ +}; +} // system +} // boost + +namespace boost { +namespace beast { +namespace test { + +BOOST_BEAST_DECL +error_code +make_error_code(error e) noexcept; + +} // test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/test/impl/error.ipp b/boost/beast/_experimental/test/impl/error.ipp new file mode 100644 index 0000000000..f444713761 --- /dev/null +++ b/boost/beast/_experimental/test/impl/error.ipp @@ -0,0 +1,65 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_IMPL_ERROR_IPP +#define BOOST_BEAST_TEST_IMPL_ERROR_IPP + +#include <boost/beast/_experimental/test/error.hpp> + +namespace boost { +namespace beast { +namespace test { + +namespace detail { + +class error_codes : public error_category +{ +public: + BOOST_BEAST_DECL + const char* + name() const noexcept override + { + return "boost.beast.test"; + } + + BOOST_BEAST_DECL + std::string + message(int ev) const override + { + switch(static_cast<error>(ev)) + { + default: + case error::test_failure: return + "An automatic unit test failure occurred"; + } + } + + BOOST_BEAST_DECL + error_condition + default_error_condition(int ev) const noexcept override + { + return error_condition{ev, *this}; + } +}; + +} // detail + +error_code +make_error_code(error e) noexcept +{ + static detail::error_codes const cat{}; + return error_code{static_cast< + std::underlying_type<error>::type>(e), cat}; +} + +} // test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/test/impl/fail_count.ipp b/boost/beast/_experimental/test/impl/fail_count.ipp new file mode 100644 index 0000000000..c0c2564220 --- /dev/null +++ b/boost/beast/_experimental/test/impl/fail_count.ipp @@ -0,0 +1,58 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_IMPL_FAIL_COUNT_IPP +#define BOOST_BEAST_TEST_IMPL_FAIL_COUNT_IPP + +#include <boost/beast/_experimental/test/fail_count.hpp> +#include <boost/throw_exception.hpp> + +namespace boost { +namespace beast { +namespace test { + +fail_count:: +fail_count( + std::size_t n, + error_code ev) + : n_(n) + , ec_(ev) +{ +} + +void +fail_count:: +fail() +{ + if(i_ < n_) + ++i_; + if(i_ == n_) + BOOST_THROW_EXCEPTION(system_error{ec_}); +} + +bool +fail_count:: +fail(error_code& ec) +{ + if(i_ < n_) + ++i_; + if(i_ == n_) + { + ec = ec_; + return true; + } + ec = {}; + return false; +} + +} // test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/test/impl/stream.hpp b/boost/beast/_experimental/test/impl/stream.hpp new file mode 100644 index 0000000000..53bd4db7c8 --- /dev/null +++ b/boost/beast/_experimental/test/impl/stream.hpp @@ -0,0 +1,453 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_IMPL_STREAM_HPP +#define BOOST_BEAST_TEST_IMPL_STREAM_HPP + +#include <boost/beast/core/bind_handler.hpp> +#include <boost/beast/core/buffer_traits.hpp> +#include <boost/beast/core/buffers_prefix.hpp> +#include <boost/beast/core/detail/service_base.hpp> +#include <boost/beast/core/detail/type_traits.hpp> +#include <mutex> +#include <stdexcept> +#include <vector> + +namespace boost { +namespace beast { +namespace test { + +//------------------------------------------------------------------------------ + +struct stream::service_impl +{ + std::mutex m_; + std::vector<state*> v_; + + BOOST_BEAST_DECL + void + remove(state& impl); +}; + +class stream::service + : public beast::detail::service_base<service> +{ + boost::shared_ptr<service_impl> sp_; + + BOOST_BEAST_DECL + void + shutdown() override; + +public: + BOOST_BEAST_DECL + explicit + service(net::execution_context& ctx); + + BOOST_BEAST_DECL + static + auto + make_impl( + net::io_context& ctx, + test::fail_count* fc) -> + boost::shared_ptr<state>; +}; + +//------------------------------------------------------------------------------ + +template<class Handler, class Buffers> +class stream::read_op : public stream::read_op_base +{ + using ex1_type = + net::io_context::executor_type; + using ex2_type + = net::associated_executor_t<Handler, ex1_type>; + + struct lambda + { + Handler h_; + boost::weak_ptr<state> wp_; + Buffers b_; + net::executor_work_guard<ex2_type> wg2_; + + lambda(lambda&&) = default; + lambda(lambda const&) = default; + + template<class Handler_> + lambda( + Handler_&& h, + boost::shared_ptr<state> const& s, + Buffers const& b) + : h_(std::forward<Handler_>(h)) + , wp_(s) + , b_(b) + , wg2_(net::get_associated_executor( + h_, s->ioc.get_executor())) + { + } + + void + operator()(error_code ec) + { + std::size_t bytes_transferred = 0; + auto sp = wp_.lock(); + if(! sp) + ec = net::error::operation_aborted; + if(! ec) + { + std::lock_guard<std::mutex> lock(sp->m); + BOOST_ASSERT(! sp->op); + if(sp->b.size() > 0) + { + bytes_transferred = + net::buffer_copy( + b_, sp->b.data(), sp->read_max); + sp->b.consume(bytes_transferred); + } + else if (buffer_bytes(b_) > 0) + { + ec = net::error::eof; + } + } + + auto alloc = net::get_associated_allocator(h_); + wg2_.get_executor().dispatch( + beast::bind_front_handler(std::move(h_), + ec, bytes_transferred), alloc); + wg2_.reset(); + } + }; + + lambda fn_; + net::executor_work_guard<ex1_type> wg1_; + +public: + template<class Handler_> + read_op( + Handler_&& h, + boost::shared_ptr<state> const& s, + Buffers const& b) + : fn_(std::forward<Handler_>(h), s, b) + , wg1_(s->ioc.get_executor()) + { + } + + void + operator()(error_code ec) override + { + + auto alloc = net::get_associated_allocator(fn_.h_); + wg1_.get_executor().post( + beast::bind_front_handler(std::move(fn_), ec), alloc); + wg1_.reset(); + } +}; + +struct stream::run_read_op +{ + template< + class ReadHandler, + class MutableBufferSequence> + void + operator()( + ReadHandler&& h, + boost::shared_ptr<state> const& in, + 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. + + static_assert( + beast::detail::is_invocable<ReadHandler, + void(error_code, std::size_t)>::value, + "ReadHandler type requirements not met"); + + initiate_read( + in, + std::unique_ptr<read_op_base>{ + new read_op< + typename std::decay<ReadHandler>::type, + MutableBufferSequence>( + std::move(h), + in, + buffers)}, + buffer_bytes(buffers)); + } +}; + +struct stream::run_write_op +{ + template< + class WriteHandler, + class ConstBufferSequence> + void + operator()( + WriteHandler&& h, + boost::shared_ptr<state> in_, + boost::weak_ptr<state> 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. + + static_assert( + beast::detail::is_invocable<WriteHandler, + void(error_code, std::size_t)>::value, + "WriteHandler type requirements not met"); + + ++in_->nwrite; + auto const upcall = [&](error_code ec, std::size_t n) + { + net::post( + in_->ioc.get_executor(), + beast::bind_front_handler(std::move(h), ec, n)); + }; + + // test failure + 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(buffer_bytes(buffers) == 0) + return upcall(ec, n); + + // connection closed + auto out = out_.lock(); + if(! out) + return upcall(net::error::connection_reset, n); + + // copy buffers + n = std::min<std::size_t>( + buffer_bytes(buffers), in_->write_max); + { + std::lock_guard<std::mutex> lock(out->m); + n = net::buffer_copy(out->b.prepare(n), buffers); + out->b.commit(n); + out->notify_read(); + } + BOOST_ASSERT(! ec); + upcall(ec, n); + } +}; + +//------------------------------------------------------------------------------ + +template<class MutableBufferSequence> +std::size_t +stream:: +read_some(MutableBufferSequence const& buffers) +{ + static_assert(net::is_mutable_buffer_sequence< + MutableBufferSequence>::value, + "MutableBufferSequence type requirements not met"); + error_code ec; + auto const n = read_some(buffers, ec); + if(ec) + BOOST_THROW_EXCEPTION(system_error{ec}); + return n; +} + +template<class MutableBufferSequence> +std::size_t +stream:: +read_some(MutableBufferSequence const& buffers, + error_code& ec) +{ + static_assert(net::is_mutable_buffer_sequence< + MutableBufferSequence>::value, + "MutableBufferSequence type requirements not met"); + + ++in_->nread; + + // test failure + if(in_->fc && in_->fc->fail(ec)) + return 0; + + // A request to read 0 bytes from a stream is a no-op. + if(buffer_bytes(buffers) == 0) + { + ec = {}; + return 0; + } + + std::unique_lock<std::mutex> lock{in_->m}; + BOOST_ASSERT(! in_->op); + in_->cv.wait(lock, + [&]() + { + return + in_->b.size() > 0 || + in_->code != status::ok; + }); + + // deliver bytes before eof + if(in_->b.size() > 0) + { + auto const n = net::buffer_copy( + buffers, in_->b.data(), in_->read_max); + in_->b.consume(n); + return n; + } + + // deliver error + BOOST_ASSERT(in_->code != status::ok); + ec = net::error::eof; + return 0; +} + +template<class MutableBufferSequence, class ReadHandler> +BOOST_BEAST_ASYNC_RESULT2(ReadHandler) +stream:: +async_read_some( + MutableBufferSequence const& buffers, + ReadHandler&& handler) +{ + static_assert(net::is_mutable_buffer_sequence< + MutableBufferSequence>::value, + "MutableBufferSequence type requirements not met"); + + return net::async_initiate< + ReadHandler, + void(error_code, std::size_t)>( + run_read_op{}, + handler, + in_, + buffers); +} + +template<class ConstBufferSequence> +std::size_t +stream:: +write_some(ConstBufferSequence const& buffers) +{ + static_assert(net::is_const_buffer_sequence< + ConstBufferSequence>::value, + "ConstBufferSequence type requirements not met"); + error_code ec; + auto const bytes_transferred = + write_some(buffers, ec); + if(ec) + BOOST_THROW_EXCEPTION(system_error{ec}); + return bytes_transferred; +} + +template<class ConstBufferSequence> +std::size_t +stream:: +write_some( + ConstBufferSequence const& buffers, error_code& ec) +{ + static_assert(net::is_const_buffer_sequence< + ConstBufferSequence>::value, + "ConstBufferSequence type requirements not met"); + + ++in_->nwrite; + + // test failure + if(in_->fc && in_->fc->fail(ec)) + return 0; + + // A request to write 0 bytes to a stream is a no-op. + if(buffer_bytes(buffers) == 0) + { + ec = {}; + return 0; + } + + // connection closed + auto out = out_.lock(); + if(! out) + { + ec = net::error::connection_reset; + return 0; + } + + // copy buffers + auto n = std::min<std::size_t>( + buffer_bytes(buffers), in_->write_max); + { + std::lock_guard<std::mutex> lock(out->m); + n = net::buffer_copy(out->b.prepare(n), buffers); + out->b.commit(n); + out->notify_read(); + } + return n; +} + +template<class ConstBufferSequence, class WriteHandler> +BOOST_BEAST_ASYNC_RESULT2(WriteHandler) +stream:: +async_write_some( + ConstBufferSequence const& buffers, + WriteHandler&& handler) +{ + static_assert(net::is_const_buffer_sequence< + ConstBufferSequence>::value, + "ConstBufferSequence type requirements not met"); + + return net::async_initiate< + WriteHandler, + void(error_code, std::size_t)>( + run_write_op{}, + handler, + in_, + out_, + buffers); +} + +//------------------------------------------------------------------------------ + +template<class TeardownHandler> +void +async_teardown( + role_type, + stream& s, + TeardownHandler&& handler) +{ + error_code ec; + if( s.in_->fc && + s.in_->fc->fail(ec)) + return net::post( + s.get_executor(), + beast::bind_front_handler( + std::move(handler), ec)); + s.close(); + if( s.in_->fc && + s.in_->fc->fail(ec)) + ec = net::error::eof; + else + ec = {}; + + net::post( + s.get_executor(), + beast::bind_front_handler( + std::move(handler), ec)); +} + +//------------------------------------------------------------------------------ + +template<class Arg1, class... ArgN> +stream +connect(stream& to, Arg1&& arg1, ArgN&&... argn) +{ + stream from{ + std::forward<Arg1>(arg1), + std::forward<ArgN>(argn)...}; + from.connect(to); + return from; +} + +} // test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/test/impl/stream.ipp b/boost/beast/_experimental/test/impl/stream.ipp new file mode 100644 index 0000000000..38b8a8b2a4 --- /dev/null +++ b/boost/beast/_experimental/test/impl/stream.ipp @@ -0,0 +1,375 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_IMPL_STREAM_IPP +#define BOOST_BEAST_TEST_IMPL_STREAM_IPP + +#include <boost/beast/_experimental/test/stream.hpp> +#include <boost/beast/core/bind_handler.hpp> +#include <boost/beast/core/buffer_traits.hpp> +#include <boost/beast/core/buffers_prefix.hpp> +#include <boost/make_shared.hpp> +#include <stdexcept> +#include <vector> + +namespace boost { +namespace beast { +namespace test { + +//------------------------------------------------------------------------------ + +stream:: +service:: +service(net::execution_context& ctx) + : beast::detail::service_base<service>(ctx) + , sp_(boost::make_shared<service_impl>()) +{ +} + +void +stream:: +service:: +shutdown() +{ + std::vector<std::unique_ptr<read_op_base>> v; + std::lock_guard<std::mutex> g1(sp_->m_); + v.reserve(sp_->v_.size()); + for(auto p : sp_->v_) + { + std::lock_guard<std::mutex> g2(p->m); + v.emplace_back(std::move(p->op)); + p->code = status::eof; + } +} + +auto +stream:: +service:: +make_impl( + net::io_context& ctx, + test::fail_count* fc) -> + boost::shared_ptr<state> +{ + auto& svc = net::use_service<service>(ctx); + auto sp = boost::make_shared<state>(ctx, svc.sp_, fc); + std::lock_guard<std::mutex> g(svc.sp_->m_); + svc.sp_->v_.push_back(sp.get()); + return sp; +} + +void +stream:: +service_impl:: +remove(state& impl) +{ + std::lock_guard<std::mutex> g(m_); + *std::find( + v_.begin(), v_.end(), + &impl) = std::move(v_.back()); + v_.pop_back(); +} + +//------------------------------------------------------------------------------ + +void stream::initiate_read( + boost::shared_ptr<state> const& in_, + std::unique_ptr<stream::read_op_base>&& op, + std::size_t buf_size) +{ + std::unique_lock<std::mutex> lock(in_->m); + + ++in_->nread; + if(in_->op != nullptr) + BOOST_THROW_EXCEPTION( + std::logic_error{"in_->op != nullptr"}); + + // test failure + 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 || buffer_bytes(in_->b.data()) > 0) + { + lock.unlock(); + (*op)(ec); + return; + } + + // deliver error + if(in_->code != status::ok) + { + lock.unlock(); + (*op)(net::error::eof); + return; + } + + // complete when bytes available or closed + in_->op = std::move(op); +} + +stream:: +state:: +state( + net::io_context& ioc_, + boost::weak_ptr<service_impl> wp_, + fail_count* fc_) + : ioc(ioc_) + , wp(std::move(wp_)) + , fc(fc_) +{ +} + +stream:: +state:: +~state() +{ + // cancel outstanding read + if(op != nullptr) + (*op)(net::error::operation_aborted); +} + +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); +} + +void +stream:: +state:: +notify_read() +{ + if(op) + { + auto op_ = std::move(op); + op_->operator()(error_code{}); + } + else + { + cv.notify_all(); + } +} + +void +stream:: +state:: +cancel_read() +{ + std::unique_ptr<read_op_base> p; + { + std::lock_guard<std::mutex> lock(m); + code = status::eof; + p = std::move(op); + } + if(p != nullptr) + (*p)(net::error::operation_aborted); +} + +//------------------------------------------------------------------------------ + +stream:: +~stream() +{ + close(); + in_->remove(); +} + +stream:: +stream(stream&& other) +{ + auto in = service::make_impl( + other.in_->ioc, other.in_->fc); + in_ = std::move(other.in_); + out_ = std::move(other.out_); + other.in_ = in; +} + +stream& +stream:: +operator=(stream&& other) +{ + close(); + auto in = service::make_impl( + other.in_->ioc, other.in_->fc); + in_->remove(); + in_ = std::move(other.in_); + out_ = std::move(other.out_); + other.in_ = in; + return *this; +} + +//------------------------------------------------------------------------------ + +stream:: +stream(net::io_context& ioc) + : in_(service::make_impl(ioc, nullptr)) +{ +} + +stream:: +stream( + net::io_context& ioc, + fail_count& fc) + : in_(service::make_impl(ioc, &fc)) +{ +} + +stream:: +stream( + net::io_context& ioc, + string_view s) + : in_(service::make_impl(ioc, nullptr)) +{ + in_->b.commit(net::buffer_copy( + in_->b.prepare(s.size()), + net::buffer(s.data(), s.size()))); +} + +stream:: +stream( + net::io_context& ioc, + fail_count& fc, + string_view s) + : in_(service::make_impl(ioc, &fc)) +{ + in_->b.commit(net::buffer_copy( + in_->b.prepare(s.size()), + net::buffer(s.data(), s.size()))); +} + +void +stream:: +connect(stream& remote) +{ + BOOST_ASSERT(! out_.lock()); + BOOST_ASSERT(! remote.out_.lock()); + out_ = remote.in_; + remote.out_ = in_; + in_->code = status::ok; + remote.in_->code = status::ok; +} + +string_view +stream:: +str() const +{ + auto const bs = in_->b.data(); + if(buffer_bytes(bs) == 0) + return {}; + auto const b = beast::buffers_front(bs); + return {static_cast<char const*>(b.data()), b.size()}; +} + +void +stream:: +append(string_view s) +{ + std::lock_guard<std::mutex> lock{in_->m}; + in_->b.commit(net::buffer_copy( + in_->b.prepare(s.size()), + net::buffer(s.data(), s.size()))); +} + +void +stream:: +clear() +{ + std::lock_guard<std::mutex> lock{in_->m}; + in_->b.consume(in_->b.size()); +} + +void +stream:: +close() +{ + in_->cancel_read(); + + // disconnect + { + auto out = out_.lock(); + out_.reset(); + + // notify peer + if(out) + { + std::lock_guard<std::mutex> lock(out->m); + if(out->code == status::ok) + { + out->code = status::eof; + out->notify_read(); + } + } + } +} + +void +stream:: +close_remote() +{ + std::lock_guard<std::mutex> lock{in_->m}; + if(in_->code == status::ok) + { + in_->code = status::eof; + in_->notify_read(); + } +} + +void +teardown( + role_type, + stream& s, + boost::system::error_code& ec) +{ + if( s.in_->fc && + s.in_->fc->fail(ec)) + return; + + s.close(); + + if( s.in_->fc && + s.in_->fc->fail(ec)) + ec = net::error::eof; + else + ec = {}; +} + +//------------------------------------------------------------------------------ + +stream +connect(stream& to) +{ + stream from{to.get_executor().context()}; + from.connect(to); + return from; +} + +void +connect(stream& s1, stream& s2) +{ + s1.connect(s2); +} + +} // test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/test/stream.hpp b/boost/beast/_experimental/test/stream.hpp new file mode 100644 index 0000000000..181acec9aa --- /dev/null +++ b/boost/beast/_experimental/test/stream.hpp @@ -0,0 +1,607 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_STREAM_HPP +#define BOOST_BEAST_TEST_STREAM_HPP + +#include <boost/beast/core/detail/config.hpp> +#include <boost/beast/core/bind_handler.hpp> +#include <boost/beast/core/flat_buffer.hpp> +#include <boost/beast/core/role.hpp> +#include <boost/beast/core/string.hpp> +#include <boost/beast/_experimental/test/fail_count.hpp> +#include <boost/asio/async_result.hpp> +#include <boost/asio/buffer.hpp> +#include <boost/asio/error.hpp> +#include <boost/asio/executor_work_guard.hpp> +#include <boost/asio/io_context.hpp> +#include <boost/asio/post.hpp> +#include <boost/assert.hpp> +#include <boost/shared_ptr.hpp> +#include <boost/weak_ptr.hpp> +#include <boost/throw_exception.hpp> +#include <condition_variable> +#include <limits> +#include <memory> +#include <mutex> +#include <utility> + +#if ! BOOST_BEAST_DOXYGEN +namespace boost { +namespace asio { +namespace ssl { +template<typename> class stream; +} // ssl +} // asio +} // boost +#endif + +namespace boost { +namespace beast { +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 + `net::read_until`, or in a call to + @ref boost::beast::http::async_write for example. + + As with Boost.Asio I/O objects, a @ref stream constructs + with a reference to the `net::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 beast::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 <em>SyncReadStream</em> + @li <em>SyncWriteStream</em> + @li <em>AsyncReadStream</em> + @li <em>AsyncWriteStream</em> +*/ +class stream +{ + struct state; + + boost::shared_ptr<state> in_; + boost::weak_ptr<state> out_; + + enum class status + { + ok, + eof, + }; + + class service; + struct service_impl; + + struct read_op_base + { + virtual ~read_op_base() = default; + virtual void operator()(error_code ec) = 0; + }; + + struct state + { + friend class stream; + + net::io_context& ioc; + boost::weak_ptr<service_impl> wp; + std::mutex m; + flat_buffer b; + std::condition_variable cv; + std::unique_ptr<read_op_base> op; + status code = status::ok; + fail_count* fc = nullptr; + std::size_t nread = 0; + std::size_t nwrite = 0; + std::size_t read_max = + (std::numeric_limits<std::size_t>::max)(); + std::size_t write_max = + (std::numeric_limits<std::size_t>::max)(); + + BOOST_BEAST_DECL + state( + net::io_context& ioc_, + boost::weak_ptr<service_impl> wp_, + fail_count* fc_); + + + BOOST_BEAST_DECL + ~state(); + + BOOST_BEAST_DECL + void + remove() noexcept; + + BOOST_BEAST_DECL + void + notify_read(); + + BOOST_BEAST_DECL + void + cancel_read(); + }; + + template<class Handler, class Buffers> + class read_op; + + struct run_read_op; + struct run_write_op; + + BOOST_BEAST_DECL + static + void + initiate_read( + boost::shared_ptr<state> const& in, + std::unique_ptr<read_op_base>&& op, + std::size_t buf_size); + +#if ! BOOST_BEAST_DOXYGEN + // boost::asio::ssl::stream needs these + // DEPRECATED + template<class> + friend class boost::asio::ssl::stream; + // DEPRECATED + using lowest_layer_type = 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 = flat_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 `net::error::connection_reset` + when performing any reads or writes. + */ + BOOST_BEAST_DECL + ~stream(); + + /** Move Constructor + + Moving the stream while asynchronous operations are pending + results in undefined behavior. + */ + BOOST_BEAST_DECL + stream(stream&& other); + + /** Move Assignment + + Moving the stream while asynchronous operations are pending + results in undefined behavior. + */ + BOOST_BEAST_DECL + stream& + operator=(stream&& other); + + /** 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. + */ + BOOST_BEAST_DECL + explicit + stream(net::io_context& ioc); + + /** 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. + */ + BOOST_BEAST_DECL + stream( + net::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. + */ + BOOST_BEAST_DECL + stream( + net::io_context& ioc, + 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. + */ + BOOST_BEAST_DECL + stream( + net::io_context& ioc, + fail_count& fc, + string_view s); + + /// Establish a connection + BOOST_BEAST_DECL + void + connect(stream& remote); + + /// The type of the executor associated with the object. + using executor_type = + net::io_context::executor_type; + + /// Return the executor associated with the object. + executor_type + get_executor() noexcept + { + return in_->ioc.get_executor(); + }; + + /// 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 + BOOST_BEAST_DECL + string_view + str() const; + + /// Appends a string to the pending input data + BOOST_BEAST_DECL + void + append(string_view s); + + /// Clear the pending input area + BOOST_BEAST_DECL + void + clear(); + + /// Return the number of reads + std::size_t + nread() const noexcept + { + return in_->nread; + } + + /// Return the number of writes + std::size_t + nwrite() const noexcept + { + return in_->nwrite; + } + + /** Close the stream. + + The other end of the connection will see + `error::eof` after reading all the remaining data. + */ + BOOST_BEAST_DECL + void + close(); + + /** Close the other end of the stream. + + This end of the connection will see + `error::eof` after reading all the remaining data. + */ + BOOST_BEAST_DECL + void + close_remote(); + + /** Read some data from the stream. + + This function is used to read data from the stream. The function call will + block until one or more bytes of data has been read successfully, or until + an error occurs. + + @param buffers The buffers into which the data will be read. + + @returns The number of bytes read. + + @throws boost::system::system_error Thrown on failure. + + @note The `read_some` operation may not read all of the requested number of + bytes. Consider using the function `net::read` if you need to ensure + that the requested amount of data is read before the blocking operation + completes. + */ + template<class MutableBufferSequence> + std::size_t + read_some(MutableBufferSequence const& buffers); + + /** Read some data from the stream. + + This function is used to read data from the stream. The function call will + block until one or more bytes of data has been read successfully, or until + an error occurs. + + @param buffers The buffers into which the data will be read. + + @param ec Set to indicate what error occurred, if any. + + @returns The number of bytes read. + + @note The `read_some` operation may not read all of the requested number of + bytes. Consider using the function `net::read` if you need to ensure + that the requested amount of data is read before the blocking operation + completes. + */ + template<class MutableBufferSequence> + std::size_t + read_some(MutableBufferSequence const& buffers, + error_code& ec); + + /** 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( + error_code const& ec, // Result of operation. + std::size_t bytes_transferred // Number of bytes read. + ); + @endcode + Regardless of whether the asynchronous operation completes + immediately or not, the handler will not be invoked from within + this function. Invocation of the handler will be performed in a + manner equivalent to using `net::post`. + + @note The `async_read_some` operation may not read all of the requested number of + bytes. Consider using the function `net::async_read` if you need + to ensure that the requested amount of data is read before the asynchronous + operation completes. + */ + template<class MutableBufferSequence, class ReadHandler> + BOOST_BEAST_ASYNC_RESULT2(ReadHandler) + async_read_some(MutableBufferSequence const& buffers, + ReadHandler&& handler); + + /** Write some data to the stream. + + This function is used to write data on the stream. The function call will + block until one or more bytes of data has been written successfully, or + until an error occurs. + + @param buffers The data to be written. + + @returns The number of bytes written. + + @throws boost::system::system_error Thrown on failure. + + @note The `write_some` operation may not transmit all of the data to the + peer. Consider using the function `net::write` if you need to + ensure that all data is written before the blocking operation completes. + */ + template<class ConstBufferSequence> + std::size_t + write_some(ConstBufferSequence const& buffers); + + /** Write some data to the stream. + + This function is used to write data on the stream. The function call will + block until one or more bytes of data has been written successfully, or + until an error occurs. + + @param buffers The data to be written. + + @param ec Set to indicate what error occurred, if any. + + @returns The number of bytes written. + + @note The `write_some` operation may not transmit all of the data to the + peer. Consider using the function `net::write` if you need to + ensure that all data is written before the blocking operation completes. + */ + template<class ConstBufferSequence> + std::size_t + write_some( + ConstBufferSequence const& buffers, error_code& ec); + + /** 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( + error_code const& ec, // Result of operation. + std::size_t bytes_transferred // Number of bytes written. + ); + @endcode + Regardless of whether the asynchronous operation completes + immediately or not, the handler will not be invoked from within + this function. Invocation of the handler will be performed in a + manner equivalent to using `net::post`. + + @note The `async_write_some` operation may not transmit all of the data to + the peer. Consider using the function `net::async_write` if you need + to ensure that all data is written before the asynchronous operation completes. + */ + template<class ConstBufferSequence, class WriteHandler> + BOOST_BEAST_ASYNC_RESULT2(WriteHandler) + async_write_some(ConstBufferSequence const& buffers, + WriteHandler&& handler); + +#if ! BOOST_BEAST_DOXYGEN + friend + BOOST_BEAST_DECL + void + teardown( + role_type, + stream& s, + boost::system::error_code& ec); + + template<class TeardownHandler> + friend + BOOST_BEAST_DECL + void + async_teardown( + role_type role, + stream& s, + TeardownHandler&& handler); +#endif +}; + +#if ! BOOST_BEAST_DOXYGEN +inline +void +beast_close_socket(stream& s) +{ + s.close(); +} +#endif + +#if BOOST_BEAST_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<class... Args> +stream +connect(stream& to, Args&&... args); + +#else +BOOST_BEAST_DECL +stream +connect(stream& to); + +BOOST_BEAST_DECL +void +connect(stream& s1, stream& s2); + +template<class Arg1, class... ArgN> +stream +connect(stream& to, Arg1&& arg1, ArgN&&... argn); +#endif + +} // test +} // beast +} // boost + +#include <boost/beast/_experimental/test/impl/stream.hpp> +#ifdef BOOST_BEAST_HEADER_ONLY +#include <boost/beast/_experimental/test/impl/stream.ipp> +#endif + +#endif diff --git a/boost/beast/_experimental/test/tcp.hpp b/boost/beast/_experimental/test/tcp.hpp new file mode 100644 index 0000000000..d794b4ee42 --- /dev/null +++ b/boost/beast/_experimental/test/tcp.hpp @@ -0,0 +1,77 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_TEST_TCP_HPP +#define BOOST_BEAST_TEST_TCP_HPP + +#include <boost/beast/core/detail/config.hpp> +#include <boost/beast/core/detail/get_io_context.hpp> +#include <boost/beast/_experimental/unit_test/suite.hpp> +#include <boost/beast/_experimental/test/handler.hpp> +#include <boost/asio/ip/tcp.hpp> +#include <chrono> + +namespace boost { +namespace beast { +namespace test { + +/** Connect two TCP sockets together. +*/ +template<class Executor> +bool +connect( + net::basic_stream_socket<net::ip::tcp, Executor>& s1, + net::basic_stream_socket<net::ip::tcp, Executor>& s2) + +{ + auto ioc1 = beast::detail::get_io_context(s1); + auto ioc2 = beast::detail::get_io_context(s2); + if(! BEAST_EXPECT(ioc1 != nullptr)) + return false; + if(! BEAST_EXPECT(ioc2 != nullptr)) + return false; + if(! BEAST_EXPECT(ioc1 == ioc2)) + return false; + auto& ioc = *ioc1; + try + { + net::basic_socket_acceptor< + net::ip::tcp, Executor> a(s1.get_executor()); + auto ep = net::ip::tcp::endpoint( + net::ip::make_address_v4("127.0.0.1"), 0); + a.open(ep.protocol()); + a.set_option( + net::socket_base::reuse_address(true)); + a.bind(ep); + a.listen(0); + ep = a.local_endpoint(); + a.async_accept(s2, test::success_handler()); + s1.async_connect(ep, test::success_handler()); + run(ioc); + if(! BEAST_EXPECT( + s1.remote_endpoint() == s2.local_endpoint())) + return false; + if(! BEAST_EXPECT( + s2.remote_endpoint() == s1.local_endpoint())) + return false; + } + catch(std::exception const& e) + { + beast::unit_test::suite::this_suite()->fail( + e.what(), __FILE__, __LINE__); + return false; + } + return true; +} + +} // test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/amount.hpp b/boost/beast/_experimental/unit_test/amount.hpp new file mode 100644 index 0000000000..2d2969eacd --- /dev/null +++ b/boost/beast/_experimental/unit_test/amount.hpp @@ -0,0 +1,59 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_AMOUNT_HPP +#define BOOST_BEAST_UNIT_TEST_AMOUNT_HPP + +#include <cstddef> +#include <ostream> +#include <string> + +namespace boost { +namespace beast { +namespace unit_test { + +/** Utility for producing nicely composed output of amounts with units. */ +class amount +{ +private: + std::size_t n_; + std::string const& what_; + +public: + amount(amount const&) = default; + amount& operator=(amount const&) = delete; + + template<class = void> + amount(std::size_t n, std::string const& what); + + friend + std::ostream& + operator<<(std::ostream& s, amount const& t); +}; + +template<class> +amount::amount(std::size_t n, std::string const& what) + : n_(n) + , what_(what) +{ +} + +inline +std::ostream& +operator<<(std::ostream& s, amount const& t) +{ + s << t.n_ << " " << t.what_ <<((t.n_ != 1) ? "s" : ""); + return s; +} + +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/detail/const_container.hpp b/boost/beast/_experimental/unit_test/detail/const_container.hpp new file mode 100644 index 0000000000..2f77912323 --- /dev/null +++ b/boost/beast/_experimental/unit_test/detail/const_container.hpp @@ -0,0 +1,95 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_DETAIL_CONST_CONTAINER_HPP +#define BOOST_BEAST_UNIT_TEST_DETAIL_CONST_CONTAINER_HPP + +namespace boost { +namespace beast { +namespace unit_test { +namespace detail { + +/** Adapter to constrain a container interface. + The interface allows for limited read only operations. Derived classes + provide additional behavior. +*/ +template<class Container> +class const_container +{ +private: + using cont_type = Container; + + cont_type m_cont; + +protected: + cont_type& cont() + { + return m_cont; + } + + cont_type const& cont() const + { + return m_cont; + } + +public: + using value_type = typename cont_type::value_type; + using size_type = typename cont_type::size_type; + using difference_type = typename cont_type::difference_type; + using iterator = typename cont_type::const_iterator; + using const_iterator = typename cont_type::const_iterator; + + /** Returns `true` if the container is empty. */ + bool + empty() const + { + return m_cont.empty(); + } + + /** Returns the number of items in the container. */ + size_type + size() const + { + return m_cont.size(); + } + + /** Returns forward iterators for traversal. */ + /** @{ */ + const_iterator + begin() const + { + return m_cont.cbegin(); + } + + const_iterator + cbegin() const + { + return m_cont.cbegin(); + } + + const_iterator + end() const + { + return m_cont.cend(); + } + + const_iterator + cend() const + { + return m_cont.cend(); + } + /** @} */ +}; + +} // detail +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/dstream.hpp b/boost/beast/_experimental/unit_test/dstream.hpp new file mode 100644 index 0000000000..60066ac7bd --- /dev/null +++ b/boost/beast/_experimental/unit_test/dstream.hpp @@ -0,0 +1,131 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_DSTREAM_HPP +#define BOOST_BEAST_UNIT_TEST_DSTREAM_HPP + +#include <boost/config.hpp> +#include <ios> +#include <memory> +#include <ostream> +#include <sstream> +#include <streambuf> +#include <string> + +#ifdef BOOST_WINDOWS +#include <boost/winapi/basic_types.hpp> +#include <boost/winapi/debugapi.hpp> +#endif + +namespace boost { +namespace beast { +namespace unit_test { + +#ifdef BOOST_WINDOWS + +namespace detail { + +template<class CharT, class Traits, class Allocator> +class dstream_buf + : public std::basic_stringbuf<CharT, Traits, Allocator> +{ + using ostream = std::basic_ostream<CharT, Traits>; + + ostream& os_; + bool dbg_; + + template<class T> + void write(T const*) = delete; + + void write(char const* s) + { + if(dbg_) + boost::winapi::OutputDebugStringA(s); + os_ << s; + } + + void write(wchar_t const* s) + { + if(dbg_) + boost::winapi::OutputDebugStringW(s); + os_ << s; + } + +public: + explicit + dstream_buf(ostream& os) + : os_(os) + , dbg_(boost::winapi::IsDebuggerPresent() != 0) + { + } + + ~dstream_buf() + { + sync(); + } + + int + sync() override + { + write(this->str().c_str()); + this->str(""); + return 0; + } +}; + +} // detail + +/** std::ostream with Visual Studio IDE redirection. + + Instances of this stream wrap a specified `std::ostream` + (such as `std::cout` or `std::cerr`). If the IDE debugger + is attached when the stream is created, output will be + additionally copied to the Visual Studio Output window. +*/ +template< + class CharT, + class Traits = std::char_traits<CharT>, + class Allocator = std::allocator<CharT> +> +class basic_dstream + : public std::basic_ostream<CharT, Traits> +{ + detail::dstream_buf< + CharT, Traits, Allocator> buf_; + +public: + /** Construct a stream. + + @param os The output stream to wrap. + */ + explicit + basic_dstream(std::ostream& os) + : std::basic_ostream<CharT, Traits>(&buf_) + , buf_(os) + { + if(os.flags() & std::ios::unitbuf) + std::unitbuf(*this); + } +}; + +using dstream = basic_dstream<char>; +using dwstream = basic_dstream<wchar_t>; + +#else + +using dstream = std::ostream&; +using dwstream = std::wostream&; + +#endif + +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/global_suites.hpp b/boost/beast/_experimental/unit_test/global_suites.hpp new file mode 100644 index 0000000000..2ad5dc7051 --- /dev/null +++ b/boost/beast/_experimental/unit_test/global_suites.hpp @@ -0,0 +1,55 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_GLOBAL_SUITES_HPP +#define BOOST_BEAST_UNIT_TEST_GLOBAL_SUITES_HPP + +#include <boost/beast/_experimental/unit_test/suite_list.hpp> + +namespace boost { +namespace beast { +namespace unit_test { + +namespace detail { + +/// Holds test suites registered during static initialization. +inline +suite_list& +global_suites() +{ + static suite_list s; + return s; +} + +template<class Suite> +struct insert_suite +{ + insert_suite(char const* name, char const* module, + char const* library, bool manual) + { + global_suites().insert<Suite>( + name, module, library, manual); + } +}; + +} // detail + +/// Holds test suites registered during static initialization. +inline +suite_list const& +global_suites() +{ + return detail::global_suites(); +} + +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/main.ipp b/boost/beast/_experimental/unit_test/main.ipp new file mode 100644 index 0000000000..0a4d4c5555 --- /dev/null +++ b/boost/beast/_experimental/unit_test/main.ipp @@ -0,0 +1,87 @@ +// +// 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/boostorg/beast +// + +#include <boost/beast/_experimental/unit_test/amount.hpp> +#include <boost/beast/_experimental/unit_test/dstream.hpp> +#include <boost/beast/_experimental/unit_test/global_suites.hpp> +#include <boost/beast/_experimental/unit_test/match.hpp> +#include <boost/beast/_experimental/unit_test/reporter.hpp> +#include <boost/beast/_experimental/unit_test/suite.hpp> +#include <boost/config.hpp> +#include <cstdlib> +#include <iostream> +#include <vector> + +#ifdef BOOST_MSVC +# ifndef WIN32_LEAN_AND_MEAN // VC_EXTRALEAN +# define WIN32_LEAN_AND_MEAN +# include <windows.h> +# undef WIN32_LEAN_AND_MEAN +# else +# include <windows.h> +# endif +#endif + +// Simple main used to produce stand +// alone executables that run unit tests. +int main(int ac, char const* av[]) +{ + using namespace std; + using namespace boost::beast::unit_test; + + dstream log(std::cerr); + std::unitbuf(log); + +#ifdef BOOST_MSVC + { + int flags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); + flags |= _CRTDBG_LEAK_CHECK_DF; + _CrtSetDbgFlag(flags); + } +#endif + + if(ac == 2) + { + std::string const s{av[1]}; + if(s == "-h" || s == "--help") + { + log << + "Usage:\n" + " " << av[0] << ": { <suite-name>... }" << + std::endl; + return EXIT_SUCCESS; + } + } + + reporter r(log); + bool failed; + if(ac > 1) + { + std::vector<selector> v; + v.reserve(ac - 1); + for(int i = 1; i < ac; ++i) + v.emplace_back(selector::automatch, av[i]); + auto pred = + [&v](suite_info const& si) mutable + { + for(auto& p : v) + if(p(si)) + return true; + return false; + }; + failed = r.run_each_if(global_suites(), pred); + } + else + { + failed = r.run_each(global_suites()); + } + if(failed) + return EXIT_FAILURE; + return EXIT_SUCCESS; +} diff --git a/boost/beast/_experimental/unit_test/match.hpp b/boost/beast/_experimental/unit_test/match.hpp new file mode 100644 index 0000000000..0168af802e --- /dev/null +++ b/boost/beast/_experimental/unit_test/match.hpp @@ -0,0 +1,177 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_MATCH_HPP +#define BOOST_BEAST_UNIT_TEST_MATCH_HPP + +#include <boost/beast/_experimental/unit_test/suite_info.hpp> +#include <string> + +namespace boost { +namespace beast { +namespace unit_test { + +// Predicate for implementing matches +class selector +{ +public: + enum mode_t + { + // Run all tests except manual ones + all, + + // Run tests that match in any field + automatch, + + // Match on suite + suite, + + // Match on library + library, + + // Match on module (used internally) + module, + + // Match nothing (used internally) + none + }; + +private: + mode_t mode_; + std::string pat_; + std::string library_; + +public: + template<class = void> + explicit + selector(mode_t mode, std::string const& pattern = ""); + + template<class = void> + bool + operator()(suite_info const& s); +}; + +//------------------------------------------------------------------------------ + +template<class> +selector::selector(mode_t mode, std::string const& pattern) + : mode_(mode) + , pat_(pattern) +{ + if(mode_ == automatch && pattern.empty()) + mode_ = all; +} + +template<class> +bool +selector::operator()(suite_info const& s) +{ + switch(mode_) + { + case automatch: + // suite or full name + if(s.name() == pat_ || s.full_name() == pat_) + { + mode_ = none; + return true; + } + + // check module + if(pat_ == s.module()) + { + mode_ = module; + library_ = s.library(); + return ! s.manual(); + } + + // check library + if(pat_ == s.library()) + { + mode_ = library; + return ! s.manual(); + } + + return false; + + case suite: + return pat_ == s.name(); + + case module: + return pat_ == s.module() && ! s.manual(); + + case library: + return pat_ == s.library() && ! s.manual(); + + case none: + return false; + + case all: + default: + // fall through + break; + }; + + return ! s.manual(); +} + +//------------------------------------------------------------------------------ + +// Utility functions for producing predicates to select suites. + +/** Returns a predicate that implements a smart matching rule. + The predicate checks the suite, module, and library fields of the + suite_info in that order. When it finds a match, it changes modes + depending on what was found: + + If a suite is matched first, then only the suite is selected. The + suite may be marked manual. + + If a module is matched first, then only suites from that module + and library not marked manual are selected from then on. + + If a library is matched first, then only suites from that library + not marked manual are selected from then on. + +*/ +inline +selector +match_auto(std::string const& name) +{ + return selector(selector::automatch, name); +} + +/** Return a predicate that matches all suites not marked manual. */ +inline +selector +match_all() +{ + return selector(selector::all); +} + +/** Returns a predicate that matches a specific suite. */ +inline +selector +match_suite(std::string const& name) +{ + return selector(selector::suite, name); +} + +/** Returns a predicate that matches all suites in a library. */ +inline +selector +match_library(std::string const& name) +{ + return selector(selector::library, name); +} + +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/recorder.hpp b/boost/beast/_experimental/unit_test/recorder.hpp new file mode 100644 index 0000000000..980fb78ab7 --- /dev/null +++ b/boost/beast/_experimental/unit_test/recorder.hpp @@ -0,0 +1,93 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_RECORDER_HPP +#define BOOST_BEAST_UNIT_TEST_RECORDER_HPP + +#include <boost/beast/_experimental/unit_test/results.hpp> +#include <boost/beast/_experimental/unit_test/runner.hpp> + +namespace boost { +namespace beast { +namespace unit_test { + +/** A test runner that stores the results. */ +class recorder : public runner +{ + results m_results; + suite_results m_suite; + case_results m_case; + +public: + recorder() = default; + + /** Returns a report with the results of all completed suites. */ + results const& + report() const + { + return m_results; + } + +private: + virtual + void + on_suite_begin(suite_info const& info) override + { + m_suite = suite_results(info.full_name()); + } + + virtual + void + on_suite_end() override + { + m_results.insert(std::move(m_suite)); + } + + virtual + void + on_case_begin(std::string const& name) override + { + m_case = case_results(name); + } + + virtual + void + on_case_end() override + { + if(m_case.tests.size() > 0) + m_suite.insert(std::move(m_case)); + } + + virtual + void + on_pass() override + { + m_case.tests.pass(); + } + + virtual + void + on_fail(std::string const& reason) override + { + m_case.tests.fail(reason); + } + + virtual + void + on_log(std::string const& s) override + { + m_case.log.insert(s); + } +}; + +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/reporter.hpp b/boost/beast/_experimental/unit_test/reporter.hpp new file mode 100644 index 0000000000..12a94f88dc --- /dev/null +++ b/boost/beast/_experimental/unit_test/reporter.hpp @@ -0,0 +1,292 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_REPORTER_HPP +#define BOOST_BEAST_UNIT_TEST_REPORTER_HPP + +#include <boost/beast/_experimental/unit_test/amount.hpp> +#include <boost/beast/_experimental/unit_test/recorder.hpp> +#include <algorithm> +#include <chrono> +#include <functional> +#include <iomanip> +#include <iostream> +#include <sstream> +#include <string> +#include <utility> + +namespace boost { +namespace beast { +namespace unit_test { + +namespace detail { + +/** A simple test runner that writes everything to a stream in real time. + The totals are output when the object is destroyed. +*/ +template<class = void> +class reporter : public runner +{ +private: + using clock_type = std::chrono::steady_clock; + + struct case_results + { + std::string name; + std::size_t total = 0; + std::size_t failed = 0; + + explicit + case_results(std::string name_ = "") + : name(std::move(name_)) + { + } + }; + + struct suite_results + { + std::string name; + std::size_t cases = 0; + std::size_t total = 0; + std::size_t failed = 0; + typename clock_type::time_point start = clock_type::now(); + + explicit + suite_results(std::string name_ = "") + : name(std::move(name_)) + { + } + + void + add(case_results const& r); + }; + + struct results + { + using run_time = std::pair<std::string, + typename clock_type::duration>; + + enum + { + max_top = 10 + }; + + std::size_t suites = 0; + std::size_t cases = 0; + std::size_t total = 0; + std::size_t failed = 0; + std::vector<run_time> top; + typename clock_type::time_point start = clock_type::now(); + + void + add(suite_results const& r); + }; + + std::ostream& os_; + results results_; + suite_results suite_results_; + case_results case_results_; + +public: + reporter(reporter const&) = delete; + reporter& operator=(reporter const&) = delete; + + ~reporter(); + + explicit + reporter(std::ostream& os = std::cout); + +private: + static + std::string + fmtdur(typename clock_type::duration const& d); + + virtual + void + on_suite_begin(suite_info const& info) override; + + virtual + void + on_suite_end() override; + + virtual + void + on_case_begin(std::string const& name) override; + + virtual + void + on_case_end() override; + + virtual + void + on_pass() override; + + virtual + void + on_fail(std::string const& reason) override; + + virtual + void + on_log(std::string const& s) override; +}; + +//------------------------------------------------------------------------------ + +template<class _> +void +reporter<_>:: +suite_results::add(case_results const& r) +{ + ++cases; + total += r.total; + failed += r.failed; +} + +template<class _> +void +reporter<_>:: +results::add(suite_results const& r) +{ + ++suites; + total += r.total; + cases += r.cases; + failed += r.failed; + auto const elapsed = clock_type::now() - r.start; + if(elapsed >= std::chrono::seconds{1}) + { + auto const iter = std::lower_bound(top.begin(), + top.end(), elapsed, + [](run_time const& t1, + typename clock_type::duration const& t2) + { + return t1.second > t2; + }); + if(iter != top.end()) + { + top.emplace(iter, r.name, elapsed); + if(top.size() > max_top) + top.resize(max_top); + } + } +} + +//------------------------------------------------------------------------------ + +template<class _> +reporter<_>:: +reporter(std::ostream& os) + : os_(os) +{ +} + +template<class _> +reporter<_>::~reporter() +{ + if(results_.top.size() > 0) + { + os_ << "Longest suite times:\n"; + for(auto const& i : results_.top) + os_ << std::setw(8) << + fmtdur(i.second) << " " << i.first << '\n'; + } + auto const elapsed = clock_type::now() - results_.start; + os_ << + fmtdur(elapsed) << ", " << + amount{results_.suites, "suite"} << ", " << + amount{results_.cases, "case"} << ", " << + amount{results_.total, "test"} << " total, " << + amount{results_.failed, "failure"} << + std::endl; +} + +template<class _> +std::string +reporter<_>::fmtdur(typename clock_type::duration const& d) +{ + using namespace std::chrono; + auto const ms = duration_cast<milliseconds>(d); + if(ms < seconds{1}) + return std::to_string(ms.count()) + "ms"; + std::stringstream ss; + ss << std::fixed << std::setprecision(1) << + (ms.count()/1000.) << "s"; + return ss.str(); +} + +template<class _> +void +reporter<_>:: +on_suite_begin(suite_info const& info) +{ + suite_results_ = suite_results{info.full_name()}; +} + +template<class _> +void +reporter<_>::on_suite_end() +{ + results_.add(suite_results_); +} + +template<class _> +void +reporter<_>:: +on_case_begin(std::string const& name) +{ + case_results_ = case_results(name); + os_ << suite_results_.name << + (case_results_.name.empty() ? "" : + (" " + case_results_.name)) << std::endl; +} + +template<class _> +void +reporter<_>:: +on_case_end() +{ + suite_results_.add(case_results_); +} + +template<class _> +void +reporter<_>:: +on_pass() +{ + ++case_results_.total; +} + +template<class _> +void +reporter<_>:: +on_fail(std::string const& reason) +{ + ++case_results_.failed; + ++case_results_.total; + os_ << + "#" << case_results_.total << " failed" << + (reason.empty() ? "" : ": ") << reason << std::endl; +} + +template<class _> +void +reporter<_>:: +on_log(std::string const& s) +{ + os_ << s; +} + +} // detail + +using reporter = detail::reporter<>; + +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/results.hpp b/boost/beast/_experimental/unit_test/results.hpp new file mode 100644 index 0000000000..243d185f6b --- /dev/null +++ b/boost/beast/_experimental/unit_test/results.hpp @@ -0,0 +1,246 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_RESULTS_HPP +#define BOOST_BEAST_UNIT_TEST_RESULTS_HPP + +#include <boost/beast/_experimental/unit_test/detail/const_container.hpp> + +#include <string> +#include <vector> + +namespace boost { +namespace beast { +namespace unit_test { + +/** Holds a set of test condition outcomes in a testcase. */ +class case_results +{ +public: + /** Holds the result of evaluating one test condition. */ + struct test + { + explicit test(bool pass_) + : pass(pass_) + { + } + + test(bool pass_, std::string const& reason_) + : pass(pass_) + , reason(reason_) + { + } + + bool pass; + std::string reason; + }; + +private: + class tests_t + : public detail::const_container <std::vector <test>> + { + private: + std::size_t failed_; + + public: + tests_t() + : failed_(0) + { + } + + /** Returns the total number of test conditions. */ + std::size_t + total() const + { + return cont().size(); + } + + /** Returns the number of failed test conditions. */ + std::size_t + failed() const + { + return failed_; + } + + /** Register a successful test condition. */ + void + pass() + { + cont().emplace_back(true); + } + + /** Register a failed test condition. */ + void + fail(std::string const& reason = "") + { + ++failed_; + cont().emplace_back(false, reason); + } + }; + + class log_t + : public detail::const_container <std::vector <std::string>> + { + public: + /** Insert a string into the log. */ + void + insert(std::string const& s) + { + cont().push_back(s); + } + }; + + std::string name_; + +public: + explicit case_results(std::string const& name = "") + : name_(name) + { + } + + /** Returns the name of this testcase. */ + std::string const& + name() const + { + return name_; + } + + /** Memberspace for a container of test condition outcomes. */ + tests_t tests; + + /** Memberspace for a container of testcase log messages. */ + log_t log; +}; + +//-------------------------------------------------------------------------- + +/** Holds the set of testcase results in a suite. */ +class suite_results + : public detail::const_container <std::vector <case_results>> +{ +private: + std::string name_; + std::size_t total_ = 0; + std::size_t failed_ = 0; + +public: + explicit suite_results(std::string const& name = "") + : name_(name) + { + } + + /** Returns the name of this suite. */ + std::string const& + name() const + { + return name_; + } + + /** Returns the total number of test conditions. */ + std::size_t + total() const + { + return total_; + } + + /** Returns the number of failures. */ + std::size_t + failed() const + { + return failed_; + } + + /** Insert a set of testcase results. */ + /** @{ */ + void + insert(case_results&& r) + { + cont().emplace_back(std::move(r)); + total_ += r.tests.total(); + failed_ += r.tests.failed(); + } + + void + insert(case_results const& r) + { + cont().push_back(r); + total_ += r.tests.total(); + failed_ += r.tests.failed(); + } + /** @} */ +}; + +//------------------------------------------------------------------------------ + +// VFALCO TODO Make this a template class using scoped allocators +/** Holds the results of running a set of testsuites. */ +class results + : public detail::const_container <std::vector <suite_results>> +{ +private: + std::size_t m_cases; + std::size_t total_; + std::size_t failed_; + +public: + results() + : m_cases(0) + , total_(0) + , failed_(0) + { + } + + /** Returns the total number of test cases. */ + std::size_t + cases() const + { + return m_cases; + } + + /** Returns the total number of test conditions. */ + std::size_t + total() const + { + return total_; + } + + /** Returns the number of failures. */ + std::size_t + failed() const + { + return failed_; + } + + /** Insert a set of suite results. */ + /** @{ */ + void + insert(suite_results&& r) + { + m_cases += r.size(); + total_ += r.total(); + failed_ += r.failed(); + cont().emplace_back(std::move(r)); + } + + void + insert(suite_results const& r) + { + m_cases += r.size(); + total_ += r.total(); + failed_ += r.failed(); + cont().push_back(r); + } + /** @} */ +}; + +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/runner.hpp b/boost/beast/_experimental/unit_test/runner.hpp new file mode 100644 index 0000000000..d9a68fdda7 --- /dev/null +++ b/boost/beast/_experimental/unit_test/runner.hpp @@ -0,0 +1,292 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_RUNNER_H_INCLUDED +#define BOOST_BEAST_UNIT_TEST_RUNNER_H_INCLUDED + +#include <boost/beast/_experimental/unit_test/suite_info.hpp> +#include <boost/assert.hpp> +#include <mutex> +#include <ostream> +#include <string> + +namespace boost { +namespace beast { +namespace unit_test { + +/** Unit test runner interface. + + Derived classes can customize the reporting behavior. This interface is + injected into the unit_test class to receive the results of the tests. +*/ +class runner +{ + std::string arg_; + bool default_ = false; + bool failed_ = false; + bool cond_ = false; + std::recursive_mutex mutex_; + +public: + runner() = default; + virtual ~runner() = default; + runner(runner const&) = delete; + runner& operator=(runner const&) = delete; + + /** Set the argument string. + + The argument string is available to suites and + allows for customization of the test. Each suite + defines its own syntax for the argument string. + The same argument is passed to all suites. + */ + void + arg(std::string const& s) + { + arg_ = s; + } + + /** Returns the argument string. */ + std::string const& + arg() const + { + return arg_; + } + + /** Run the specified suite. + @return `true` if any conditions failed. + */ + template<class = void> + bool + run(suite_info const& s); + + /** Run a sequence of suites. + The expression + `FwdIter::value_type` + must be convertible to `suite_info`. + @return `true` if any conditions failed. + */ + template<class FwdIter> + bool + run(FwdIter first, FwdIter last); + + /** Conditionally run a sequence of suites. + pred will be called as: + @code + bool pred(suite_info const&); + @endcode + @return `true` if any conditions failed. + */ + template<class FwdIter, class Pred> + bool + run_if(FwdIter first, FwdIter last, Pred pred = Pred{}); + + /** Run all suites in a container. + @return `true` if any conditions failed. + */ + template<class SequenceContainer> + bool + run_each(SequenceContainer const& c); + + /** Conditionally run suites in a container. + pred will be called as: + @code + bool pred(suite_info const&); + @endcode + @return `true` if any conditions failed. + */ + template<class SequenceContainer, class Pred> + bool + run_each_if(SequenceContainer const& c, Pred pred = Pred{}); + +protected: + /// Called when a new suite starts. + virtual + void + on_suite_begin(suite_info const&) + { + } + + /// Called when a suite ends. + virtual + void + on_suite_end() + { + } + + /// Called when a new case starts. + virtual + void + on_case_begin(std::string const&) + { + } + + /// Called when a new case ends. + virtual + void + on_case_end() + { + } + + /// Called for each passing condition. + virtual + void + on_pass() + { + } + + /// Called for each failing condition. + virtual + void + on_fail(std::string const&) + { + } + + /// Called when a test logs output. + virtual + void + on_log(std::string const&) + { + } + +private: + friend class suite; + + // Start a new testcase. + template<class = void> + void + testcase(std::string const& name); + + template<class = void> + void + pass(); + + template<class = void> + void + fail(std::string const& reason); + + template<class = void> + void + log(std::string const& s); +}; + +//------------------------------------------------------------------------------ + +template<class> +bool +runner::run(suite_info const& s) +{ + // Enable 'default' testcase + default_ = true; + failed_ = false; + on_suite_begin(s); + s.run(*this); + // Forgot to call pass or fail. + BOOST_ASSERT(cond_); + on_case_end(); + on_suite_end(); + return failed_; +} + +template<class FwdIter> +bool +runner::run(FwdIter first, FwdIter last) +{ + bool failed(false); + for(;first != last; ++first) + failed = run(*first) || failed; + return failed; +} + +template<class FwdIter, class Pred> +bool +runner::run_if(FwdIter first, FwdIter last, Pred pred) +{ + bool failed(false); + for(;first != last; ++first) + if(pred(*first)) + failed = run(*first) || failed; + return failed; +} + +template<class SequenceContainer> +bool +runner::run_each(SequenceContainer const& c) +{ + bool failed(false); + for(auto const& s : c) + failed = run(s) || failed; + return failed; +} + +template<class SequenceContainer, class Pred> +bool +runner::run_each_if(SequenceContainer const& c, Pred pred) +{ + bool failed(false); + for(auto const& s : c) + if(pred(s)) + failed = run(s) || failed; + return failed; +} + +template<class> +void +runner::testcase(std::string const& name) +{ + std::lock_guard<std::recursive_mutex> lock(mutex_); + // Name may not be empty + BOOST_ASSERT(default_ || ! name.empty()); + // Forgot to call pass or fail + BOOST_ASSERT(default_ || cond_); + if(! default_) + on_case_end(); + default_ = false; + cond_ = false; + on_case_begin(name); +} + +template<class> +void +runner::pass() +{ + std::lock_guard<std::recursive_mutex> lock(mutex_); + if(default_) + testcase(""); + on_pass(); + cond_ = true; +} + +template<class> +void +runner::fail(std::string const& reason) +{ + std::lock_guard<std::recursive_mutex> lock(mutex_); + if(default_) + testcase(""); + on_fail(reason); + failed_ = true; + cond_ = true; +} + +template<class> +void +runner::log(std::string const& s) +{ + std::lock_guard<std::recursive_mutex> lock(mutex_); + if(default_) + testcase(""); + on_log(s); +} + +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/suite.hpp b/boost/beast/_experimental/unit_test/suite.hpp new file mode 100644 index 0000000000..3cda8fe057 --- /dev/null +++ b/boost/beast/_experimental/unit_test/suite.hpp @@ -0,0 +1,707 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_SUITE_HPP +#define BOOST_BEAST_UNIT_TEST_SUITE_HPP + +#include <boost/beast/_experimental/unit_test/runner.hpp> +#include <boost/throw_exception.hpp> +#include <ostream> +#include <sstream> +#include <string> + +namespace boost { +namespace beast { +namespace unit_test { + +namespace detail { + +template<class String> +std::string +make_reason(String const& reason, + char const* file, int line) +{ + std::string s(reason); + if(! s.empty()) + s.append(": "); + char const* path = file + strlen(file); + while(path != file) + { + #ifdef _MSC_VER + if(path[-1] == '\\') + #else + if(path[-1] == '/') + #endif + break; + --path; + } + s.append(path); + s.append("("); + s.append(std::to_string(line)); + s.append(")"); + return s; +} + +} // detail + +class thread; + +enum abort_t +{ + no_abort_on_fail, + abort_on_fail +}; + +/** A testsuite class. + + Derived classes execute a series of testcases, where each testcase is + a series of pass/fail tests. To provide a unit test using this class, + derive from it and use the BOOST_BEAST_DEFINE_UNIT_TEST macro in a + translation unit. +*/ +class suite +{ +private: + bool abort_ = false; + bool aborted_ = false; + runner* runner_ = nullptr; + + // This exception is thrown internally to stop the current suite + // in the event of a failure, if the option to stop is set. + struct abort_exception : public std::exception + { + char const* + what() const noexcept override + { + return "test suite aborted"; + } + }; + + template<class CharT, class Traits, class Allocator> + class log_buf + : public std::basic_stringbuf<CharT, Traits, Allocator> + { + suite& suite_; + + public: + explicit + log_buf(suite& self) + : suite_(self) + { + } + + ~log_buf() + { + sync(); + } + + int + sync() override + { + auto const& s = this->str(); + if(s.size() > 0) + suite_.runner_->log(s); + this->str(""); + return 0; + } + }; + + template< + class CharT, + class Traits = std::char_traits<CharT>, + class Allocator = std::allocator<CharT> + > + class log_os : public std::basic_ostream<CharT, Traits> + { + log_buf<CharT, Traits, Allocator> buf_; + + public: + explicit + log_os(suite& self) + : std::basic_ostream<CharT, Traits>(&buf_) + , buf_(self) + { + } + }; + + class scoped_testcase; + + class testcase_t + { + suite& suite_; + std::stringstream ss_; + + public: + explicit + testcase_t(suite& self) + : suite_(self) + { + } + + /** Open a new testcase. + + A testcase is a series of evaluated test conditions. A test + suite may have multiple test cases. A test is associated with + the last opened testcase. When the test first runs, a default + unnamed case is opened. Tests with only one case may omit the + call to testcase. + + @param abort Determines if suite continues running after a failure. + */ + void + operator()(std::string const& name, + abort_t abort = no_abort_on_fail); + + scoped_testcase + operator()(abort_t abort); + + template<class T> + scoped_testcase + operator<<(T const& t); + }; + +public: + /** Logging output stream. + + Text sent to the log output stream will be forwarded to + the output stream associated with the runner. + */ + log_os<char> log; + + /** Memberspace for declaring test cases. */ + testcase_t testcase; + + /** Returns the "current" running suite. + If no suite is running, nullptr is returned. + */ + static + suite* + this_suite() + { + return *p_this_suite(); + } + + suite() + : log(*this) + , testcase(*this) + { + } + + /** Invokes the test using the specified runner. + + Data members are set up here instead of the constructor as a + convenience to writing the derived class to avoid repetition of + forwarded constructor arguments to the base. + Normally this is called by the framework for you. + */ + template<class = void> + void + operator()(runner& r); + + /** Record a successful test condition. */ + template<class = void> + void + pass(); + + /** Record a failure. + + @param reason Optional text added to the output on a failure. + + @param file The source code file where the test failed. + + @param line The source code line number where the test failed. + */ + /** @{ */ + template<class String> + void + fail(String const& reason, char const* file, int line); + + template<class = void> + void + fail(std::string const& reason = ""); + /** @} */ + + /** Evaluate a test condition. + + This function provides improved logging by incorporating the + file name and line number into the reported output on failure, + as well as additional text specified by the caller. + + @param shouldBeTrue The condition to test. The condition + is evaluated in a boolean context. + + @param reason Optional added text to output on a failure. + + @param file The source code file where the test failed. + + @param line The source code line number where the test failed. + + @return `true` if the test condition indicates success. + */ + /** @{ */ + template<class Condition> + bool + expect(Condition const& shouldBeTrue) + { + return expect(shouldBeTrue, ""); + } + + template<class Condition, class String> + bool + expect(Condition const& shouldBeTrue, String const& reason); + + template<class Condition> + bool + expect(Condition const& shouldBeTrue, + char const* file, int line) + { + return expect(shouldBeTrue, "", file, line); + } + + template<class Condition, class String> + bool + expect(Condition const& shouldBeTrue, + String const& reason, char const* file, int line); + /** @} */ + + // + // DEPRECATED + // + // Expect an exception from f() + template<class F, class String> + bool + except(F&& f, String const& reason); + template<class F> + bool + except(F&& f) + { + return except(f, ""); + } + template<class E, class F, class String> + bool + except(F&& f, String const& reason); + template<class E, class F> + bool + except(F&& f) + { + return except<E>(f, ""); + } + template<class F, class String> + bool + unexcept(F&& f, String const& reason); + template<class F> + bool + unexcept(F&& f) + { + return unexcept(f, ""); + } + + /** Return the argument associated with the runner. */ + std::string const& + arg() const + { + return runner_->arg(); + } + + // DEPRECATED + // @return `true` if the test condition indicates success(a false value) + template<class Condition, class String> + bool + unexpected(Condition shouldBeFalse, + String const& reason); + + template<class Condition> + bool + unexpected(Condition shouldBeFalse) + { + return unexpected(shouldBeFalse, ""); + } + +private: + friend class thread; + + static + suite** + p_this_suite() + { + static suite* pts = nullptr; + return &pts; + } + + /** Runs the suite. */ + virtual + void + run() = 0; + + void + propagate_abort(); + + template<class = void> + void + run(runner& r); +}; + +//------------------------------------------------------------------------------ + +// Helper for streaming testcase names +class suite::scoped_testcase +{ +private: + suite& suite_; + std::stringstream& ss_; + +public: + scoped_testcase& operator=(scoped_testcase const&) = delete; + + ~scoped_testcase() + { + auto const& name = ss_.str(); + if(! name.empty()) + suite_.runner_->testcase(name); + } + + scoped_testcase(suite& self, std::stringstream& ss) + : suite_(self) + , ss_(ss) + { + ss_.clear(); + ss_.str({}); + } + + template<class T> + scoped_testcase(suite& self, + std::stringstream& ss, T const& t) + : suite_(self) + , ss_(ss) + { + ss_.clear(); + ss_.str({}); + ss_ << t; + } + + template<class T> + scoped_testcase& + operator<<(T const& t) + { + ss_ << t; + return *this; + } +}; + +//------------------------------------------------------------------------------ + +inline +void +suite::testcase_t::operator()( + std::string const& name, abort_t abort) +{ + suite_.abort_ = abort == abort_on_fail; + suite_.runner_->testcase(name); +} + +inline +suite::scoped_testcase +suite::testcase_t::operator()(abort_t abort) +{ + suite_.abort_ = abort == abort_on_fail; + return { suite_, ss_ }; +} + +template<class T> +inline +suite::scoped_testcase +suite::testcase_t::operator<<(T const& t) +{ + return { suite_, ss_, t }; +} + +//------------------------------------------------------------------------------ + +template<class> +void +suite:: +operator()(runner& r) +{ + *p_this_suite() = this; + try + { + run(r); + *p_this_suite() = nullptr; + } + catch(...) + { + *p_this_suite() = nullptr; + throw; + } +} + +template<class Condition, class String> +bool +suite:: +expect( + Condition const& shouldBeTrue, String const& reason) +{ + if(shouldBeTrue) + { + pass(); + return true; + } + fail(reason); + return false; +} + +template<class Condition, class String> +bool +suite:: +expect(Condition const& shouldBeTrue, + String const& reason, char const* file, int line) +{ + if(shouldBeTrue) + { + pass(); + return true; + } + fail(detail::make_reason(reason, file, line)); + return false; +} + +// DEPRECATED + +template<class F, class String> +bool +suite:: +except(F&& f, String const& reason) +{ + try + { + f(); + fail(reason); + return false; + } + catch(...) + { + pass(); + } + return true; +} + +template<class E, class F, class String> +bool +suite:: +except(F&& f, String const& reason) +{ + try + { + f(); + fail(reason); + return false; + } + catch(E const&) + { + pass(); + } + return true; +} + +template<class F, class String> +bool +suite:: +unexcept(F&& f, String const& reason) +{ + try + { + f(); + pass(); + return true; + } + catch(...) + { + fail(reason); + } + return false; +} + +template<class Condition, class String> +bool +suite:: +unexpected( + Condition shouldBeFalse, String const& reason) +{ + bool const b = + static_cast<bool>(shouldBeFalse); + if(! b) + pass(); + else + fail(reason); + return ! b; +} + +template<class> +void +suite:: +pass() +{ + propagate_abort(); + runner_->pass(); +} + +// ::fail +template<class> +void +suite:: +fail(std::string const& reason) +{ + propagate_abort(); + runner_->fail(reason); + if(abort_) + { + aborted_ = true; + BOOST_THROW_EXCEPTION(abort_exception()); + } +} + +template<class String> +void +suite:: +fail(String const& reason, char const* file, int line) +{ + fail(detail::make_reason(reason, file, line)); +} + +inline +void +suite:: +propagate_abort() +{ + if(abort_ && aborted_) + BOOST_THROW_EXCEPTION(abort_exception()); +} + +template<class> +void +suite:: +run(runner& r) +{ + runner_ = &r; + + try + { + run(); + } + catch(abort_exception const&) + { + // ends the suite + } + catch(std::exception const& e) + { + runner_->fail("unhandled exception: " + + std::string(e.what())); + } + catch(...) + { + runner_->fail("unhandled exception"); + } +} + +#ifndef BEAST_PASS +#define BEAST_PASS() ::boost::beast::unit_test::suite::this_suite()->pass() +#endif + +#ifndef BEAST_FAIL +#define BEAST_FAIL() ::boost::beast::unit_test::suite::this_suite()->fail("", __FILE__, __LINE__) +#endif + +#ifndef BEAST_EXPECT +/** Check a precondition. + + If the condition is false, the file and line number are reported. +*/ +#define BEAST_EXPECT(cond) ::boost::beast::unit_test::suite::this_suite()->expect(cond, __FILE__, __LINE__) +#endif + +#ifndef BEAST_EXPECTS +/** Check a precondition. + + If the condition is false, the file and line number are reported. +*/ +#define BEAST_EXPECTS(cond, reason) ((cond) ? \ + (::boost::beast::unit_test::suite::this_suite()->pass(), true) : \ + (::boost::beast::unit_test::suite::this_suite()->fail((reason), __FILE__, __LINE__), false)) +#endif + +} // unit_test +} // beast +} // boost + +//------------------------------------------------------------------------------ + +// detail: +// This inserts the suite with the given manual flag +#define BEAST_DEFINE_TESTSUITE_INSERT(Library,Module,Class,manual) \ + static ::boost::beast::unit_test::detail::insert_suite <Class##_test> \ + Library ## Module ## Class ## _test_instance( \ + #Class, #Module, #Library, manual) + +//------------------------------------------------------------------------------ + +// Preprocessor directives for controlling unit test definitions. + +// If this is already defined, don't redefine it. This allows +// programs to provide custom behavior for testsuite definitions +// +#ifndef BEAST_DEFINE_TESTSUITE + +/** Enables insertion of test suites into the global container. + The default is to insert all test suite definitions into the global + container. If BEAST_DEFINE_TESTSUITE is user defined, this macro + has no effect. +*/ +#ifndef BEAST_NO_UNIT_TEST_INLINE +#define BEAST_NO_UNIT_TEST_INLINE 0 +#endif + +/** Define a unit test suite. + + Library Identifies the library. + Module Identifies the module. + Class The type representing the class being tested. + + The declaration for the class implementing the test should be the same + as Class ## _test. For example, if Class is aged_ordered_container, the + test class must be declared as: + + @code + + struct aged_ordered_container_test : beast::unit_test::suite + { + //... + }; + + @endcode + + The macro invocation must appear in the same namespace as the test class. +*/ + +#if BEAST_NO_UNIT_TEST_INLINE +#define BEAST_DEFINE_TESTSUITE(Class,Module,Library) + +#else +#include <boost/beast/_experimental/unit_test/global_suites.hpp> +#define BEAST_DEFINE_TESTSUITE(Library,Module,Class) \ + BEAST_DEFINE_TESTSUITE_INSERT(Library,Module,Class,false) +#define BEAST_DEFINE_TESTSUITE_MANUAL(Library,Module,Class) \ + BEAST_DEFINE_TESTSUITE_INSERT(Library,Module,Class,true) + +#endif + +#endif + +//------------------------------------------------------------------------------ + +#endif diff --git a/boost/beast/_experimental/unit_test/suite_info.hpp b/boost/beast/_experimental/unit_test/suite_info.hpp new file mode 100644 index 0000000000..4e42183c92 --- /dev/null +++ b/boost/beast/_experimental/unit_test/suite_info.hpp @@ -0,0 +1,126 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_SUITE_INFO_HPP +#define BOOST_BEAST_UNIT_TEST_SUITE_INFO_HPP + +#include <cstring> +#include <functional> +#include <string> +#include <utility> + +namespace boost { +namespace beast { +namespace unit_test { + +class runner; + +/** Associates a unit test type with metadata. */ +class suite_info +{ + using run_type = std::function<void(runner&)>; + + std::string name_; + std::string module_; + std::string library_; + bool manual_; + run_type run_; + +public: + suite_info( + std::string name, + std::string module, + std::string library, + bool manual, + run_type run) + : name_(std::move(name)) + , module_(std::move(module)) + , library_(std::move(library)) + , manual_(manual) + , run_(std::move(run)) + { + } + + std::string const& + name() const + { + return name_; + } + + std::string const& + module() const + { + return module_; + } + + std::string const& + library() const + { + return library_; + } + + /// Returns `true` if this suite only runs manually. + bool + manual() const + { + return manual_; + } + + /// Return the canonical suite name as a string. + std::string + full_name() const + { + return library_ + "." + module_ + "." + name_; + } + + /// Run a new instance of the associated test suite. + void + run(runner& r) const + { + run_(r); + } + + friend + bool + operator<(suite_info const& lhs, suite_info const& rhs) + { + return + std::tie(lhs.library_, lhs.module_, lhs.name_) < + std::tie(rhs.library_, rhs.module_, rhs.name_); + } +}; + +//------------------------------------------------------------------------------ + +/// Convenience for producing suite_info for a given test type. +template<class Suite> +suite_info +make_suite_info( + std::string name, + std::string module, + std::string library, + bool manual) +{ + return suite_info( + std::move(name), + std::move(module), + std::move(library), + manual, + [](runner& r) + { + Suite{}(r); + } + ); +} + +} // unit_test +} // beast +} // boost + +#endif diff --git a/boost/beast/_experimental/unit_test/suite_list.hpp b/boost/beast/_experimental/unit_test/suite_list.hpp new file mode 100644 index 0000000000..93ab16a431 --- /dev/null +++ b/boost/beast/_experimental/unit_test/suite_list.hpp @@ -0,0 +1,81 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_SUITE_LIST_HPP +#define BOOST_BEAST_UNIT_TEST_SUITE_LIST_HPP + +#include <boost/beast/_experimental/unit_test/suite_info.hpp> +#include <boost/beast/_experimental/unit_test/detail/const_container.hpp> +#include <boost/assert.hpp> +#include <typeindex> +#include <set> +#include <unordered_set> + +namespace boost { +namespace beast { +namespace unit_test { + +/// A container of test suites. +class suite_list + : public detail::const_container <std::set <suite_info>> +{ +private: +#ifndef NDEBUG + std::unordered_set<std::string> names_; + std::unordered_set<std::type_index> classes_; +#endif + +public: + /** Insert a suite into the set. + + The suite must not already exist. + */ + template<class Suite> + void + insert( + char const* name, + char const* module, + char const* library, + bool manual); +}; + +//------------------------------------------------------------------------------ + +template<class Suite> +void +suite_list::insert( + char const* name, + char const* module, + char const* library, + bool manual) +{ +#ifndef NDEBUG + { + std::string s; + s = std::string(library) + "." + module + "." + name; + auto const result(names_.insert(s)); + BOOST_ASSERT(result.second); // Duplicate name + } + + { + auto const result(classes_.insert( + std::type_index(typeid(Suite)))); + BOOST_ASSERT(result.second); // Duplicate type + } +#endif + cont().emplace(make_suite_info<Suite>( + name, module, library, manual)); +} + +} // unit_test +} // beast +} // boost + +#endif + diff --git a/boost/beast/_experimental/unit_test/thread.hpp b/boost/beast/_experimental/unit_test/thread.hpp new file mode 100644 index 0000000000..ed06cb3731 --- /dev/null +++ b/boost/beast/_experimental/unit_test/thread.hpp @@ -0,0 +1,128 @@ +// +// 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/boostorg/beast +// + +#ifndef BOOST_BEAST_UNIT_TEST_THREAD_HPP +#define BOOST_BEAST_UNIT_TEST_THREAD_HPP + +#include <boost/beast/_experimental/unit_test/suite.hpp> +#include <functional> +#include <thread> +#include <utility> + +namespace boost { +namespace beast { +namespace unit_test { + +/** Replacement for std::thread that handles exceptions in unit tests. */ +class thread +{ +private: + suite* s_ = nullptr; + std::thread t_; + +public: + using id = std::thread::id; + using native_handle_type = std::thread::native_handle_type; + + thread() = default; + thread(thread const&) = delete; + thread& operator=(thread const&) = delete; + + thread(thread&& other) + : s_(other.s_) + , t_(std::move(other.t_)) + { + } + + thread& operator=(thread&& other) + { + s_ = other.s_; + t_ = std::move(other.t_); + return *this; + } + + template<class F, class... Args> + explicit + thread(suite& s, F&& f, Args&&... args) + : s_(&s) + { + std::function<void(void)> b = + std::bind(std::forward<F>(f), + std::forward<Args>(args)...); + t_ = std::thread(&thread::run, this, + std::move(b)); + } + + bool + joinable() const + { + return t_.joinable(); + } + + std::thread::id + get_id() const + { + return t_.get_id(); + } + + static + unsigned + hardware_concurrency() noexcept + { + return std::thread::hardware_concurrency(); + } + + void + join() + { + t_.join(); + s_->propagate_abort(); + } + + void + detach() + { + t_.detach(); + } + + void + swap(thread& other) + { + std::swap(s_, other.s_); + std::swap(t_, other.t_); + } + +private: + void + run(std::function <void(void)> f) + { + try + { + f(); + } + catch(suite::abort_exception const&) + { + } + catch(std::exception const& e) + { + s_->fail("unhandled exception: " + + std::string(e.what())); + } + catch(...) + { + s_->fail("unhandled exception"); + } + } +}; + +} // unit_test +} // beast +} // boost + +#endif |