diff --git a/.travis.yml b/.travis.yml index 015147e5..271692c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,7 @@ addons: - libboost-system-dev - libboost-regex-dev - libboost-date-time-dev + - libboost-filesystem-dev - libboost-program-options-dev - libboost-test-dev - google-mock diff --git a/CMakeLists.txt b/CMakeLists.txt index f73f66ff..c3c4c81d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,7 @@ else() set(BOOST_MIN_VERSION "1.40") endif() -set(CUKE_CORE_BOOST_LIBS thread system regex date_time program_options) +set(CUKE_CORE_BOOST_LIBS thread system regex date_time program_options filesystem) if(NOT CUKE_DISABLE_BOOST_TEST) set(CUKE_TEST_BOOST_LIBS unit_test_framework) endif() @@ -72,7 +72,7 @@ endif() if(Boost_FOUND) include_directories(${Boost_INCLUDE_DIRS}) - set(CUKE_EXTRA_LIBRARIES ${CUKE_EXTRA_LIBRARIES} ${Boost_THREAD_LIBRARY} ${Boost_SYSTEM_LIBRARY} ${Boost_REGEX_LIBRARY} ${Boost_DATE_TIME_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY}) + set(CUKE_EXTRA_LIBRARIES ${CUKE_EXTRA_LIBRARIES} ${Boost_THREAD_LIBRARY} ${Boost_SYSTEM_LIBRARY} ${Boost_REGEX_LIBRARY} ${Boost_DATE_TIME_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_FILESYSTEM_LIBRARY}) endif() # diff --git a/include/cucumber-cpp/internal/connectors/wire/WireServer.hpp b/include/cucumber-cpp/internal/connectors/wire/WireServer.hpp index 80c8d945..f0900698 100644 --- a/include/cucumber-cpp/internal/connectors/wire/WireServer.hpp +++ b/include/cucumber-cpp/internal/connectors/wire/WireServer.hpp @@ -12,11 +12,41 @@ namespace internal { using namespace boost::asio; using namespace boost::asio::ip; +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) +using namespace boost::asio::local; +#endif /** * Socket server that calls a protocol handler line by line */ class SocketServer { +public: + /** + * Constructor for DI + */ + SocketServer(const ProtocolHandler *protocolHandler); + + /** + * Accept one connection + */ + virtual void acceptOnce() = 0; + +protected: + const ProtocolHandler *protocolHandler; + io_service ios; + + template + void doListen(basic_socket_acceptor& acceptor, + const typename Protocol::endpoint& endpoint); + template + void doAcceptOnce(basic_socket_acceptor& acceptor); + void processStream(std::iostream &stream); +}; + +/** + * Socket server that calls a protocol handler line by line + */ +class TCPSocketServer : public SocketServer { public: /** * Type definition for TCP port @@ -26,7 +56,7 @@ class SocketServer { /** * Constructor for DI */ - SocketServer(const ProtocolHandler *protocolHandler); + TCPSocketServer(const ProtocolHandler *protocolHandler); /** * Bind and listen to a TCP port @@ -34,27 +64,52 @@ class SocketServer { void listen(const port_type port); /** - * Port number that this server is currently listening on. + * Endpoint (IP address and port number) that this server is currently + * listening on. * - * @throw boost::system::system_error when not listening on any TCP port or - * the port cannot be determined. + * @throw boost::system::system_error when not listening on any socket or + * the endpoint cannot be determined. + */ + tcp::endpoint listenEndpoint() const; + + virtual void acceptOnce(); + +private: + tcp::acceptor acceptor; +}; + +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) +/** + * Socket server that calls a protocol handler line by line + */ +class UnixSocketServer : public SocketServer { +public: + /** + * Constructor for DI + */ + UnixSocketServer(const ProtocolHandler *protocolHandler); + + /** + * Bind and listen on a local stream socket */ - port_type listenPort() const; + void listen(const std::string& unixPath); /** - * Accept one connection + * Port number that this server is currently listening on. + * + * @throw boost::system::system_error when not listening on any socket or + * the endpoint cannot be determined. */ - void acceptOnce(); + stream_protocol::endpoint listenEndpoint() const; - ~SocketServer() {}; // Forbid inheritance + virtual void acceptOnce(); -private: - const ProtocolHandler *protocolHandler; - io_service ios; - tcp::acceptor acceptor; + ~UnixSocketServer(); - void processStream(tcp::iostream &stream); +private: + stream_protocol::acceptor acceptor; }; +#endif } } diff --git a/src/connectors/wire/WireServer.cpp b/src/connectors/wire/WireServer.cpp index 0b28efa1..737ca1c5 100644 --- a/src/connectors/wire/WireServer.cpp +++ b/src/connectors/wire/WireServer.cpp @@ -1,40 +1,91 @@ #include +#include namespace cucumber { namespace internal { +using namespace boost::asio::ip; +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) +using namespace boost::asio::local; +#endif + SocketServer::SocketServer(const ProtocolHandler *protocolHandler) : protocolHandler(protocolHandler), - ios(), - acceptor(ios) { + ios() { } -void SocketServer::listen(const port_type port) { - tcp::endpoint endpoint(tcp::v4(), port); +template +void SocketServer::doListen(basic_socket_acceptor& acceptor, + const typename Protocol::endpoint& endpoint) { + if (acceptor.is_open()) + throw boost::system::system_error(boost::asio::error::already_open); acceptor.open(endpoint.protocol()); - acceptor.set_option(tcp::acceptor::reuse_address(true)); - acceptor.set_option(tcp::no_delay(true)); + acceptor.set_option(typename Protocol::acceptor::reuse_address(true)); acceptor.bind(endpoint); acceptor.listen(1); } -SocketServer::port_type SocketServer::listenPort() const { - const tcp::endpoint ep(acceptor.local_endpoint()); - return ep.port(); -} - -void SocketServer::acceptOnce() { - tcp::iostream stream; +template +void SocketServer::doAcceptOnce(basic_socket_acceptor& acceptor) { + typename Protocol::iostream stream; acceptor.accept(*stream.rdbuf()); processStream(stream); } -void SocketServer::processStream(tcp::iostream &stream) { +void SocketServer::processStream(std::iostream& stream) { std::string request; while (getline(stream, request)) { stream << protocolHandler->handle(request) << std::endl << std::flush; } } +TCPSocketServer::TCPSocketServer(const ProtocolHandler *protocolHandler) : + SocketServer(protocolHandler), + acceptor(ios) { +} + +void TCPSocketServer::listen(const port_type port) { + doListen(acceptor, tcp::endpoint(tcp::v4(), port)); + acceptor.set_option(tcp::no_delay(true)); +} + +tcp::endpoint TCPSocketServer::listenEndpoint() const { + return acceptor.local_endpoint(); +} + +void TCPSocketServer::acceptOnce() { + doAcceptOnce(acceptor); +} + +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) +UnixSocketServer::UnixSocketServer(const ProtocolHandler *protocolHandler) : + SocketServer(protocolHandler), + acceptor(ios) { +} + +void UnixSocketServer::listen(const std::string& unixPath) { + if (boost::filesystem::status(unixPath).type() == boost::filesystem::socket_file) + boost::filesystem::remove(unixPath); + + doListen(acceptor, stream_protocol::endpoint(unixPath)); +} + +stream_protocol::endpoint UnixSocketServer::listenEndpoint() const { + return acceptor.local_endpoint(); +} + +void UnixSocketServer::acceptOnce() { + doAcceptOnce(acceptor); +} + +UnixSocketServer::~UnixSocketServer() { + if (!acceptor.is_open()) + return; + std::string path = acceptor.local_endpoint().path(); + // NOTE: this will fail if this path got deleted manually or represents an abstract-namespace socket + boost::filesystem::remove(path); +} +#endif + } } diff --git a/src/main.cpp b/src/main.cpp index b5570df9..71946687 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,19 +3,35 @@ #include #include #include +#include namespace { -void acceptWireProtocol(int port, bool verbose) { +void acceptWireProtocol(int port, const std::string& unixPath, bool verbose) { using namespace ::cucumber::internal; CukeEngineImpl cukeEngine; JsonSpiritWireMessageCodec wireCodec; WireProtocolHandler protocolHandler(&wireCodec, &cukeEngine); - SocketServer server(&protocolHandler); - server.listen(port); - if (verbose) - std::clog << "Listening on port " << server.listenPort() << std::endl; - server.acceptOnce(); + boost::scoped_ptr server; +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) + if (!unixPath.empty()) + { + UnixSocketServer* const unixServer = new UnixSocketServer(&protocolHandler); + server.reset(unixServer); + unixServer->listen(unixPath); + if (verbose) + std::clog << "Listening on socket " << unixServer->listenEndpoint() << std::endl; + } + else +#endif + { + TCPSocketServer* const tcpServer = new TCPSocketServer(&protocolHandler); + server.reset(tcpServer); + tcpServer->listen(port); + if (verbose) + std::clog << "Listening on port " << tcpServer->listenEndpoint() << std::endl; + } + server->acceptOnce(); } } @@ -27,6 +43,9 @@ int main(int argc, char **argv) { ("help,h", "help for cucumber-cpp") ("verbose,v", "verbose output") ("port,p", value(), "listening port of wireserver, use '0' (zero) to select an ephemeral port") +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) + ("unix,u", value(), "listening unix socket of wireserver (disables listening on port)") +#endif ; boost::program_options::variables_map optionVariableMap; boost::program_options::store(boost::program_options::parse_command_line(argc, argv, optionDescription), optionVariableMap); @@ -42,13 +61,20 @@ int main(int argc, char **argv) { port = optionVariableMap["port"].as(); } + std::string unixPath; +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) + if (optionVariableMap.count("unix")) { + unixPath = optionVariableMap["unix"].as(); + } +#endif + bool verbose = false; if (optionVariableMap.count("verbose")) { verbose = true; } try { - acceptWireProtocol(port, verbose); + acceptWireProtocol(port, unixPath, verbose); } catch (std::exception &e) { std::cerr << e.what() << std::endl; exit(1); diff --git a/tests/integration/WireServerTest.cpp b/tests/integration/WireServerTest.cpp index a89e81d5..a2f122c2 100644 --- a/tests/integration/WireServerTest.cpp +++ b/tests/integration/WireServerTest.cpp @@ -2,18 +2,24 @@ #include +#include #include #include +#include #include using namespace cucumber::internal; using namespace boost::posix_time; using namespace boost::asio::ip; +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) +using namespace boost::asio::local; +#endif using namespace std; using namespace testing; using boost::bind; using boost::thread; +namespace fs = boost::filesystem; static const time_duration THREAD_TEST_TIMEOUT = milliseconds(4000); @@ -30,7 +36,8 @@ MATCHER(EventuallyTerminates, "") { } MATCHER_P(EventuallyReceives, value, "") { - tcp::iostream *stream = const_cast(&arg); + std::basic_iostream *stream = const_cast *>( + static_cast *>(&arg)); std::string output; // FIXME It should not block (*stream) >> output; @@ -55,30 +62,48 @@ class SocketServerTest : public Test { protected: StrictMock protocolHandler; - SocketServer *server; thread *serverThread; virtual void SetUp() { - server = new SocketServer(&protocolHandler); - server->listen(0); - serverThread = new thread(bind(&SocketServer::acceptOnce, server)); + SocketServer* server = createListeningServer(); + serverThread = new thread(&SocketServer::acceptOnce, server); } virtual void TearDown() { if (serverThread) { serverThread->timed_join(THREAD_TEST_TIMEOUT); delete serverThread; + serverThread = NULL; } - if (server) { - delete server; - } + destroyListeningServer(); } + + virtual SocketServer* createListeningServer() = 0; + virtual void destroyListeningServer() = 0; }; +class TCPSocketServerTest : public SocketServerTest { +protected: + TCPSocketServer *server; + TCPSocketServerTest() : + server(NULL) { + } + + virtual TCPSocketServer* createListeningServer() { + server = new TCPSocketServer(&protocolHandler); + server->listen(0); + return server; + } + + virtual void destroyListeningServer() { + delete server; + server = NULL; + } +}; -TEST_F(SocketServerTest, exitsOnFirstConnectionClosed) { +TEST_F(TCPSocketServerTest, exitsOnFirstConnectionClosed) { // given - tcp::iostream client(tcp::endpoint(tcp::v4(), server->listenPort())); + tcp::iostream client(server->listenEndpoint()); ASSERT_THAT(client, IsConnected()); // when @@ -88,19 +113,19 @@ TEST_F(SocketServerTest, exitsOnFirstConnectionClosed) { EXPECT_THAT(serverThread, EventuallyTerminates()); } -TEST_F(SocketServerTest, moreThanOneClientCanConnect) { +TEST_F(TCPSocketServerTest, moreThanOneClientCanConnect) { // given - tcp::iostream client1(tcp::endpoint(tcp::v4(), server->listenPort())); + tcp::iostream client1(server->listenEndpoint()); ASSERT_THAT(client1, IsConnected()); // when - tcp::iostream client2(tcp::endpoint(tcp::v4(), server->listenPort())); + tcp::iostream client2(server->listenEndpoint()); //then ASSERT_THAT(client2, IsConnected()); } -TEST_F(SocketServerTest, receiveAndSendsSingleLineMassages) { +TEST_F(TCPSocketServerTest, receiveAndSendsSingleLineMassages) { { InSequence s; EXPECT_CALL(protocolHandler, handle("12")).WillRepeatedly(Return("A")); @@ -109,7 +134,7 @@ TEST_F(SocketServerTest, receiveAndSendsSingleLineMassages) { } // given - tcp::iostream client(tcp::endpoint(tcp::v4(), server->listenPort())); + tcp::iostream client(server->listenEndpoint()); ASSERT_THAT(client, IsConnected()); // when @@ -121,3 +146,53 @@ TEST_F(SocketServerTest, receiveAndSendsSingleLineMassages) { EXPECT_THAT(client, EventuallyReceives("B")); EXPECT_THAT(client, EventuallyReceives("C")); } + +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) +class UnixSocketServerTest : public SocketServerTest { +protected: + UnixSocketServer *server; + UnixSocketServerTest() : + server(NULL) { + } + + virtual UnixSocketServer* createListeningServer() { + fs::path socket = fs::temp_directory_path() / fs::unique_path(); + server = new UnixSocketServer(&protocolHandler); + server->listen(socket.string()); + return server; + } + + virtual void destroyListeningServer() { + delete server; + server = NULL; + } +}; + +/* + * Tests are flickering on OSX when testing without traffic flowing. + * + * This full lifecycle test is not optimal but it should be enough + * given that the main difference between Unix and TCP is the socket + * created at startup and removed on shutdown. + */ +TEST_F(UnixSocketServerTest, fullLifecycle) { + stream_protocol::endpoint socketName = server->listenEndpoint(); + EXPECT_CALL(protocolHandler, handle("X")).WillRepeatedly(Return("Y")); + + // socket created at startup + ASSERT_TRUE(fs::exists(socketName.path())); + + // traffic flows + stream_protocol::iostream client(socketName); + client << "X" << endl << flush; + EXPECT_THAT(client, EventuallyReceives("Y")); + + // client disconnection terminates server + client.close(); + EXPECT_THAT(serverThread, EventuallyTerminates()); + + // socket removed by destructor + TearDown(); + EXPECT_FALSE(fs::exists(socketName.path())); +} +#endif