diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4e8b5b10..52028f2f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -112,6 +112,9 @@ if (ODR_WITH_HTTP_SERVER) PRIVATE "src/http_server_test.cpp" ) + # a client to make a request with, so the server can be tested serving one + find_package(httplib REQUIRED) + target_link_libraries(odr_test PRIVATE httplib::httplib) endif () target_include_directories(odr_test PRIVATE diff --git a/test/src/http_server_test.cpp b/test/src/http_server_test.cpp index 25c99cd7..9e3e985e 100644 --- a/test/src/http_server_test.cpp +++ b/test/src/http_server_test.cpp @@ -1,6 +1,8 @@ #include #include +#include + #include #include @@ -20,6 +22,31 @@ void wait_until_running(const HttpServer &server) { } } +/// Stops the server and joins the thread however the test leaves - a fatal +/// assertion returns, and a joinable `std::thread` destructor would take the +/// whole test binary down with `std::terminate` instead of reporting it. +class ListenGuard { +public: + ListenGuard(const HttpServer &server, std::thread thread) + : m_server{&server}, m_thread{std::move(thread)} {} + + ~ListenGuard() { + m_server->stop(); + if (m_thread.joinable()) { + m_thread.join(); + } + } + + ListenGuard(const ListenGuard &) = delete; + ListenGuard &operator=(const ListenGuard &) = delete; + ListenGuard(ListenGuard &&) = delete; + ListenGuard &operator=(ListenGuard &&) = delete; + +private: + const HttpServer *m_server; + std::thread m_thread; +}; + } // namespace TEST(HttpServer, bind_reports_the_port_it_got) { @@ -150,3 +177,32 @@ TEST(HttpServer, listen_after_stop_returns) { // to serve, but nothing went wrong either EXPECT_NO_THROW(server.listen()); } + +/// A kept-alive connection used to hold the accept loop until it idled out, so +/// stop() spanned cpp-httplib's 5 s keep-alive timeout - the whole of it, on +/// the path every consumer takes (#641). Fixed upstream in 0.47.0, where the +/// keep-alive wait watches the listening socket; this is what says so. +TEST(HttpServer, stop_is_prompt_after_serving_a_kept_alive_request) { + const HttpServer server; + + const std::uint32_t port = server.bind("127.0.0.1", 0); + // stops and joins whatever happens below, including a fatal assertion + const ListenGuard guard{server, std::thread{[&server] { server.listen(); }}}; + wait_until_running(server); + + // alive across the stop() below, so the connection it holds is open there + httplib::Client client{"127.0.0.1", static_cast(port)}; + client.set_keep_alive(true); + const httplib::Result response = client.Get("/"); + ASSERT_TRUE(response); + EXPECT_EQ(response->status, 200); + + const auto before = std::chrono::steady_clock::now(); + server.stop(); + const auto elapsed = std::chrono::steady_clock::now() - before; + + EXPECT_LT(elapsed, 2s) + << "stop() took " + << std::chrono::duration_cast(elapsed).count() + << " ms"; +}