From a4b670558432e32ea13b38d1456df466addb505c Mon Sep 17 00:00:00 2001 From: JCW Date: Tue, 20 Jan 2026 12:58:18 +0000 Subject: [PATCH 01/14] Add a debug sink Signed-off-by: JCW --- src/tests/libxrpl/CMakeLists.txt | 12 ++++++++++- src/tests/libxrpl/helpers/DebugSink.cpp | 28 +++++++++++++++++++++++++ src/tests/libxrpl/helpers/DebugSink.h | 28 +++++++++++++++++++++++++ src/tests/libxrpl/net/HTTPClient.cpp | 4 +++- 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/tests/libxrpl/helpers/DebugSink.cpp create mode 100644 src/tests/libxrpl/helpers/DebugSink.h diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 74dc184700d..8dd32568d82 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -6,9 +6,19 @@ find_package(GTest REQUIRED) # Custom target for all tests defined in this file add_custom_target(xrpl.tests) +# Test helpers +add_library(xrpl.helpers.test STATIC) +target_sources(xrpl.helpers.test PRIVATE + helpers/DebugSink.cpp +) +target_include_directories(xrpl.helpers.test PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(xrpl.helpers.test PRIVATE xrpl.libxrpl) + # Common library dependencies for the rest of the tests. add_library(xrpl.imports.test INTERFACE) -target_link_libraries(xrpl.imports.test INTERFACE gtest::gtest xrpl.libxrpl) +target_link_libraries(xrpl.imports.test INTERFACE gtest::gtest xrpl.libxrpl xrpl.helpers.test) # One test for each module. xrpl_add_test(basics) diff --git a/src/tests/libxrpl/helpers/DebugSink.cpp b/src/tests/libxrpl/helpers/DebugSink.cpp new file mode 100644 index 00000000000..0b7dd751d2d --- /dev/null +++ b/src/tests/libxrpl/helpers/DebugSink.cpp @@ -0,0 +1,28 @@ +#include + +#include + +namespace xrpl { + +DebugSink::DebugSink(beast::severities::Severity threshold) + : Sink(threshold, false) +{ +} + +void +DebugSink::write(beast::severities::Severity level, std::string const& text) +{ + if (level < threshold()) + return; + writeAlways(level, text); +} + +void +DebugSink::writeAlways( + beast::severities::Severity level, + std::string const& text) +{ + std::cerr << text << std::endl; +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/helpers/DebugSink.h b/src/tests/libxrpl/helpers/DebugSink.h new file mode 100644 index 00000000000..e0368ce2fed --- /dev/null +++ b/src/tests/libxrpl/helpers/DebugSink.h @@ -0,0 +1,28 @@ +#ifndef XRPL_DEBUGSINK_H +#define XRPL_DEBUGSINK_H + +#include + +namespace xrpl { +class DebugSink : public beast::Journal::Sink +{ +public: + static DebugSink& + instance() + { + static DebugSink _; + return _; + } + + DebugSink( + beast::severities::Severity threshold = beast::severities::kDebug); + + void + write(beast::severities::Severity level, std::string const& text) override; + + void + writeAlways(beast::severities::Severity level, std::string const& text) + override; +}; +} // namespace xrpl +#endif // XRPL_DEBUGSINK_H diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index cfd206edded..078714aaa83 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -7,7 +7,9 @@ #include #include +#include "../helpers/DebugSink.h" #include +#include #include #include @@ -177,7 +179,7 @@ runHTTPTest( boost::system::error_code& result_error) { // Create a null journal for testing - beast::Journal j{beast::Journal::getNullSink()}; + beast::Journal j{DebugSink::instance()}; // Initialize HTTPClient SSL context HTTPClient::initializeSSLContext("", "", false, j); From cea9894925849167e72235e84b7b017f3712a2f3 Mon Sep 17 00:00:00 2001 From: JCW Date: Tue, 20 Jan 2026 14:10:39 +0000 Subject: [PATCH 02/14] Fix the deadlock issue Signed-off-by: JCW --- src/tests/libxrpl/net/HTTPClient.cpp | 114 +++++++++++++++------------ 1 file changed, 65 insertions(+), 49 deletions(-) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index 078714aaa83..0556e435f52 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -13,6 +13,7 @@ #include #include +#include #include using namespace xrpl; @@ -30,9 +31,9 @@ class TestHTTPServer unsigned short port_; // Custom headers to return - std::map custom_headers_; - std::string response_body_; - unsigned int status_code_{200}; + std::map customHeaders_; + std::string responseBody_; + unsigned int statusCode_{200}; public: TestHTTPServer() : acceptor_(ioc_), port_(0) @@ -70,19 +71,19 @@ class TestHTTPServer void setHeader(std::string const& name, std::string const& value) { - custom_headers_[name] = value; + customHeaders_[name] = value; } void setResponseBody(std::string const& body) { - response_body_ = body; + responseBody_ = body; } void setStatusCode(unsigned int code) { - status_code_ = code; + statusCode_ = code; } private: @@ -117,54 +118,69 @@ class TestHTTPServer void handleConnection(boost::asio::ip::tcp::socket socket) { - try - { - // Read the HTTP request - boost::beast::flat_buffer buffer; - boost::beast::http::request req; - boost::beast::http::read(socket, buffer, req); - - // Create response - boost::beast::http::response res; - res.version(req.version()); - res.result(status_code_); - res.set(boost::beast::http::field::server, "TestServer"); - - // Add custom headers - for (auto const& [name, value] : custom_headers_) - { - res.set(name, value); - } - - // Set body and prepare payload first - res.body() = response_body_; - res.prepare_payload(); - - // Override Content-Length with custom headers after prepare_payload - // This allows us to test case-insensitive header parsing - for (auto const& [name, value] : custom_headers_) - { - if (boost::iequals(name, "Content-Length")) + // Use async operations to avoid blocking the io_context thread + // Use shared_ptr to keep objects alive during async operations + auto socketPtr = + std::make_shared(std::move(socket)); + auto buffer = std::make_shared(); + auto req = std::make_shared< + boost::beast::http::request>(); + + // Read the HTTP request asynchronously + boost::beast::http::async_read( + *socketPtr, + *buffer, + *req, + [this, socketPtr, buffer, req]( + boost::beast::error_code ec, std::size_t) { + if (ec) { - res.erase(boost::beast::http::field::content_length); - res.set(name, value); + // Error reading, just close the connection + return; } - } - // Send response - boost::beast::http::write(socket, res); + // Create response + auto res = std::make_shared>(); + res->version(req->version()); + res->result(statusCode_); + res->set(boost::beast::http::field::server, "TestServer"); - // Shutdown socket gracefully - boost::system::error_code ec; - socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec); - } - catch (std::exception const&) - { - // Connection handling errors are expected - } + // Add custom headers + for (auto const& [name, value] : customHeaders_) + { + res->set(name, value); + } + + // Set body and prepare payload first + res->body() = responseBody_; + res->prepare_payload(); + + // Override Content-Length with custom headers after + // prepare_payload This allows us to test case-insensitive + // header parsing + for (auto const& [name, value] : customHeaders_) + { + if (boost::iequals(name, "Content-Length")) + { + res->erase(boost::beast::http::field::content_length); + res->set(name, value); + } + } - if (running_) - accept(); + // Send response asynchronously + boost::beast::http::async_write( + *socketPtr, + *res, + [socketPtr, res](boost::beast::error_code ec, std::size_t) { + // Shutdown socket gracefully + boost::system::error_code shutdownEc; + socketPtr->shutdown( + boost::asio::ip::tcp::socket::shutdown_send, + shutdownEc); + // Socket will close when shared_ptr is destroyed + }); + }); } }; From b60e8b5fe338f8620c7947b2cad42bd73a99fae0 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Tue, 20 Jan 2026 16:28:04 +0000 Subject: [PATCH 03/14] Update src/tests/libxrpl/net/HTTPClient.cpp Co-authored-by: Bart --- src/tests/libxrpl/net/HTTPClient.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index 0556e435f52..2cb7f0f0651 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -7,7 +7,6 @@ #include #include -#include "../helpers/DebugSink.h" #include #include From 490512b43984dd1739da6d7dbc4f2c86da693fca Mon Sep 17 00:00:00 2001 From: Jingchen Date: Wed, 21 Jan 2026 12:00:18 +0000 Subject: [PATCH 04/14] Update src/tests/libxrpl/net/HTTPClient.cpp Co-authored-by: Bart --- src/tests/libxrpl/net/HTTPClient.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index 2cb7f0f0651..18999ad969f 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -156,8 +156,8 @@ class TestHTTPServer res->prepare_payload(); // Override Content-Length with custom headers after - // prepare_payload This allows us to test case-insensitive - // header parsing + // prepare_payload. This allows us to test case-insensitive + // header parsing. for (auto const& [name, value] : customHeaders_) { if (boost::iequals(name, "Content-Length")) From 8efea797e6bda22f8480ee42f1d67f5a1272613f Mon Sep 17 00:00:00 2001 From: JCW Date: Wed, 21 Jan 2026 12:01:55 +0000 Subject: [PATCH 05/14] Rename the file Signed-off-by: JCW --- src/tests/libxrpl/CMakeLists.txt | 2 +- .../libxrpl/helpers/{DebugSink.cpp => TestSink.cpp} | 8 ++++---- src/tests/libxrpl/helpers/{DebugSink.h => TestSink.h} | 9 ++++----- 3 files changed, 9 insertions(+), 10 deletions(-) rename src/tests/libxrpl/helpers/{DebugSink.cpp => TestSink.cpp} (60%) rename src/tests/libxrpl/helpers/{DebugSink.h => TestSink.h} (68%) diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 8dd32568d82..72d7d0fa92e 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -9,7 +9,7 @@ add_custom_target(xrpl.tests) # Test helpers add_library(xrpl.helpers.test STATIC) target_sources(xrpl.helpers.test PRIVATE - helpers/DebugSink.cpp + helpers/TestSink.cpp ) target_include_directories(xrpl.helpers.test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} diff --git a/src/tests/libxrpl/helpers/DebugSink.cpp b/src/tests/libxrpl/helpers/TestSink.cpp similarity index 60% rename from src/tests/libxrpl/helpers/DebugSink.cpp rename to src/tests/libxrpl/helpers/TestSink.cpp index 0b7dd751d2d..247365b4f3f 100644 --- a/src/tests/libxrpl/helpers/DebugSink.cpp +++ b/src/tests/libxrpl/helpers/TestSink.cpp @@ -1,16 +1,16 @@ -#include +#include #include namespace xrpl { -DebugSink::DebugSink(beast::severities::Severity threshold) +TestSink::TestSink(beast::severities::Severity threshold) : Sink(threshold, false) { } void -DebugSink::write(beast::severities::Severity level, std::string const& text) +TestSink::write(beast::severities::Severity level, std::string const& text) { if (level < threshold()) return; @@ -18,7 +18,7 @@ DebugSink::write(beast::severities::Severity level, std::string const& text) } void -DebugSink::writeAlways( +TestSink::writeAlways( beast::severities::Severity level, std::string const& text) { diff --git a/src/tests/libxrpl/helpers/DebugSink.h b/src/tests/libxrpl/helpers/TestSink.h similarity index 68% rename from src/tests/libxrpl/helpers/DebugSink.h rename to src/tests/libxrpl/helpers/TestSink.h index e0368ce2fed..33d0a6267ad 100644 --- a/src/tests/libxrpl/helpers/DebugSink.h +++ b/src/tests/libxrpl/helpers/TestSink.h @@ -4,18 +4,17 @@ #include namespace xrpl { -class DebugSink : public beast::Journal::Sink +class TestSink : public beast::Journal::Sink { public: - static DebugSink& + static TestSink& instance() { - static DebugSink _; + static TestSink _; return _; } - DebugSink( - beast::severities::Severity threshold = beast::severities::kDebug); + TestSink(beast::severities::Severity threshold = beast::severities::kDebug); void write(beast::severities::Severity level, std::string const& text) override; From 78818e49df4c1d94a267a36561b4b275bfefbe69 Mon Sep 17 00:00:00 2001 From: JCW Date: Wed, 21 Jan 2026 12:45:16 +0000 Subject: [PATCH 06/14] Address PR comments Signed-off-by: JCW --- src/tests/libxrpl/helpers/TestSink.cpp | 99 +++++++++++++++++++++++++- src/tests/libxrpl/net/HTTPClient.cpp | 22 +++--- 2 files changed, 111 insertions(+), 10 deletions(-) diff --git a/src/tests/libxrpl/helpers/TestSink.cpp b/src/tests/libxrpl/helpers/TestSink.cpp index 247365b4f3f..d160298bd85 100644 --- a/src/tests/libxrpl/helpers/TestSink.cpp +++ b/src/tests/libxrpl/helpers/TestSink.cpp @@ -1,5 +1,16 @@ +#include + #include +#include // for getenv + +#if BOOST_OS_WINDOWS +#include // for _isatty, _fileno +#include // for stdout +#else +#include // for isatty, STDOUT_FILENO +#endif + #include namespace xrpl { @@ -22,7 +33,93 @@ TestSink::writeAlways( beast::severities::Severity level, std::string const& text) { - std::cerr << text << std::endl; + auto supportsColor = [] { + // 1. Check for "NO_COLOR" environment variable (Standard convention) + if (std::getenv("NO_COLOR") != nullptr) + { + return false; + } + + // 2. Check for "CLICOLOR_FORCE" (Force color) + if (std::getenv("CLICOLOR_FORCE") != nullptr) + { + return true; + } + + // 3. Platform-specific check to see if stdout is a terminal +#if BOOST_OS_WINDOWS + // Windows: Check if the output handle is a character device + // _fileno(stdout) is usually 1 + return _isatty(_fileno(stdout)) != 0; +#else + // Linux/macOS: Check if file descriptor 1 (stdout) is a TTY + // STDOUT_FILENO is 1 + return isatty(STDOUT_FILENO) != 0; +#endif + }(); + + auto color = [level]() { + switch (level) + { + case beast::severities::kTrace: + return "\033[34m"; // blue + case beast::severities::kDebug: + return "\033[32m"; // green + case beast::severities::kInfo: + return "\033[36m"; // cyan + case beast::severities::kWarning: + return "\033[33m"; // yellow + case beast::severities::kError: + return "\033[31m"; // red + case beast::severities::kFatal: + default: + break; + } + return "\033[31m"; // red + }(); + + auto prefix = [level]() { + switch (level) + { + case beast::severities::kTrace: + return "TRC:"; + case beast::severities::kDebug: + return "DBG:"; + case beast::severities::kInfo: + return "INF:"; + case beast::severities::kWarning: + return "WRN:"; + case beast::severities::kError: + return "ERR:"; + case beast::severities::kFatal: + default: + break; + } + return "FTL:"; + }(); + + auto& stream = [level]() -> std::ostream& { + switch (level) + { + case beast::severities::kError: + case beast::severities::kFatal: + return std::cerr; + default: + break; + } + return std::cout; + }(); + + constexpr auto reset = "\033[0m"; + + if (supportsColor) + { + stream << color << prefix << " " << text << reset << std::endl; + } + else + { + stream << prefix << " " << text << std::endl; + } } } // namespace xrpl diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index 18999ad969f..f2b0759e479 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include @@ -34,8 +34,10 @@ class TestHTTPServer std::string responseBody_; unsigned int statusCode_{200}; + beast::Journal j_; + public: - TestHTTPServer() : acceptor_(ioc_), port_(0) + TestHTTPServer() : acceptor_(ioc_), port_(0), j_(TestSink::instance()) { // Bind to any available port endpoint_ = {boost::asio::ip::tcp::v4(), 0}; @@ -119,7 +121,7 @@ class TestHTTPServer { // Use async operations to avoid blocking the io_context thread // Use shared_ptr to keep objects alive during async operations - auto socketPtr = + auto sock = std::make_shared(std::move(socket)); auto buffer = std::make_shared(); auto req = std::make_shared< @@ -127,14 +129,16 @@ class TestHTTPServer // Read the HTTP request asynchronously boost::beast::http::async_read( - *socketPtr, + *sock, *buffer, *req, - [this, socketPtr, buffer, req]( + [this, sock, buffer, req]( boost::beast::error_code ec, std::size_t) { if (ec) { // Error reading, just close the connection + JLOG(j_.debug()) << "Error reading: " << ec.message() + << ", code: " << ec.value(); return; } @@ -169,12 +173,12 @@ class TestHTTPServer // Send response asynchronously boost::beast::http::async_write( - *socketPtr, + *sock, *res, - [socketPtr, res](boost::beast::error_code ec, std::size_t) { + [sock, res](boost::beast::error_code ec, std::size_t) { // Shutdown socket gracefully boost::system::error_code shutdownEc; - socketPtr->shutdown( + sock->shutdown( boost::asio::ip::tcp::socket::shutdown_send, shutdownEc); // Socket will close when shared_ptr is destroyed @@ -194,7 +198,7 @@ runHTTPTest( boost::system::error_code& result_error) { // Create a null journal for testing - beast::Journal j{DebugSink::instance()}; + beast::Journal j{TestSink::instance()}; // Initialize HTTPClient SSL context HTTPClient::initializeSSLContext("", "", false, j); From 710f6f235a70d01327851f8f4804ba066e42fc41 Mon Sep 17 00:00:00 2001 From: JCW Date: Wed, 21 Jan 2026 13:36:32 +0000 Subject: [PATCH 07/14] Set the header after prepare_payload Signed-off-by: JCW --- src/tests/libxrpl/net/HTTPClient.cpp | 115 ++++++++++++--------------- 1 file changed, 52 insertions(+), 63 deletions(-) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index f2b0759e479..2e033d0648b 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -149,12 +149,6 @@ class TestHTTPServer res->result(statusCode_); res->set(boost::beast::http::field::server, "TestServer"); - // Add custom headers - for (auto const& [name, value] : customHeaders_) - { - res->set(name, value); - } - // Set body and prepare payload first res->body() = responseBody_; res->prepare_payload(); @@ -193,9 +187,9 @@ runHTTPTest( TestHTTPServer& server, std::string const& path, std::atomic& completed, - std::atomic& result_status, - std::string& result_data, - boost::system::error_code& result_error) + std::atomic& resultStatus, + std::string& resultData, + boost::system::error_code& resultError) { // Create a null journal for testing beast::Journal j{TestSink::instance()}; @@ -214,9 +208,9 @@ runHTTPTest( [&](boost::system::error_code const& ec, int status, std::string const& data) -> bool { - result_error = ec; - result_status = status; - result_data = data; + resultError = ec; + resultStatus = status; + resultData = data; completed = true; return false; // don't retry }, @@ -241,7 +235,7 @@ runHTTPTest( TEST(HTTPClient, case_insensitive_content_length) { // Test different cases of Content-Length header - std::vector header_cases = { + std::vector headerCases = { "Content-Length", // Standard case "content-length", // Lowercase - this tests the regex icase fix "CONTENT-LENGTH", // Uppercase @@ -249,53 +243,48 @@ TEST(HTTPClient, case_insensitive_content_length) "content-Length" // Mixed case 2 }; - for (auto const& header_name : header_cases) + for (auto const& headerName : headerCases) { TestHTTPServer server; - std::string test_body = "Hello World!"; - server.setResponseBody(test_body); - server.setHeader(header_name, std::to_string(test_body.size())); + std::string testBody = "Hello World!"; + server.setResponseBody(testBody); + server.setHeader(headerName, std::to_string(testBody.size())); std::atomic completed{false}; - std::atomic result_status{0}; - std::string result_data; - boost::system::error_code result_error; + std::atomic resultStatus{0}; + std::string resultData; + boost::system::error_code resultError; - bool test_completed = runHTTPTest( - server, - "/test", - completed, - result_status, - result_data, - result_error); + bool testCompleted = runHTTPTest( + server, "/test", completed, resultStatus, resultData, resultError); // Verify results - EXPECT_TRUE(test_completed); - EXPECT_FALSE(result_error); - EXPECT_EQ(result_status, 200); - EXPECT_EQ(result_data, test_body); + EXPECT_TRUE(testCompleted); + EXPECT_FALSE(resultError); + EXPECT_EQ(resultStatus, 200); + EXPECT_EQ(resultData, testBody); } } TEST(HTTPClient, basic_http_request) { TestHTTPServer server; - std::string test_body = "Test response body"; - server.setResponseBody(test_body); + std::string testBody = "Test response body"; + server.setResponseBody(testBody); server.setHeader("Content-Type", "text/plain"); std::atomic completed{false}; - std::atomic result_status{0}; - std::string result_data; - boost::system::error_code result_error; + std::atomic resultStatus{0}; + std::string resultData; + boost::system::error_code resultError; - bool test_completed = runHTTPTest( - server, "/basic", completed, result_status, result_data, result_error); + bool testCompleted = runHTTPTest( + server, "/basic", completed, resultStatus, resultData, resultError); - EXPECT_TRUE(test_completed); - EXPECT_FALSE(result_error); - EXPECT_EQ(result_status, 200); - EXPECT_EQ(result_data, test_body); + EXPECT_TRUE(testCompleted); + EXPECT_FALSE(resultError); + EXPECT_EQ(resultStatus, 200); + EXPECT_EQ(resultData, testBody); } TEST(HTTPClient, empty_response) @@ -305,44 +294,44 @@ TEST(HTTPClient, empty_response) server.setHeader("Content-Length", "0"); std::atomic completed{false}; - std::atomic result_status{0}; - std::string result_data; - boost::system::error_code result_error; + std::atomic resultStatus{0}; + std::string resultData; + boost::system::error_code resultError; - bool test_completed = runHTTPTest( - server, "/empty", completed, result_status, result_data, result_error); + bool testCompleted = runHTTPTest( + server, "/empty", completed, resultStatus, resultData, resultError); - EXPECT_TRUE(test_completed); - EXPECT_FALSE(result_error); - EXPECT_EQ(result_status, 200); - EXPECT_TRUE(result_data.empty()); + EXPECT_TRUE(testCompleted); + EXPECT_FALSE(resultError); + EXPECT_EQ(resultStatus, 200); + EXPECT_TRUE(resultData.empty()); } TEST(HTTPClient, different_status_codes) { - std::vector status_codes = {200, 404, 500}; + std::vector statusCodes = {200, 404, 500}; - for (auto status : status_codes) + for (auto status : statusCodes) { TestHTTPServer server; server.setStatusCode(status); server.setResponseBody("Status " + std::to_string(status)); std::atomic completed{false}; - std::atomic result_status{0}; - std::string result_data; - boost::system::error_code result_error; + std::atomic resultStatus{0}; + std::string resultData; + boost::system::error_code resultError; - bool test_completed = runHTTPTest( + bool testCompleted = runHTTPTest( server, "/status", completed, - result_status, - result_data, - result_error); + resultStatus, + resultData, + resultError); - EXPECT_TRUE(test_completed); - EXPECT_FALSE(result_error); - EXPECT_EQ(result_status, static_cast(status)); + EXPECT_TRUE(testCompleted); + EXPECT_FALSE(resultError); + EXPECT_EQ(resultStatus, static_cast(status)); } } From 4f44bb9e3b68e14d945420edde0c4448779a034d Mon Sep 17 00:00:00 2001 From: Jingchen Date: Wed, 21 Jan 2026 14:34:16 +0000 Subject: [PATCH 08/14] Update src/tests/libxrpl/net/HTTPClient.cpp Co-authored-by: Bart --- src/tests/libxrpl/net/HTTPClient.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index 2e033d0648b..f560d9dfed3 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -158,11 +158,7 @@ class TestHTTPServer // header parsing. for (auto const& [name, value] : customHeaders_) { - if (boost::iequals(name, "Content-Length")) - { - res->erase(boost::beast::http::field::content_length); - res->set(name, value); - } + res->set(name, value); } // Send response asynchronously From dcd9a8b372e8b49289b47b887517c4b170a066f8 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Wed, 21 Jan 2026 14:34:25 +0000 Subject: [PATCH 09/14] Update src/tests/libxrpl/helpers/TestSink.cpp Co-authored-by: Bart --- src/tests/libxrpl/helpers/TestSink.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tests/libxrpl/helpers/TestSink.cpp b/src/tests/libxrpl/helpers/TestSink.cpp index d160298bd85..9dea05cedce 100644 --- a/src/tests/libxrpl/helpers/TestSink.cpp +++ b/src/tests/libxrpl/helpers/TestSink.cpp @@ -105,9 +105,8 @@ TestSink::writeAlways( case beast::severities::kFatal: return std::cerr; default: - break; + return std::cout; } - return std::cout; }(); constexpr auto reset = "\033[0m"; From fa19361c297e8490c5233f01c79de44581ca3cb0 Mon Sep 17 00:00:00 2001 From: JCW Date: Wed, 21 Jan 2026 14:37:29 +0000 Subject: [PATCH 10/14] Update comments Signed-off-by: JCW --- src/tests/libxrpl/helpers/TestSink.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tests/libxrpl/helpers/TestSink.cpp b/src/tests/libxrpl/helpers/TestSink.cpp index 9dea05cedce..a71e9f7cc0b 100644 --- a/src/tests/libxrpl/helpers/TestSink.cpp +++ b/src/tests/libxrpl/helpers/TestSink.cpp @@ -50,10 +50,13 @@ TestSink::writeAlways( #if BOOST_OS_WINDOWS // Windows: Check if the output handle is a character device // _fileno(stdout) is usually 1 + // _isatty returns non-zero if the handle is a character device, 0 + // otherwise. return _isatty(_fileno(stdout)) != 0; #else // Linux/macOS: Check if file descriptor 1 (stdout) is a TTY // STDOUT_FILENO is 1 + // isatty returns 1 if the file descriptor is a TTY, 0 otherwise. return isatty(STDOUT_FILENO) != 0; #endif }(); From 6995e8e44dd715e64405eeee0c71625a90f1b3cf Mon Sep 17 00:00:00 2001 From: JCW Date: Tue, 27 Jan 2026 14:52:40 +0000 Subject: [PATCH 11/14] Address comments Signed-off-by: JCW --- src/tests/libxrpl/helpers/TestSink.h | 4 +- src/tests/libxrpl/net/HTTPClient.cpp | 152 ++++++++++++++------------- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/src/tests/libxrpl/helpers/TestSink.h b/src/tests/libxrpl/helpers/TestSink.h index 33d0a6267ad..fc3223b04b5 100644 --- a/src/tests/libxrpl/helpers/TestSink.h +++ b/src/tests/libxrpl/helpers/TestSink.h @@ -10,8 +10,8 @@ class TestSink : public beast::Journal::Sink static TestSink& instance() { - static TestSink _; - return _; + static TestSink sink{}; + return sink; } TestSink(beast::severities::Severity threshold = beast::severities::kDebug); diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index f560d9dfed3..7486c1c52d5 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -2,7 +2,10 @@ #include #include +#include +#include #include +#include #include #include #include @@ -13,6 +16,7 @@ #include #include #include +#include #include using namespace xrpl; @@ -27,6 +31,7 @@ class TestHTTPServer boost::asio::ip::tcp::acceptor acceptor_; boost::asio::ip::tcp::endpoint endpoint_; std::atomic running_{true}; + std::binary_semaphore asyncSem_; unsigned short port_; // Custom headers to return @@ -37,7 +42,8 @@ class TestHTTPServer beast::Journal j_; public: - TestHTTPServer() : acceptor_(ioc_), port_(0), j_(TestSink::instance()) + TestHTTPServer() + : acceptor_(ioc_), port_(0), j_(TestSink::instance()), asyncSem_(0) { // Bind to any available port endpoint_ = {boost::asio::ip::tcp::v4(), 0}; @@ -49,9 +55,14 @@ class TestHTTPServer // Get the actual port that was assigned port_ = acceptor_.local_endpoint().port(); - accept(); + // Start the accept coroutine + boost::asio::co_spawn(ioc_, accept(), boost::asio::detached); } + TestHTTPServer(TestHTTPServer&&) = delete; + TestHTTPServer& + operator=(TestHTTPServer&&) = delete; + ~TestHTTPServer() { stop(); @@ -95,85 +106,78 @@ class TestHTTPServer acceptor_.close(); } - void + boost::asio::awaitable accept() { - if (!running_) - return; - - acceptor_.async_accept( - ioc_, - endpoint_, - [&](boost::system::error_code const& error, - boost::asio::ip::tcp::socket peer) { - if (!running_) - return; + while (running_) + { + try + { + auto socket = + co_await acceptor_.async_accept(boost::asio::use_awaitable); - if (!error) - { - handleConnection(std::move(peer)); - } - }); + if (!running_) + co_return; + + // Handle this connection concurrently + boost::asio::co_spawn( + ioc_, + handleConnection(std::move(socket)), + boost::asio::detached); + } + catch (std::exception const& e) + { + // Accept failed, stop accepting + JLOG(j_.debug()) << "Accept error: " << e.what(); + co_return; + } + } } - void + boost::asio::awaitable handleConnection(boost::asio::ip::tcp::socket socket) { - // Use async operations to avoid blocking the io_context thread - // Use shared_ptr to keep objects alive during async operations - auto sock = - std::make_shared(std::move(socket)); - auto buffer = std::make_shared(); - auto req = std::make_shared< - boost::beast::http::request>(); - - // Read the HTTP request asynchronously - boost::beast::http::async_read( - *sock, - *buffer, - *req, - [this, sock, buffer, req]( - boost::beast::error_code ec, std::size_t) { - if (ec) - { - // Error reading, just close the connection - JLOG(j_.debug()) << "Error reading: " << ec.message() - << ", code: " << ec.value(); - return; - } - - // Create response - auto res = std::make_shared>(); - res->version(req->version()); - res->result(statusCode_); - res->set(boost::beast::http::field::server, "TestServer"); - - // Set body and prepare payload first - res->body() = responseBody_; - res->prepare_payload(); - - // Override Content-Length with custom headers after - // prepare_payload. This allows us to test case-insensitive - // header parsing. - for (auto const& [name, value] : customHeaders_) - { - res->set(name, value); - } - - // Send response asynchronously - boost::beast::http::async_write( - *sock, - *res, - [sock, res](boost::beast::error_code ec, std::size_t) { - // Shutdown socket gracefully - boost::system::error_code shutdownEc; - sock->shutdown( - boost::asio::ip::tcp::socket::shutdown_send, - shutdownEc); - // Socket will close when shared_ptr is destroyed - }); - }); + try + { + boost::beast::flat_buffer buffer; + boost::beast::http::request req; + + // Read the HTTP request asynchronously + co_await boost::beast::http::async_read( + socket, buffer, req, boost::asio::use_awaitable); + + // Create response + boost::beast::http::response res; + res.version(req.version()); + res.result(statusCode_); + res.set(boost::beast::http::field::server, "TestServer"); + + // Set body and prepare payload first + res.body() = responseBody_; + res.prepare_payload(); + + // Override Content-Length with custom headers after + // prepare_payload. This allows us to test case-insensitive + // header parsing. + for (auto const& [name, value] : customHeaders_) + { + res.set(name, value); + } + + // Send response asynchronously + co_await boost::beast::http::async_write( + socket, res, boost::asio::use_awaitable); + + // Shutdown socket gracefully + boost::system::error_code shutdownEc; + socket.shutdown( + boost::asio::ip::tcp::socket::shutdown_send, shutdownEc); + } + catch (std::exception const& e) + { + // Error reading or writing, just close the connection + JLOG(j_.debug()) << "Connection error: " << e.what(); + } } }; From 6a56ef64b0a476e5e7fcf93f6808dedc536c5ff8 Mon Sep 17 00:00:00 2001 From: JCW Date: Tue, 27 Jan 2026 14:55:36 +0000 Subject: [PATCH 12/14] Address comments Signed-off-by: JCW --- src/tests/libxrpl/net/HTTPClient.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index 7486c1c52d5..4b3f2c275bd 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -31,7 +31,6 @@ class TestHTTPServer boost::asio::ip::tcp::acceptor acceptor_; boost::asio::ip::tcp::endpoint endpoint_; std::atomic running_{true}; - std::binary_semaphore asyncSem_; unsigned short port_; // Custom headers to return @@ -42,8 +41,7 @@ class TestHTTPServer beast::Journal j_; public: - TestHTTPServer() - : acceptor_(ioc_), port_(0), j_(TestSink::instance()), asyncSem_(0) + TestHTTPServer() : acceptor_(ioc_), port_(0), j_(TestSink::instance()) { // Bind to any available port endpoint_ = {boost::asio::ip::tcp::v4(), 0}; @@ -119,16 +117,13 @@ class TestHTTPServer if (!running_) co_return; - // Handle this connection concurrently - boost::asio::co_spawn( - ioc_, - handleConnection(std::move(socket)), - boost::asio::detached); + // Handle this connection + co_await handleConnection(std::move(socket)); } catch (std::exception const& e) { - // Accept failed, stop accepting - JLOG(j_.debug()) << "Accept error: " << e.what(); + // Accept or handle failed, stop accepting + JLOG(j_.debug()) << "Error: " << e.what(); co_return; } } From 904c03583293460a10805d5e357e0397f65b91d7 Mon Sep 17 00:00:00 2001 From: JCW Date: Wed, 28 Jan 2026 13:57:46 +0000 Subject: [PATCH 13/14] Address comments Signed-off-by: JCW --- src/tests/libxrpl/net/HTTPClient.cpp | 31 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index 4b3f2c275bd..fe710f0b240 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,8 @@ class TestHTTPServer std::string responseBody_; unsigned int statusCode_{200}; + std::future acceptFuture_; + beast::Journal j_; public: @@ -54,7 +57,8 @@ class TestHTTPServer port_ = acceptor_.local_endpoint().port(); // Start the accept coroutine - boost::asio::co_spawn(ioc_, accept(), boost::asio::detached); + acceptFuture_ = + boost::asio::co_spawn(ioc_, accept(), boost::asio::use_future); } TestHTTPServer(TestHTTPServer&&) = delete; @@ -63,7 +67,9 @@ class TestHTTPServer ~TestHTTPServer() { - stop(); + XRPL_ASSERT( + stopped(), + "xrpl::TestHTTPServer::~TestHTTPServer : accept future ready"); } boost::asio::io_context& @@ -96,7 +102,6 @@ class TestHTTPServer statusCode_ = code; } -private: void stop() { @@ -104,6 +109,14 @@ class TestHTTPServer acceptor_.close(); } + bool + stopped() const + { + using namespace std::chrono_literals; + return acceptFuture_.wait_for(0ms) == std::future_status::ready; + } + +private: boost::asio::awaitable accept() { @@ -213,13 +226,19 @@ runHTTPTest( // Run the IO context until completion auto start = std::chrono::steady_clock::now(); - while (!completed && - std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) + while (server.ioc().run_one() != 0) { - if (server.ioc().run_one() == 0) + if (std::chrono::steady_clock::now() - start >= + std::chrono::seconds(10) || + server.stopped()) { break; } + + if (completed) + { + server.stop(); + } } return completed; From d9ae9dbb01936730d7d0abbf7c56d2aefa53bac8 Mon Sep 17 00:00:00 2001 From: JCW Date: Wed, 28 Jan 2026 14:29:29 +0000 Subject: [PATCH 14/14] Address comments Signed-off-by: JCW --- src/tests/libxrpl/net/HTTPClient.cpp | 43 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index fe710f0b240..5ff0bfc3365 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -31,7 +31,8 @@ class TestHTTPServer boost::asio::io_context ioc_; boost::asio::ip::tcp::acceptor acceptor_; boost::asio::ip::tcp::endpoint endpoint_; - std::atomic running_{true}; + bool running_{true}; + bool finished_{false}; unsigned short port_; // Custom headers to return @@ -39,8 +40,6 @@ class TestHTTPServer std::string responseBody_; unsigned int statusCode_{200}; - std::future acceptFuture_; - beast::Journal j_; public: @@ -57,8 +56,7 @@ class TestHTTPServer port_ = acceptor_.local_endpoint().port(); // Start the accept coroutine - acceptFuture_ = - boost::asio::co_spawn(ioc_, accept(), boost::asio::use_future); + boost::asio::co_spawn(ioc_, accept(), boost::asio::detached); } TestHTTPServer(TestHTTPServer&&) = delete; @@ -68,7 +66,7 @@ class TestHTTPServer ~TestHTTPServer() { XRPL_ASSERT( - stopped(), + finished(), "xrpl::TestHTTPServer::~TestHTTPServer : accept future ready"); } @@ -110,10 +108,9 @@ class TestHTTPServer } bool - stopped() const + finished() const { - using namespace std::chrono_literals; - return acceptFuture_.wait_for(0ms) == std::future_status::ready; + return finished_; } private: @@ -128,7 +125,7 @@ class TestHTTPServer co_await acceptor_.async_accept(boost::asio::use_awaitable); if (!running_) - co_return; + break; // Handle this connection co_await handleConnection(std::move(socket)); @@ -137,9 +134,11 @@ class TestHTTPServer { // Accept or handle failed, stop accepting JLOG(j_.debug()) << "Error: " << e.what(); - co_return; + break; } } + + finished_ = true; } boost::asio::awaitable @@ -194,8 +193,8 @@ bool runHTTPTest( TestHTTPServer& server, std::string const& path, - std::atomic& completed, - std::atomic& resultStatus, + bool& completed, + int& resultStatus, std::string& resultData, boost::system::error_code& resultError) { @@ -230,7 +229,7 @@ runHTTPTest( { if (std::chrono::steady_clock::now() - start >= std::chrono::seconds(10) || - server.stopped()) + server.finished()) { break; } @@ -264,8 +263,8 @@ TEST(HTTPClient, case_insensitive_content_length) server.setResponseBody(testBody); server.setHeader(headerName, std::to_string(testBody.size())); - std::atomic completed{false}; - std::atomic resultStatus{0}; + bool completed{false}; + int resultStatus{0}; std::string resultData; boost::system::error_code resultError; @@ -287,8 +286,8 @@ TEST(HTTPClient, basic_http_request) server.setResponseBody(testBody); server.setHeader("Content-Type", "text/plain"); - std::atomic completed{false}; - std::atomic resultStatus{0}; + bool completed{false}; + int resultStatus{0}; std::string resultData; boost::system::error_code resultError; @@ -307,8 +306,8 @@ TEST(HTTPClient, empty_response) server.setResponseBody(""); // Empty body server.setHeader("Content-Length", "0"); - std::atomic completed{false}; - std::atomic resultStatus{0}; + bool completed{false}; + int resultStatus{0}; std::string resultData; boost::system::error_code resultError; @@ -331,8 +330,8 @@ TEST(HTTPClient, different_status_codes) server.setStatusCode(status); server.setResponseBody("Status " + std::to_string(status)); - std::atomic completed{false}; - std::atomic resultStatus{0}; + bool completed{false}; + int resultStatus{0}; std::string resultData; boost::system::error_code resultError;