Run the pong timeout tests in the main test suite

696b94b built and ran test_websocket_heartbeat in the Windows jobs to
cover the pong timeout wake, which added more than a minute to each of
them. Only its two pong timeout tests exercise what differs by
platform, and both work with the ping interval set at runtime. Move
them into test.cc, where every job runs them in its shards, and drop
the Windows heartbeat build and step.

The heartbeat file keeps CompileTimeClientReadTimeoutIsFail, whose
fixture is renamed WebSocketSilentPeerTest after the peer it sets up.
This commit is contained in:
yhirose committed 2026-10-18 18:03:08 -04:00
1 parent 696b94b5ed
commit 344aceb446
4 files changed
+78 -73

No files matched your search

-4
View File
@@ -623,10 +623,6 @@ jobs:
} }
if ($failed) { exit 1 } if ($failed) { exit 1 }
Write-Host "All shards passed." Write-Host "All shards passed."
- name: Run WebSocket heartbeat test ${{ matrix.config.name }}
if: ${{ matrix.config.run_tests }}
working-directory: build/test
run: ./Release/httplib-heartbeat-test.exe --gtest_color=yes
env: env:
VCPKG_ROOT: "C:/vcpkg" VCPKG_ROOT: "C:/vcpkg"
-8
View File
@@ -31,14 +31,6 @@ target_compile_options(httplib-test PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8;/b
target_link_libraries(httplib-test PRIVATE httplib GTest::gtest_main CURL::libcurl) target_link_libraries(httplib-test PRIVATE httplib GTest::gtest_main CURL::libcurl)
gtest_discover_tests(httplib-test) gtest_discover_tests(httplib-test)
# It overrides the heartbeat macros, which a compiled library has fixed
if(NOT HTTPLIB_COMPILE)
add_executable(httplib-heartbeat-test test_websocket_heartbeat.cc)
target_compile_options(httplib-heartbeat-test PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8;/bigobj>")
target_link_libraries(httplib-heartbeat-test PRIVATE httplib GTest::gtest_main)
gtest_discover_tests(httplib-heartbeat-test)
endif()
file( file(
COPY www www2 www3 ca-bundle.crt image.jpg COPY www www2 www3 ca-bundle.crt image.jpg
DESTINATION ${CMAKE_CURRENT_BINARY_DIR} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
+62
View File
@@ -28414,6 +28414,68 @@ TEST(WebSocketTest, NegativePingIntervalSendsNoPings) {
EXPECT_EQ(-1, ::recv(sock, &c, 1, 0)); EXPECT_EQ(-1, ::recv(sock, &c, 1, 0));
} }
// The client's heartbeat gives up on a peer that leaves its pings unanswered,
// and that has to end a read() already waiting on the peer, on every platform.
static void check_pong_timeout_ends_read(Server &svr, const char *scheme) {
// The peer sends no pings, and its handler never calls read(), which is
// where pongs are written.
std::mutex mutex;
std::condition_variable cv;
auto release = false;
svr.set_websocket_ping_interval(0);
svr.WebSocket("/ws", [&](const Request &, ws::WebSocket &) {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [&] { return release; });
});
auto port = svr.bind_to_any_port(HOST);
std::thread t([&]() { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
{
std::lock_guard<std::mutex> lock(mutex);
release = true;
}
cv.notify_all();
svr.stop();
t.join();
});
svr.wait_until_ready();
ws::WebSocketClient client(std::string(scheme) +
"://localhost:" + std::to_string(port) + "/ws");
#ifdef CPPHTTPLIB_SSL_ENABLED
client.enable_server_certificate_verification(false);
#endif
// A ping a second, given up on after two go unanswered: about 3s
client.set_websocket_ping_interval(1);
client.set_websocket_max_missed_pongs(2);
// Well past that, so that a read the timeout ends instead stands out
client.set_read_timeout(10);
ASSERT_TRUE(client.connect());
auto start = std::chrono::steady_clock::now();
std::string msg;
EXPECT_EQ(ws::Fail, client.read(msg));
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
6000);
EXPECT_FALSE(client.is_open());
}
TEST(WebSocketTest, PongTimeoutEndsABlockedRead) {
Server svr;
check_pong_timeout_ends_read(svr, "ws");
}
#ifdef CPPHTTPLIB_SSL_ENABLED
TEST(WebSocketTest, PongTimeoutEndsABlockedReadOverTLS) {
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
check_pong_timeout_ends_read(svr, "wss");
}
#endif
TEST(WebSocketTest, AnswersAMalformedCloseWithProtocolError) { TEST(WebSocketTest, AnswersAMalformedCloseWithProtocolError) {
Server svr; Server svr;
svr.set_websocket_ping_interval(0); svr.set_websocket_ping_interval(0);
+16 -61
View File
@@ -139,28 +139,22 @@ TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
client.close(); client.close();
} }
// Verify that the client detects a non-responsive peer via unacked-ping count. // A peer that never sends anything: the server's heartbeat is disabled and its
// Setup: the server's heartbeat is disabled AND its handler never calls // handler never calls read(). The pong timeout against such a peer is tested
// read(), so no automatic Pong reply is ever produced. The client sends // in test.cc (WebSocketTest.PongTimeoutEndsABlockedRead), where every CI job
// pings but receives no pongs, and should close itself once the unacked // runs it.
// ping count reaches max_missed_pongs. class WebSocketSilentPeerTest : public ::testing::Test {
class WebSocketPongTimeoutTest : public ::testing::Test {
protected: protected:
virtual std::unique_ptr<Server> make_server() {
return std::unique_ptr<Server>(new Server());
}
void SetUp() override { void SetUp() override {
svr_ = make_server(); svr_.set_websocket_ping_interval(0);
svr_->set_websocket_ping_interval(0); svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &) {
svr_->WebSocket("/ws", [this](const Request &, ws::WebSocket &) {
std::unique_lock<std::mutex> lock(handler_mutex_); std::unique_lock<std::mutex> lock(handler_mutex_);
handler_cv_.wait(lock, [this]() { return release_; }); handler_cv_.wait(lock, [this]() { return release_; });
}); });
port_ = svr_->bind_to_any_port("localhost"); port_ = svr_.bind_to_any_port("localhost");
thread_ = std::thread([this]() { svr_->listen_after_bind(); }); thread_ = std::thread([this]() { svr_.listen_after_bind(); });
svr_->wait_until_ready(); svr_.wait_until_ready();
} }
void TearDown() override { void TearDown() override {
@@ -169,11 +163,11 @@ protected:
release_ = true; release_ = true;
} }
handler_cv_.notify_all(); handler_cv_.notify_all();
svr_->stop(); svr_.stop();
thread_.join(); thread_.join();
} }
std::unique_ptr<Server> svr_; Server svr_;
int port_; int port_;
std::thread thread_; std::thread thread_;
std::mutex handler_mutex_; std::mutex handler_mutex_;
@@ -181,50 +175,10 @@ protected:
bool release_ = false; bool release_ = false;
}; };
// Client pings every 1s (compile-time default in this test file). With
// max_missed_pongs = 2, the heartbeat thread should self-close within roughly
// 3s, and that has to end a read() already waiting on the peer.
static void expect_pong_timeout_ends_read(ws::WebSocketClient &client) {
client.set_websocket_max_missed_pongs(2);
// A read timeout asked for at runtime is reported as Timeout, so it cannot
// be mistaken for the Fail a pong timeout produces.
client.set_read_timeout(10);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.is_open());
auto start = std::chrono::steady_clock::now();
std::string msg;
EXPECT_EQ(client.read(msg), ws::Fail);
EXPECT_TRUE(std::chrono::steady_clock::now() - start <
std::chrono::seconds(6));
EXPECT_FALSE(client.is_open());
}
TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
expect_pong_timeout_ends_read(client);
}
#ifdef CPPHTTPLIB_SSL_ENABLED
class WebSocketPongTimeoutSSLTest : public WebSocketPongTimeoutTest {
protected:
std::unique_ptr<Server> make_server() override {
return std::unique_ptr<Server>(new SSLServer("./cert.pem", "./key.pem"));
}
};
TEST_F(WebSocketPongTimeoutSSLTest, ClientDetectsNonResponsivePeer) {
ws::WebSocketClient client("wss://localhost:" + std::to_string(port_) +
"/ws");
client.enable_server_certificate_verification(false);
expect_pong_timeout_ends_read(client);
}
#endif
// The compile-time client read timeout (3s here) was never asked for through // The compile-time client read timeout (3s here) was never asked for through
// set_read_timeout(), so when it elapses read() reports Fail and closes the // set_read_timeout(), so when it elapses read() reports Fail and closes the
// connection rather than handing back a Timeout on a still-open one. // connection rather than handing back a Timeout on a still-open one.
TEST_F(WebSocketPongTimeoutTest, CompileTimeClientReadTimeoutIsFail) { TEST_F(WebSocketSilentPeerTest, CompileTimeClientReadTimeoutIsFail) {
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws"); ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
client.set_websocket_ping_interval(0); client.set_websocket_ping_interval(0);
ASSERT_TRUE(client.connect()); ASSERT_TRUE(client.connect());
@@ -293,8 +247,9 @@ TEST_F(WebSocketServerReadTimeoutTest, BackstopEndsHandlerLoop) {
// Verify that a responsive peer does NOT trigger the pong-timeout mechanism, // Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
// even with a small max_missed_pongs budget. This is the positive counterpart // even with a small max_missed_pongs budget. This is the positive counterpart
// of ClientDetectsNonResponsivePeer: the client must actively drive read() so // of WebSocketTest.PongTimeoutEndsABlockedRead in test.cc: the client must
// that incoming Pong frames are consumed and the unacked counter is reset. // actively drive read() so that incoming Pong frames are consumed and the
// unacked counter is reset.
TEST_F(WebSocketHeartbeatTest, ResponsivePeerNeverTimesOut) { TEST_F(WebSocketHeartbeatTest, ResponsivePeerNeverTimesOut) {
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws"); ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
client.set_websocket_max_missed_pongs(2); client.set_websocket_max_missed_pongs(2);