From 7d15f59d20079ba94224e0bc308682aa5a004483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 2 Dec 2024 20:15:05 +0100 Subject: [PATCH 18/44] tests: make SSEClient reusable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Organization: Wires We will need it in yang push tests Change-Id: I22432553f3abff0de91b3c406abc5567de656065 Signed-off-by: Mattias Walström --- tests/restconf-notifications.cpp | 108 +------------------------------ tests/restconf_utils.cpp | 69 ++++++++++++++++++++ tests/restconf_utils.h | 47 ++++++++++++++ 3 files changed, 119 insertions(+), 105 deletions(-) diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp index 6c8c51a..4496b63 100644 --- a/tests/restconf-notifications.cpp +++ b/tests/restconf-notifications.cpp @@ -7,121 +7,19 @@ #include "trompeloeil_doctest.h" static const auto SERVER_PORT = "10088"; -#include #include #include #include #include #include "restconf/Server.h" #include "tests/aux-utils.h" +#include "tests/event_watchers.h" #include "tests/pretty_printers.h" #define SEND_NOTIFICATION(DATA) notifSession.sendNotification(*ctx.parseOp(DATA, libyang::DataFormat::JSON, libyang::OperationType::NotificationYang).op, sysrepo::Wait::No); using namespace std::chrono_literals; -struct SSEClient { - std::shared_ptr client; - boost::asio::deadline_timer t; - - SSEClient( - boost::asio::io_service& io, - std::latch& requestSent, - const RestconfNotificationWatcher& notification, - const std::string& uri, - const std::map& headers, - const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1)) // test code; the server should respond "soon" - : client(std::make_shared(io, SERVER_ADDRESS, SERVER_PORT)) - , t(io, silenceTimeout) - { - ng::header_map reqHeaders; - for (const auto& [name, value] : headers) { - reqHeaders.insert({name, {value, false}}); - } - - // shutdown the client after a period of no traffic - t.async_wait([maybeClient = std::weak_ptr{client}](const boost::system::error_code& ec) { - if (ec == boost::asio::error::operation_aborted) { - return; - } - if (auto client = maybeClient.lock()) { - client->shutdown(); - } - }); - - client->on_connect([&, uri, reqHeaders, silenceTimeout](auto) { - boost::system::error_code ec; - - static const auto server_address_and_port = std::string("http://[") + SERVER_ADDRESS + "]" + ":" + SERVER_PORT; - auto req = client->submit(ec, "GET", server_address_and_port + uri, "", reqHeaders); - req->on_response([&, silenceTimeout](const ng_client::response& res) { - requestSent.count_down(); - res.on_data([&, silenceTimeout](const uint8_t* data, std::size_t len) { - // not a production-ready code. In real-life condition the data received in one callback might probably be incomplete - for (const auto& event : parseEvents(std::string(reinterpret_cast(data), len))) { - notification(event); - } - t.expires_from_now(silenceTimeout); - }); - }); - }); - - client->on_error([&](const boost::system::error_code& ec) { - throw std::runtime_error{"HTTP client error: " + ec.message()}; - }); - } - - static std::vector parseEvents(const std::string& msg) - { - static const std::string prefix = "data:"; - - std::vector res; - std::istringstream iss(msg); - std::string line; - std::string event; - - while (std::getline(iss, line)) { - if (line.compare(0, prefix.size(), prefix) == 0) { - event += line.substr(prefix.size()); - } else if (line.empty()) { - res.emplace_back(std::move(event)); - event.clear(); - } else { - FAIL("Unprefixed response"); - } - } - return res; - } -}; - -#define PREPARE_LOOP_WITH_EXCEPTIONS \ - boost::asio::io_service io; \ - std::promise bg; \ - std::latch requestSent(1); - -#define RUN_LOOP_WITH_EXCEPTIONS \ - do { \ - io.run(); \ - auto fut = bg.get_future(); \ - REQUIRE(fut.wait_for(666ms /* "plenty of time" for the notificationThread to exit after it has called io.stop() */) == std::future_status::ready); \ - fut.get(); \ - } while (false) - -auto wrap_exceptions_and_asio(std::promise& bg, boost::asio::io_service& io, std::function func) -{ - return [&bg, &io, func]() - { - try { - func(); - } catch (...) { - bg.set_exception(std::current_exception()); - return; - } - bg.set_value(); - io.stop(); - }; -} - TEST_CASE("NETCONF notification streams") { trompeloeil::sequence seqMod1, seqMod2; @@ -237,7 +135,7 @@ TEST_CASE("NETCONF notification streams") waitForCompletionAndBitMore(seqMod2); })); - SSEClient cli(io, requestSent, netconfWatcher, uri, headers); + SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, headers); RUN_LOOP_WITH_EXCEPTIONS; } @@ -399,7 +297,7 @@ TEST_CASE("NETCONF notification streams") } oldNotificationsDone.wait(); - SSEClient cli(io, requestSent, netconfWatcher, uri, {AUTH_ROOT}); + SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_ROOT}); RUN_LOOP_WITH_EXCEPTIONS; } } diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp index 83f568f..8252bba 100644 --- a/tests/restconf_utils.cpp +++ b/tests/restconf_utils.cpp @@ -160,3 +160,72 @@ void setupRealNacm(sysrepo::Session session) session.applyChanges(); } +SSEClient::SSEClient( + boost::asio::io_service& io, + const std::string& server_address, + const std::string& server_port, + std::latch& requestSent, + const RestconfNotificationWatcher& notification, + const std::string& uri, + const std::map& headers, + const boost::posix_time::seconds silenceTimeout) + : client(std::make_shared(io, server_address, server_port)) + , t(io, silenceTimeout) +{ + ng::header_map reqHeaders; + for (const auto& [name, value] : headers) { + reqHeaders.insert({name, {value, false}}); + } + + // shutdown the client after a period of no traffic + t.async_wait([maybeClient = std::weak_ptr{client}](const boost::system::error_code& ec) { + if (ec == boost::asio::error::operation_aborted) { + return; + } + if (auto client = maybeClient.lock()) { + client->shutdown(); + } + }); + + client->on_connect([&, uri, reqHeaders, silenceTimeout, server_address, server_port](auto) { + boost::system::error_code ec; + + auto req = client->submit(ec, "GET", serverAddressAndPort(server_address, server_port) + uri, "", reqHeaders); + req->on_response([&, silenceTimeout](const ng_client::response& res) { + requestSent.count_down(); + res.on_data([&, silenceTimeout](const uint8_t* data, std::size_t len) { + // not a production-ready code. In real-life condition the data received in one callback might probably be incomplete + for (const auto& event : parseEvents(std::string(reinterpret_cast(data), len))) { + notification(event); + } + t.expires_from_now(silenceTimeout); + }); + }); + }); + + client->on_error([&](const boost::system::error_code& ec) { + throw std::runtime_error{"HTTP client error: " + ec.message()}; + }); +} + +std::vector SSEClient::parseEvents(const std::string& msg) +{ + static const std::string prefix = "data:"; + + std::vector res; + std::istringstream iss(msg); + std::string line; + std::string event; + + while (std::getline(iss, line)) { + if (line.compare(0, prefix.size(), prefix) == 0) { + event += line.substr(prefix.size()); + } else if (line.empty()) { + res.emplace_back(std::move(event)); + event.clear(); + } else { + FAIL("Unprefixed response"); + } + } + return res; +} diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h index 26f0803..8b7386e 100644 --- a/tests/restconf_utils.h +++ b/tests/restconf_utils.h @@ -8,7 +8,9 @@ #pragma once #include "trompeloeil_doctest.h" +#include #include +#include "event_watchers.h" #include "UniqueResource.h" namespace sysrepo { @@ -81,3 +83,48 @@ Response clientRequest( UniqueResource manageNacm(sysrepo::Session session); void setupRealNacm(sysrepo::Session session); + +struct SSEClient { + std::shared_ptr client; + boost::asio::deadline_timer t; + + SSEClient( + boost::asio::io_service& io, + const std::string& server_address, + const std::string& server_port, + std::latch& requestSent, + const RestconfNotificationWatcher& notification, + const std::string& uri, + const std::map& headers, + const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1)); // test code; the server should respond "soon" + + static std::vector parseEvents(const std::string& msg); +}; + +#define PREPARE_LOOP_WITH_EXCEPTIONS \ + boost::asio::io_service io; \ + std::promise bg; \ + std::latch requestSent(1); + +#define RUN_LOOP_WITH_EXCEPTIONS \ + do { \ + io.run(); \ + auto fut = bg.get_future(); \ + REQUIRE(fut.wait_for(666ms /* "plenty of time" for the notificationThread to exit after it has called io.stop() */) == std::future_status::ready); \ + fut.get(); \ + } while (false) + +inline auto wrap_exceptions_and_asio(std::promise& bg, boost::asio::io_service& io, std::function func) +{ + return [&bg, &io, func]() + { + try { + func(); + } catch (...) { + bg.set_exception(std::current_exception()); + return; + } + bg.set_value(); + io.stop(); + }; +} -- 2.43.0