| 1 | // |
| 2 | // Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) |
| 3 | // |
| 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying |
| 5 | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) |
| 6 | // |
| 7 | // Official repository: https://github.com/boostorg/beast |
| 8 | // |
| 9 | |
| 10 | #ifndef BOOST_BEAST_TEST_STREAM_HPP |
| 11 | #define BOOST_BEAST_TEST_STREAM_HPP |
| 12 | |
| 13 | #include <boost/beast/core/detail/config.hpp> |
| 14 | #include <boost/beast/core/flat_buffer.hpp> |
| 15 | #include <boost/beast/core/role.hpp> |
| 16 | #include <boost/beast/core/string.hpp> |
| 17 | #include <boost/beast/_experimental/test/fail_count.hpp> |
| 18 | #include <boost/beast/_experimental/test/detail/stream_state.hpp> |
| 19 | #include <boost/asio/async_result.hpp> |
| 20 | #include <boost/asio/buffer.hpp> |
| 21 | #include <boost/asio/error.hpp> |
| 22 | #include <boost/asio/executor_work_guard.hpp> |
| 23 | #include <boost/asio/any_io_executor.hpp> |
| 24 | #include <boost/asio/io_context.hpp> |
| 25 | #include <boost/assert.hpp> |
| 26 | #include <boost/shared_ptr.hpp> |
| 27 | #include <boost/weak_ptr.hpp> |
| 28 | #include <boost/throw_exception.hpp> |
| 29 | #include <condition_variable> |
| 30 | #include <limits> |
| 31 | #include <memory> |
| 32 | #include <mutex> |
| 33 | #include <utility> |
| 34 | |
| 35 | #if ! BOOST_BEAST_DOXYGEN |
| 36 | namespace boost { |
| 37 | namespace asio { |
| 38 | namespace ssl { |
| 39 | template<typename> class stream; |
| 40 | } // ssl |
| 41 | } // asio |
| 42 | } // boost |
| 43 | #endif |
| 44 | |
| 45 | namespace boost { |
| 46 | namespace beast { |
| 47 | namespace test { |
| 48 | |
| 49 | /** A two-way socket useful for unit testing |
| 50 | |
| 51 | An instance of this class simulates a traditional socket, |
| 52 | while also providing features useful for unit testing. |
| 53 | Each endpoint maintains an independent buffer called |
| 54 | the input area. Writes from one endpoint append data |
| 55 | to the peer's pending input area. When an endpoint performs |
| 56 | a read and data is present in the input area, the data is |
| 57 | delivered to the blocking or asynchronous operation. Otherwise |
| 58 | the operation is blocked or deferred until data is made |
| 59 | available, or until the endpoints become disconnected. |
| 60 | |
| 61 | These streams may be used anywhere an algorithm accepts a |
| 62 | reference to a synchronous or asynchronous read or write |
| 63 | stream. It is possible to use a test stream in a call to |
| 64 | `net::read_until`, or in a call to |
| 65 | @ref boost::beast::http::async_write for example. |
| 66 | |
| 67 | As with Boost.Asio I/O objects, a @ref stream constructs |
| 68 | with a reference to the `net::io_context` to use for |
| 69 | handling asynchronous I/O. For asynchronous operations, the |
| 70 | stream follows the same rules as a traditional asio socket |
| 71 | with respect to how completion handlers for asynchronous |
| 72 | operations are performed. |
| 73 | |
| 74 | To facilitate testing, these streams support some additional |
| 75 | features: |
| 76 | |
| 77 | @li The input area, represented by a @ref beast::basic_flat_buffer, |
| 78 | may be directly accessed by the caller to inspect the contents |
| 79 | before or after the remote endpoint writes data. This allows |
| 80 | a unit test to verify that the received data matches. |
| 81 | |
| 82 | @li Data may be manually appended to the input area. This data |
| 83 | will delivered in the next call to |
| 84 | @ref stream::read_some or @ref stream::async_read_some. |
| 85 | This allows predefined test vectors to be set up for testing |
| 86 | read algorithms. |
| 87 | |
| 88 | @li The stream may be constructed with a fail count. The |
| 89 | stream will eventually fail with a predefined error after a |
| 90 | certain number of operations, where the number of operations |
| 91 | is controlled by the test. When a test loops over a range of |
| 92 | operation counts, it is possible to exercise every possible |
| 93 | point of failure in the algorithm being tested. When used |
| 94 | correctly the technique allows the tests to reach a high |
| 95 | percentage of code coverage. |
| 96 | |
| 97 | @par Thread Safety |
| 98 | @e Distinct @e objects: Safe.@n |
| 99 | @e Shared @e objects: Unsafe. |
| 100 | The application must also ensure that all asynchronous |
| 101 | operations are performed within the same implicit or explicit strand. |
| 102 | |
| 103 | @par Concepts |
| 104 | @li <em>SyncReadStream</em> |
| 105 | @li <em>SyncWriteStream</em> |
| 106 | @li <em>AsyncReadStream</em> |
| 107 | @li <em>AsyncWriteStream</em> |
| 108 | */ |
| 109 | template<class Executor = net::any_io_executor> |
| 110 | class basic_stream; |
| 111 | |
| 112 | template<class Executor> |
| 113 | void |
| 114 | teardown( |
| 115 | role_type, |
| 116 | basic_stream<Executor>& s, |
| 117 | boost::system::error_code& ec); |
| 118 | |
| 119 | template<class Executor, class TeardownHandler> |
| 120 | void |
| 121 | async_teardown( |
| 122 | role_type role, |
| 123 | basic_stream<Executor>& s, |
| 124 | TeardownHandler&& handler); |
| 125 | |
| 126 | template<class Executor> |
| 127 | class basic_stream |
| 128 | { |
| 129 | public: |
| 130 | /// The type of the executor associated with the object. |
| 131 | using executor_type = |
| 132 | Executor; |
| 133 | |
| 134 | /// Rebinds the socket type to another executor. |
| 135 | template <typename Executor1> |
| 136 | struct rebind_executor |
| 137 | { |
| 138 | /// The socket type when rebound to the specified executor. |
| 139 | typedef basic_stream<Executor1> other; |
| 140 | }; |
| 141 | |
| 142 | private: |
| 143 | template<class Executor2> |
| 144 | friend class basic_stream; |
| 145 | |
| 146 | boost::shared_ptr<detail::stream_state> in_; |
| 147 | boost::weak_ptr<detail::stream_state> out_; |
| 148 | |
| 149 | template<class Handler, class Buffers> |
| 150 | class read_op; |
| 151 | |
| 152 | struct run_read_op; |
| 153 | struct run_write_op; |
| 154 | |
| 155 | static |
| 156 | void |
| 157 | initiate_read( |
| 158 | boost::shared_ptr<detail::stream_state> const& in, |
| 159 | std::unique_ptr<detail::stream_read_op_base>&& op, |
| 160 | std::size_t buf_size); |
| 161 | |
| 162 | #if ! BOOST_BEAST_DOXYGEN |
| 163 | // boost::asio::ssl::stream needs these |
| 164 | // DEPRECATED |
| 165 | template<class> |
| 166 | friend class boost::asio::ssl::stream; |
| 167 | // DEPRECATED |
| 168 | using lowest_layer_type = basic_stream; |
| 169 | // DEPRECATED |
| 170 | lowest_layer_type& |
| 171 | lowest_layer() noexcept |
| 172 | { |
| 173 | return *this; |
| 174 | } |
| 175 | // DEPRECATED |
| 176 | lowest_layer_type const& |
| 177 | lowest_layer() const noexcept |
| 178 | { |
| 179 | return *this; |
| 180 | } |
| 181 | #endif |
| 182 | |
| 183 | public: |
| 184 | using buffer_type = flat_buffer; |
| 185 | |
| 186 | /** Destructor |
| 187 | |
| 188 | If an asynchronous read operation is pending, it will |
| 189 | simply be discarded with no notification to the completion |
| 190 | handler. |
| 191 | |
| 192 | If a connection is established while the stream is destroyed, |
| 193 | the peer will see the error `net::error::connection_reset` |
| 194 | when performing any reads or writes. |
| 195 | */ |
| 196 | ~basic_stream(); |
| 197 | |
| 198 | /** Move Constructor |
| 199 | |
| 200 | Moving the stream while asynchronous operations are pending |
| 201 | results in undefined behavior. |
| 202 | */ |
| 203 | basic_stream(basic_stream&& other); |
| 204 | |
| 205 | /** Move Constructor |
| 206 | |
| 207 | Moving the stream while asynchronous operations are pending |
| 208 | results in undefined behavior. |
| 209 | */ |
| 210 | template<class Executor2> |
| 211 | basic_stream(basic_stream<Executor2>&& other) |
| 212 | : in_(std::move(other.in_)) |
| 213 | , out_(std::move(other.out_)) |
| 214 | { |
| 215 | BOOST_ASSERT(in_->exec.template target<Executor2>() != nullptr); |
| 216 | in_->exec = executor_type(*in_->exec.template target<Executor2>()); |
| 217 | } |
| 218 | |
| 219 | /** Move Assignment |
| 220 | |
| 221 | Moving the stream while asynchronous operations are pending |
| 222 | results in undefined behavior. |
| 223 | */ |
| 224 | basic_stream& |
| 225 | operator=(basic_stream&& other); |
| 226 | |
| 227 | template<class Executor2> |
| 228 | basic_stream& |
| 229 | operator==(basic_stream<Executor2>&& other); |
| 230 | |
| 231 | /** Construct a stream |
| 232 | |
| 233 | The stream will be created in a disconnected state. |
| 234 | |
| 235 | @param context The `io_context` object that the stream will use to |
| 236 | dispatch handlers for any asynchronous operations. |
| 237 | */ |
| 238 | template <typename ExecutionContext> |
| 239 | explicit basic_stream(ExecutionContext& context, |
| 240 | typename std::enable_if< |
| 241 | std::is_convertible<ExecutionContext&, net::execution_context&>::value |
| 242 | >::type* = 0) |
| 243 | : basic_stream(context.get_executor()) |
| 244 | { |
| 245 | } |
| 246 | |
| 247 | /** Construct a stream |
| 248 | |
| 249 | The stream will be created in a disconnected state. |
| 250 | |
| 251 | @param exec The `executor` object that the stream will use to |
| 252 | dispatch handlers for any asynchronous operations. |
| 253 | */ |
| 254 | explicit |
| 255 | basic_stream(executor_type exec); |
| 256 | |
| 257 | /** Construct a stream |
| 258 | |
| 259 | The stream will be created in a disconnected state. |
| 260 | |
| 261 | @param ioc The `io_context` object that the stream will use to |
| 262 | dispatch handlers for any asynchronous operations. |
| 263 | |
| 264 | @param fc The @ref fail_count to associate with the stream. |
| 265 | Each I/O operation performed on the stream will increment the |
| 266 | fail count. When the fail count reaches its internal limit, |
| 267 | a simulated failure error will be raised. |
| 268 | */ |
| 269 | basic_stream( |
| 270 | net::io_context& ioc, |
| 271 | fail_count& fc); |
| 272 | |
| 273 | /** Construct a stream |
| 274 | |
| 275 | The stream will be created in a disconnected state. |
| 276 | |
| 277 | @param ioc The `io_context` object that the stream will use to |
| 278 | dispatch handlers for any asynchronous operations. |
| 279 | |
| 280 | @param s A string which will be appended to the input area, not |
| 281 | including the null terminator. |
| 282 | */ |
| 283 | basic_stream( |
| 284 | net::io_context& ioc, |
| 285 | string_view s); |
| 286 | |
| 287 | /** Construct a stream |
| 288 | |
| 289 | The stream will be created in a disconnected state. |
| 290 | |
| 291 | @param ioc The `io_context` object that the stream will use to |
| 292 | dispatch handlers for any asynchronous operations. |
| 293 | |
| 294 | @param fc The @ref fail_count to associate with the stream. |
| 295 | Each I/O operation performed on the stream will increment the |
| 296 | fail count. When the fail count reaches its internal limit, |
| 297 | a simulated failure error will be raised. |
| 298 | |
| 299 | @param s A string which will be appended to the input area, not |
| 300 | including the null terminator. |
| 301 | */ |
| 302 | basic_stream( |
| 303 | net::io_context& ioc, |
| 304 | fail_count& fc, |
| 305 | string_view s); |
| 306 | |
| 307 | /// Establish a connection |
| 308 | void |
| 309 | connect(basic_stream& remote); |
| 310 | |
| 311 | /// Return the executor associated with the object. |
| 312 | executor_type |
| 313 | get_executor() noexcept; |
| 314 | |
| 315 | /// Set the maximum number of bytes returned by read_some |
| 316 | void |
| 317 | read_size(std::size_t n) noexcept |
| 318 | { |
| 319 | in_->read_max = n; |
| 320 | } |
| 321 | |
| 322 | /// Set the maximum number of bytes returned by write_some |
| 323 | void |
| 324 | write_size(std::size_t n) noexcept |
| 325 | { |
| 326 | in_->write_max = n; |
| 327 | } |
| 328 | |
| 329 | /// Direct input buffer access |
| 330 | buffer_type& |
| 331 | buffer() noexcept |
| 332 | { |
| 333 | return in_->b; |
| 334 | } |
| 335 | |
| 336 | /// Returns a string view representing the pending input data |
| 337 | string_view |
| 338 | str() const; |
| 339 | |
| 340 | /// Appends a string to the pending input data |
| 341 | void |
| 342 | append(string_view s); |
| 343 | |
| 344 | /// Clear the pending input area |
| 345 | void |
| 346 | clear(); |
| 347 | |
| 348 | /// Return the number of reads |
| 349 | std::size_t |
| 350 | nread() const noexcept |
| 351 | { |
| 352 | return in_->nread; |
| 353 | } |
| 354 | |
| 355 | /// Return the number of bytes read |
| 356 | std::size_t |
| 357 | nread_bytes() const noexcept |
| 358 | { |
| 359 | return in_->nread_bytes; |
| 360 | } |
| 361 | |
| 362 | /// Return the number of writes |
| 363 | std::size_t |
| 364 | nwrite() const noexcept |
| 365 | { |
| 366 | return in_->nwrite; |
| 367 | } |
| 368 | |
| 369 | /// Return the number of bytes written |
| 370 | std::size_t |
| 371 | nwrite_bytes() const noexcept |
| 372 | { |
| 373 | return in_->nwrite_bytes; |
| 374 | } |
| 375 | |
| 376 | /** Close the stream. |
| 377 | |
| 378 | The other end of the connection will see |
| 379 | `error::eof` after reading all the remaining data. |
| 380 | */ |
| 381 | void |
| 382 | close(); |
| 383 | |
| 384 | /** Close the other end of the stream. |
| 385 | |
| 386 | This end of the connection will see |
| 387 | `error::eof` after reading all the remaining data. |
| 388 | */ |
| 389 | void |
| 390 | close_remote(); |
| 391 | |
| 392 | /** Read some data from the stream. |
| 393 | |
| 394 | This function is used to read data from the stream. The function call will |
| 395 | block until one or more bytes of data has been read successfully, or until |
| 396 | an error occurs. |
| 397 | |
| 398 | @param buffers The buffers into which the data will be read. |
| 399 | |
| 400 | @returns The number of bytes read. |
| 401 | |
| 402 | @throws boost::system::system_error Thrown on failure. |
| 403 | |
| 404 | @note The `read_some` operation may not read all of the requested number of |
| 405 | bytes. Consider using the function `net::read` if you need to ensure |
| 406 | that the requested amount of data is read before the blocking operation |
| 407 | completes. |
| 408 | */ |
| 409 | template<class MutableBufferSequence> |
| 410 | std::size_t |
| 411 | read_some(MutableBufferSequence const& buffers); |
| 412 | |
| 413 | /** Read some data from the stream. |
| 414 | |
| 415 | This function is used to read data from the stream. The function call will |
| 416 | block until one or more bytes of data has been read successfully, or until |
| 417 | an error occurs. |
| 418 | |
| 419 | @param buffers The buffers into which the data will be read. |
| 420 | |
| 421 | @param ec Set to indicate what error occurred, if any. |
| 422 | |
| 423 | @returns The number of bytes read. |
| 424 | |
| 425 | @note The `read_some` operation may not read all of the requested number of |
| 426 | bytes. Consider using the function `net::read` if you need to ensure |
| 427 | that the requested amount of data is read before the blocking operation |
| 428 | completes. |
| 429 | */ |
| 430 | template<class MutableBufferSequence> |
| 431 | std::size_t |
| 432 | read_some(MutableBufferSequence const& buffers, |
| 433 | error_code& ec); |
| 434 | |
| 435 | /** Start an asynchronous read. |
| 436 | |
| 437 | This function is used to asynchronously read one or more bytes of data from |
| 438 | the stream. The function call always returns immediately. |
| 439 | |
| 440 | @param buffers The buffers into which the data will be read. Although the |
| 441 | buffers object may be copied as necessary, ownership of the underlying |
| 442 | buffers is retained by the caller, which must guarantee that they remain |
| 443 | valid until the handler is called. |
| 444 | |
| 445 | @param handler The completion handler to invoke when the operation |
| 446 | completes. The implementation takes ownership of the handler by |
| 447 | performing a decay-copy. The equivalent function signature of |
| 448 | the handler must be: |
| 449 | @code |
| 450 | void handler( |
| 451 | error_code const& ec, // Result of operation. |
| 452 | std::size_t bytes_transferred // Number of bytes read. |
| 453 | ); |
| 454 | @endcode |
| 455 | If the handler has an associated immediate executor, |
| 456 | an immediate completion will be dispatched to it. |
| 457 | Otherwise, the handler will not be invoked from within |
| 458 | this function. Invocation of the handler will be performed |
| 459 | by dispatching to the immediate executor. If no |
| 460 | immediate executor is specified, this is equivalent |
| 461 | to using `net::post`. |
| 462 | @note The `async_read_some` operation may not read all of the requested number of |
| 463 | bytes. Consider using the function `net::async_read` if you need |
| 464 | to ensure that the requested amount of data is read before the asynchronous |
| 465 | operation completes. |
| 466 | */ |
| 467 | template< |
| 468 | class MutableBufferSequence, |
| 469 | BOOST_ASIO_COMPLETION_TOKEN_FOR(void(error_code, std::size_t)) ReadHandler |
| 470 | BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)> |
| 471 | BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(ReadHandler, void(error_code, std::size_t)) |
| 472 | async_read_some( |
| 473 | MutableBufferSequence const& buffers, |
| 474 | ReadHandler&& handler BOOST_ASIO_DEFAULT_COMPLETION_TOKEN(executor_type)); |
| 475 | |
| 476 | /** Write some data to the stream. |
| 477 | |
| 478 | This function is used to write data on the stream. The function call will |
| 479 | block until one or more bytes of data has been written successfully, or |
| 480 | until an error occurs. |
| 481 | |
| 482 | @param buffers The data to be written. |
| 483 | |
| 484 | @returns The number of bytes written. |
| 485 | |
| 486 | @throws boost::system::system_error Thrown on failure. |
| 487 | |
| 488 | @note The `write_some` operation may not transmit all of the data to the |
| 489 | peer. Consider using the function `net::write` if you need to |
| 490 | ensure that all data is written before the blocking operation completes. |
| 491 | */ |
| 492 | template<class ConstBufferSequence> |
| 493 | std::size_t |
| 494 | write_some(ConstBufferSequence const& buffers); |
| 495 | |
| 496 | /** Write some data to the stream. |
| 497 | |
| 498 | This function is used to write data on the stream. The function call will |
| 499 | block until one or more bytes of data has been written successfully, or |
| 500 | until an error occurs. |
| 501 | |
| 502 | @param buffers The data to be written. |
| 503 | |
| 504 | @param ec Set to indicate what error occurred, if any. |
| 505 | |
| 506 | @returns The number of bytes written. |
| 507 | |
| 508 | @note The `write_some` operation may not transmit all of the data to the |
| 509 | peer. Consider using the function `net::write` if you need to |
| 510 | ensure that all data is written before the blocking operation completes. |
| 511 | */ |
| 512 | template<class ConstBufferSequence> |
| 513 | std::size_t |
| 514 | write_some( |
| 515 | ConstBufferSequence const& buffers, error_code& ec); |
| 516 | |
| 517 | /** Start an asynchronous write. |
| 518 | |
| 519 | This function is used to asynchronously write one or more bytes of data to |
| 520 | the stream. The function call always returns immediately. |
| 521 | |
| 522 | @param buffers The data to be written to the stream. Although the buffers |
| 523 | object may be copied as necessary, ownership of the underlying buffers is |
| 524 | retained by the caller, which must guarantee that they remain valid until |
| 525 | the handler is called. |
| 526 | |
| 527 | @param handler The completion handler to invoke when the operation |
| 528 | completes. The implementation takes ownership of the handler by |
| 529 | performing a decay-copy. The equivalent function signature of |
| 530 | the handler must be: |
| 531 | @code |
| 532 | void handler( |
| 533 | error_code const& ec, // Result of operation. |
| 534 | std::size_t bytes_transferred // Number of bytes written. |
| 535 | ); |
| 536 | @endcode |
| 537 | If the handler has an associated immediate executor, |
| 538 | an immediate completion will be dispatched to it. |
| 539 | Otherwise, the handler will not be invoked from within |
| 540 | this function. Invocation of the handler will be performed |
| 541 | by dispatching to the immediate executor. If no |
| 542 | immediate executor is specified, this is equivalent |
| 543 | to using `net::post`. |
| 544 | @note The `async_write_some` operation may not transmit all of the data to |
| 545 | the peer. Consider using the function `net::async_write` if you need |
| 546 | to ensure that all data is written before the asynchronous operation completes. |
| 547 | */ |
| 548 | template< |
| 549 | class ConstBufferSequence, |
| 550 | BOOST_ASIO_COMPLETION_TOKEN_FOR(void(error_code, std::size_t)) WriteHandler |
| 551 | BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)> |
| 552 | BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(WriteHandler, void(error_code, std::size_t)) |
| 553 | async_write_some( |
| 554 | ConstBufferSequence const& buffers, |
| 555 | WriteHandler&& handler BOOST_ASIO_DEFAULT_COMPLETION_TOKEN(executor_type) |
| 556 | ); |
| 557 | |
| 558 | #if ! BOOST_BEAST_DOXYGEN |
| 559 | friend |
| 560 | void |
| 561 | teardown<>( |
| 562 | role_type, |
| 563 | basic_stream& s, |
| 564 | boost::system::error_code& ec); |
| 565 | |
| 566 | template<class Ex2, class TeardownHandler> |
| 567 | friend |
| 568 | void |
| 569 | async_teardown( |
| 570 | role_type role, |
| 571 | basic_stream<Ex2>& s, |
| 572 | TeardownHandler&& handler); |
| 573 | #endif |
| 574 | }; |
| 575 | |
| 576 | #if ! BOOST_BEAST_DOXYGEN |
| 577 | template<class Executor> |
| 578 | void |
| 579 | beast_close_socket(basic_stream<Executor>& s) |
| 580 | { |
| 581 | s.close(); |
| 582 | } |
| 583 | #endif |
| 584 | |
| 585 | #if BOOST_BEAST_DOXYGEN |
| 586 | /** Return a new stream connected to the given stream |
| 587 | |
| 588 | @param to The stream to connect to. |
| 589 | |
| 590 | @param args Optional arguments forwarded to the new stream's constructor. |
| 591 | |
| 592 | @return The new, connected stream. |
| 593 | */ |
| 594 | template<class Executor> |
| 595 | template<class... Args> |
| 596 | basic_stream |
| 597 | connect(basic_stream& to, Args&&... args); |
| 598 | |
| 599 | #else |
| 600 | template<class Executor> |
| 601 | basic_stream<Executor> |
| 602 | connect(basic_stream<Executor>& to); |
| 603 | |
| 604 | template<class Executor> |
| 605 | void |
| 606 | connect(basic_stream<Executor>& s1, basic_stream<Executor>& s2); |
| 607 | |
| 608 | template<class Executor, class Arg1, class... ArgN> |
| 609 | basic_stream<Executor> |
| 610 | connect(basic_stream<Executor>& to, Arg1&& arg1, ArgN&&... argn); |
| 611 | #endif |
| 612 | |
| 613 | using stream = basic_stream<>; |
| 614 | |
| 615 | } // test |
| 616 | } // beast |
| 617 | } // boost |
| 618 | |
| 619 | #include <boost/beast/_experimental/test/impl/stream.hpp> |
| 620 | //#ifdef BOOST_BEAST_HEADER_ONLY |
| 621 | #include <boost/beast/_experimental/test/impl/stream.ipp> |
| 622 | //#endif |
| 623 | |
| 624 | #endif |
| 625 | |