diff options
Diffstat (limited to 'src/lib/asiolink/tests')
-rw-r--r-- | src/lib/asiolink/tests/Makefile.am | 8 | ||||
-rw-r--r-- | src/lib/asiolink/tests/interval_timer_unittest.cc | 3 | ||||
-rw-r--r-- | src/lib/asiolink/tests/tcp_acceptor_unittest.cc | 442 | ||||
-rw-r--r-- | src/lib/asiolink/tests/tcp_socket_unittest.cc | 27 | ||||
-rw-r--r-- | src/lib/asiolink/tests/unix_domain_socket_unittest.cc | 313 |
5 files changed, 780 insertions, 13 deletions
diff --git a/src/lib/asiolink/tests/Makefile.am b/src/lib/asiolink/tests/Makefile.am index 6e5592a566..4d6ce6b4e5 100644 --- a/src/lib/asiolink/tests/Makefile.am +++ b/src/lib/asiolink/tests/Makefile.am @@ -1,6 +1,7 @@ AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib AM_CPPFLAGS += $(BOOST_INCLUDES) AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(srcdir)/testdata\" +AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/asiolink/tests\" AM_CXXFLAGS = $(KEA_CXXFLAGS) @@ -8,7 +9,7 @@ if USE_STATIC_LINK AM_LDFLAGS = -static endif -CLEANFILES = *.gcno *.gcda +CLEANFILES = *.gcno *.gcda test-socket TESTS_ENVIRONMENT = \ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) @@ -27,10 +28,13 @@ run_unittests_SOURCES += udp_endpoint_unittest.cc run_unittests_SOURCES += udp_socket_unittest.cc run_unittests_SOURCES += io_service_unittest.cc run_unittests_SOURCES += dummy_io_callback_unittest.cc +run_unittests_SOURCES += tcp_acceptor_unittest.cc +run_unittests_SOURCES += unix_domain_socket_unittest.cc run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) -run_unittests_LDADD = $(top_builddir)/src/lib/asiolink/libkea-asiolink.la +run_unittests_LDADD = $(top_builddir)/src/lib/asiolink/testutils/libasiolinktest.la +run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la run_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la run_unittests_LDADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la diff --git a/src/lib/asiolink/tests/interval_timer_unittest.cc b/src/lib/asiolink/tests/interval_timer_unittest.cc index 54da9f8935..957486e253 100644 --- a/src/lib/asiolink/tests/interval_timer_unittest.cc +++ b/src/lib/asiolink/tests/interval_timer_unittest.cc @@ -154,8 +154,7 @@ TEST_F(IntervalTimerTest, invalidArgumentToIntervalTimer) { // expect throw if call back function is empty EXPECT_THROW(itimer.setup(IntervalTimer::Callback(), 1), isc::InvalidParameter); - // expect throw if interval is not greater than 0 - EXPECT_THROW(itimer.setup(TimerCallBack(this), 0), isc::BadValue); + // expect throw if interval is negative. EXPECT_THROW(itimer.setup(TimerCallBack(this), -1), isc::BadValue); } diff --git a/src/lib/asiolink/tests/tcp_acceptor_unittest.cc b/src/lib/asiolink/tests/tcp_acceptor_unittest.cc new file mode 100644 index 0000000000..a88a07a971 --- /dev/null +++ b/src/lib/asiolink/tests/tcp_acceptor_unittest.cc @@ -0,0 +1,442 @@ +// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <asiolink/asio_wrapper.h> +#include <asiolink/interval_timer.h> +#include <asiolink/io_address.h> +#include <asiolink/io_service.h> +#include <asiolink/tcp_acceptor.h> +#include <asiolink/tcp_endpoint.h> +#include <boost/bind.hpp> +#include <boost/function.hpp> +#include <boost/noncopyable.hpp> +#include <boost/shared_ptr.hpp> +#include <gtest/gtest.h> +#include <list> +#include <netinet/in.h> +#include <string> + +using namespace isc::asiolink; + +namespace { + +/// @brief Local server address used for testing. +const char SERVER_ADDRESS[] = "127.0.0.1"; + +/// @brief Local server port used for testing. +const unsigned short SERVER_PORT = 18123; + +/// @brief Test timeout in ms. +const long TEST_TIMEOUT = 10000; + +/// @brief Simple class representing TCP socket callback. +class SocketCallback { +public: + + /// @brief Implements callback for the asynchronous operation on the socket. + /// + /// This callback merely checks if error has occurred and reports this + /// error. It does nothing in case of success. + /// + /// @param ec Error code. + /// @param length Length of received data. + void operator()(boost::system::error_code ec, size_t length = 0) { + if (ec) { + ADD_FAILURE() << "error occurred for a socket: " << ec.message(); + } + } + +}; + +/// @brief Entity which can connect to the TCP server endpoint and close the +/// connection. +class TCPClient : public boost::noncopyable { +public: + + /// @brief Constructor. + /// + /// This constructor creates new socket instance. It doesn't connect. Call + /// connect() to connect to the server. + /// + /// @param io_service IO service to be stopped on error. + explicit TCPClient(IOService& io_service) + : io_service_(io_service.get_io_service()), socket_(io_service_) { + } + + /// @brief Destructor. + /// + /// Closes the underlying socket if it is open. + ~TCPClient() { + close(); + } + + /// @brief Connect to the test server address and port. + /// + /// This method asynchronously connects to the server endpoint and uses the + /// connectHandler as a callback function. + void connect() { + boost::asio::ip::tcp::endpoint + endpoint(boost::asio::ip::address::from_string(SERVER_ADDRESS), + SERVER_PORT); + socket_.async_connect(endpoint, + boost::bind(&TCPClient::connectHandler, this,_1)); + } + + /// @brief Callback function for connect(). + /// + /// This function stops the IO service upon error. + /// + /// @param ec Error code. + void connectHandler(const boost::system::error_code& ec) { + if (ec) { + // One would expect that async_connect wouldn't return EINPROGRESS + // error code, but simply wait for the connection to get + // established before the handler is invoked. It turns out, however, + // that on some OSes the connect handler may receive this error code + // which doesn't necessarily indicate a problem. Making an attempt + // to write and read from this socket will typically succeed. So, + // we ignore this error. + if (ec.value() != boost::asio::error::in_progress) { + ADD_FAILURE() << "error occurred while connecting: " + << ec.message(); + io_service_.stop(); + } + } + } + + /// @brief Close connection. + void close() { + socket_.close(); + } + +private: + + /// @brief Holds reference to the IO service. + boost::asio::io_service& io_service_; + + /// @brief A socket used for the connection. + boost::asio::ip::tcp::socket socket_; + +}; + +/// @brief Pointer to the TCPClient. +typedef boost::shared_ptr<TCPClient> TCPClientPtr; + +/// @brief A signature of the function implementing callback for the +/// TCPAcceptor. +typedef boost::function<void(const boost::system::error_code&)> TCPAcceptorCallback; + +/// @brief TCPAcceptor using TCPAcceptorCallback. +typedef TCPAcceptor<TCPAcceptorCallback> TestTCPAcceptor; + +/// @brief Implements asynchronous TCP acceptor service. +/// +/// It creates a new socket into which connection is accepted. The socket +/// is retained until class instance exists. +class Acceptor { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service. + /// @param acceptor Reference to the TCP acceptor on which asyncAccept + /// will be called. + /// @param callback Callback function for the asyncAccept. + explicit Acceptor(IOService& io_service, TestTCPAcceptor& acceptor, + const TCPAcceptorCallback& callback) + : socket_(io_service), acceptor_(acceptor), callback_(callback) { + } + + /// @brief Destructor. + /// + /// Closes socket. + ~Acceptor() { + socket_.close(); + } + + /// @brief Asynchronous accept new connection. + void accept() { + acceptor_.asyncAccept(socket_, callback_); + } + + /// @brief Close connection. + void close() { + socket_.close(); + } + +private: + + /// @brief Socket into which connection is accepted. + TCPSocket<SocketCallback> socket_; + + /// @brief Reference to the TCPAcceptor on which asyncAccept is called. + TestTCPAcceptor& acceptor_; + + /// @brief Instance of the callback used for asyncAccept. + TCPAcceptorCallback callback_; + +}; + +/// @brief Pointer to the Acceptor object. +typedef boost::shared_ptr<Acceptor> AcceptorPtr; + +/// @brief Test fixture class for TCPAcceptor. +/// +/// This class provides means for creating new TCP connections, i.e. simulates +/// clients connecting to the servers via TCPAcceptor. It is possible to create +/// multiple simultaneous connections, which are retained by the test fixture +/// class and closed cleanly when the test fixture is destroyed. +class TCPAcceptorTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Besides initializing class members it also sets the test timer to guard + /// against endlessly running IO service when TCP connections are + /// unsuccessful. + TCPAcceptorTest() + : io_service_(), acceptor_(io_service_), + asio_endpoint_(boost::asio::ip::address::from_string(SERVER_ADDRESS), + SERVER_PORT), + endpoint_(asio_endpoint_), test_timer_(io_service_), connections_(), + clients_(), connections_num_(0), aborted_connections_num_(0), + max_connections_(1) { + test_timer_.setup(boost::bind(&TCPAcceptorTest::timeoutHandler, this), + TEST_TIMEOUT, IntervalTimer::ONE_SHOT); + } + + /// @brief Destructor. + virtual ~TCPAcceptorTest() { + } + + /// @brief Specifies how many new connections are expected before the IO + /// service is stopped. + /// + /// @param max_connections Connections limit. + void setMaxConnections(const unsigned int max_connections) { + max_connections_ = max_connections; + } + + /// @brief Create ASIO endpoint from the provided endpoint by retaining the + /// IP address and modifying the port. + /// + /// This convenience method is useful to create new endpoint from the + /// existing endpoint to test reusing IP address for multiple acceptors. + /// The returned endpoint has the same IP address but different port. + /// + /// @param endpoint Source endpoint. + /// + /// @return New endpoint with the port number increased by 1. + boost::asio::ip::tcp::endpoint + createSiblingEndpoint(const boost::asio::ip::tcp::endpoint& endpoint) const { + boost::asio::ip::tcp::endpoint endpoint_copy(endpoint); + endpoint_copy.port(endpoint.port() + 1); + return (endpoint_copy); + } + + /// @brief Opens TCP acceptor and sets 'reuse address' option. + void acceptorOpen() { + acceptor_.open(endpoint_); + acceptor_.setOption(TestTCPAcceptor::ReuseAddress(true)); + } + + /// @brief Starts accepting TCP connections. + /// + /// This method creates new Acceptor instance and calls accept() to start + /// accepting new connections. The instance of the Acceptor object is + /// retained in the connections_ list. + void accept() { + TCPAcceptorCallback cb = boost::bind(&TCPAcceptorTest::acceptHandler, + this, _1); + AcceptorPtr conn(new Acceptor(io_service_, acceptor_, cb)); + connections_.push_back(conn); + connections_.back()->accept(); + } + + /// @brief Connect to the endpoint. + /// + /// This method creates TCPClient instance and retains it in the clients_ + /// list. + void connect() { + TCPClientPtr client(new TCPClient(io_service_)); + clients_.push_back(client); + clients_.back()->connect(); + } + + /// @brief Callback function for asynchronous accept calls. + /// + /// It stops the IO service upon error or when the number of accepted + /// connections reaches the max_connections_ value. Otherwise it calls + /// accept() to start accepting next connections. + /// + /// @param ec Error code. + void acceptHandler(const boost::system::error_code& ec) { + if (ec) { + if (ec.value() != boost::asio::error::operation_aborted) { + ADD_FAILURE() << "error occurred while accepting connection: " + << ec.message(); + } else { + ++aborted_connections_num_; + } + io_service_.stop(); + } + + // We have reached the maximum number of connections - end the test. + if (++connections_num_ >= max_connections_) { + io_service_.stop(); + return; + } + + accept(); + } + + /// @brief Callback function invoke upon test timeout. + /// + /// It stops the IO service and reports test timeout. + void timeoutHandler() { + ADD_FAILURE() << "Timeout occurred while running the test!"; + io_service_.stop(); + } + + /// @brief IO service. + IOService io_service_; + + /// @brief TCPAcceptor under test. + TestTCPAcceptor acceptor_; + + /// @brief Server endpoint. + boost::asio::ip::tcp::endpoint asio_endpoint_; + + /// @brief asiolink server endpoint (uses asio_endpoint_). + TCPEndpoint endpoint_; + + /// @brief Asynchronous timer service to detect timeouts. + IntervalTimer test_timer_; + + /// @brief List of connections on the server side. + std::list<AcceptorPtr> connections_; + + /// @brief List of client connections. + std::list<TCPClientPtr> clients_; + + /// @brief Current number of established connections. + unsigned int connections_num_; + + /// @brief Current number of aborted connections. + unsigned int aborted_connections_num_; + + /// @brief Connections limit. + unsigned int max_connections_; +}; + +// Test TCPAcceptor::asyncAccept. +TEST_F(TCPAcceptorTest, asyncAccept) { + // Establish up to 10 connections. + setMaxConnections(10); + + // Initialize acceptor. + acceptorOpen(); + acceptor_.bind(endpoint_); + acceptor_.listen(); + + // Start accepting new connections. + accept(); + + // Create 10 new TCP connections (client side). + for (unsigned int i = 0; i < 10; ++i) { + connect(); + } + + // Run the IO service until we have accepted 10 connections, an error + // or test timeout occurred. + io_service_.run(); + + // Make sure that all accepted connections have been recorded. + EXPECT_EQ(10, connections_num_); + EXPECT_EQ(10, connections_.size()); +} + +// Check that it is possible to set SO_REUSEADDR flag for the TCPAcceptor. +TEST_F(TCPAcceptorTest, reuseAddress) { + // We need at least two acceptors using common address. Let's create the + // second endpoint which has the same address but different port. + boost::asio::ip::tcp::endpoint asio_endpoint2(createSiblingEndpoint(asio_endpoint_)); + TCPEndpoint endpoint2(asio_endpoint2); + + // Create and open two acceptors. + TestTCPAcceptor acceptor1(io_service_); + TestTCPAcceptor acceptor2(io_service_); + ASSERT_NO_THROW(acceptor1.open(endpoint_)); + ASSERT_NO_THROW(acceptor2.open(endpoint2)); + + // Set SO_REUSEADDR socket option so as acceptors can bind to the + /// same address. + ASSERT_NO_THROW( + acceptor1.setOption(TestTCPAcceptor::ReuseAddress(true)) + ); + ASSERT_NO_THROW( + acceptor2.setOption(TestTCPAcceptor::ReuseAddress(true)) + ); + ASSERT_NO_THROW(acceptor1.bind(endpoint_)); + ASSERT_NO_THROW(acceptor2.bind(endpoint2)); + + // Create third acceptor, but don't set the SO_REUSEADDR. It should + // refuse to bind. + TCPEndpoint endpoint3(createSiblingEndpoint(asio_endpoint2)); + TestTCPAcceptor acceptor3(io_service_); + ASSERT_NO_THROW(acceptor3.open(endpoint3)); + EXPECT_THROW(acceptor3.bind(endpoint_), boost::system::system_error); +} + +// Test that TCPAcceptor::getProtocol returns IPPROTO_TCP. +TEST_F(TCPAcceptorTest, getProtocol) { + EXPECT_EQ(IPPROTO_TCP, acceptor_.getProtocol()); +} + +// Test that TCPAcceptor::getNative returns valid socket descriptor. +TEST_F(TCPAcceptorTest, getNative) { + // Initially the descriptor should be invalid (negative). + ASSERT_LT(acceptor_.getNative(), 0); + // Now open the socket and make sure the returned descriptor is now valid. + ASSERT_NO_THROW(acceptorOpen()); + EXPECT_GE(acceptor_.getNative(), 0); +} + +// macOS 10.12.3 has a bug which causes the connections to not enter +// the TIME-WAIT state and they never get closed. +#if !defined (OS_OSX) + +// Test that TCPAcceptor::close works properly. +TEST_F(TCPAcceptorTest, close) { + // Initialize acceptor. + acceptorOpen(); + acceptor_.bind(endpoint_); + acceptor_.listen(); + + // Start accepting new connections. + accept(); + + // Create 10 new TCP connections (client side). + for (unsigned int i = 0; i < 10; ++i) { + connect(); + } + + // Close the acceptor before connections are accepted. + acceptor_.close(); + + // Run the IO service. + io_service_.run(); + + // The connections should have been aborted. + EXPECT_EQ(1, connections_num_); + EXPECT_EQ(1, aborted_connections_num_); + EXPECT_EQ(1, connections_.size()); +} + +#endif + +} diff --git a/src/lib/asiolink/tests/tcp_socket_unittest.cc b/src/lib/asiolink/tests/tcp_socket_unittest.cc index 15bb779ce5..e247722099 100644 --- a/src/lib/asiolink/tests/tcp_socket_unittest.cc +++ b/src/lib/asiolink/tests/tcp_socket_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2011-2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2011-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -22,14 +22,15 @@ #include <boost/shared_ptr.hpp> #include <gtest/gtest.h> -#include <string> +#include <algorithm> #include <arpa/inet.h> +#include <cstddef> +#include <cstdlib> +#include <errno.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> -#include <algorithm> -#include <cstdlib> -#include <cstddef> +#include <string> #include <vector> using namespace boost::asio; @@ -62,7 +63,7 @@ public: NONE = 4 ///< "Not set" state }; - /// \brief Minimim size of buffers + /// \brief Minimum size of buffers enum { MIN_SIZE = (64 * 1024 + 2) ///< 64kB + two bytes for a count }; @@ -149,7 +150,7 @@ public: return (ptr_->expected_); } - /// \brief Get offset intodData + /// \brief Get offset into data size_t& offset() { return (ptr_->offset_); } @@ -355,7 +356,15 @@ TEST(TCPSocket, sequenceTest) { EXPECT_EQ(0, server_cb.getCode()); EXPECT_EQ(TCPCallback::OPEN, client_cb.called()); - EXPECT_EQ(0, client_cb.getCode()); + + // On some operating system the async_connect may return EINPROGRESS. + // This doesn't necessarily indicate an error. In most cases trying + // to asynchronously write and read from the socket would work just + // fine. + if ((client_cb.getCode()) != 0 && (client_cb.getCode() != EINPROGRESS)) { + ADD_FAILURE() << "expected error code of 0 or " << EINPROGRESS + << " as a result of async_connect, got " << client_cb.getCode(); + } // Step 2. Get the client to write to the server asynchronously. The // server will loop reading the data synchronously. @@ -418,7 +427,7 @@ TEST(TCPSocket, sequenceTest) { // Run the callbacks. Several options are possible depending on how ASIO // is implemented and whether the message gets fragmented: // - // 1) The send handler may complete immediately, regardess of whether the + // 1) The send handler may complete immediately, regardless of whether the // data has been read by the client. (This is the most likely.) // 2) The send handler may only run after all the data has been read by // the client. (This could happen if the client's TCP buffers were too diff --git a/src/lib/asiolink/tests/unix_domain_socket_unittest.cc b/src/lib/asiolink/tests/unix_domain_socket_unittest.cc new file mode 100644 index 0000000000..62c16c9f3a --- /dev/null +++ b/src/lib/asiolink/tests/unix_domain_socket_unittest.cc @@ -0,0 +1,313 @@ +// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <asiolink/asio_wrapper.h> +#include <asiolink/io_service.h> +#include <asiolink/unix_domain_socket.h> +#include <asiolink/testutils/test_server_unix_socket.h> +#include <gtest/gtest.h> +#include <array> +#include <cstdio> +#include <cstdlib> +#include <sstream> +#include <string> + +using namespace isc::asiolink; + +namespace { + +/// @brief Test unix socket file name. +const std::string TEST_SOCKET = "test-socket"; + +/// @brief Test timeout in ms. +const long TEST_TIMEOUT = 10000; + +/// @brief Test fixture class for @ref UnixDomainSocket class. +class UnixDomainSocketTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Removes unix socket descriptor before the test. + UnixDomainSocketTest() : + io_service_(), + test_socket_(new test::TestServerUnixSocket(io_service_, + unixSocketFilePath())), + response_(), + read_buf_() { + test_socket_->startTimer(TEST_TIMEOUT); + removeUnixSocketFile(); + } + + /// @brief Destructor. + /// + /// Removes unix socket descriptor after the test. + virtual ~UnixDomainSocketTest() { + removeUnixSocketFile(); + } + + /// @brief Returns socket file path. + /// + /// If the KEA_SOCKET_TEST_DIR environment variable is specified, the + /// socket file is created in the location pointed to by this variable. + /// Otherwise, it is created in the build directory. + static std::string unixSocketFilePath() { + std::ostringstream s; + const char* env = getenv("KEA_SOCKET_TEST_DIR"); + if (env) { + s << std::string(env); + } else { + s << TEST_DATA_BUILDDIR; + } + + s << "/" << TEST_SOCKET; + return (s.str()); + } + + /// @brief Removes unix socket descriptor. + void removeUnixSocketFile() { + static_cast<void>(remove(unixSocketFilePath().c_str())); + } + + /// @brief Performs asynchronous receive on unix domain socket. + /// + /// This function performs partial read from the unix domain socket. + /// It uses @c read_buf_ or small size to ensure that the buffer fills + /// in before all that have been read. The partial responses are + /// appended to the @c response_ class member. + /// + /// If the response received so far is shorter than the expected + /// response, another partial read is scheduled. + /// + /// @param socket Reference to the unix domain socket. + /// @param expected_response Expected response. + void doReceive(UnixDomainSocket& socket, + const std::string& expected_response) { + socket.asyncReceive(&read_buf_[0], read_buf_.size(), + [this, &socket, expected_response] + (const boost::system::error_code& ec, size_t length) { + if (!ec) { + // Append partial response received and see if the + // size of the response received so far is still + // smaller than expected. If it is, schedule another + // partial read. + response_.append(&read_buf_[0], length); + if (expected_response.size() > response_.size()) { + doReceive(socket, expected_response); + } + + } else if (ec.value() != boost::asio::error::operation_aborted) { + ADD_FAILURE() << "error occurred while asynchronously receiving" + " data via unix domain socket: " << ec.message(); + } + }); + } + + /// @brief IO service used by the tests. + IOService io_service_; + + /// @brief Server side unix socket used in these tests. + test::TestServerUnixSocketPtr test_socket_; + + /// @brief String containing a response received with @c doReceive. + std::string response_; + + /// @brief Read buffer used by @c doReceive. + std::array<char, 2> read_buf_; +}; + +// This test verifies that the client can send data over the unix +// domain socket and receive a response. +TEST_F(UnixDomainSocketTest, sendReceive) { + // Start the server. + test_socket_->bindServerSocket(); + + // Setup client side. + UnixDomainSocket socket(io_service_); + ASSERT_NO_THROW(socket.connect(unixSocketFilePath())); + + // Send "foo". + const std::string outbound_data = "foo"; + size_t sent_size = 0; + ASSERT_NO_THROW(sent_size = socket.write(outbound_data.c_str(), + outbound_data.size())); + // Make sure all data have been sent. + ASSERT_EQ(outbound_data.size(), sent_size); + + // Run IO service to generate server's response. + while ((test_socket_->getResponseNum() < 1) && + (!test_socket_->isStopped())) { + io_service_.run_one(); + } + + // Receive response from the socket. + std::array<char, 1024> read_buf; + size_t bytes_read = 0; + ASSERT_NO_THROW(bytes_read = socket.receive(&read_buf[0], read_buf.size())); + std::string response(&read_buf[0], bytes_read); + + // The server should prepend "received" to the data we had sent. + EXPECT_EQ("received foo", response); +} + +// This test verifies that the client can send the data over the unix +// domain socket and receive a response asynchronously. +TEST_F(UnixDomainSocketTest, asyncSendReceive) { + // Start the server. + test_socket_->bindServerSocket(); + + // Setup client side. + UnixDomainSocket socket(io_service_); + + // We're going to asynchronously connect to the server. The boolean value + // below will be modified by the connect handler function (lambda) invoked + // when the connection is established or if an error occurs. + bool connect_handler_invoked = false; + ASSERT_NO_THROW(socket.asyncConnect(unixSocketFilePath(), + [this, &connect_handler_invoked](const boost::system::error_code& ec) { + // Indicate that the handler has been called so as the loop below gets + // interrupted. + connect_handler_invoked = true; + // Operation aborted indicates that IO service has been stopped. This + // shouldn't happen here. + if (ec && (ec.value() != boost::asio::error::operation_aborted)) { + ADD_FAILURE() << "error occurred while asynchronously connecting" + " via unix domain socket: " << ec.message(); + } + } + )); + // Run IO service until connect handler is invoked. + while (!connect_handler_invoked && (!test_socket_->isStopped())) { + io_service_.run_one(); + } + + // We are going to asynchronously send the 'foo' over the unix socket. + const std::string outbound_data = "foo"; + size_t sent_size = 0; + ASSERT_NO_THROW(socket.asyncSend(outbound_data.c_str(), outbound_data.size(), + [this, &sent_size](const boost::system::error_code& ec, size_t length) { + // If we have been successful sending the data, record the number of + // bytes we have sent. + if (!ec) { + sent_size = length; + + } else if (ec.value() != boost::asio::error::operation_aborted) { + ADD_FAILURE() << "error occurred while asynchronously sending the" + " data over unix domain socket: " << ec.message(); + } + } + )); + + // Run IO service to generate server's response. + while ((test_socket_->getResponseNum() < 1) && + (!test_socket_->isStopped())) { + io_service_.run_one(); + } + + // There is no guarantee that all data have been sent so we only check that + // some data have been sent. + ASSERT_GT(sent_size, 0); + + std::string expected_response = "received foo"; + doReceive(socket, expected_response); + + // Run IO service until we get the full response from the server. + while ((response_.size() < expected_response.size()) && + !test_socket_->isStopped()) { + io_service_.run_one(); + } + + // Check that the entire response has been received and is correct. + EXPECT_EQ(expected_response, response_); +} + +// This test verifies that UnixDomainSocketError exception is thrown +// on attempt to connect, write or receive when the server socket +// is not available. +TEST_F(UnixDomainSocketTest, clientErrors) { + UnixDomainSocket socket(io_service_); + ASSERT_THROW(socket.connect(unixSocketFilePath()), UnixDomainSocketError); + const std::string outbound_data = "foo"; + ASSERT_THROW(socket.write(outbound_data.c_str(), outbound_data.size()), + UnixDomainSocketError); + std::array<char, 1024> read_buf; + ASSERT_THROW(socket.receive(&read_buf[0], read_buf.size()), + UnixDomainSocketError); +} + +// This test verifies that an error is returned on attempt to asynchronously +// connect, write or receive when the server socket is not available. +TEST_F(UnixDomainSocketTest, asyncClientErrors) { + UnixDomainSocket socket(io_service_); + + // Asynchronous operations signal errors through boost::system::error_code + // object passed to the handler function. This object casts to boolean. + // In case of success the object casts to false. In case of an error it + // casts to true. The actual error codes can be retrieved by comparing the + // ec objects to predefined error objects. We don't check for the actual + // errors here, because it is not certain that the same error codes would + // be returned on various operating systems. + + // In the following tests we use C++11 lambdas as callbacks. + + // Connect + bool connect_handler_invoked = false; + socket.asyncConnect(unixSocketFilePath(), + [this, &connect_handler_invoked](const boost::system::error_code& ec) { + connect_handler_invoked = true; + EXPECT_TRUE(ec); + }); + while (!connect_handler_invoked && !test_socket_->isStopped()) { + io_service_.run_one(); + } + + // Send + const std::string outbound_data = "foo"; + bool send_handler_invoked = false; + socket.asyncSend(outbound_data.c_str(), outbound_data.size(), + [this, &send_handler_invoked] + (const boost::system::error_code& ec, size_t length) { + send_handler_invoked = true; + EXPECT_TRUE(ec); + }); + while (!send_handler_invoked && !test_socket_->isStopped()) { + io_service_.run_one(); + } + + // Receive + bool receive_handler_invoked = false; + std::array<char, 1024> read_buf; + socket.asyncReceive(&read_buf[0], read_buf.size(), + [this, &receive_handler_invoked] + (const boost::system::error_code& ec, size_t length) { + receive_handler_invoked = true; + EXPECT_TRUE(ec); + }); + while (!receive_handler_invoked && !test_socket_->isStopped()) { + io_service_.run_one(); + } +} + +// Check that native socket descriptor is returned correctly when +// the socket is connected. +TEST_F(UnixDomainSocketTest, getNative) { + // Start the server. + test_socket_->bindServerSocket(); + + // Setup client side. + UnixDomainSocket socket(io_service_); + ASSERT_NO_THROW(socket.connect(unixSocketFilePath())); + ASSERT_GE(socket.getNative(), 0); +} + +// Check that protocol returned is 0. +TEST_F(UnixDomainSocketTest, getProtocol) { + UnixDomainSocket socket(io_service_); + EXPECT_EQ(0, socket.getProtocol()); +} + +} |