From 02eb6b3397a889c90d3f52bcb0f204390fa77f5a Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 9 Mar 2025 19:02:03 +0100 Subject: [PATCH 001/120] first version --- .clang-format | 15 ++ CMakeLists.txt | 19 +++ async_client.cpp | 48 ++++++ async_tcp_client.cpp | 311 +++++++++++++++++++++++++++++++++++ blocking_tcp_echo_client.cpp | 73 ++++++++ blocking_tcp_echo_server.cpp | 69 ++++++++ rrcp_helper.cpp | 102 ++++++++++++ rrcp_helper.hpp | 23 +++ 8 files changed, 660 insertions(+) create mode 100644 .clang-format create mode 100644 CMakeLists.txt create mode 100644 async_client.cpp create mode 100644 async_tcp_client.cpp create mode 100644 blocking_tcp_echo_client.cpp create mode 100644 blocking_tcp_echo_server.cpp create mode 100644 rrcp_helper.cpp create mode 100644 rrcp_helper.hpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e73f6d5 --- /dev/null +++ b/.clang-format @@ -0,0 +1,15 @@ +BasedOnStyle: WebKit +Standard: Cpp03 +AlignAfterOpenBracket: false +AlignEscapedNewlinesLeft: true +AlwaysBreakAfterDefinitionReturnType: None +BreakBeforeBraces: Allman +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 80 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 0 +IndentCaseLabels: false +SortIncludes: false +AlignTrailingComments: false + +SpacesInAngles: true diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..97e5bab --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.28...4.0) + +project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) + +find_package(Boost 1.87 COMPONENTS asio REQUIRED HINTS ../../../../stagedir) + +add_library(rrcp_helper STATIC rrcp_helper.cpp rrcp_helper.hpp) + +add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) +target_link_libraries(blocking_tcp_echo_client PRIVATE rrcp_helper PUBLIC Boost::asio) + +add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) +target_link_libraries(blocking_tcp_echo_server PRIVATE rrcp_helper PUBLIC Boost::asio) + +add_executable(async_client async_client.cpp) +target_link_libraries(async_client PUBLIC Boost::asio) + +add_executable(async_tcp_client async_tcp_client.cpp) +target_link_libraries(async_tcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) diff --git a/async_client.cpp b/async_client.cpp new file mode 100644 index 0000000..aa6d645 --- /dev/null +++ b/async_client.cpp @@ -0,0 +1,48 @@ +#include + +#include +#include +#include +#include + +const auto noop = std::bind([] {}); +const std::string delimiter{"\r\n\r\n"}; + +boost::asio::io_context io_context; +boost::asio::ip::tcp::acceptor + acceptor(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); +boost::asio::ip::tcp::socket socket1(io_context); +boost::asio::ip::tcp::socket socket2(io_context); +boost::asio::streambuf streambuf; + +// void do_read(); + +void handle_read(boost::system::error_code, std::size_t xfer) +{ + assert(streambuf.size() >= xfer); + + std::string command{buffers_begin(streambuf.data()), + buffers_begin(streambuf.data()) + xfer - delimiter.length()}; + + streambuf.consume(xfer); + + // XXX assert(command == "cmd1"); + std::cout << "received command: " << command << "\n" + << "streambuf contains " << streambuf.size() << " bytes.\n"; + + boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); +} + +int main() +{ + acceptor.async_accept(socket1, noop); + socket2.async_connect(acceptor.local_endpoint(), noop); + io_context.run(); + io_context.restart(); + + boost::asio::write(socket1, boost::asio::buffer("cmd1" + delimiter)); + boost::asio::write(socket1, boost::asio::buffer("cmd2" + delimiter)); + boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); + + io_context.run(); +} diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp new file mode 100644 index 0000000..ea9b184 --- /dev/null +++ b/async_tcp_client.cpp @@ -0,0 +1,311 @@ +// +// async_tcp_client.cpp +// ~~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using boost::asio::steady_timer; +using boost::asio::ip::tcp; +using std::placeholders::_1; +using std::placeholders::_2; + +// +// This class manages socket timeouts by applying the concept of a deadline. +// Some asynchronous operations are given deadlines by which they must complete. +// Deadlines are enforced by an "actor" that persists for the lifetime of the +// client object: +// +// +----------------+ +// | | +// | check_deadline |<---+ +// | | | +// +----------------+ | async_wait() +// | | +// +---------+ +// +// If the deadline actor determines that the deadline has expired, the socket +// is closed and any outstanding operations are consequently cancelled. +// +// Connection establishment involves trying each endpoint in turn until a +// connection is successful, or the available endpoints are exhausted. If the +// deadline actor closes the socket, the connect actor is woken up and moves to +// the next endpoint. +// +// +---------------+ +// | | +// | start_connect |<---+ +// | | | +// +---------------+ | +// | | +// async_- | +----------------+ +// connect() | | | +// +--->| handle_connect | +// | | +// +----------------+ +// : +// Once a connection is : +// made, the connect : +// actor forks in two - : +// : +// an actor for reading : and an actor for +// inbound messages: : sending heartbeats: +// : +// +------------+ : +-------------+ +// | |<- - - - -+- - - - ->| | +// | start_read | | start_write |<---+ +// | |<---+ | | | +// +------------+ | +-------------+ | async_wait() +// | | | | +// async_- | +-------------+ async_- | +--------------+ +// read_- | | | write() | | | +// until() +--->| handle_read | +--->| handle_write | +// | | | | +// +-------------+ +--------------+ +// +// The input actor reads messages from the socket, where messages are delimited +// by the newline character. The deadline for a complete message is 30 seconds. +// +// The heartbeat actor sends a heartbeat (a message that consists of a single +// newline character) every 10 seconds. In this example, no deadline is applied +// to message sending. +// +class client +{ +public: + client(boost::asio::io_context& io_context) + : socket_(io_context), + deadline_(io_context), + heartbeat_timer_(io_context) + { + } + + // Called by the user of the client class to initiate the connection process. + // The endpoints will have been obtained using a tcp::resolver. + void start(tcp::resolver::results_type endpoints) + { + // Start the connect actor. + endpoints_ = endpoints; + start_connect(endpoints_.begin()); + + // Start the deadline actor. You will note that we're not setting any + // particular deadline here. Instead, the connect and input actors will + // update the deadline prior to each asynchronous operation. + deadline_.async_wait(std::bind(&client::check_deadline, this)); + } + + // This function terminates all the actors to shut down the connection. It + // may be called by the user of the client class, or by the class itself in + // response to graceful termination or an unrecoverable error. + void stop() + { + stopped_ = true; + boost::system::error_code ignored_error; + socket_.close(ignored_error); + deadline_.cancel(); + heartbeat_timer_.cancel(); + } + +private: + void start_connect(tcp::resolver::results_type::iterator endpoint_iter) + { + if (endpoint_iter != endpoints_.end()) + { + std::cout << "Trying " << endpoint_iter->endpoint() << "...\n"; + + // Set a deadline for the connect operation. + deadline_.expires_after(std::chrono::seconds(60)); + + // Start the asynchronous connect operation. + socket_.async_connect(endpoint_iter->endpoint(), + std::bind(&client::handle_connect, + this, _1, endpoint_iter)); + } + else + { + // There are no more endpoints to try. Shut down the client. + stop(); + } + } + + void handle_connect(const boost::system::error_code& error, + tcp::resolver::results_type::iterator endpoint_iter) + { + if (stopped_) + return; + + // The async_connect() function automatically opens the socket at the start + // of the asynchronous operation. If the socket is closed at this time then + // the timeout handler must have run first. + if (!socket_.is_open()) + { + std::cout << "Connect timed out\n"; + + // Try the next available endpoint. + start_connect(++endpoint_iter); + } + + // Check if the connect operation failed before the deadline expired. + else if (error) + { + std::cout << "Connect error: " << error.message() << "\n"; + + // We need to close the socket used in the previous connection attempt + // before starting a new one. + socket_.close(); + + // Try the next available endpoint. + start_connect(++endpoint_iter); + } + + // Otherwise we have successfully established a connection. + else + { + std::cout << "Connected to " << endpoint_iter->endpoint() << "\n"; + + // Start the input actor. + start_read(); + + // Start the heartbeat actor. + start_write(); + } + } + + void start_read() + { + // Set a deadline for the read operation. + deadline_.expires_after(std::chrono::seconds(30)); + + // Start an asynchronous operation to read a newline-delimited message. + boost::asio::async_read_until(socket_, + boost::asio::dynamic_buffer(input_buffer_), '\n', + std::bind(&client::handle_read, this, _1, _2)); + } + + void handle_read(const boost::system::error_code& error, std::size_t n) + { + if (stopped_) + return; + + if (!error) + { + // Extract the newline-delimited message from the buffer. + std::string line(input_buffer_.substr(0, n - 1)); + input_buffer_.erase(0, n); + + // Empty messages are heartbeats and so ignored. + if (!line.empty()) + { + std::cout << "Received: " << line << "\n"; + } + + start_read(); + } + else + { + std::cout << "Error on receive: " << error.message() << "\n"; + + stop(); + } + } + + void start_write() + { + if (stopped_) + return; + + // Start an asynchronous operation to send a heartbeat message. + boost::asio::async_write(socket_, boost::asio::buffer("\n", 1), + std::bind(&client::handle_write, this, _1)); + } + + void handle_write(const boost::system::error_code& error) + { + if (stopped_) + return; + + if (!error) + { + // Wait 10 seconds before sending the next heartbeat. + heartbeat_timer_.expires_after(std::chrono::seconds(10)); + heartbeat_timer_.async_wait(std::bind(&client::start_write, this)); + } + else + { + std::cout << "Error on heartbeat: " << error.message() << "\n"; + + stop(); + } + } + + void check_deadline() + { + if (stopped_) + return; + + // Check whether the deadline has passed. We compare the deadline against + // the current time since a new asynchronous operation may have moved the + // deadline before this actor had a chance to run. + if (deadline_.expiry() <= steady_timer::clock_type::now()) + { + // The deadline has passed. The socket is closed so that any outstanding + // asynchronous operations are cancelled. + socket_.close(); + + // There is no longer an active deadline. The expiry is set to the + // maximum time point so that the actor takes no action until a new + // deadline is set. + deadline_.expires_at(steady_timer::time_point::max()); + } + + // Put the actor back to sleep. + deadline_.async_wait(std::bind(&client::check_deadline, this)); + } + +private: + bool stopped_ = false; + tcp::resolver::results_type endpoints_; + tcp::socket socket_; + std::string input_buffer_; + steady_timer deadline_; + steady_timer heartbeat_timer_; +}; + +int main(int argc, char* argv[]) +{ + try + { + if (argc != 3) + { + std::cerr << "Usage: client \n"; + return 1; + } + + boost::asio::io_context io_context; + tcp::resolver r(io_context); + client c(io_context); + + c.start(r.resolve(argv[1], argv[2])); + + io_context.run(); + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp new file mode 100644 index 0000000..40bdc37 --- /dev/null +++ b/blocking_tcp_echo_client.cpp @@ -0,0 +1,73 @@ +// +// blocking_tcp_echo_client.cpp +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include "rrcp_helper.hpp" + +#include +#include + +#include +#include +#include + +using boost::asio::ip::tcp; + +enum +{ + max_length = 1024 +}; + +int main(int argc, char *argv[]) +{ + try { + if (argc != 3) { + std::cerr << "Usage: blocking_tcp_echo_client \n"; + return 1; + } + + boost::asio::io_context io_context; + + tcp::socket s(io_context); + tcp::resolver resolver(io_context); + boost::asio::connect(s, resolver.resolve(argv[1], argv[2])); + + do { + std::cout << "Enter message: "; + char request[max_length]; + std::cin.getline(request, max_length); + size_t request_length = std::strlen(request); + if (request_length == 0) + break; + + std::string data = char2esc(std::string(request, request_length)); + + boost::asio::write(s, boost::asio::buffer(data.c_str(), data.length())); + + // TODO: wait for endchar with timeout! + std::string reply; + boost::asio::dynamic_string_buffer + sb2 = boost::asio::dynamic_buffer(reply, max_length); + boost::system::error_code ec; + + size_t reply_length{}; + do { + reply_length = boost::asio::read_until(s, sb2, ';'); + std::cout << "Reply is: "; + std::cout.write(reply.c_str(), reply_length); + std::cout << "\n"; + } while (reply_length > 0); + } while (true); + } catch (std::exception &e) { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp new file mode 100644 index 0000000..3863884 --- /dev/null +++ b/blocking_tcp_echo_server.cpp @@ -0,0 +1,69 @@ +// +// blocking_tcp_echo_server.cpp +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include + +#include +#include +#include +#include + +using boost::asio::ip::tcp; + +const int max_length = 1024; + +void session(tcp::socket sock) +{ + try { + for (;;) { + char data[max_length]; + + boost::system::error_code error; + size_t length = sock.read_some(boost::asio::buffer(data), error); + if (error == boost::asio::stream_errc::eof) + break; // Connection closed cleanly by peer. + else if (error) + throw boost::system::system_error(error); // Some other error. + + boost::asio::write(sock, boost::asio::buffer(data, length)); + } + } catch (std::exception &e) { + std::cerr << "Exception in thread: " << e.what() << "\n"; + } +} + +void server(boost::asio::io_context &io_context, unsigned short port) +{ + tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port)); + for (;;) { + tcp::socket sock(io_context); + a.accept(sock); + std::thread(session, std::move(sock)).detach(); + } +} + +int main(int argc, char *argv[]) +{ + try { + if (argc != 2) { + std::cerr << "Usage: blocking_tcp_echo_server \n"; + return 1; + } + + boost::asio::io_context io_context; + + server(io_context, std::atoi(argv[1])); + } catch (std::exception &e) { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp new file mode 100644 index 0000000..db0459f --- /dev/null +++ b/rrcp_helper.cpp @@ -0,0 +1,102 @@ +#include "rrcp_helper.hpp" + +constexpr const char ESC = 0x1B; +constexpr const char REPLACE_LF = 0x01; +constexpr const char REPLACE_CR = 0x02; +constexpr const char REPLACE_ESC = 0x03; + +constexpr const char SP = 0x20; +constexpr const char LF = 0x0A; +constexpr const char CR = 0x0D; + +// constexpr const char DOT = 0x2E; +// constexpr const char SEMICOLON = 0x3B; +// constexpr const char COMMA = 0x2C; +// constexpr const char SHARP = 0x23; +// constexpr const char DQUOTE = '\"'; +// constexpr const char BACKSLASH = '\\'; +// constexpr const char COLON = 0x3A; + +std::string esc2char(const std::string &data) +{ + std::string message; + size_t const len = data.size(); + char c; + unsigned int i = 0; + while (i < len) { + // get next char + c = data[i]; + + // end mark found, the message is complete, the + // CR is not needed anymore + if (c == CR) { + return message; + } + + // An escape is found thus we want to + // replace the escape sequence + if (c == ESC) { + // On the ESC should follow an replacement character + // REPLACE_LF... REPLACE_ESC + if (i != (len - 1)) { + // get next character + i++; + + c = data[i]; + if (REPLACE_LF == c) { + c = static_cast(LF); + } else if (REPLACE_CR == c) { + c = static_cast(CR); + } else if (REPLACE_ESC == c) { + c = static_cast(ESC); + } else { + //"Parser Error contains unexpected ESC character" + return ""; + } + + } else { + //"Parser Error message ends with escape character" + return ""; + } + } + + // enter next character + message += c; + // next character + i++; + } + return message; +} + +std::string char2esc(const std::string &data) +{ + std::string message; + size_t const len = data.size(); + char c; + unsigned int i = 0; + + while (i < len) { + // get next char + c = data[i++]; + // and replace CR and LF and the ESC itself + switch (c) { + case LF: { + message += static_cast(ESC); + message += static_cast(REPLACE_LF); + }; break; + case CR: { + message += static_cast(ESC); + message += static_cast(REPLACE_CR); + }; break; + + case ESC: { + message += static_cast(ESC); + message += static_cast(REPLACE_ESC); + }; break; + default: { + message += c; + } break; + } + } + return message; +} diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp new file mode 100644 index 0000000..0569a18 --- /dev/null +++ b/rrcp_helper.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +/** + * @brief Gets the message between message and + * replaces the escape sequences for LF and CR + * + * + * @param data: read from socket + * + * @return message string like 'M:IBIT SStart' + */ +extern std::string esc2char(const std::string& data); + +/** + * @brief Replaces LF, CR with Escape sequence + * + * @param data: data to send + * + * @return translated data + */ +extern std::string char2esc(const std::string& data); From ebeb90a53e679b780e215d06e357d540347781b0 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 9 Mar 2025 19:36:50 +0100 Subject: [PATCH 002/120] clang-format all files --- .clang-format | 2 +- async_client.cpp | 36 ++++---- async_tcp_client.cpp | 64 +++++++------- blocking_tcp_echo_client.cpp | 81 ++++++++++-------- blocking_tcp_echo_server.cpp | 73 +++++++++------- rrcp_helper.cpp | 162 ++++++++++++++++++++--------------- 6 files changed, 226 insertions(+), 192 deletions(-) diff --git a/.clang-format b/.clang-format index e73f6d5..8e1b25e 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,4 @@ -BasedOnStyle: WebKit +BasedOnStyle: Google Standard: Cpp03 AlignAfterOpenBracket: false AlignEscapedNewlinesLeft: true diff --git a/async_client.cpp b/async_client.cpp index aa6d645..9f2cb40 100644 --- a/async_client.cpp +++ b/async_client.cpp @@ -9,8 +9,8 @@ const auto noop = std::bind([] {}); const std::string delimiter{"\r\n\r\n"}; boost::asio::io_context io_context; -boost::asio::ip::tcp::acceptor - acceptor(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); +boost::asio::ip::tcp::acceptor acceptor( + io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); boost::asio::ip::tcp::socket socket1(io_context); boost::asio::ip::tcp::socket socket2(io_context); boost::asio::streambuf streambuf; @@ -19,30 +19,30 @@ boost::asio::streambuf streambuf; void handle_read(boost::system::error_code, std::size_t xfer) { - assert(streambuf.size() >= xfer); + assert(streambuf.size() >= xfer); - std::string command{buffers_begin(streambuf.data()), - buffers_begin(streambuf.data()) + xfer - delimiter.length()}; + std::string command{buffers_begin(streambuf.data()), + buffers_begin(streambuf.data()) + xfer - delimiter.length()}; - streambuf.consume(xfer); + streambuf.consume(xfer); - // XXX assert(command == "cmd1"); - std::cout << "received command: " << command << "\n" - << "streambuf contains " << streambuf.size() << " bytes.\n"; + // XXX assert(command == "cmd1"); + std::cout << "received command: " << command << "\n" + << "streambuf contains " << streambuf.size() << " bytes.\n"; - boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); + boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); } int main() { - acceptor.async_accept(socket1, noop); - socket2.async_connect(acceptor.local_endpoint(), noop); - io_context.run(); - io_context.restart(); + acceptor.async_accept(socket1, noop); + socket2.async_connect(acceptor.local_endpoint(), noop); + io_context.run(); + io_context.restart(); - boost::asio::write(socket1, boost::asio::buffer("cmd1" + delimiter)); - boost::asio::write(socket1, boost::asio::buffer("cmd2" + delimiter)); - boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); + boost::asio::write(socket1, boost::asio::buffer("cmd1" + delimiter)); + boost::asio::write(socket1, boost::asio::buffer("cmd2" + delimiter)); + boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); - io_context.run(); + io_context.run(); } diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index ea9b184..7c6c3ef 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -14,6 +14,11 @@ #include #include #include +#include + +#include +#include +#include #include #include #include @@ -23,6 +28,8 @@ using boost::asio::ip::tcp; using std::placeholders::_1; using std::placeholders::_2; +using namespace std::chrono_literals; + // // This class manages socket timeouts by applying the concept of a deadline. // Some asynchronous operations are given deadlines by which they must complete. @@ -85,16 +92,14 @@ using std::placeholders::_2; // class client { -public: + public: client(boost::asio::io_context& io_context) - : socket_(io_context), - deadline_(io_context), - heartbeat_timer_(io_context) + : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) { } - // Called by the user of the client class to initiate the connection process. - // The endpoints will have been obtained using a tcp::resolver. + // Called by the user of the client class to initiate the connection + // process. The endpoints will have been obtained using a tcp::resolver. void start(tcp::resolver::results_type endpoints) { // Start the connect actor. @@ -119,7 +124,7 @@ class client heartbeat_timer_.cancel(); } -private: + private: void start_connect(tcp::resolver::results_type::iterator endpoint_iter) { if (endpoint_iter != endpoints_.end()) @@ -127,12 +132,11 @@ class client std::cout << "Trying " << endpoint_iter->endpoint() << "...\n"; // Set a deadline for the connect operation. - deadline_.expires_after(std::chrono::seconds(60)); + deadline_.expires_after(60s); // Start the asynchronous connect operation. socket_.async_connect(endpoint_iter->endpoint(), - std::bind(&client::handle_connect, - this, _1, endpoint_iter)); + std::bind(&client::handle_connect, this, _1, endpoint_iter)); } else { @@ -144,12 +148,11 @@ class client void handle_connect(const boost::system::error_code& error, tcp::resolver::results_type::iterator endpoint_iter) { - if (stopped_) - return; + if (stopped_) return; - // The async_connect() function automatically opens the socket at the start - // of the asynchronous operation. If the socket is closed at this time then - // the timeout handler must have run first. + // The async_connect() function automatically opens the socket at the + // start of the asynchronous operation. If the socket is closed at this + // time then the timeout handler must have run first. if (!socket_.is_open()) { std::cout << "Connect timed out\n"; @@ -163,8 +166,8 @@ class client { std::cout << "Connect error: " << error.message() << "\n"; - // We need to close the socket used in the previous connection attempt - // before starting a new one. + // We need to close the socket used in the previous connection + // attempt before starting a new one. socket_.close(); // Try the next available endpoint. @@ -187,7 +190,7 @@ class client void start_read() { // Set a deadline for the read operation. - deadline_.expires_after(std::chrono::seconds(30)); + deadline_.expires_after(30s); // Start an asynchronous operation to read a newline-delimited message. boost::asio::async_read_until(socket_, @@ -197,8 +200,7 @@ class client void handle_read(const boost::system::error_code& error, std::size_t n) { - if (stopped_) - return; + if (stopped_) return; if (!error) { @@ -224,8 +226,7 @@ class client void start_write() { - if (stopped_) - return; + if (stopped_) return; // Start an asynchronous operation to send a heartbeat message. boost::asio::async_write(socket_, boost::asio::buffer("\n", 1), @@ -234,13 +235,12 @@ class client void handle_write(const boost::system::error_code& error) { - if (stopped_) - return; + if (stopped_) return; if (!error) { // Wait 10 seconds before sending the next heartbeat. - heartbeat_timer_.expires_after(std::chrono::seconds(10)); + heartbeat_timer_.expires_after(10s); heartbeat_timer_.async_wait(std::bind(&client::start_write, this)); } else @@ -253,16 +253,15 @@ class client void check_deadline() { - if (stopped_) - return; + if (stopped_) return; - // Check whether the deadline has passed. We compare the deadline against - // the current time since a new asynchronous operation may have moved the - // deadline before this actor had a chance to run. + // Check whether the deadline has passed. We compare the deadline + // against the current time since a new asynchronous operation may have + // moved the deadline before this actor had a chance to run. if (deadline_.expiry() <= steady_timer::clock_type::now()) { - // The deadline has passed. The socket is closed so that any outstanding - // asynchronous operations are cancelled. + // The deadline has passed. The socket is closed so that any + // outstanding asynchronous operations are cancelled. socket_.close(); // There is no longer an active deadline. The expiry is set to the @@ -275,7 +274,6 @@ class client deadline_.async_wait(std::bind(&client::check_deadline, this)); } -private: bool stopped_ = false; tcp::resolver::results_type endpoints_; tcp::socket socket_; diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index 40bdc37..5a7986f 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -21,53 +21,58 @@ using boost::asio::ip::tcp; enum { - max_length = 1024 + max_length = 1024 }; -int main(int argc, char *argv[]) +int main(int argc, char* argv[]) { - try { - if (argc != 3) { - std::cerr << "Usage: blocking_tcp_echo_client \n"; - return 1; - } + try + { + if (argc != 3) + { + std::cerr << "Usage: blocking_tcp_echo_client \n"; + return 1; + } - boost::asio::io_context io_context; + boost::asio::io_context io_context; - tcp::socket s(io_context); - tcp::resolver resolver(io_context); - boost::asio::connect(s, resolver.resolve(argv[1], argv[2])); + tcp::socket s(io_context); + tcp::resolver resolver(io_context); + boost::asio::connect(s, resolver.resolve(argv[1], argv[2])); - do { - std::cout << "Enter message: "; - char request[max_length]; - std::cin.getline(request, max_length); - size_t request_length = std::strlen(request); - if (request_length == 0) - break; + do + { + std::cout << "Enter message: "; + char request[max_length]; + std::cin.getline(request, max_length); + size_t request_length = std::strlen(request); + if (request_length == 0) break; - std::string data = char2esc(std::string(request, request_length)); + std::string data = char2esc(std::string(request, request_length)); - boost::asio::write(s, boost::asio::buffer(data.c_str(), data.length())); + boost::asio::write(s, boost::asio::buffer(data.c_str(), data.length())); - // TODO: wait for endchar with timeout! - std::string reply; - boost::asio::dynamic_string_buffer - sb2 = boost::asio::dynamic_buffer(reply, max_length); - boost::system::error_code ec; + // TODO: wait for endchar with timeout! + std::string reply; + boost::asio::dynamic_string_buffer< char, std::string::traits_type, + std::string::allocator_type > + sb2 = boost::asio::dynamic_buffer(reply, max_length); + boost::system::error_code ec; - size_t reply_length{}; - do { - reply_length = boost::asio::read_until(s, sb2, ';'); - std::cout << "Reply is: "; - std::cout.write(reply.c_str(), reply_length); - std::cout << "\n"; - } while (reply_length > 0); - } while (true); - } catch (std::exception &e) { - std::cerr << "Exception: " << e.what() << "\n"; - } + size_t reply_length{}; + do + { + reply_length = boost::asio::read_until(s, sb2, ';'); + std::cout << "Reply is: "; + std::cout.write(reply.c_str(), reply_length); + std::cout << "\n"; + } while (reply_length > 0); + } while (true); + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + } - return 0; + return 0; } diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index 3863884..1b954d1 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -22,48 +22,57 @@ const int max_length = 1024; void session(tcp::socket sock) { - try { - for (;;) { - char data[max_length]; + try + { + for (;;) + { + char data[max_length]; - boost::system::error_code error; - size_t length = sock.read_some(boost::asio::buffer(data), error); - if (error == boost::asio::stream_errc::eof) - break; // Connection closed cleanly by peer. - else if (error) - throw boost::system::system_error(error); // Some other error. + boost::system::error_code error; + size_t length = sock.read_some(boost::asio::buffer(data), error); + if (error == boost::asio::stream_errc::eof) + break; // Connection closed cleanly by peer. + else if (error) + throw boost::system::system_error(error); // Some other error. - boost::asio::write(sock, boost::asio::buffer(data, length)); - } - } catch (std::exception &e) { - std::cerr << "Exception in thread: " << e.what() << "\n"; + boost::asio::write(sock, boost::asio::buffer(data, length)); } + } + catch (std::exception& e) + { + std::cerr << "Exception in thread: " << e.what() << "\n"; + } } -void server(boost::asio::io_context &io_context, unsigned short port) +void server(boost::asio::io_context& io_context, unsigned short port) { - tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port)); - for (;;) { - tcp::socket sock(io_context); - a.accept(sock); - std::thread(session, std::move(sock)).detach(); - } + tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port)); + for (;;) + { + tcp::socket sock(io_context); + a.accept(sock); + std::thread(session, std::move(sock)).detach(); + } } -int main(int argc, char *argv[]) +int main(int argc, char* argv[]) { - try { - if (argc != 2) { - std::cerr << "Usage: blocking_tcp_echo_server \n"; - return 1; - } + try + { + if (argc != 2) + { + std::cerr << "Usage: blocking_tcp_echo_server \n"; + return 1; + } - boost::asio::io_context io_context; + boost::asio::io_context io_context; - server(io_context, std::atoi(argv[1])); - } catch (std::exception &e) { - std::cerr << "Exception: " << e.what() << "\n"; - } + server(io_context, std::atoi(argv[1])); + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + } - return 0; + return 0; } diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index db0459f..fe211da 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -17,86 +17,108 @@ constexpr const char CR = 0x0D; // constexpr const char BACKSLASH = '\\'; // constexpr const char COLON = 0x3A; -std::string esc2char(const std::string &data) +std::string esc2char(const std::string& data) { - std::string message; - size_t const len = data.size(); - char c; - unsigned int i = 0; - while (i < len) { - // get next char - c = data[i]; - - // end mark found, the message is complete, the - // CR is not needed anymore - if (c == CR) { - return message; - } + std::string message; + size_t const len = data.size(); + char c; + unsigned int i = 0; + while (i < len) + { + // get next char + c = data[i]; - // An escape is found thus we want to - // replace the escape sequence - if (c == ESC) { - // On the ESC should follow an replacement character - // REPLACE_LF... REPLACE_ESC - if (i != (len - 1)) { - // get next character - i++; + // end mark found, the message is complete, the + // CR is not needed anymore + if (c == CR) + { + return message; + } - c = data[i]; - if (REPLACE_LF == c) { - c = static_cast(LF); - } else if (REPLACE_CR == c) { - c = static_cast(CR); - } else if (REPLACE_ESC == c) { - c = static_cast(ESC); - } else { - //"Parser Error contains unexpected ESC character" - return ""; - } + // An escape is found thus we want to + // replace the escape sequence + if (c == ESC) + { + // On the ESC should follow an replacement character + // REPLACE_LF... REPLACE_ESC + if (i != (len - 1)) + { + // get next character + i++; - } else { - //"Parser Error message ends with escape character" - return ""; - } + c = data[i]; + if (REPLACE_LF == c) + { + c = static_cast< char >(LF); } - - // enter next character - message += c; - // next character - i++; + else if (REPLACE_CR == c) + { + c = static_cast< char >(CR); + } + else if (REPLACE_ESC == c) + { + c = static_cast< char >(ESC); + } + else + { + //"Parser Error contains unexpected ESC character" + return ""; + } + } + else + { + //"Parser Error message ends with escape character" + return ""; + } } - return message; + + // enter next character + message += c; + // next character + i++; + } + return message; } -std::string char2esc(const std::string &data) +std::string char2esc(const std::string& data) { - std::string message; - size_t const len = data.size(); - char c; - unsigned int i = 0; + std::string message; + size_t const len = data.size(); + char c; + unsigned int i = 0; - while (i < len) { - // get next char - c = data[i++]; - // and replace CR and LF and the ESC itself - switch (c) { - case LF: { - message += static_cast(ESC); - message += static_cast(REPLACE_LF); - }; break; - case CR: { - message += static_cast(ESC); - message += static_cast(REPLACE_CR); - }; break; + while (i < len) + { + // get next char + c = data[i++]; + // and replace CR and LF and the ESC itself + switch (c) + { + case LF: + { + message += static_cast< char >(ESC); + message += static_cast< char >(REPLACE_LF); + }; + break; + case CR: + { + message += static_cast< char >(ESC); + message += static_cast< char >(REPLACE_CR); + }; + break; - case ESC: { - message += static_cast(ESC); - message += static_cast(REPLACE_ESC); - }; break; - default: { - message += c; - } break; - } + case ESC: + { + message += static_cast< char >(ESC); + message += static_cast< char >(REPLACE_ESC); + }; + break; + default: + { + message += c; + } + break; } - return message; + } + return message; } From b6691f24cb0c789f88f4b3a8aa4da0069a5f17e1 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 9 Mar 2025 22:50:30 +0100 Subject: [PATCH 003/120] add rrcp_client --- CMakeLists.txt | 3 + rrcp_client.cpp | 166 +++++++++++++++++++++++++++++++++++++++++++++++ rrcp_message.hpp | 70 ++++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 rrcp_client.cpp create mode 100644 rrcp_message.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 97e5bab..b58bfd5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,9 @@ target_link_libraries(blocking_tcp_echo_client PRIVATE rrcp_helper PUBLIC Boost: add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) target_link_libraries(blocking_tcp_echo_server PRIVATE rrcp_helper PUBLIC Boost::asio) +add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) +target_link_libraries(rrcp_client PUBLIC Boost::asio) + add_executable(async_client async_client.cpp) target_link_libraries(async_client PUBLIC Boost::asio) diff --git a/rrcp_client.cpp b/rrcp_client.cpp new file mode 100644 index 0000000..b6b150c --- /dev/null +++ b/rrcp_client.cpp @@ -0,0 +1,166 @@ +// +// rrcp_client.cpp +// ~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include "rrcp_message.hpp" + +using boost::asio::ip::tcp; + +typedef std::deque< rrcp_message > rrcp_message_queue; + +class rrcp_client +{ + public: + rrcp_client(boost::asio::io_context& io_context, + const tcp::resolver::results_type& endpoints) + : io_context_(io_context), socket_(io_context) + { + do_connect(endpoints); + } + + void write(const rrcp_message& msg) + { + boost::asio::post(io_context_, + [this, msg]() + { + bool write_in_progress = !write_msgs_.empty(); + write_msgs_.push_back(msg); + if (!write_in_progress) + { + do_write(); + } + }); + } + + void close() + { + boost::asio::post(io_context_, [this]() { socket_.close(); }); + } + + private: + void do_connect(const tcp::resolver::results_type& endpoints) + { + boost::asio::async_connect(socket_, endpoints, + [this](boost::system::error_code ec, tcp::endpoint) + { + if (!ec) + { + do_read_header(); + } + }); + } + + void do_read_header() + { + boost::asio::async_read(socket_, + boost::asio::buffer(read_msg_.data(), rrcp_message::header_length), + [this](boost::system::error_code ec, std::size_t /*length*/) + { + if (!ec && read_msg_.decode_header()) + { + do_read_body(); + } + else + { + socket_.close(); + } + }); + } + + void do_read_body() + { + boost::asio::async_read(socket_, + boost::asio::buffer(read_msg_.body(), read_msg_.body_length()), + [this](boost::system::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + std::cout.write(read_msg_.body(), read_msg_.body_length()); + std::cout << "\n"; + do_read_header(); + } + else + { + socket_.close(); + } + }); + } + + void do_write() + { + boost::asio::async_write(socket_, + boost::asio::buffer( + write_msgs_.front().data(), write_msgs_.front().length()), + [this](boost::system::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + write_msgs_.pop_front(); + if (!write_msgs_.empty()) + { + do_write(); + } + } + else + { + socket_.close(); + } + }); + } + + private: + boost::asio::io_context& io_context_; + tcp::socket socket_; + rrcp_message read_msg_; + rrcp_message_queue write_msgs_; +}; + +int main(int argc, char* argv[]) +{ + try + { + if (argc != 3) + { + std::cerr << "Usage: rrcp_client \n"; + return 1; + } + + boost::asio::io_context io_context; + + tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve(argv[1], argv[2]); + rrcp_client c(io_context, endpoints); + + std::thread t([&io_context]() { io_context.run(); }); + + char line[rrcp_message::max_msg_length + 1]; + while (std::cin.getline(line, rrcp_message::max_msg_length + 1)) + { + rrcp_message msg; + msg.body_length(std::strlen(line)); + std::memcpy(msg.body(), line, msg.body_length()); + msg.encode_header(); + c.write(msg); + } + + c.close(); + t.join(); + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/rrcp_message.hpp b/rrcp_message.hpp new file mode 100644 index 0000000..b2a1ef9 --- /dev/null +++ b/rrcp_message.hpp @@ -0,0 +1,70 @@ +// +// rrcp_message.hpp +// ~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef RRCP_MESSAGE_HPP +#define RRCP_MESSAGE_HPP + +#include +#include +#include + +class rrcp_message +{ + public: + static constexpr std::size_t header_length = 4; + static constexpr std::size_t max_msg_length = 65432; + + rrcp_message() : msg_length_(0) {} + + const char* data() const { return data_; } + + char* data() { return data_; } + + std::size_t length() const { return header_length + msg_length_; } + + const char* body() const { return data_ + header_length; } + + char* body() { return data_ + header_length; } + + std::size_t body_length() const { return msg_length_; } + + void body_length(std::size_t new_length) + { + msg_length_ = new_length; + if (msg_length_ > max_msg_length) msg_length_ = max_msg_length; + } + + bool decode_header() + { + char header[header_length + 1] = ""; + std::strncat(header, data_, header_length); + msg_length_ = std::atoi(header); + if (msg_length_ > max_msg_length) + { + msg_length_ = 0; + return false; + } + return true; + } + + void encode_header() + { + char header[header_length + 1] = ""; + std::snprintf( + header, header_length + 1, "%4d", static_cast< int >(msg_length_)); + std::memcpy(data_, header, header_length); + } + + private: + char data_[header_length + max_msg_length]; + std::size_t msg_length_; +}; + +#endif // RRCP_MESSAGE_HPP From 5f7ad12dd53568db85fb1fb4aab204ffb31034d9 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 9 Mar 2025 23:12:19 +0100 Subject: [PATCH 004/120] fix clang-tidy warnings --- async_client.cpp | 2 +- async_tcp_client.cpp | 2 +- blocking_tcp_echo_client.cpp | 8 ++++---- blocking_tcp_echo_server.cpp | 2 +- rrcp_client.cpp | 18 ++++++++++++++---- rrcp_message.hpp | 12 ++++++------ 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/async_client.cpp b/async_client.cpp index 9f2cb40..1df4f7e 100644 --- a/async_client.cpp +++ b/async_client.cpp @@ -21,7 +21,7 @@ void handle_read(boost::system::error_code, std::size_t xfer) { assert(streambuf.size() >= xfer); - std::string command{buffers_begin(streambuf.data()), + std::string const command{buffers_begin(streambuf.data()), buffers_begin(streambuf.data()) + xfer - delimiter.length()}; streambuf.consume(xfer); diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 7c6c3ef..583d5e8 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -205,7 +205,7 @@ class client if (!error) { // Extract the newline-delimited message from the buffer. - std::string line(input_buffer_.substr(0, n - 1)); + std::string const line(input_buffer_.substr(0, n - 1)); input_buffer_.erase(0, n); // Empty messages are heartbeats and so ignored. diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index 5a7986f..63f6673 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -45,10 +45,10 @@ int main(int argc, char* argv[]) std::cout << "Enter message: "; char request[max_length]; std::cin.getline(request, max_length); - size_t request_length = std::strlen(request); + size_t const request_length = std::strlen(request); if (request_length == 0) break; - std::string data = char2esc(std::string(request, request_length)); + std::string const data = char2esc(std::string(request, request_length)); boost::asio::write(s, boost::asio::buffer(data.c_str(), data.length())); @@ -56,8 +56,8 @@ int main(int argc, char* argv[]) std::string reply; boost::asio::dynamic_string_buffer< char, std::string::traits_type, std::string::allocator_type > - sb2 = boost::asio::dynamic_buffer(reply, max_length); - boost::system::error_code ec; + const sb2 = boost::asio::dynamic_buffer(reply, max_length); + boost::system::error_code const ec; size_t reply_length{}; do diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index 1b954d1..9d3e51e 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -29,7 +29,7 @@ void session(tcp::socket sock) char data[max_length]; boost::system::error_code error; - size_t length = sock.read_some(boost::asio::buffer(data), error); + size_t const length = sock.read_some(boost::asio::buffer(data), error); if (error == boost::asio::stream_errc::eof) break; // Connection closed cleanly by peer. else if (error) diff --git a/rrcp_client.cpp b/rrcp_client.cpp index b6b150c..177a329 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -8,12 +8,23 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include "rrcp_message.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include +#include #include #include #include -#include -#include "rrcp_message.hpp" using boost::asio::ip::tcp; @@ -34,7 +45,7 @@ class rrcp_client boost::asio::post(io_context_, [this, msg]() { - bool write_in_progress = !write_msgs_.empty(); + bool const write_in_progress = !write_msgs_.empty(); write_msgs_.push_back(msg); if (!write_in_progress) { @@ -119,7 +130,6 @@ class rrcp_client }); } - private: boost::asio::io_context& io_context_; tcp::socket socket_; rrcp_message read_msg_; diff --git a/rrcp_message.hpp b/rrcp_message.hpp index b2a1ef9..83d75d4 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -21,19 +21,19 @@ class rrcp_message static constexpr std::size_t header_length = 4; static constexpr std::size_t max_msg_length = 65432; - rrcp_message() : msg_length_(0) {} + rrcp_message() {} - const char* data() const { return data_; } + [[nodiscard]] const char* data() const { return data_; } char* data() { return data_; } - std::size_t length() const { return header_length + msg_length_; } + [[nodiscard]] std::size_t length() const { return header_length + msg_length_; } - const char* body() const { return data_ + header_length; } + [[nodiscard]] const char* body() const { return data_ + header_length; } char* body() { return data_ + header_length; } - std::size_t body_length() const { return msg_length_; } + [[nodiscard]] std::size_t body_length() const { return msg_length_; } void body_length(std::size_t new_length) { @@ -64,7 +64,7 @@ class rrcp_message private: char data_[header_length + max_msg_length]; - std::size_t msg_length_; + std::size_t msg_length_{0}; }; #endif // RRCP_MESSAGE_HPP From f54030f549abc48a9b2ca5678491338bc3d6f5b2 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Mon, 10 Mar 2025 00:08:32 +0100 Subject: [PATCH 005/120] sort includes --- .clang-format | 2 +- CMakeLists.txt | 4 ++++ async_client.cpp | 1 - async_tcp_client.cpp | 6 ++++-- blocking_tcp_echo_client.cpp | 9 ++++----- blocking_tcp_echo_server.cpp | 1 - rrcp_client.cpp | 7 +++---- rrcp_message.hpp | 7 +++++-- 8 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.clang-format b/.clang-format index 8e1b25e..8e077a8 100644 --- a/.clang-format +++ b/.clang-format @@ -9,7 +9,7 @@ ColumnLimit: 80 ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 0 IndentCaseLabels: false -SortIncludes: false +SortIncludes: true AlignTrailingComments: false SpacesInAngles: true diff --git a/CMakeLists.txt b/CMakeLists.txt index b58bfd5..b524b7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.28...4.0) project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + find_package(Boost 1.87 COMPONENTS asio REQUIRED HINTS ../../../../stagedir) add_library(rrcp_helper STATIC rrcp_helper.cpp rrcp_helper.hpp) diff --git a/async_client.cpp b/async_client.cpp index 1df4f7e..cb43a7e 100644 --- a/async_client.cpp +++ b/async_client.cpp @@ -1,5 +1,4 @@ #include - #include #include #include diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 583d5e8..1dcba90 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -9,13 +9,12 @@ // #include +#include #include #include #include #include #include -#include - #include #include #include @@ -110,6 +109,7 @@ class client // particular deadline here. Instead, the connect and input actors will // update the deadline prior to each asynchronous operation. deadline_.async_wait(std::bind(&client::check_deadline, this)); + // FIXME: deadline_.async_wait([this] { check_deadline(); }); } // This function terminates all the actors to shut down the connection. It @@ -242,6 +242,7 @@ class client // Wait 10 seconds before sending the next heartbeat. heartbeat_timer_.expires_after(10s); heartbeat_timer_.async_wait(std::bind(&client::start_write, this)); + // FIXME: heartbeat_timer_.async_wait([this] { start_write(); }); } else { @@ -272,6 +273,7 @@ class client // Put the actor back to sleep. deadline_.async_wait(std::bind(&client::check_deadline, this)); + // FIXME: deadline_.async_wait([this] { check_deadline(); }); } bool stopped_ = false; diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index 63f6673..a75eb86 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -8,15 +8,14 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include "rrcp_helper.hpp" - #include #include - #include #include #include +#include "rrcp_helper.hpp" + using boost::asio::ip::tcp; enum @@ -55,8 +54,8 @@ int main(int argc, char* argv[]) // TODO: wait for endchar with timeout! std::string reply; boost::asio::dynamic_string_buffer< char, std::string::traits_type, - std::string::allocator_type > - const sb2 = boost::asio::dynamic_buffer(reply, max_length); + std::string::allocator_type > const sb2 = + boost::asio::dynamic_buffer(reply, max_length); boost::system::error_code const ec; size_t reply_length{}; diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index 9d3e51e..1036b16 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -10,7 +10,6 @@ #include #include - #include #include #include diff --git a/rrcp_client.cpp b/rrcp_client.cpp index 177a329..e776861 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -8,8 +8,6 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include "rrcp_message.hpp" - #include #include #include @@ -19,13 +17,14 @@ #include #include #include - #include -#include #include +#include #include #include +#include "rrcp_message.hpp" + using boost::asio::ip::tcp; typedef std::deque< rrcp_message > rrcp_message_queue; diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 83d75d4..b5c4233 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -21,13 +21,16 @@ class rrcp_message static constexpr std::size_t header_length = 4; static constexpr std::size_t max_msg_length = 65432; - rrcp_message() {} + rrcp_message() {} [[nodiscard]] const char* data() const { return data_; } char* data() { return data_; } - [[nodiscard]] std::size_t length() const { return header_length + msg_length_; } + [[nodiscard]] std::size_t length() const + { + return header_length + msg_length_; + } [[nodiscard]] const char* body() const { return data_ + header_length; } From f5fb98a968b93e09550654849959fa168723f6e3 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Mon, 10 Mar 2025 00:38:02 +0100 Subject: [PATCH 006/120] Add timer example --- CMakeLists.txt | 15 +++++++++++++-- timer.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 timer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b524b7b..73acfea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,14 +11,25 @@ find_package(Boost 1.87 COMPONENTS asio REQUIRED HINTS ../../../../stagedir) add_library(rrcp_helper STATIC rrcp_helper.cpp rrcp_helper.hpp) add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) -target_link_libraries(blocking_tcp_echo_client PRIVATE rrcp_helper PUBLIC Boost::asio) +target_link_libraries( + blocking_tcp_echo_client + PRIVATE rrcp_helper + PUBLIC Boost::asio +) add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) -target_link_libraries(blocking_tcp_echo_server PRIVATE rrcp_helper PUBLIC Boost::asio) +target_link_libraries( + blocking_tcp_echo_server + PRIVATE rrcp_helper + PUBLIC Boost::asio +) add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) target_link_libraries(rrcp_client PUBLIC Boost::asio) +add_executable(timer timer.cpp) +target_link_libraries(timer PUBLIC Boost::asio) + add_executable(async_client async_client.cpp) target_link_libraries(async_client PUBLIC Boost::asio) diff --git a/timer.cpp b/timer.cpp new file mode 100644 index 0000000..d48e54c --- /dev/null +++ b/timer.cpp @@ -0,0 +1,44 @@ +// +// timer.cpp +// ~~~~~~~~~ +// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +void print(const boost::system::error_code& /*e*/, boost::asio::steady_timer* t, + int* count) +{ + if (*count < 5) + { + std::cout << *count << '\n'; + ++(*count); + + t->expires_at(t->expiry() + boost::asio::chrono::seconds(1)); + t->async_wait(std::bind(print, boost::asio::placeholders::error, t, count)); + } +} + +int main() +{ + boost::asio::io_context io; + + int count = 0; + boost::asio::steady_timer t(io, boost::asio::chrono::seconds(1)); + t.async_wait(std::bind(print, boost::asio::placeholders::error, &t, &count)); + + io.run(); + + std::cout << "Final count is " << count << '\n'; + + return 0; +} From 186c3bc703c364c82c9ccf1d7086ce0a0736c7cf Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 11 Mar 2025 21:48:29 +0100 Subject: [PATCH 007/120] Add GNUmakefile for test --- GNUmakefile | 26 ++++++++++++++++++++++++++ rrcp.txt | 22 ++++++++++++++++++++++ rrcp_client.cpp | 10 ++++++++-- rrcp_helper.cpp | 3 +++ rrcp_message.hpp | 30 +++++++++++++++++------------- 5 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 GNUmakefile create mode 100644 rrcp.txt diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..d7eb86b --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,26 @@ +# Standard stuff + +.SUFFIXES: + +MAKEFLAGS+= --no-builtin-rules +MAKEFLAGS+= --warn-undefined-variables + +.PHONY: all format test lint + +all: build + ninja -C build + +build: CMakeLists.txt + cmake -S . -B $@ + +lint: build + run-clang-tidy -p build -check='-*,readability-use-std-min-max,misc-include-cleaner' -fix rrcp_*.cpp + +test: all + -killall blocking_tcp_echo_server + build/blocking_tcp_echo_server 8000 & + cat rrcp.txt | build/rrcp_client localhost 8000 + +format: .clang-format + git ls-files ::*.cpp ::*.hpp | xargs clang-format -i + gersemi -i CMakeLists.txt diff --git a/rrcp.txt b/rrcp.txt new file mode 100644 index 0000000..c8dfa0a --- /dev/null +++ b/rrcp.txt @@ -0,0 +1,22 @@ +// GET-request TU SET-request TU GET-request TU: +GFRQ;MOD SFRQ10000000;MOD13;BW80000 GFRQ;MOD +// GET-response TU SET-response TU GET-response TU: +gFRQ18000000;MOD12 s5BW gFRQ18000000;MOD12 +// NOTE: +// There is an error within the BW command, the complete SET-request TU is cancelled. +// The GET-request TU is replied by the corresponding GET-response TU. +M:WF.FF.Main 123456 T Octet 1 // without Logical Address: +M:WF.FF.Main 123456 t +M:Bit L:1 123456 G Octet +M:Audio SOctet // without optionl parts +M:Radio SString"\rhallo\tworld\n" +M:Log SStruct1,-1,3.14 // multiple parameters +M:MultilCmd S Octet 1;Long-1;String"hallo world\n";Struct 1,+1,+3.14 // multiple commands's +M:Test 123456 S FREQ123456;MOD12;LOGIN"user","pasword" G FREQ;MOD;STATUS // multiple TU +M:Test gFREQ180000;MOD12 s5BW gFREQ180000;MOD12;STATUS"Running" // TU partly failed +M:eRADIO S FREQUENCY 123456789 +E:12 // MU error +M:RADIO T FREQUENCY 1 +M:RADIO t +M:RADIO d FREQUENCY 123456789 // trap data +M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data diff --git a/rrcp_client.cpp b/rrcp_client.cpp index e776861..ac25b3b 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -14,10 +14,11 @@ #include #include #include -#include #include -#include +#include +#include #include +#include #include #include #include @@ -153,15 +154,20 @@ int main(int argc, char* argv[]) std::thread t([&io_context]() { io_context.run(); }); + std::this_thread::sleep_for(std::chrono::seconds(1)); + int i{}; char line[rrcp_message::max_msg_length + 1]; while (std::cin.getline(line, rrcp_message::max_msg_length + 1)) { + std::cout << ++i << line << '\n'; + rrcp_message msg; msg.body_length(std::strlen(line)); std::memcpy(msg.body(), line, msg.body_length()); msg.encode_header(); c.write(msg); } + std::this_thread::sleep_for(std::chrono::seconds(1)); c.close(); t.join(); diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index fe211da..9e6a1cc 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -1,5 +1,8 @@ #include "rrcp_helper.hpp" +#include +#include + constexpr const char ESC = 0x1B; constexpr const char REPLACE_LF = 0x01; constexpr const char REPLACE_CR = 0x02; diff --git a/rrcp_message.hpp b/rrcp_message.hpp index b5c4233..312b65a 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -11,9 +11,11 @@ #ifndef RRCP_MESSAGE_HPP #define RRCP_MESSAGE_HPP +#include #include #include #include +#include class rrcp_message { @@ -23,32 +25,34 @@ class rrcp_message rrcp_message() {} - [[nodiscard]] const char* data() const { return data_; } + [[nodiscard]] const char* data() const { return data_.data(); } - char* data() { return data_; } + char* data() { return data_.data(); } [[nodiscard]] std::size_t length() const { return header_length + msg_length_; } - [[nodiscard]] const char* body() const { return data_ + header_length; } + [[nodiscard]] const char* body() const + { + return data_.data() + header_length; + } - char* body() { return data_ + header_length; } + char* body() { return data_.data() + header_length; } [[nodiscard]] std::size_t body_length() const { return msg_length_; } void body_length(std::size_t new_length) { msg_length_ = new_length; - if (msg_length_ > max_msg_length) msg_length_ = max_msg_length; + msg_length_ = std::min(msg_length_, max_msg_length); } bool decode_header() { - char header[header_length + 1] = ""; - std::strncat(header, data_, header_length); - msg_length_ = std::atoi(header); + std::string header(data_.data(), header_length); + msg_length_ = std::stoi(header); if (msg_length_ > max_msg_length) { msg_length_ = 0; @@ -59,14 +63,14 @@ class rrcp_message void encode_header() { - char header[header_length + 1] = ""; - std::snprintf( - header, header_length + 1, "%4d", static_cast< int >(msg_length_)); - std::memcpy(data_, header, header_length); + std::array< char, header_length + 1 > header; + std::snprintf(header.data(), header_length + 1, "%4d", + static_cast< int >(msg_length_)); + std::memcpy(data_.data(), header.data(), header_length); } private: - char data_[header_length + max_msg_length]; + std::array< char, header_length + max_msg_length > data_; std::size_t msg_length_{0}; }; From d70909c5e0bd133ccd751e103463712ba9d41fb0 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 11 Mar 2025 23:13:25 +0100 Subject: [PATCH 008/120] Trim comments and WS --- GNUmakefile | 11 ++++++++--- rrcp_client.cpp | 34 +++++++++++++++++++++++++++------- rrcp_helper.cpp | 4 ++-- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index d7eb86b..efb7131 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -5,16 +5,21 @@ MAKEFLAGS+= --no-builtin-rules MAKEFLAGS+= --warn-undefined-variables -.PHONY: all format test lint +.PHONY: all format test check distclean all: build ninja -C build +distclean: + rm -rf build + build: CMakeLists.txt cmake -S . -B $@ -lint: build - run-clang-tidy -p build -check='-*,readability-use-std-min-max,misc-include-cleaner' -fix rrcp_*.cpp +check: build + run-clang-tidy -p build -fix \ + -check='-*,readability-use-std-min-max,misc-include-cleaner,cppcoreguidelines-init-variables' \ + rrcp_*.cpp test: all -killall blocking_tcp_echo_server diff --git a/rrcp_client.cpp b/rrcp_client.cpp index ac25b3b..659d516 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -8,12 +8,14 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include #include #include #include +#include #include #include #include @@ -56,6 +58,7 @@ class rrcp_client void close() { + boost::asio::post(io_context_, [this]() { write_msgs_.clear(); }); boost::asio::post(io_context_, [this]() { socket_.close(); }); } @@ -91,6 +94,12 @@ class rrcp_client void do_read_body() { + // std::string reply; + // boost::asio::dynamic_string_buffer< char, std::string::traits_type, + // std::string::allocator_type > const dsb = + // boost::asio::dynamic_buffer(reply, read_msg_.body_length()); + // boost::asio::async_read_until(socket_, dsb, '\n', + boost::asio::async_read(socket_, boost::asio::buffer(read_msg_.body(), read_msg_.body_length()), [this](boost::system::error_code ec, std::size_t /*length*/) @@ -154,20 +163,31 @@ int main(int argc, char* argv[]) std::thread t([&io_context]() { io_context.run(); }); - std::this_thread::sleep_for(std::chrono::seconds(1)); int i{}; - char line[rrcp_message::max_msg_length + 1]; - while (std::cin.getline(line, rrcp_message::max_msg_length + 1)) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + for (std::string line; std::getline(std::cin, line);) { - std::cout << ++i << line << '\n'; + std::cerr << ++i << '\t' << line << '\n'; + + std::string::size_type sz = line.find_first_of("//"); + if ((sz != std::string::npos)) + { + line.resize(sz); + } + + boost::trim_right(line); + if (line.empty()) + { + continue; + } rrcp_message msg; - msg.body_length(std::strlen(line)); - std::memcpy(msg.body(), line, msg.body_length()); + msg.body_length(line.length()); + std::memcpy(msg.body(), line.c_str(), msg.body_length()); msg.encode_header(); c.write(msg); } - std::this_thread::sleep_for(std::chrono::seconds(1)); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); c.close(); t.join(); diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 9e6a1cc..8f8a02c 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -24,7 +24,7 @@ std::string esc2char(const std::string& data) { std::string message; size_t const len = data.size(); - char c; + char c = 0; unsigned int i = 0; while (i < len) { @@ -87,7 +87,7 @@ std::string char2esc(const std::string& data) { std::string message; size_t const len = data.size(); - char c; + char c = 0; unsigned int i = 0; while (i < len) From c3c965e515eca898ccea09cd492f213366b0447a Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 12 Mar 2025 09:42:10 +0100 Subject: [PATCH 009/120] Use msg length as hex --- rrcp_client.cpp | 8 ++-- rrcp_message.hpp | 9 +++-- rrcp_message_buffer.hpp | 81 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 rrcp_message_buffer.hpp diff --git a/rrcp_client.cpp b/rrcp_client.cpp index 659d516..d363f79 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -147,6 +147,8 @@ class rrcp_client int main(int argc, char* argv[]) { + using namespace std::chrono_literals; + try { if (argc != 3) @@ -164,12 +166,12 @@ int main(int argc, char* argv[]) std::thread t([&io_context]() { io_context.run(); }); int i{}; - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(10ms); for (std::string line; std::getline(std::cin, line);) { std::cerr << ++i << '\t' << line << '\n'; - std::string::size_type sz = line.find_first_of("//"); + std::string::size_type sz = line.find("//"); if ((sz != std::string::npos)) { line.resize(sz); @@ -187,7 +189,7 @@ int main(int argc, char* argv[]) msg.encode_header(); c.write(msg); } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(10ms); c.close(); t.join(); diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 312b65a..ee3e828 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -16,6 +16,7 @@ #include #include #include +// XXX #include class rrcp_message { @@ -25,6 +26,7 @@ class rrcp_message rrcp_message() {} + // TODO: or better const &std::string_view? [[nodiscard]] const char* data() const { return data_.data(); } char* data() { return data_.data(); } @@ -34,6 +36,7 @@ class rrcp_message return header_length + msg_length_; } + // TODO: or better const &std::string_view? [[nodiscard]] const char* body() const { return data_.data() + header_length; @@ -52,7 +55,7 @@ class rrcp_message bool decode_header() { std::string header(data_.data(), header_length); - msg_length_ = std::stoi(header); + msg_length_ = std::stoul(header, nullptr, 16); if (msg_length_ > max_msg_length) { msg_length_ = 0; @@ -64,8 +67,8 @@ class rrcp_message void encode_header() { std::array< char, header_length + 1 > header; - std::snprintf(header.data(), header_length + 1, "%4d", - static_cast< int >(msg_length_)); + std::snprintf(header.data(), header_length + 1, "%4x", + static_cast< uint16_t >(msg_length_)); std::memcpy(data_.data(), header.data(), header_length); } diff --git a/rrcp_message_buffer.hpp b/rrcp_message_buffer.hpp new file mode 100644 index 0000000..6376f37 --- /dev/null +++ b/rrcp_message_buffer.hpp @@ -0,0 +1,81 @@ +// +// rrcp_message_buffer.hpp +// ~~~~~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef RRCP_MESSAGE_HPP +#define RRCP_MESSAGE_HPP + +#include +#include +#include +#include +#include +#include + +class rrcp_message +{ + public: + static constexpr std::size_t header_length = 4; + static constexpr std::size_t max_msg_length = 65432; + + rrcp_message() {} + + // TODO: or better const &std::string_view? + [[nodiscard]] const std::string_view data() const { return data_; } + + std::string data() { return data_; } + + [[nodiscard]] std::size_t length() const + { + return header_length + msg_length_; + } + + // TODO: or better const &std::string_view? + [[nodiscard]] const std::string body() const + { + return data_.substr(header_length); + } + + std::string body() { return data_.substr(header_length); } + + [[nodiscard]] std::size_t body_length() const { return msg_length_; } + + void body_length(std::size_t new_length) + { + msg_length_ = new_length; + msg_length_ = std::min(msg_length_, max_msg_length); + } + + bool decode_header() + { + std::string header(data_, header_length); + msg_length_ = std::stoul(header, nullptr, 16); + if (msg_length_ > max_msg_length) + { + msg_length_ = 0; + return false; + } + return true; + } + + void encode_header() + { + std::array< char, header_length + 1 > header; + std::snprintf(header.data(), header_length + 1, "%4x", + static_cast< uint16_t >(msg_length_)); + data_.substr(0, header_length) = std::string(header.data(), header_length); + } + + private: + std::array< char, header_length + max_msg_length > data_; + // TODO: used std::string data_; + std::size_t msg_length_{0}; +}; + +#endif // RRCP_MESSAGE_HPP From 7f0f10191c14d9f25ee8bb097f1cd5eaf7e5a368 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 12 Mar 2025 15:35:40 +0100 Subject: [PATCH 010/120] Add esc test --- CMakeLists.txt | 2 +- GNUmakefile | 5 ++++- rrcp.txt | 4 ++-- rrcp_client.cpp | 31 ++++++++++++++++++++------- rrcp_helper.cpp | 15 +++++++------- rrcp_helper.hpp | 4 ++-- rrcp_message.hpp | 54 ++++++++++++++++++++++++++++++++++++++---------- 7 files changed, 84 insertions(+), 31 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 73acfea..758a631 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ target_link_libraries( ) add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) -target_link_libraries(rrcp_client PUBLIC Boost::asio) +target_link_libraries(rrcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) add_executable(timer timer.cpp) target_link_libraries(timer PUBLIC Boost::asio) diff --git a/GNUmakefile b/GNUmakefile index efb7131..f161414 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -17,8 +17,11 @@ build: CMakeLists.txt cmake -S . -B $@ check: build + run-clang-tidy -p build -check='-*,bugprone-*,hicpp-*,modernize-*,misc-*' rrcp_*.cpp + +fix: build run-clang-tidy -p build -fix \ - -check='-*,readability-use-std-min-max,misc-include-cleaner,cppcoreguidelines-init-variables' \ + -check='-*,readability-use-std-min-max,-misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,modernize-*' \ rrcp_*.cpp test: all diff --git a/rrcp.txt b/rrcp.txt index c8dfa0a..574e83d 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -1,3 +1,4 @@ +M:Radio SString"\rHallo\tWorld\n" // GET-request TU SET-request TU GET-request TU: GFRQ;MOD SFRQ10000000;MOD13;BW80000 GFRQ;MOD // GET-response TU SET-response TU GET-response TU: @@ -9,9 +10,8 @@ M:WF.FF.Main 123456 T Octet 1 // without Logical Address: M:WF.FF.Main 123456 t M:Bit L:1 123456 G Octet M:Audio SOctet // without optionl parts -M:Radio SString"\rhallo\tworld\n" M:Log SStruct1,-1,3.14 // multiple parameters -M:MultilCmd S Octet 1;Long-1;String"hallo world\n";Struct 1,+1,+3.14 // multiple commands's +M:MultilCmd S Octet 1;Long-1;String"Hallo World\n";Struct 1,+1,+3.14 // multiple commands's M:Test 123456 S FREQ123456;MOD12;LOGIN"user","pasword" G FREQ;MOD;STATUS // multiple TU M:Test gFREQ180000;MOD12 s5BW gFREQ180000;MOD12;STATUS"Running" // TU partly failed M:eRADIO S FREQUENCY 123456789 diff --git a/rrcp_client.cpp b/rrcp_client.cpp index d363f79..f9e81b8 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -8,29 +8,31 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +#include #include #include #include #include #include #include -#include #include #include +#include #include #include #include #include #include +#include #include +#include #include #include "rrcp_message.hpp" using boost::asio::ip::tcp; -typedef std::deque< rrcp_message > rrcp_message_queue; +using rrcp_message_queue = std::deque< rrcp_message >; class rrcp_client { @@ -104,7 +106,7 @@ class rrcp_client boost::asio::buffer(read_msg_.body(), read_msg_.body_length()), [this](boost::system::error_code ec, std::size_t /*length*/) { - if (!ec) + if (!ec && read_msg_.decode_body()) { std::cout.write(read_msg_.body(), read_msg_.body_length()); std::cout << "\n"; @@ -145,9 +147,10 @@ class rrcp_client rrcp_message_queue write_msgs_; }; -int main(int argc, char* argv[]) +auto main(int argc, char* argv[]) -> int { using namespace std::chrono_literals; + using namespace std::string_literals; try { @@ -165,8 +168,21 @@ int main(int argc, char* argv[]) std::thread t([&io_context]() { io_context.run(); }); + //================================================================ + std::string binary = + "AB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD"s; + std::cerr << binary.length() << ' ' << std::quoted(binary) << '\n'; + auto quoted = char2esc(binary); + std::cerr << quoted.length() << ' ' << std::quoted(quoted) << '\n'; + + assert(binary == esc2char(quoted)); + assert(binary.length() < quoted.length()); + assert(binary.length() == 26); + assert(quoted.length() == 29); + //================================================================ + int i{}; - std::this_thread::sleep_for(10ms); + std::this_thread::sleep_for(100ms); for (std::string line; std::getline(std::cin, line);) { std::cerr << ++i << '\t' << line << '\n'; @@ -186,10 +202,11 @@ int main(int argc, char* argv[]) rrcp_message msg; msg.body_length(line.length()); std::memcpy(msg.body(), line.c_str(), msg.body_length()); + msg.encode_body(); msg.encode_header(); c.write(msg); } - std::this_thread::sleep_for(10ms); + std::this_thread::sleep_for(100ms); c.close(); t.join(); diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 8f8a02c..3b7a441 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -1,6 +1,7 @@ #include "rrcp_helper.hpp" #include +#include #include constexpr const char ESC = 0x1B; @@ -8,10 +9,10 @@ constexpr const char REPLACE_LF = 0x01; constexpr const char REPLACE_CR = 0x02; constexpr const char REPLACE_ESC = 0x03; -constexpr const char SP = 0x20; constexpr const char LF = 0x0A; constexpr const char CR = 0x0D; +// constexpr const char SP = 0x20; // constexpr const char DOT = 0x2E; // constexpr const char SEMICOLON = 0x3B; // constexpr const char COMMA = 0x2C; @@ -20,7 +21,7 @@ constexpr const char CR = 0x0D; // constexpr const char BACKSLASH = '\\'; // constexpr const char COLON = 0x3A; -std::string esc2char(const std::string& data) +auto esc2char(const std::string& data) -> std::string { std::string message; size_t const len = data.size(); @@ -64,26 +65,26 @@ std::string esc2char(const std::string& data) } else { - //"Parser Error contains unexpected ESC character" + std::cout << "esc2char: Error contains unexpected ESC character!\n"; return ""; } } else { - //"Parser Error message ends with escape character" + std::cout << "esc2char: Error message ends with escape character!\n"; return ""; } } - // enter next character + // append current character message += c; - // next character + // continue with next character i++; } return message; } -std::string char2esc(const std::string& data) +auto char2esc(const std::string& data) -> std::string { std::string message; size_t const len = data.size(); diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index 0569a18..7f2edb0 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -11,7 +11,7 @@ * * @return message string like 'M:IBIT SStart' */ -extern std::string esc2char(const std::string& data); +extern auto esc2char(const std::string& data) -> std::string; /** * @brief Replaces LF, CR with Escape sequence @@ -20,4 +20,4 @@ extern std::string esc2char(const std::string& data); * * @return translated data */ -extern std::string char2esc(const std::string& data); +extern auto char2esc(const std::string& data) -> std::string; diff --git a/rrcp_message.hpp b/rrcp_message.hpp index ee3e828..61d080e 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -15,8 +15,11 @@ #include #include #include +#include #include -// XXX #include +// #include + +#include "rrcp_helper.hpp" class rrcp_message { @@ -24,56 +27,85 @@ class rrcp_message static constexpr std::size_t header_length = 4; static constexpr std::size_t max_msg_length = 65432; - rrcp_message() {} + rrcp_message() = default; // TODO: or better const &std::string_view? - [[nodiscard]] const char* data() const { return data_.data(); } + [[nodiscard]] auto data() const -> const char* { return data_.data(); } - char* data() { return data_.data(); } + auto data() -> char* { return data_.data(); } - [[nodiscard]] std::size_t length() const + [[nodiscard]] auto length() const -> std::size_t { return header_length + msg_length_; } // TODO: or better const &std::string_view? - [[nodiscard]] const char* body() const + [[nodiscard]] auto body() const -> const char* { return data_.data() + header_length; } - char* body() { return data_.data() + header_length; } + auto body() -> char* { return data_.data() + header_length; } - [[nodiscard]] std::size_t body_length() const { return msg_length_; } + [[nodiscard]] auto body_length() const -> std::size_t { return msg_length_; } void body_length(std::size_t new_length) { msg_length_ = new_length; msg_length_ = std::min(msg_length_, max_msg_length); + std::cerr << "body_length(" << msg_length_ << ")\n"; + } + + auto decode_body() -> bool + { + // TODO: or better const &std::string_view? + auto result = esc2char(std::string(body(), msg_length_)); + if (result.length() != msg_length_) + { + std::cerr << result << '\n'; + + body_length(result.length()); + std::memcpy(body(), result.c_str(), msg_length_); + } + + return result.empty(); } - bool decode_header() + auto decode_header() -> bool { std::string header(data_.data(), header_length); msg_length_ = std::stoul(header, nullptr, 16); if (msg_length_ > max_msg_length) { + std::cerr << "Invalid msg_length!\n"; + msg_length_ = 0; return false; } return true; } + void encode_body() + { + // TODO: or better const &std::string_view? + auto msg = char2esc(std::string(body(), msg_length_)); + if (msg.length() != msg_length_) + { + body_length(msg.length()); + std::memcpy(body(), msg.c_str(), msg_length_); + } + } + void encode_header() { - std::array< char, header_length + 1 > header; + std::array< char, header_length + 1 > header{}; std::snprintf(header.data(), header_length + 1, "%4x", static_cast< uint16_t >(msg_length_)); std::memcpy(data_.data(), header.data(), header_length); } private: - std::array< char, header_length + max_msg_length > data_; + std::array< char, header_length + max_msg_length > data_{}; std::size_t msg_length_{0}; }; From d7fc083008b9b201f6872ce3ab3f497276eaea5e Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 12 Mar 2025 17:03:05 +0100 Subject: [PATCH 011/120] The final cut --- .clang-format | 2 +- GNUmakefile | 6 +++--- rrcp_client.cpp | 25 ++++++++++--------------- rrcp_helper.cpp | 17 +++++++++-------- rrcp_message.hpp | 9 ++++----- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/.clang-format b/.clang-format index 8e077a8..cec4ed8 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,5 @@ BasedOnStyle: Google -Standard: Cpp03 +Standard: Auto AlignAfterOpenBracket: false AlignEscapedNewlinesLeft: true AlwaysBreakAfterDefinitionReturnType: None diff --git a/GNUmakefile b/GNUmakefile index f161414..b415997 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -16,10 +16,10 @@ distclean: build: CMakeLists.txt cmake -S . -B $@ -check: build - run-clang-tidy -p build -check='-*,bugprone-*,hicpp-*,modernize-*,misc-*' rrcp_*.cpp +check: all + run-clang-tidy -p build -check='-*,bugprone-*,hicpp-*,modernize-*,misc-*,-misc-no-recursion' rrcp_*.cpp -fix: build +fix: all run-clang-tidy -p build -fix \ -check='-*,readability-use-std-min-max,-misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,modernize-*' \ rrcp_*.cpp diff --git a/rrcp_client.cpp b/rrcp_client.cpp index f9e81b8..913a6d4 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -18,7 +18,7 @@ #include #include #include -#include +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -28,6 +28,7 @@ #include #include +#include "rrcp_helper.hpp" #include "rrcp_message.hpp" using boost::asio::ip::tcp; @@ -96,18 +97,13 @@ class rrcp_client void do_read_body() { - // std::string reply; - // boost::asio::dynamic_string_buffer< char, std::string::traits_type, - // std::string::allocator_type > const dsb = - // boost::asio::dynamic_buffer(reply, read_msg_.body_length()); - // boost::asio::async_read_until(socket_, dsb, '\n', - boost::asio::async_read(socket_, boost::asio::buffer(read_msg_.body(), read_msg_.body_length()), [this](boost::system::error_code ec, std::size_t /*length*/) { if (!ec && read_msg_.decode_body()) { + // NOLINTNEXTLINE(bugprone-narrowing-conversions) std::cout.write(read_msg_.body(), read_msg_.body_length()); std::cout << "\n"; do_read_header(); @@ -170,24 +166,22 @@ auto main(int argc, char* argv[]) -> int //================================================================ std::string binary = - "AB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD"s; + "\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"s; std::cerr << binary.length() << ' ' << std::quoted(binary) << '\n'; auto quoted = char2esc(binary); std::cerr << quoted.length() << ' ' << std::quoted(quoted) << '\n'; assert(binary == esc2char(quoted)); assert(binary.length() < quoted.length()); - assert(binary.length() == 26); - assert(quoted.length() == 29); + assert(binary.length() == 28); + assert(quoted.length() == 33); //================================================================ int i{}; - std::this_thread::sleep_for(100ms); + std::this_thread::sleep_for(100ms); // NOLINT(misc-include-cleaner) for (std::string line; std::getline(std::cin, line);) { - std::cerr << ++i << '\t' << line << '\n'; - - std::string::size_type sz = line.find("//"); + const std::string::size_type sz = line.find("//"); if ((sz != std::string::npos)) { line.resize(sz); @@ -199,6 +193,7 @@ auto main(int argc, char* argv[]) -> int continue; } + std::cerr << ++i << '\t' << line << '\n'; rrcp_message msg; msg.body_length(line.length()); std::memcpy(msg.body(), line.c_str(), msg.body_length()); @@ -206,7 +201,7 @@ auto main(int argc, char* argv[]) -> int msg.encode_header(); c.write(msg); } - std::this_thread::sleep_for(100ms); + std::this_thread::sleep_for(100ms); // NOLINT(misc-include-cleaner) c.close(); t.join(); diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 3b7a441..f9cad80 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -9,8 +9,8 @@ constexpr const char REPLACE_LF = 0x01; constexpr const char REPLACE_CR = 0x02; constexpr const char REPLACE_ESC = 0x03; -constexpr const char LF = 0x0A; -constexpr const char CR = 0x0D; +constexpr const char LF = 0x0A; // \n +constexpr const char CR = 0x0D; // \r // constexpr const char SP = 0x20; // constexpr const char DOT = 0x2E; @@ -26,16 +26,17 @@ auto esc2char(const std::string& data) -> std::string std::string message; size_t const len = data.size(); char c = 0; - unsigned int i = 0; + size_t i = 0; while (i < len) { // get next char c = data[i]; - // end mark found, the message is complete, the - // CR is not needed anymore + // end mark found, the message is complete. + // TODO: Why is the CR not needed anymore? CK if (c == CR) { + // XXX message += c; return message; } @@ -65,13 +66,13 @@ auto esc2char(const std::string& data) -> std::string } else { - std::cout << "esc2char: Error contains unexpected ESC character!\n"; + std::cerr << "esc2char: Error contains unexpected ESC character!\n"; return ""; } } else { - std::cout << "esc2char: Error message ends with escape character!\n"; + std::cerr << "esc2char: Error message ends with escape character!\n"; return ""; } } @@ -89,7 +90,7 @@ auto char2esc(const std::string& data) -> std::string std::string message; size_t const len = data.size(); char c = 0; - unsigned int i = 0; + size_t i = 0; while (i < len) { diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 61d080e..70df76c 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -68,12 +68,12 @@ class rrcp_message std::memcpy(body(), result.c_str(), msg_length_); } - return result.empty(); + return !result.empty(); } auto decode_header() -> bool { - std::string header(data_.data(), header_length); + const std::string header(data_.data(), header_length); msg_length_ = std::stoul(header, nullptr, 16); if (msg_length_ > max_msg_length) { @@ -98,9 +98,8 @@ class rrcp_message void encode_header() { - std::array< char, header_length + 1 > header{}; - std::snprintf(header.data(), header_length + 1, "%4x", - static_cast< uint16_t >(msg_length_)); + std::string header = + std::format("{:04x}", static_cast< uint16_t >(msg_length_)); std::memcpy(data_.data(), header.data(), header_length); } From e87ef0acaec46e35d6000b7200c6b0ad331b34fe Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 12 Mar 2025 17:06:27 +0100 Subject: [PATCH 012/120] Cleanup --- rrcp_message_buffer.hpp | 81 ----------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 rrcp_message_buffer.hpp diff --git a/rrcp_message_buffer.hpp b/rrcp_message_buffer.hpp deleted file mode 100644 index 6376f37..0000000 --- a/rrcp_message_buffer.hpp +++ /dev/null @@ -1,81 +0,0 @@ -// -// rrcp_message_buffer.hpp -// ~~~~~~~~~~~~~~~~~~~~~~~ -// -// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef RRCP_MESSAGE_HPP -#define RRCP_MESSAGE_HPP - -#include -#include -#include -#include -#include -#include - -class rrcp_message -{ - public: - static constexpr std::size_t header_length = 4; - static constexpr std::size_t max_msg_length = 65432; - - rrcp_message() {} - - // TODO: or better const &std::string_view? - [[nodiscard]] const std::string_view data() const { return data_; } - - std::string data() { return data_; } - - [[nodiscard]] std::size_t length() const - { - return header_length + msg_length_; - } - - // TODO: or better const &std::string_view? - [[nodiscard]] const std::string body() const - { - return data_.substr(header_length); - } - - std::string body() { return data_.substr(header_length); } - - [[nodiscard]] std::size_t body_length() const { return msg_length_; } - - void body_length(std::size_t new_length) - { - msg_length_ = new_length; - msg_length_ = std::min(msg_length_, max_msg_length); - } - - bool decode_header() - { - std::string header(data_, header_length); - msg_length_ = std::stoul(header, nullptr, 16); - if (msg_length_ > max_msg_length) - { - msg_length_ = 0; - return false; - } - return true; - } - - void encode_header() - { - std::array< char, header_length + 1 > header; - std::snprintf(header.data(), header_length + 1, "%4x", - static_cast< uint16_t >(msg_length_)); - data_.substr(0, header_length) = std::string(header.data(), header_length); - } - - private: - std::array< char, header_length + max_msg_length > data_; - // TODO: used std::string data_; - std::size_t msg_length_{0}; -}; - -#endif // RRCP_MESSAGE_HPP From 1520c960084507ea8faa6c4bc37b91ed024db7f2 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 12 Mar 2025 21:42:24 +0100 Subject: [PATCH 013/120] Prevent more clang-tidy warnings --- GNUmakefile | 6 ++--- async_client.cpp | 2 +- async_tcp_client.cpp | 48 +++++++++++++++++++++++++++--------- blocking_tcp_echo_client.cpp | 7 ++++-- blocking_tcp_echo_server.cpp | 6 ++++- rrcp_client.cpp | 2 +- timer.cpp | 12 +++++---- 7 files changed, 58 insertions(+), 25 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index b415997..6615d34 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -17,12 +17,12 @@ build: CMakeLists.txt cmake -S . -B $@ check: all - run-clang-tidy -p build -check='-*,bugprone-*,hicpp-*,modernize-*,misc-*,-misc-no-recursion' rrcp_*.cpp + run-clang-tidy -p build -check='-*,bugprone-*,hicpp-*,modernize-*,misc-*,-misc-no-recursion' *.cpp fix: all run-clang-tidy -p build -fix \ - -check='-*,readability-use-std-min-max,-misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,modernize-*' \ - rrcp_*.cpp + -check='-*,readability-use-std-min-max,misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,-modernize-*,-modernize-avoid-bind,readability-braces-around-statements,hicpp-named-parameter' \ + *.cpp test: all -killall blocking_tcp_echo_server diff --git a/async_client.cpp b/async_client.cpp index cb43a7e..96bbc21 100644 --- a/async_client.cpp +++ b/async_client.cpp @@ -32,7 +32,7 @@ void handle_read(boost::system::error_code, std::size_t xfer) boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); } -int main() +auto main() -> int { acceptor.async_accept(socket1, noop); socket2.async_connect(acceptor.local_endpoint(), noop); diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 1dcba90..a4388da 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -108,8 +108,8 @@ class client // Start the deadline actor. You will note that we're not setting any // particular deadline here. Instead, the connect and input actors will // update the deadline prior to each asynchronous operation. - deadline_.async_wait(std::bind(&client::check_deadline, this)); - // FIXME: deadline_.async_wait([this] { check_deadline(); }); + deadline_.async_wait( + [this](const boost::system::error_code& /*e*/) { check_deadline(); }); } // This function terminates all the actors to shut down the connection. It @@ -137,6 +137,8 @@ class client // Start the asynchronous connect operation. socket_.async_connect(endpoint_iter->endpoint(), std::bind(&client::handle_connect, this, _1, endpoint_iter)); + // XXX [this, endpoint_iter](auto && PH1) { + // handle_connect(std::forward(PH1), endpoint_iter); }); } else { @@ -148,7 +150,10 @@ class client void handle_connect(const boost::system::error_code& error, tcp::resolver::results_type::iterator endpoint_iter) { - if (stopped_) return; + if (stopped_) + { + return; + } // The async_connect() function automatically opens the socket at the // start of the asynchronous operation. If the socket is closed at this @@ -196,11 +201,17 @@ class client boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), '\n', std::bind(&client::handle_read, this, _1, _2)); + // XXX [this](auto && PH1, auto && PH2) { + // handle_read(std::forward(PH1), + // std::forward(PH2)); }); } void handle_read(const boost::system::error_code& error, std::size_t n) { - if (stopped_) return; + if (stopped_) + { + return; + } if (!error) { @@ -226,23 +237,32 @@ class client void start_write() { - if (stopped_) return; + if (stopped_) + { + return; + } // Start an asynchronous operation to send a heartbeat message. boost::asio::async_write(socket_, boost::asio::buffer("\n", 1), std::bind(&client::handle_write, this, _1)); + // XXX [this](const boost::system::error_code& /*e*/, PH1) { + // handle_write(std::forward(PH1)); }); } void handle_write(const boost::system::error_code& error) { - if (stopped_) return; + if (stopped_) + { + return; + } if (!error) { // Wait 10 seconds before sending the next heartbeat. heartbeat_timer_.expires_after(10s); - heartbeat_timer_.async_wait(std::bind(&client::start_write, this)); - // FIXME: heartbeat_timer_.async_wait([this] { start_write(); }); + // heartbeat_timer_.async_wait(std::bind(&client::start_write, this)); + heartbeat_timer_.async_wait( + [this](const boost::system::error_code& /*e*/) { start_write(); }); } else { @@ -254,7 +274,10 @@ class client void check_deadline() { - if (stopped_) return; + if (stopped_) + { + return; + } // Check whether the deadline has passed. We compare the deadline // against the current time since a new asynchronous operation may have @@ -272,8 +295,9 @@ class client } // Put the actor back to sleep. - deadline_.async_wait(std::bind(&client::check_deadline, this)); - // FIXME: deadline_.async_wait([this] { check_deadline(); }); + // deadline_.async_wait(std::bind(&client::check_deadline, this)); + deadline_.async_wait( + [this](const boost::system::error_code& /*e*/) { check_deadline(); }); } bool stopped_ = false; @@ -284,7 +308,7 @@ class client steady_timer heartbeat_timer_; }; -int main(int argc, char* argv[]) +auto main(int argc, char* argv[]) -> int { try { diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index a75eb86..b9d02dd 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -23,7 +23,7 @@ enum max_length = 1024 }; -int main(int argc, char* argv[]) +auto main(int argc, char* argv[]) -> int { try { @@ -45,7 +45,10 @@ int main(int argc, char* argv[]) char request[max_length]; std::cin.getline(request, max_length); size_t const request_length = std::strlen(request); - if (request_length == 0) break; + if (request_length == 0) + { + break; + } std::string const data = char2esc(std::string(request, request_length)); diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index 1036b16..bdab8b5 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -30,9 +30,13 @@ void session(tcp::socket sock) boost::system::error_code error; size_t const length = sock.read_some(boost::asio::buffer(data), error); if (error == boost::asio::stream_errc::eof) + { break; // Connection closed cleanly by peer. + } else if (error) + { throw boost::system::system_error(error); // Some other error. + } boost::asio::write(sock, boost::asio::buffer(data, length)); } @@ -54,7 +58,7 @@ void server(boost::asio::io_context& io_context, unsigned short port) } } -int main(int argc, char* argv[]) +auto main(int argc, char* argv[]) -> int { try { diff --git a/rrcp_client.cpp b/rrcp_client.cpp index 913a6d4..f455450 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -140,7 +140,7 @@ class rrcp_client boost::asio::io_context& io_context_; tcp::socket socket_; rrcp_message read_msg_; - rrcp_message_queue write_msgs_; + rrcp_message_queue write_msgs_{}; }; auto main(int argc, char* argv[]) -> int diff --git a/timer.cpp b/timer.cpp index d48e54c..3e2f71a 100644 --- a/timer.cpp +++ b/timer.cpp @@ -15,8 +15,7 @@ #include #include -void print(const boost::system::error_code& /*e*/, boost::asio::steady_timer* t, - int* count) +void print(boost::asio::steady_timer* t, int* count) { if (*count < 5) { @@ -24,17 +23,20 @@ void print(const boost::system::error_code& /*e*/, boost::asio::steady_timer* t, ++(*count); t->expires_at(t->expiry() + boost::asio::chrono::seconds(1)); - t->async_wait(std::bind(print, boost::asio::placeholders::error, t, count)); + t->async_wait([t, count](const boost::system::error_code& /*e*/) + { print(t, count); }); } } -int main() +auto main() -> int { boost::asio::io_context io; int count = 0; boost::asio::steady_timer t(io, boost::asio::chrono::seconds(1)); - t.async_wait(std::bind(print, boost::asio::placeholders::error, &t, &count)); + t.async_wait( + [capture0 = &t, capture1 = &count](const boost::system::error_code& /*e*/) + { print(capture0, capture1); }); io.run(); From a88110a92a19cf56ad7aa6f28448f6489acf72ea Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 12 Mar 2025 22:05:09 +0100 Subject: [PATCH 014/120] Fix misc_include_cleaner warnings --- GNUmakefile | 2 +- async_client.cpp | 12 ++++++++++-- async_tcp_client.cpp | 4 ++-- blocking_tcp_echo_client.cpp | 9 ++++++++- blocking_tcp_echo_server.cpp | 9 +++++++-- rrcp_client.cpp | 2 +- timer.cpp | 4 +--- 7 files changed, 30 insertions(+), 12 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 6615d34..89231d3 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -21,7 +21,7 @@ check: all fix: all run-clang-tidy -p build -fix \ - -check='-*,readability-use-std-min-max,misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,-modernize-*,-modernize-avoid-bind,readability-braces-around-statements,hicpp-named-parameter' \ + -check='-*,readability-use-std-min-max,-misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,-modernize-*,-modernize-avoid-bind,readability-braces-around-statements,hicpp-named-parameter' \ *.cpp test: all diff --git a/async_client.cpp b/async_client.cpp index 96bbc21..abd0339 100644 --- a/async_client.cpp +++ b/async_client.cpp @@ -1,5 +1,13 @@ -#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include @@ -16,7 +24,7 @@ boost::asio::streambuf streambuf; // void do_read(); -void handle_read(boost::system::error_code, std::size_t xfer) +void handle_read(boost::system::error_code /*unused*/, std::size_t xfer) { assert(streambuf.size() >= xfer); diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index a4388da..88bc3e4 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -9,13 +9,13 @@ // #include -#include #include #include #include #include #include -#include +#include +#include // NOLINT(misc-include-cleaner) #include #include #include diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index b9d02dd..f310661 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -8,11 +8,18 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +#include +#include +#include +#include #include +#include +#include #include #include +#include #include +#include #include "rrcp_helper.hpp" diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index bdab8b5..0e9c6d9 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -8,9 +8,14 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include -#include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include diff --git a/rrcp_client.cpp b/rrcp_client.cpp index f455450..b3b0c3a 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include #include // NOLINT(misc-include-cleaner) #include diff --git a/timer.cpp b/timer.cpp index 3e2f71a..d66534d 100644 --- a/timer.cpp +++ b/timer.cpp @@ -8,11 +8,9 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include #include -#include #include -#include +#include #include void print(boost::asio::steady_timer* t, int* count) From 4807b1067494e592fd94cd52ae12bfb516b6e899 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 12 Mar 2025 23:32:02 +0100 Subject: [PATCH 015/120] Finish the simplest rrcp client clang-format files again --- GNUmakefile | 2 +- blocking_tcp_echo_client.cpp | 186 +++++++++++++++++++---------------- blocking_tcp_echo_server.cpp | 172 ++++++++++++++++---------------- rrcp_helper.cpp | 3 - rrcp_helper.hpp | 3 + timer.cpp | 9 +- 6 files changed, 196 insertions(+), 179 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 89231d3..362c40b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -17,7 +17,7 @@ build: CMakeLists.txt cmake -S . -B $@ check: all - run-clang-tidy -p build -check='-*,bugprone-*,hicpp-*,modernize-*,misc-*,-misc-no-recursion' *.cpp + run-clang-tidy -p build -check='-*,bugprone-*,hicpp-*,modernize-*,-modernize-avoid-bind,misc-*,-misc-include-cleaner,-misc-no-recursion' *.cpp fix: all run-clang-tidy -p build -fix \ diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index f310661..8a73d0c 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -1,87 +1,99 @@ -// -// blocking_tcp_echo_client.cpp -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// -// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "rrcp_helper.hpp" - -using boost::asio::ip::tcp; - -enum -{ - max_length = 1024 -}; - -auto main(int argc, char* argv[]) -> int -{ - try - { - if (argc != 3) - { - std::cerr << "Usage: blocking_tcp_echo_client \n"; - return 1; - } - - boost::asio::io_context io_context; - - tcp::socket s(io_context); - tcp::resolver resolver(io_context); - boost::asio::connect(s, resolver.resolve(argv[1], argv[2])); - - do - { - std::cout << "Enter message: "; - char request[max_length]; - std::cin.getline(request, max_length); - size_t const request_length = std::strlen(request); - if (request_length == 0) - { - break; - } - - std::string const data = char2esc(std::string(request, request_length)); - - boost::asio::write(s, boost::asio::buffer(data.c_str(), data.length())); - - // TODO: wait for endchar with timeout! - std::string reply; - boost::asio::dynamic_string_buffer< char, std::string::traits_type, - std::string::allocator_type > const sb2 = - boost::asio::dynamic_buffer(reply, max_length); - boost::system::error_code const ec; - - size_t reply_length{}; - do - { - reply_length = boost::asio::read_until(s, sb2, ';'); - std::cout << "Reply is: "; - std::cout.write(reply.c_str(), reply_length); - std::cout << "\n"; - } while (reply_length > 0); - } while (true); - } - catch (std::exception& e) - { - std::cerr << "Exception: " << e.what() << "\n"; - } - - return 0; -} +// +// blocking_tcp_echo_client.cpp +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rrcp_helper.hpp" + +using boost::asio::ip::tcp; + +enum +{ + max_length = 1024 +}; + +auto main(int argc, char* argv[]) -> int +{ + try + { + if (argc != 3) + { + std::cerr << "Usage: blocking_tcp_echo_client \n"; + return EXIT_FAILURE; + } + + boost::asio::io_context io_context; + + tcp::socket s(io_context); + tcp::resolver resolver(io_context); + boost::asio::connect(s, resolver.resolve(argv[1], argv[2])); + + for (std::string line; std::getline(std::cin, line); + std::cerr << "Enter command: ") + { + const std::string::size_type sz = line.find("//"); + if ((sz != std::string::npos)) + { + line.resize(sz); + } + + boost::trim_right(line); + if (line.empty()) + { + continue; + } + + // TODO: check boost::system::error_code ec; + std::string command = char2esc(line); + command.insert(0, 1, LF); + command += CR; + boost::asio::write( + s, boost::asio::buffer(command.c_str(), command.length())); + + // TODO: wait for endchar with timeout! + std::string data; + boost::asio::dynamic_string_buffer< char, std::string::traits_type, + std::string::allocator_type > const sb2 = + boost::asio::dynamic_buffer(data, max_length); + + do + { + size_t reply_length = boost::asio::read_until(s, sb2, CR); + std::string const response = esc2char(data); + if (response.empty()) + { + break; + } + + std::cerr << "Response is: "; + std::cout << response << "\n"; + } while (false); + }; + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index 0e9c6d9..1f504aa 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -1,86 +1,86 @@ -// -// blocking_tcp_echo_server.cpp -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// -// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using boost::asio::ip::tcp; - -const int max_length = 1024; - -void session(tcp::socket sock) -{ - try - { - for (;;) - { - char data[max_length]; - - boost::system::error_code error; - size_t const length = sock.read_some(boost::asio::buffer(data), error); - if (error == boost::asio::stream_errc::eof) - { - break; // Connection closed cleanly by peer. - } - else if (error) - { - throw boost::system::system_error(error); // Some other error. - } - - boost::asio::write(sock, boost::asio::buffer(data, length)); - } - } - catch (std::exception& e) - { - std::cerr << "Exception in thread: " << e.what() << "\n"; - } -} - -void server(boost::asio::io_context& io_context, unsigned short port) -{ - tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port)); - for (;;) - { - tcp::socket sock(io_context); - a.accept(sock); - std::thread(session, std::move(sock)).detach(); - } -} - -auto main(int argc, char* argv[]) -> int -{ - try - { - if (argc != 2) - { - std::cerr << "Usage: blocking_tcp_echo_server \n"; - return 1; - } - - boost::asio::io_context io_context; - - server(io_context, std::atoi(argv[1])); - } - catch (std::exception& e) - { - std::cerr << "Exception: " << e.what() << "\n"; - } - - return 0; -} +// +// blocking_tcp_echo_server.cpp +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using boost::asio::ip::tcp; + +const int max_length = 1024; + +void session(tcp::socket sock) +{ + try + { + for (;;) + { + char data[max_length]; + + boost::system::error_code error; + size_t const length = sock.read_some(boost::asio::buffer(data), error); + if (error == boost::asio::stream_errc::eof) + { + break; // Connection closed cleanly by peer. + } + else if (error) + { + throw boost::system::system_error(error); // Some other error. + } + + boost::asio::write(sock, boost::asio::buffer(data, length)); + } + } + catch (std::exception& e) + { + std::cerr << "Exception in thread: " << e.what() << "\n"; + } +} + +void server(boost::asio::io_context& io_context, unsigned short port) +{ + tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port)); + for (;;) + { + tcp::socket sock(io_context); + a.accept(sock); + std::thread(session, std::move(sock)).detach(); + } +} + +auto main(int argc, char* argv[]) -> int +{ + try + { + if (argc != 2) + { + std::cerr << "Usage: blocking_tcp_echo_server \n"; + return 1; + } + + boost::asio::io_context io_context; + + server(io_context, std::atoi(argv[1])); + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index f9cad80..6847d4f 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -9,9 +9,6 @@ constexpr const char REPLACE_LF = 0x01; constexpr const char REPLACE_CR = 0x02; constexpr const char REPLACE_ESC = 0x03; -constexpr const char LF = 0x0A; // \n -constexpr const char CR = 0x0D; // \r - // constexpr const char SP = 0x20; // constexpr const char DOT = 0x2E; // constexpr const char SEMICOLON = 0x3B; diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index 7f2edb0..bd9a577 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -2,6 +2,9 @@ #include +constexpr const char LF{0x0A}; // \n +constexpr const char CR{0x0D}; // \r + /** * @brief Gets the message between message and * replaces the escape sequences for LF and CR diff --git a/timer.cpp b/timer.cpp index d66534d..07fac2a 100644 --- a/timer.cpp +++ b/timer.cpp @@ -1,6 +1,6 @@ // -// timer.cpp -// ~~~~~~~~~ +// timer3/timer.cpp +// ~~~~~~~~~~~~~~~~ // // Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) // @@ -13,6 +13,9 @@ #include #include +namespace +{ + void print(boost::asio::steady_timer* t, int* count) { if (*count < 5) @@ -26,6 +29,8 @@ void print(boost::asio::steady_timer* t, int* count) } } +} // namespace + auto main() -> int { boost::asio::io_context io; From 939c09cde8b7de8b3ade993e6473b868c660da57 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 13 Mar 2025 09:49:12 +0100 Subject: [PATCH 016/120] Add async_tcp_echo_client.cpp --- .clang-format | 2 +- .clang-tidy | 33 +++++++++ CMakeLists.txt | 38 +++++++++- GNUmakefile | 17 ++++- async_client.cpp | 6 +- async_tcp_client.cpp | 28 +++---- async_tcp_echo_client.cpp | 138 +++++++++++++++++++++++++++++++++++ blocking_tcp_echo_client.cpp | 9 +-- blocking_tcp_echo_server.cpp | 2 +- rrcp_client.cpp | 16 ++-- rrcp_message.hpp | 13 +--- timer.cpp | 7 +- 12 files changed, 248 insertions(+), 61 deletions(-) create mode 100644 .clang-tidy create mode 100644 async_tcp_echo_client.cpp diff --git a/.clang-format b/.clang-format index cec4ed8..e6902fc 100644 --- a/.clang-format +++ b/.clang-format @@ -5,7 +5,7 @@ AlignEscapedNewlinesLeft: true AlwaysBreakAfterDefinitionReturnType: None BreakBeforeBraces: Allman BreakConstructorInitializersBeforeComma: false -ColumnLimit: 80 +ColumnLimit: 123 ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 0 IndentCaseLabels: false diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..6f308da --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,33 @@ +--- +Checks: "-*,\ +bugprone-*,\ +-bugprone-reserved-identifier,\ +boost-*,\ +-cert-*,\ +clang-analyzer-*,\ +-clang-analyzer-unix.BlockInCriticalSection,\ +cppcoreguidelines-*,\ +-cppcoreguidelines-macro-to-enum,\ +-cppcoreguidelines-macro-usage,\ +-cppcoreguidelines-owning-memory,\ +-cppcoreguidelines-pro-bounds-pointer-arithmetic,\ +hicpp-*,\ +misc-*,\ +-misc-include-cleaner,\ +-misc-no-recursion,\ +modernize-*,\ +-modernize-avoid-bind,\ +-modernize-macro-to-enum,\ +performance-*,\ +-performance-enum-size,\ +portability-*,\ +readability-*,\ +-readability-identifier-length,\ +-*magic-numbers,\ +-*avoid-c-arrays,\ +" +WarningsAsErrors: 'clang-*' +HeaderFilterRegex: '.*' +FormatStyle: file +User: clausklein +... diff --git a/CMakeLists.txt b/CMakeLists.txt index 758a631..010808e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,14 +2,48 @@ cmake_minimum_required(VERSION 3.28...4.0) project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 20) +# ---- add dependencies ---- + +find_package(Boost 1.87 COMPONENTS asio REQUIRED HINTS ../../../../stagedir) + +# ---- default settings ---- + +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Boost 1.87 COMPONENTS asio REQUIRED HINTS ../../../../stagedir) +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if(APPLE) + execute_process( + OUTPUT_VARIABLE LLVM_PREFIX + COMMAND brew --prefix llvm@19 + COMMAND_ECHO STDOUT + ) + string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) + elseif(LINUX) + set(LLVM_PREFIX $ENV{LLVM_ROOT}) + endif() + + add_compile_options(-fexperimental-library) + add_link_options(-L${LLVM_PREFIX}/lib/c++ -lc++experimental) + + # ---- code coverage ---- + + if(ENABLE_TEST_COVERAGE) + compile_options(-O0 -g -fprofile-arcs -ftest-coverage) + link_options(-fprofile-arcs -ftest-coverage) + endif() +endif() add_library(rrcp_helper STATIC rrcp_helper.cpp rrcp_helper.hpp) +add_executable(async_tcp_echo_client async_tcp_echo_client.cpp) +target_link_libraries( + async_tcp_echo_client + PRIVATE rrcp_helper + PUBLIC Boost::asio +) + add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) target_link_libraries( blocking_tcp_echo_client diff --git a/GNUmakefile b/GNUmakefile index 362c40b..287df0a 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -17,11 +17,11 @@ build: CMakeLists.txt cmake -S . -B $@ check: all - run-clang-tidy -p build -check='-*,bugprone-*,hicpp-*,modernize-*,-modernize-avoid-bind,misc-*,-misc-include-cleaner,-misc-no-recursion' *.cpp + run-clang-tidy -p build *.cpp fix: all run-clang-tidy -p build -fix \ - -check='-*,readability-use-std-min-max,-misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,-modernize-*,-modernize-avoid-bind,readability-braces-around-statements,hicpp-named-parameter' \ + -check='-*,readability-use-std-min-max,-misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,-modernize-*,-modernize-avoid-bind,readability-braces-around-statements,hicpp-named-parameter,readability-else-after-return' \ *.cpp test: all @@ -32,3 +32,16 @@ test: all format: .clang-format git ls-files ::*.cpp ::*.hpp | xargs clang-format -i gersemi -i CMakeLists.txt + +# These rules keep make from trying to use the match-anything rule below +# to rebuild the makefiles--ouch! + +CMakeLists.txt :: ; +GNUmakefile :: ; +.clang-tidy :: ; +.clang-format :: ; + +# Anything we don't know how to build will use this rule. The command is +# a do-nothing command. +% :: build + ninja -C $< $@ diff --git a/async_client.cpp b/async_client.cpp index abd0339..981a2c9 100644 --- a/async_client.cpp +++ b/async_client.cpp @@ -16,8 +16,7 @@ const auto noop = std::bind([] {}); const std::string delimiter{"\r\n\r\n"}; boost::asio::io_context io_context; -boost::asio::ip::tcp::acceptor acceptor( - io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); +boost::asio::ip::tcp::acceptor acceptor(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); boost::asio::ip::tcp::socket socket1(io_context); boost::asio::ip::tcp::socket socket2(io_context); boost::asio::streambuf streambuf; @@ -28,8 +27,7 @@ void handle_read(boost::system::error_code /*unused*/, std::size_t xfer) { assert(streambuf.size() >= xfer); - std::string const command{buffers_begin(streambuf.data()), - buffers_begin(streambuf.data()) + xfer - delimiter.length()}; + std::string const command{buffers_begin(streambuf.data()), buffers_begin(streambuf.data()) + xfer - delimiter.length()}; streambuf.consume(xfer); diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 88bc3e4..63c319f 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -92,10 +92,7 @@ using namespace std::chrono_literals; class client { public: - client(boost::asio::io_context& io_context) - : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) - { - } + client(boost::asio::io_context& io_context) : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) {} // Called by the user of the client class to initiate the connection // process. The endpoints will have been obtained using a tcp::resolver. @@ -108,8 +105,7 @@ class client // Start the deadline actor. You will note that we're not setting any // particular deadline here. Instead, the connect and input actors will // update the deadline prior to each asynchronous operation. - deadline_.async_wait( - [this](const boost::system::error_code& /*e*/) { check_deadline(); }); + deadline_.async_wait([this](const boost::system::error_code& /*e*/) { check_deadline(); }); } // This function terminates all the actors to shut down the connection. It @@ -135,8 +131,7 @@ class client deadline_.expires_after(60s); // Start the asynchronous connect operation. - socket_.async_connect(endpoint_iter->endpoint(), - std::bind(&client::handle_connect, this, _1, endpoint_iter)); + socket_.async_connect(endpoint_iter->endpoint(), std::bind(&client::handle_connect, this, _1, endpoint_iter)); // XXX [this, endpoint_iter](auto && PH1) { // handle_connect(std::forward(PH1), endpoint_iter); }); } @@ -147,8 +142,7 @@ class client } } - void handle_connect(const boost::system::error_code& error, - tcp::resolver::results_type::iterator endpoint_iter) + void handle_connect(const boost::system::error_code& error, tcp::resolver::results_type::iterator endpoint_iter) { if (stopped_) { @@ -198,9 +192,8 @@ class client deadline_.expires_after(30s); // Start an asynchronous operation to read a newline-delimited message. - boost::asio::async_read_until(socket_, - boost::asio::dynamic_buffer(input_buffer_), '\n', - std::bind(&client::handle_read, this, _1, _2)); + boost::asio::async_read_until( + socket_, boost::asio::dynamic_buffer(input_buffer_), '\n', std::bind(&client::handle_read, this, _1, _2)); // XXX [this](auto && PH1, auto && PH2) { // handle_read(std::forward(PH1), // std::forward(PH2)); }); @@ -243,8 +236,7 @@ class client } // Start an asynchronous operation to send a heartbeat message. - boost::asio::async_write(socket_, boost::asio::buffer("\n", 1), - std::bind(&client::handle_write, this, _1)); + boost::asio::async_write(socket_, boost::asio::buffer("\n", 1), std::bind(&client::handle_write, this, _1)); // XXX [this](const boost::system::error_code& /*e*/, PH1) { // handle_write(std::forward(PH1)); }); } @@ -261,8 +253,7 @@ class client // Wait 10 seconds before sending the next heartbeat. heartbeat_timer_.expires_after(10s); // heartbeat_timer_.async_wait(std::bind(&client::start_write, this)); - heartbeat_timer_.async_wait( - [this](const boost::system::error_code& /*e*/) { start_write(); }); + heartbeat_timer_.async_wait([this](const boost::system::error_code& /*e*/) { start_write(); }); } else { @@ -296,8 +287,7 @@ class client // Put the actor back to sleep. // deadline_.async_wait(std::bind(&client::check_deadline, this)); - deadline_.async_wait( - [this](const boost::system::error_code& /*e*/) { check_deadline(); }); + deadline_.async_wait([this](const boost::system::error_code& /*e*/) { check_deadline(); }); } bool stopped_ = false; diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp new file mode 100644 index 0000000..7a471de --- /dev/null +++ b/async_tcp_echo_client.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rrcp_helper.hpp" + +using boost::asio::ip::tcp; + +constexpr size_t max_length = 1024; + +class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousTCPClient > +{ + public: + AsynchronousTCPClient(boost::asio::io_context& io_context, const std::string& host, const std::string& port) + : resolver_(io_context), socket_(io_context) + { + resolver_.async_resolve(host, port, + [this](boost::system::error_code ec, tcp::resolver::results_type results) + { + if (!ec) + { + boost::asio::async_connect(socket_, results, + [this](boost::system::error_code ec, const tcp::endpoint&) + { + if (!ec) + { + std::print("Connected to server.\nEnter command: "); + do_read(); + } + }); + } + }); + } + + void write(const std::string& message) + { + auto self(shared_from_this()); + boost::asio::async_write(socket_, boost::asio::buffer(message), + [this, self](boost::system::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + std::print("Message sent.\nEnter command: "); + } + }); + } + + private: + void do_read() + { + auto self(shared_from_this()); + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(data_), CR, + [this, self](boost::system::error_code ec, std::size_t length) + { + if (!ec) + { + std::string response = esc2char(data_.substr(0, length)); + // NOTE: data_.erase(0, length); is used instead of data_.clear() + // because: + + // - Partial Data Handling: The async_read_until() function reads + // data up to the delimiter (CR) but + // doesn’t guarantee it consumes all the data in the socket. There + // might be extra data left in the buffer after the delimiter. + + //- Efficient Buffer Management: By erasing only the portion of the + // string that has been processed + // (length), we keep any remaining data intact for future reads + // instead of discarding it. + data_.erase(0, length); + + std::print("Response is: {}\n", response); + do_read(); + } + }); + } + + tcp::resolver resolver_; + tcp::socket socket_; + std::string data_; +}; + +int main(int argc, char* argv[]) +{ + try + { + if (argc != 3) + { + std::print("Usage: async_tcp_client \n"); + return EXIT_FAILURE; + } + + boost::asio::io_context io_context; + + auto client = std::make_shared< AsynchronousTCPClient >(io_context, argv[1], argv[2]); + + std::thread io_thread([&io_context]() { io_context.run(); }); + + std::string line; + while (std::getline(std::cin, line)) + { + const std::string::size_type sz = line.find("//"); + if ((sz != std::string::npos)) + { + line.resize(sz); + } + + boost::trim_right(line); + if (line.empty()) + { + continue; + } + + std::string command = char2esc(line); + command.insert(0, 1, LF); + command += CR; + + client->write(command); + } + + io_thread.join(); + } + catch (std::exception& e) + { + std::print("Exception: {}\n", e.what()); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index 8a73d0c..4a9178b 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -47,8 +47,7 @@ auto main(int argc, char* argv[]) -> int tcp::resolver resolver(io_context); boost::asio::connect(s, resolver.resolve(argv[1], argv[2])); - for (std::string line; std::getline(std::cin, line); - std::cerr << "Enter command: ") + for (std::string line; std::getline(std::cin, line); std::cerr << "Enter command: ") { const std::string::size_type sz = line.find("//"); if ((sz != std::string::npos)) @@ -66,13 +65,11 @@ auto main(int argc, char* argv[]) -> int std::string command = char2esc(line); command.insert(0, 1, LF); command += CR; - boost::asio::write( - s, boost::asio::buffer(command.c_str(), command.length())); + boost::asio::write(s, boost::asio::buffer(command.c_str(), command.length())); // TODO: wait for endchar with timeout! std::string data; - boost::asio::dynamic_string_buffer< char, std::string::traits_type, - std::string::allocator_type > const sb2 = + boost::asio::dynamic_string_buffer< char, std::string::traits_type, std::string::allocator_type > const sb2 = boost::asio::dynamic_buffer(data, max_length); do diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index 1f504aa..f29118d 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -38,7 +38,7 @@ void session(tcp::socket sock) { break; // Connection closed cleanly by peer. } - else if (error) + if (error) { throw boost::system::system_error(error); // Some other error. } diff --git a/rrcp_client.cpp b/rrcp_client.cpp index b3b0c3a..7105f23 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -38,8 +38,7 @@ using rrcp_message_queue = std::deque< rrcp_message >; class rrcp_client { public: - rrcp_client(boost::asio::io_context& io_context, - const tcp::resolver::results_type& endpoints) + rrcp_client(boost::asio::io_context& io_context, const tcp::resolver::results_type& endpoints) : io_context_(io_context), socket_(io_context) { do_connect(endpoints); @@ -80,8 +79,7 @@ class rrcp_client void do_read_header() { - boost::asio::async_read(socket_, - boost::asio::buffer(read_msg_.data(), rrcp_message::header_length), + boost::asio::async_read(socket_, boost::asio::buffer(read_msg_.data(), rrcp_message::header_length), [this](boost::system::error_code ec, std::size_t /*length*/) { if (!ec && read_msg_.decode_header()) @@ -97,8 +95,7 @@ class rrcp_client void do_read_body() { - boost::asio::async_read(socket_, - boost::asio::buffer(read_msg_.body(), read_msg_.body_length()), + boost::asio::async_read(socket_, boost::asio::buffer(read_msg_.body(), read_msg_.body_length()), [this](boost::system::error_code ec, std::size_t /*length*/) { if (!ec && read_msg_.decode_body()) @@ -117,9 +114,7 @@ class rrcp_client void do_write() { - boost::asio::async_write(socket_, - boost::asio::buffer( - write_msgs_.front().data(), write_msgs_.front().length()), + boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front().data(), write_msgs_.front().length()), [this](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) @@ -165,8 +160,7 @@ auto main(int argc, char* argv[]) -> int std::thread t([&io_context]() { io_context.run(); }); //================================================================ - std::string binary = - "\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"s; + std::string binary = "\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"s; std::cerr << binary.length() << ' ' << std::quoted(binary) << '\n'; auto quoted = char2esc(binary); std::cerr << quoted.length() << ' ' << std::quoted(quoted) << '\n'; diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 70df76c..355fc17 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -34,16 +34,10 @@ class rrcp_message auto data() -> char* { return data_.data(); } - [[nodiscard]] auto length() const -> std::size_t - { - return header_length + msg_length_; - } + [[nodiscard]] auto length() const -> std::size_t { return header_length + msg_length_; } // TODO: or better const &std::string_view? - [[nodiscard]] auto body() const -> const char* - { - return data_.data() + header_length; - } + [[nodiscard]] auto body() const -> const char* { return data_.data() + header_length; } auto body() -> char* { return data_.data() + header_length; } @@ -98,8 +92,7 @@ class rrcp_message void encode_header() { - std::string header = - std::format("{:04x}", static_cast< uint16_t >(msg_length_)); + std::string header = std::format("{:04x}", static_cast< uint16_t >(msg_length_)); std::memcpy(data_.data(), header.data(), header_length); } diff --git a/timer.cpp b/timer.cpp index 07fac2a..09aa071 100644 --- a/timer.cpp +++ b/timer.cpp @@ -24,8 +24,7 @@ void print(boost::asio::steady_timer* t, int* count) ++(*count); t->expires_at(t->expiry() + boost::asio::chrono::seconds(1)); - t->async_wait([t, count](const boost::system::error_code& /*e*/) - { print(t, count); }); + t->async_wait([t, count](const boost::system::error_code& /*e*/) { print(t, count); }); } } @@ -37,9 +36,7 @@ auto main() -> int int count = 0; boost::asio::steady_timer t(io, boost::asio::chrono::seconds(1)); - t.async_wait( - [capture0 = &t, capture1 = &count](const boost::system::error_code& /*e*/) - { print(capture0, capture1); }); + t.async_wait([capture0 = &t, capture1 = &count](const boost::system::error_code& /*e*/) { print(capture0, capture1); }); io.run(); From b46c0e2bb978639eeb8dbfc4dccb5b1af8f9a066 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 13 Mar 2025 10:08:49 +0100 Subject: [PATCH 017/120] Prevent more clang-tidy warnings clang-format code too --- .clang-tidy | 3 ++- GNUmakefile | 2 +- async_tcp_echo_client.cpp | 2 +- blocking_tcp_echo_client.cpp | 2 +- rrcp_client.cpp | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 6f308da..b369e97 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -3,10 +3,11 @@ Checks: "-*,\ bugprone-*,\ -bugprone-reserved-identifier,\ boost-*,\ --cert-*,\ +cert-*,\ clang-analyzer-*,\ -clang-analyzer-unix.BlockInCriticalSection,\ cppcoreguidelines-*,\ +-cppcoreguidelines-avoid-do-while,\ -cppcoreguidelines-macro-to-enum,\ -cppcoreguidelines-macro-usage,\ -cppcoreguidelines-owning-memory,\ diff --git a/GNUmakefile b/GNUmakefile index 287df0a..fad3846 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -21,7 +21,7 @@ check: all fix: all run-clang-tidy -p build -fix \ - -check='-*,readability-use-std-min-max,-misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,-modernize-*,-modernize-avoid-bind,readability-braces-around-statements,hicpp-named-parameter,readability-else-after-return' \ + -check='-*,readability-use-std-min-max,-misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,-modernize-avoid-bind,readability-braces-around-statements,hicpp-named-parameter,readability-else-after-return,modernize-use-trailing-return-type,readability-redundant-member-init,misc-const-correctness' \ *.cpp test: all diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index 7a471de..24142b7 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -88,7 +88,7 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT std::string data_; }; -int main(int argc, char* argv[]) +auto main(int argc, char* argv[]) -> int { try { diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index 4a9178b..21b6c40 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -74,7 +74,7 @@ auto main(int argc, char* argv[]) -> int do { - size_t reply_length = boost::asio::read_until(s, sb2, CR); + size_t const reply_length = boost::asio::read_until(s, sb2, CR); // NOLINT(clang-analyzer-deadcode.DeadStores) std::string const response = esc2char(data); if (response.empty()) { diff --git a/rrcp_client.cpp b/rrcp_client.cpp index 7105f23..41bfc72 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -135,7 +135,7 @@ class rrcp_client boost::asio::io_context& io_context_; tcp::socket socket_; rrcp_message read_msg_; - rrcp_message_queue write_msgs_{}; + rrcp_message_queue write_msgs_; }; auto main(int argc, char* argv[]) -> int From 6a2fa78450d2359d0e6ea542ef16dcd3253623bd Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 13 Mar 2025 17:20:15 +0100 Subject: [PATCH 018/120] Now it works fine --- GNUmakefile | 1 + async_read_with_timout.cpp | 159 +++++++++++++++++++++++++++++++++++++ async_tcp_echo_client.cpp | 91 ++++++++++++++++----- 3 files changed, 231 insertions(+), 20 deletions(-) create mode 100644 async_read_with_timout.cpp diff --git a/GNUmakefile b/GNUmakefile index fad3846..9a46ae4 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -28,6 +28,7 @@ test: all -killall blocking_tcp_echo_server build/blocking_tcp_echo_server 8000 & cat rrcp.txt | build/rrcp_client localhost 8000 + cat rrcp.txt | build/async_tcp_echo_client localhost 8000 format: .clang-format git ls-files ::*.cpp ::*.hpp | xargs clang-format -i diff --git a/async_read_with_timout.cpp b/async_read_with_timout.cpp new file mode 100644 index 0000000..c910454 --- /dev/null +++ b/async_read_with_timout.cpp @@ -0,0 +1,159 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rrcp_helper.hpp" + +using boost::asio::ip::tcp; +using namespace std::chrono_literals; + +constexpr size_t max_length = 1024; +constexpr auto timeout_duration = 10s; + +class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousTCPClient > +{ + public: + AsynchronousTCPClient(boost::asio::io_context& io_context, const std::string& host, const std::string& port) + : resolver_(io_context), socket_(io_context), connected_(false), timer_(io_context) + { + resolver_.async_resolve(host, port, + [this](boost::system::error_code ec, tcp::resolver::results_type results) + { + if (!ec) + { + boost::asio::async_connect(socket_, results, + [this](boost::system::error_code ec, const tcp::endpoint&) + { + if (!ec) + { + std::print(stderr, "Connected to server.\n"); + connected_ = true; + } + }); + } + }); + } + + void write(const std::string& message) + { + if (!connected_) + { + std::print(stderr, "Error: Client is not connected yet.\n"); + return; + } + + auto self(shared_from_this()); + boost::asio::async_write(socket_, boost::asio::buffer(message), + [this, self](boost::system::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + std::print(stderr, "Message sent.\n"); + do_read(); + } + }); + } + + private: + void do_read() + { + auto self(shared_from_this()); + timer_.expires_after(timeout_duration); + timer_.async_wait( + [this, self](const boost::system::error_code& ec) + { + if (!ec) + { + std::print(stderr, "Error: Read operation timed out.\n"); + socket_.close(); + } + }); + + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(data_), CR, + [this, self](boost::system::error_code ec, std::size_t length) + { + timer_.cancel(); + if (!ec) + { + std::string response = esc2char(data_.substr(0, length)); + // NOTE: data_.erase(0, length); is used instead of data_.clear() because: + + // - Partial Data Handling: The async_read_until() function reads + // data up to the delimiter (CR) but doesn’t guarantee it consumes + // all the data in the socket. + // There might be extra data left in the buffer after the delimiter! + + //- Efficient Buffer Management: By erasing only the portion of the + // string that has been processed (length), we keep any remaining + // data intact for future reads instead of discarding it. + data_.erase(0, length); + + std::print("Response is: {}\n", response); + } + }); + } + + tcp::resolver resolver_; + tcp::socket socket_; + boost::asio::steady_timer timer_; + std::string data_; + bool connected_; +}; + +auto main(int argc, char* argv[]) -> int +{ + try + { + if (argc != 3) + { + std::print(stderr, "Usage: async_tcp_echo_client \n"); + return EXIT_FAILURE; + } + + boost::asio::io_context io_context; + + auto client = std::make_shared< AsynchronousTCPClient >(io_context, argv[1], argv[2]); + + std::thread io_thread([&io_context]() { io_context.run(); }); + + for (std::string line; std::getline(std::cin, line); std::print(stderr, "Enter command: ")) + { + const std::string::size_type sz = line.find("//"); + if ((sz != std::string::npos)) + { + line.resize(sz); + } + + boost::trim_right(line); + if (line.empty()) + { + continue; + } + + std::string command = char2esc(line); + command.insert(0, 1, LF); + command += CR; + + client->write(command); + } + + io_thread.join(); + } + catch (std::exception& e) + { + std::print(stderr, "Exception: {}\n", e.what()); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index 24142b7..c6eb714 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -1,6 +1,8 @@ #include #include +#include #include +#include #include #include #include @@ -13,14 +15,16 @@ #include "rrcp_helper.hpp" using boost::asio::ip::tcp; +using namespace std::chrono_literals; -constexpr size_t max_length = 1024; +constexpr size_t max_length = 65432; +constexpr auto timeout_duration = 1s; class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousTCPClient > { public: AsynchronousTCPClient(boost::asio::io_context& io_context, const std::string& host, const std::string& port) - : resolver_(io_context), socket_(io_context) + : resolver_(io_context), socket_(io_context), timer_(io_context) { resolver_.async_resolve(host, port, [this](boost::system::error_code ec, tcp::resolver::results_type results) @@ -32,7 +36,8 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT { if (!ec) { - std::print("Connected to server.\nEnter command: "); + std::print(stderr, "Connected to server.\n"); + connected_ = true; do_read(); } }); @@ -42,39 +47,81 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT void write(const std::string& message) { + while (!connected_) + { + if (stopped_) + { + return; + } + std::print(stderr, "Client is not connected yet.\n"); + std::this_thread::sleep_for(timeout_duration); // NOLINT(misc-include-cleaner) + } + auto self(shared_from_this()); boost::asio::async_write(socket_, boost::asio::buffer(message), [this, self](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { - std::print("Message sent.\nEnter command: "); + std::print(stderr, "Message sent.\n"); } }); } + // This function terminates all the actors to shut down the connection. It + // may be called by the user of the client class, or by the class itself in + // response to graceful termination or an unrecoverable error. + void stop() + { + boost::system::error_code ignored_error; + socket_.close(ignored_error); + timer_.cancel(); + connected_ = true; + stopped_ = true; + } + private: void do_read() { + if (stopped_) + { + return; + } + auto self(shared_from_this()); + + if (!connected_) + { + timer_.expires_after(timeout_duration); + timer_.async_wait( + [this, self](const boost::system::error_code& ec) + { + if (!ec) + { + std::print(stderr, "Error: Read operation timed out.\n"); + socket_.close(); + } + }); + } + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(data_), CR, [this, self](boost::system::error_code ec, std::size_t length) { + timer_.cancel(); if (!ec) { - std::string response = esc2char(data_.substr(0, length)); - // NOTE: data_.erase(0, length); is used instead of data_.clear() - // because: + std::string response = esc2char(data_.substr(1, length)); // NOTE: w/o LF! + + // NOTE: data_.erase(0, length); is used instead of data_.clear() because: // - Partial Data Handling: The async_read_until() function reads - // data up to the delimiter (CR) but - // doesn’t guarantee it consumes all the data in the socket. There - // might be extra data left in the buffer after the delimiter. - - //- Efficient Buffer Management: By erasing only the portion of the - // string that has been processed - // (length), we keep any remaining data intact for future reads - // instead of discarding it. + // data up to the delimiter (CR) but doesn’t guarantee it consumes + // all the data in the socket. + // There might be extra data left in the buffer after the delimiter! + + // - Efficient Buffer Management: By erasing only the portion of the + // string that has been processed (length), we keep any remaining + // data intact for future reads instead of discarding it. data_.erase(0, length); std::print("Response is: {}\n", response); @@ -85,7 +132,10 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT tcp::resolver resolver_; tcp::socket socket_; + boost::asio::steady_timer timer_; std::string data_; + bool connected_{false}; + bool stopped_{false}; }; auto main(int argc, char* argv[]) -> int @@ -94,7 +144,7 @@ auto main(int argc, char* argv[]) -> int { if (argc != 3) { - std::print("Usage: async_tcp_client \n"); + std::print(stderr, "Usage: async_tcp_echo_client \n"); return EXIT_FAILURE; } @@ -104,13 +154,12 @@ auto main(int argc, char* argv[]) -> int std::thread io_thread([&io_context]() { io_context.run(); }); - std::string line; - while (std::getline(std::cin, line)) + for (std::string line; std::getline(std::cin, line); std::print(stderr, "Enter command: ")) { const std::string::size_type sz = line.find("//"); if ((sz != std::string::npos)) { - line.resize(sz); + line.resize(sz); // NOTE: w/o c++ comments } boost::trim_right(line); @@ -125,12 +174,14 @@ auto main(int argc, char* argv[]) -> int client->write(command); } + std::this_thread::sleep_for(timeout_duration); // NOLINT(misc-include-cleaner) + client->stop(); io_thread.join(); } catch (std::exception& e) { - std::print("Exception: {}\n", e.what()); + std::print(stderr, "Exception: {}\n", e.what()); return EXIT_FAILURE; } From 678e99a05a0bafa33143e4a3e546cfc3ce145513 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 14 Mar 2025 09:41:39 +0100 Subject: [PATCH 019/120] Modernize timer example --- CMakeLists.txt | 20 ++++--- GNUmakefile | 4 +- async_tcp_echo_client.cpp | 36 ++++++++---- async_tcp_echo_server.cpp | 112 ++++++++++++++++++++++++++++++++++++++ timer.cpp | 58 +++++++++++++------- 5 files changed, 190 insertions(+), 40 deletions(-) create mode 100644 async_tcp_echo_server.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 010808e..56b3323 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,8 @@ project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) # ---- add dependencies ---- -find_package(Boost 1.87 COMPONENTS asio REQUIRED HINTS ../../../../stagedir) +find_package(Boost 1.87 COMPONENTS asio REQUIRED HINTS $ENV{HOME}/.local) +find_package(fmt 11.1.4 REQUIRED HINTS $ENV{HOME}/.local) # ---- default settings ---- @@ -26,22 +27,27 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") add_compile_options(-fexperimental-library) add_link_options(-L${LLVM_PREFIX}/lib/c++ -lc++experimental) +endif() - # ---- code coverage ---- +# ---- code coverage ---- - if(ENABLE_TEST_COVERAGE) - compile_options(-O0 -g -fprofile-arcs -ftest-coverage) - link_options(-fprofile-arcs -ftest-coverage) - endif() +option(ENABLE_TEST_COVERAGE ON) +if(ENABLE_TEST_COVERAGE) + message(STATUS "ENABLE_TEST_COVERAGE is set!") + add_compile_options(-O0 -g -fprofile-arcs -ftest-coverage) + add_link_options(-fprofile-arcs -ftest-coverage) endif() +add_executable(async_tcp_echo_server async_tcp_echo_server.cpp) +target_link_libraries(async_tcp_echo_server PUBLIC Boost::asio) + add_library(rrcp_helper STATIC rrcp_helper.cpp rrcp_helper.hpp) add_executable(async_tcp_echo_client async_tcp_echo_client.cpp) target_link_libraries( async_tcp_echo_client PRIVATE rrcp_helper - PUBLIC Boost::asio + PUBLIC Boost::asio fmt::fmt-header-only ) add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) diff --git a/GNUmakefile b/GNUmakefile index 9a46ae4..43a8812 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -25,8 +25,8 @@ fix: all *.cpp test: all - -killall blocking_tcp_echo_server - build/blocking_tcp_echo_server 8000 & + -killall async_tcp_echo_server + build/async_tcp_echo_server 8000 & cat rrcp.txt | build/rrcp_client localhost 8000 cat rrcp.txt | build/async_tcp_echo_client localhost 8000 diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index c6eb714..7256568 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -8,7 +10,6 @@ #include #include #include -#include #include #include @@ -36,10 +37,15 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT { if (!ec) { - std::print(stderr, "Connected to server.\n"); + fmt::print(stderr, "Connected to server.\n"); connected_ = true; do_read(); } + else + { + // There are no more endpoints to try. Shut down the client. + stop(); + } }); } }); @@ -53,7 +59,7 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT { return; } - std::print(stderr, "Client is not connected yet.\n"); + fmt::print(stderr, "Client is not connected yet.\n"); std::this_thread::sleep_for(timeout_duration); // NOLINT(misc-include-cleaner) } @@ -63,7 +69,12 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT { if (!ec) { - std::print(stderr, "Message sent.\n"); + fmt::print(stderr, "Message sent.\n"); + } + else + { + // There are no more endpoints to try. Shut down the client. + stop(); } }); } @@ -98,8 +109,8 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT { if (!ec) { - std::print(stderr, "Error: Read operation timed out.\n"); - socket_.close(); + fmt::print(stderr, "Error: Read operation timed out.\n"); + stop(); } }); } @@ -124,9 +135,14 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT // data intact for future reads instead of discarding it. data_.erase(0, length); - std::print("Response is: {}\n", response); + fmt::print("Response is: {}\n", response); do_read(); } + else + { + // There are no more endpoints to try. Shut down the client. + stop(); + } }); } @@ -144,7 +160,7 @@ auto main(int argc, char* argv[]) -> int { if (argc != 3) { - std::print(stderr, "Usage: async_tcp_echo_client \n"); + fmt::print(stderr, "Usage: async_tcp_echo_client \n"); return EXIT_FAILURE; } @@ -154,7 +170,7 @@ auto main(int argc, char* argv[]) -> int std::thread io_thread([&io_context]() { io_context.run(); }); - for (std::string line; std::getline(std::cin, line); std::print(stderr, "Enter command: ")) + for (std::string line; std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) { const std::string::size_type sz = line.find("//"); if ((sz != std::string::npos)) @@ -181,7 +197,7 @@ auto main(int argc, char* argv[]) -> int } catch (std::exception& e) { - std::print(stderr, "Exception: {}\n", e.what()); + fmt::print(stderr, "Exception: {}\n", e.what()); return EXIT_FAILURE; } diff --git a/async_tcp_echo_server.cpp b/async_tcp_echo_server.cpp new file mode 100644 index 0000000..a58bf93 --- /dev/null +++ b/async_tcp_echo_server.cpp @@ -0,0 +1,112 @@ +// +// async_tcp_echo_server.cpp +// ~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +using boost::asio::ip::tcp; + +class session : public std::enable_shared_from_this< session > +{ + public: + session(tcp::socket socket) : socket_(std::move(socket)) {} + + void start() { do_read(); } + + private: + void do_read() + { + auto self(shared_from_this()); + socket_.async_read_some(boost::asio::buffer(data_, max_length), + [this, self](boost::system::error_code ec, std::size_t length) + { + if (!ec) + { + do_write(length); + } + }); + } + + void do_write(std::size_t length) + { + auto self(shared_from_this()); + boost::asio::async_write(socket_, boost::asio::buffer(data_, length), + [this, self](boost::system::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + do_read(); + } + }); + } + + tcp::socket socket_; + enum + { + max_length = 1024 + }; + char data_[max_length]; +}; + +class server +{ + public: + server(boost::asio::io_context& io_context, short port) + : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)), socket_(io_context) + { + do_accept(); + } + + private: + void do_accept() + { + acceptor_.async_accept(socket_, + [this](boost::system::error_code ec) + { + if (!ec) + { + std::make_shared< session >(std::move(socket_))->start(); + } + + do_accept(); + }); + } + + tcp::acceptor acceptor_; + tcp::socket socket_; +}; + +int main(int argc, char* argv[]) +{ + try + { + if (argc != 2) + { + std::cerr << "Usage: async_tcp_echo_server \n"; + return 1; + } + + boost::asio::io_context io_context; + + server s(io_context, std::atoi(argv[1])); + + io_context.run(); + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/timer.cpp b/timer.cpp index 09aa071..38be13a 100644 --- a/timer.cpp +++ b/timer.cpp @@ -1,5 +1,5 @@ // -// timer3/timer.cpp +// timer4/timer.cpp // ~~~~~~~~~~~~~~~~ // // Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) @@ -10,37 +10,53 @@ #include #include -#include +#include #include -namespace +class printer { + static constexpr int kMaxCount{5}; -void print(boost::asio::steady_timer* t, int* count) -{ - if (*count < 5) + public: + explicit printer(boost::asio::io_context& io) : timer_(io, boost::asio::chrono::seconds(1)) { - std::cout << *count << '\n'; - ++(*count); + // cpp11: timer_.async_wait(std::bind(&printer::print, this)); + timer_.async_wait([this](const boost::system::error_code& /*ec*/) { print(); }); + } + + ~printer() { std::cout << "Final count is " << count_ << std::endl; } - t->expires_at(t->expiry() + boost::asio::chrono::seconds(1)); - t->async_wait([t, count](const boost::system::error_code& /*e*/) { print(t, count); }); + void print() + { + if (count_ < kMaxCount) + { + std::cout << count_ << std::endl; + ++count_; + + timer_.expires_at(timer_.expiry() + boost::asio::chrono::seconds(1)); + // cpp11: timer_.async_wait(std::bind(&printer::print, this)); + timer_.async_wait([this](const boost::system::error_code& /*ec*/) { print(); }); + } } -} -} // namespace + private: + boost::asio::steady_timer timer_; + int count_{}; +}; auto main() -> int { - boost::asio::io_context io; - - int count = 0; - boost::asio::steady_timer t(io, boost::asio::chrono::seconds(1)); - t.async_wait([capture0 = &t, capture1 = &count](const boost::system::error_code& /*e*/) { print(capture0, capture1); }); - - io.run(); - - std::cout << "Final count is " << count << '\n'; + try + { + boost::asio::io_context io; + auto p = std::make_unique< printer >(io); + io.run(); + } + catch (const std::exception& e) + { + std::print("Error: {}\n", e.what()); + return 1; + } return 0; } From c2314c759d4f65c967fd0e44b68324fa34627841 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 14 Mar 2025 09:56:38 +0100 Subject: [PATCH 020/120] Modernize async_tcp_client example --- async_read_with_timout.cpp | 159 ------------------------------------- async_tcp_client.cpp | 8 +- async_tcp_echo_server.cpp | 17 ++-- 3 files changed, 12 insertions(+), 172 deletions(-) delete mode 100644 async_read_with_timout.cpp diff --git a/async_read_with_timout.cpp b/async_read_with_timout.cpp deleted file mode 100644 index c910454..0000000 --- a/async_read_with_timout.cpp +++ /dev/null @@ -1,159 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "rrcp_helper.hpp" - -using boost::asio::ip::tcp; -using namespace std::chrono_literals; - -constexpr size_t max_length = 1024; -constexpr auto timeout_duration = 10s; - -class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousTCPClient > -{ - public: - AsynchronousTCPClient(boost::asio::io_context& io_context, const std::string& host, const std::string& port) - : resolver_(io_context), socket_(io_context), connected_(false), timer_(io_context) - { - resolver_.async_resolve(host, port, - [this](boost::system::error_code ec, tcp::resolver::results_type results) - { - if (!ec) - { - boost::asio::async_connect(socket_, results, - [this](boost::system::error_code ec, const tcp::endpoint&) - { - if (!ec) - { - std::print(stderr, "Connected to server.\n"); - connected_ = true; - } - }); - } - }); - } - - void write(const std::string& message) - { - if (!connected_) - { - std::print(stderr, "Error: Client is not connected yet.\n"); - return; - } - - auto self(shared_from_this()); - boost::asio::async_write(socket_, boost::asio::buffer(message), - [this, self](boost::system::error_code ec, std::size_t /*length*/) - { - if (!ec) - { - std::print(stderr, "Message sent.\n"); - do_read(); - } - }); - } - - private: - void do_read() - { - auto self(shared_from_this()); - timer_.expires_after(timeout_duration); - timer_.async_wait( - [this, self](const boost::system::error_code& ec) - { - if (!ec) - { - std::print(stderr, "Error: Read operation timed out.\n"); - socket_.close(); - } - }); - - boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(data_), CR, - [this, self](boost::system::error_code ec, std::size_t length) - { - timer_.cancel(); - if (!ec) - { - std::string response = esc2char(data_.substr(0, length)); - // NOTE: data_.erase(0, length); is used instead of data_.clear() because: - - // - Partial Data Handling: The async_read_until() function reads - // data up to the delimiter (CR) but doesn’t guarantee it consumes - // all the data in the socket. - // There might be extra data left in the buffer after the delimiter! - - //- Efficient Buffer Management: By erasing only the portion of the - // string that has been processed (length), we keep any remaining - // data intact for future reads instead of discarding it. - data_.erase(0, length); - - std::print("Response is: {}\n", response); - } - }); - } - - tcp::resolver resolver_; - tcp::socket socket_; - boost::asio::steady_timer timer_; - std::string data_; - bool connected_; -}; - -auto main(int argc, char* argv[]) -> int -{ - try - { - if (argc != 3) - { - std::print(stderr, "Usage: async_tcp_echo_client \n"); - return EXIT_FAILURE; - } - - boost::asio::io_context io_context; - - auto client = std::make_shared< AsynchronousTCPClient >(io_context, argv[1], argv[2]); - - std::thread io_thread([&io_context]() { io_context.run(); }); - - for (std::string line; std::getline(std::cin, line); std::print(stderr, "Enter command: ")) - { - const std::string::size_type sz = line.find("//"); - if ((sz != std::string::npos)) - { - line.resize(sz); - } - - boost::trim_right(line); - if (line.empty()) - { - continue; - } - - std::string command = char2esc(line); - command.insert(0, 1, LF); - command += CR; - - client->write(command); - } - - io_thread.join(); - } - catch (std::exception& e) - { - std::print(stderr, "Exception: {}\n", e.what()); - return EXIT_FAILURE; - } - - return EXIT_SUCCESS; -} diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 63c319f..9717062 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -132,7 +132,7 @@ class client // Start the asynchronous connect operation. socket_.async_connect(endpoint_iter->endpoint(), std::bind(&client::handle_connect, this, _1, endpoint_iter)); - // XXX [this, endpoint_iter](auto && PH1) { + // FIXME: [this, endpoint_iter](auto && PH1) { // handle_connect(std::forward(PH1), endpoint_iter); }); } else @@ -194,7 +194,7 @@ class client // Start an asynchronous operation to read a newline-delimited message. boost::asio::async_read_until( socket_, boost::asio::dynamic_buffer(input_buffer_), '\n', std::bind(&client::handle_read, this, _1, _2)); - // XXX [this](auto && PH1, auto && PH2) { + // FIXME: [this](auto && PH1, auto && PH2) { // handle_read(std::forward(PH1), // std::forward(PH2)); }); } @@ -252,7 +252,7 @@ class client { // Wait 10 seconds before sending the next heartbeat. heartbeat_timer_.expires_after(10s); - // heartbeat_timer_.async_wait(std::bind(&client::start_write, this)); + // cpp11: heartbeat_timer_.async_wait(std::bind(&client::start_write, this)); heartbeat_timer_.async_wait([this](const boost::system::error_code& /*e*/) { start_write(); }); } else @@ -286,7 +286,7 @@ class client } // Put the actor back to sleep. - // deadline_.async_wait(std::bind(&client::check_deadline, this)); + // cpp11: deadline_.async_wait(std::bind(&client::check_deadline, this)); deadline_.async_wait([this](const boost::system::error_code& /*e*/) { check_deadline(); }); } diff --git a/async_tcp_echo_server.cpp b/async_tcp_echo_server.cpp index a58bf93..ae1d006 100644 --- a/async_tcp_echo_server.cpp +++ b/async_tcp_echo_server.cpp @@ -8,6 +8,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include @@ -19,6 +20,8 @@ using boost::asio::ip::tcp; class session : public std::enable_shared_from_this< session > { + static constexpr int max_length{1014}; + public: session(tcp::socket socket) : socket_(std::move(socket)) {} @@ -28,7 +31,7 @@ class session : public std::enable_shared_from_this< session > void do_read() { auto self(shared_from_this()); - socket_.async_read_some(boost::asio::buffer(data_, max_length), + socket_.async_read_some(boost::asio::buffer(data_.data(), max_length), [this, self](boost::system::error_code ec, std::size_t length) { if (!ec) @@ -41,7 +44,7 @@ class session : public std::enable_shared_from_this< session > void do_write(std::size_t length) { auto self(shared_from_this()); - boost::asio::async_write(socket_, boost::asio::buffer(data_, length), + boost::asio::async_write(socket_, boost::asio::buffer(data_.data(), length), [this, self](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) @@ -52,11 +55,7 @@ class session : public std::enable_shared_from_this< session > } tcp::socket socket_; - enum - { - max_length = 1024 - }; - char data_[max_length]; + std::array< char, max_length > data_{}; }; class server @@ -87,7 +86,7 @@ class server tcp::socket socket_; }; -int main(int argc, char* argv[]) +auto main(int argc, char* argv[]) -> int { try { @@ -99,7 +98,7 @@ int main(int argc, char* argv[]) boost::asio::io_context io_context; - server s(io_context, std::atoi(argv[1])); + server const s(io_context, std::atoi(argv[1])); io_context.run(); } From a65502265baa1482074439c5bf3af1a3ff77a52e Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Fri, 14 Mar 2025 10:33:04 +0100 Subject: [PATCH 021/120] Add gcovr config files --- .gitignore | 4 ++++ CMakeLists.txt | 16 +++++++++------- coverage/.keep | 0 gcovr.cfg | 16 ++++++++++++++++ rrcp.txt | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 .gitignore create mode 100644 coverage/.keep create mode 100644 gcovr.cfg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75bfe7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +coverage/* +.*swp +*.log diff --git a/CMakeLists.txt b/CMakeLists.txt index 56b3323..1ad3097 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) # ---- add dependencies ---- find_package(Boost 1.87 COMPONENTS asio REQUIRED HINTS $ENV{HOME}/.local) -find_package(fmt 11.1.4 REQUIRED HINTS $ENV{HOME}/.local) +find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) # ---- default settings ---- @@ -31,9 +31,9 @@ endif() # ---- code coverage ---- -option(ENABLE_TEST_COVERAGE ON) +option(ENABLE_TEST_COVERAGE "Compile with test-coverage flags" ON) if(ENABLE_TEST_COVERAGE) - message(STATUS "ENABLE_TEST_COVERAGE is set!") + message(WARNING "ENABLE_TEST_COVERAGE is set!") add_compile_options(-O0 -g -fprofile-arcs -ftest-coverage) add_link_options(-fprofile-arcs -ftest-coverage) endif() @@ -67,11 +67,13 @@ target_link_libraries( add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) target_link_libraries(rrcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) -add_executable(timer timer.cpp) -target_link_libraries(timer PUBLIC Boost::asio) +if(APPLE) + add_executable(timer timer.cpp) + target_link_libraries(timer PUBLIC Boost::asio) -add_executable(async_client async_client.cpp) -target_link_libraries(async_client PUBLIC Boost::asio) + add_executable(async_client async_client.cpp) + target_link_libraries(async_client PUBLIC Boost::asio) +endif() add_executable(async_tcp_client async_tcp_client.cpp) target_link_libraries(async_tcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) diff --git a/coverage/.keep b/coverage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/gcovr.cfg b/gcovr.cfg new file mode 100644 index 0000000..ec1cf05 --- /dev/null +++ b/gcovr.cfg @@ -0,0 +1,16 @@ +root = . +search-path = build + +filter = src/* + +# exclude-directories = tests +exclude-directories = stagedir +exclude-directories = .cache + +gcov-ignore-parse-errors = all +print-summary = yes + +html-details = coverage/gcovr.html + +# cobertura-pretty = yes +# cobertura = build/cobertura.xml diff --git a/rrcp.txt b/rrcp.txt index 574e83d..7e59c65 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -12,7 +12,7 @@ M:Bit L:1 123456 G Octet M:Audio SOctet // without optionl parts M:Log SStruct1,-1,3.14 // multiple parameters M:MultilCmd S Octet 1;Long-1;String"Hallo World\n";Struct 1,+1,+3.14 // multiple commands's -M:Test 123456 S FREQ123456;MOD12;LOGIN"user","pasword" G FREQ;MOD;STATUS // multiple TU +M:Test 123456 S FREQ123456;MOD12;LOGIN"user","password" G FREQ;MOD;STATUS // multiple TU M:Test gFREQ180000;MOD12 s5BW gFREQ180000;MOD12;STATUS"Running" // TU partly failed M:eRADIO S FREQUENCY 123456789 E:12 // MU error @@ -20,3 +20,50 @@ M:RADIO T FREQUENCY 1 M:RADIO t M:RADIO d FREQUENCY 123456789 // trap data M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data +// more samples +M:Utility GInitialInfo"v0.5.9","async",0 +M:Access GHasControl +M:Access THasControl1 +M:Access GOwnSession +M:Access TOwnSession1 +M:Access SReqSession"Monitoring" +M:Audio GAudioVolume +M:Audio SAudioVolume"Level 0" +M:Audio TAudioVolume1 +M:Control SActPreset0 +M:Control GCurrMission +M:Control SCurrMission"testString" +M:Control TCurrMission1 +M:Control GCurrWF +M:Control TCurrWF1 +M:Control GPresetID +M:Control TPresetID1 +M:Control GTxInhibit +M:Control STxInhibit"Disabled" +M:Control TTxInhibit1 +M:Inventory GInvCountCus +M:Inventory GInventoryCus0 +M:Inventory GInvCount +M:Inventory GInventory0 +M:IP GOwnAdrvIV"Control" +M:IP SOwnAdrvIV"Control","testString","testString" +M:Maintenance SShutdown +M:Maintenance SRestart +M:Mission GGlobalAddr +M:Mission SGlobalAddr"testString" +M:Mission GMissions +M:Mission GPresets0,1 +M:OBIT GGOState +M:OBIT TGOState1 +M:OBIT GTestErrors +M:OBIT GTestIDs +M:RxTx GPowerLevel +M:RxTx SPowerLevel"Off" +M:RxTx TPowerLevel1 +M:RxTx GVswr +M:RxTx TVswr1 +M:Utility GBattStatus +M:Utility TBattStatus1 +M:Utility GErrorText0,"English" +M:Utility GInitialInfo"testString","testString",0 +M:Utility GPing"testString" From 337a352fb44408f9fa17260a7dcca6bad2b43f99 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Fri, 14 Mar 2025 11:04:23 +0100 Subject: [PATCH 022/120] Fix more clang-tidy warnings --- GNUmakefile | 14 +++++++++++++- async_tcp_client.cpp | 11 ++++++----- async_tcp_echo_client.cpp | 2 +- async_tcp_echo_server.cpp | 4 ++-- blocking_tcp_echo_server.cpp | 7 ++++++- rrcp_client.cpp | 2 +- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 43a8812..848d73f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -21,7 +21,19 @@ check: all fix: all run-clang-tidy -p build -fix \ - -check='-*,readability-use-std-min-max,-misc-include-cleaner,cppcoreguidelines-init-variables,hicpp-member-init,-modernize-avoid-bind,readability-braces-around-statements,hicpp-named-parameter,readability-else-after-return,modernize-use-trailing-return-type,readability-redundant-member-init,misc-const-correctness' \ + -checks='-*,\ +cppcoreguidelines-init-variables,\ +hicpp-explicit-conversions,\ +hicpp-member-init,\ +hicpp-named-parameter,\ +misc-const-correctness,\ +modernize-use-trailing-return-type,\ +performance-unnecessary-value-param,\ +readability-braces-around-statements,\ +readability-else-after-return,\ +readability-redundant-member-init,\ +readability-use-std-min-max,\ +' \ *.cpp test: all diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 9717062..e1f3f3a 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -21,11 +21,12 @@ #include #include #include +#include using boost::asio::steady_timer; using boost::asio::ip::tcp; -using std::placeholders::_1; -using std::placeholders::_2; +using std::placeholders::_1; // NOLINT +using std::placeholders::_2; // NOLINT using namespace std::chrono_literals; @@ -92,14 +93,14 @@ using namespace std::chrono_literals; class client { public: - client(boost::asio::io_context& io_context) : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) {} + explicit client(boost::asio::io_context& io_context) : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) {} // Called by the user of the client class to initiate the connection // process. The endpoints will have been obtained using a tcp::resolver. void start(tcp::resolver::results_type endpoints) { // Start the connect actor. - endpoints_ = endpoints; + endpoints_ = std::move(endpoints); start_connect(endpoints_.begin()); // Start the deadline actor. You will note that we're not setting any @@ -121,7 +122,7 @@ class client } private: - void start_connect(tcp::resolver::results_type::iterator endpoint_iter) + void start_connect(const tcp::resolver::results_type::iterator& endpoint_iter) { if (endpoint_iter != endpoints_.end()) { diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index 7256568..9e19286 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -28,7 +28,7 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT : resolver_(io_context), socket_(io_context), timer_(io_context) { resolver_.async_resolve(host, port, - [this](boost::system::error_code ec, tcp::resolver::results_type results) + [this](boost::system::error_code ec, const tcp::resolver::results_type& results) { if (!ec) { diff --git a/async_tcp_echo_server.cpp b/async_tcp_echo_server.cpp index ae1d006..a8f6a93 100644 --- a/async_tcp_echo_server.cpp +++ b/async_tcp_echo_server.cpp @@ -23,7 +23,7 @@ class session : public std::enable_shared_from_this< session > static constexpr int max_length{1014}; public: - session(tcp::socket socket) : socket_(std::move(socket)) {} + explicit session(tcp::socket socket) : socket_(std::move(socket)) {} void start() { do_read(); } @@ -98,7 +98,7 @@ auto main(int argc, char* argv[]) -> int boost::asio::io_context io_context; - server const s(io_context, std::atoi(argv[1])); + server const s(io_context, std::strtol(argv[1], nullptr, 10)); io_context.run(); } diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index f29118d..b7d048e 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -22,6 +22,9 @@ using boost::asio::ip::tcp; +namespace +{ + const int max_length = 1024; void session(tcp::socket sock) @@ -63,6 +66,8 @@ void server(boost::asio::io_context& io_context, unsigned short port) } } +} // namespace + auto main(int argc, char* argv[]) -> int { try @@ -75,7 +80,7 @@ auto main(int argc, char* argv[]) -> int boost::asio::io_context io_context; - server(io_context, std::atoi(argv[1])); + server(io_context, std::strtol(argv[1], nullptr, 10)); } catch (std::exception& e) { diff --git a/rrcp_client.cpp b/rrcp_client.cpp index 41bfc72..a0fe739 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -68,7 +68,7 @@ class rrcp_client void do_connect(const tcp::resolver::results_type& endpoints) { boost::asio::async_connect(socket_, endpoints, - [this](boost::system::error_code ec, tcp::endpoint) + [this](boost::system::error_code ec, const tcp::endpoint&) { if (!ec) { From 35addf6bf76ea306963b21d6d74e7ae1ae1f2e90 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Fri, 14 Mar 2025 13:00:58 +0100 Subject: [PATCH 023/120] Use a write message queue --- async_client.cpp | 7 +++++- async_tcp_client.cpp | 5 ++++- async_tcp_echo_client.cpp | 46 +++++++++++++++++++++++++++++---------- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/async_client.cpp b/async_client.cpp index 981a2c9..8ee0cdb 100644 --- a/async_client.cpp +++ b/async_client.cpp @@ -12,7 +12,10 @@ #include #include -const auto noop = std::bind([] {}); +namespace +{ + +const auto noop = std::bind([] {}); // FIXME: deprecated! convert to std::forward<>; CK const std::string delimiter{"\r\n\r\n"}; boost::asio::io_context io_context; @@ -38,6 +41,8 @@ void handle_read(boost::system::error_code /*unused*/, std::size_t xfer) boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); } +} // namespace + auto main() -> int { acceptor.async_accept(socket1, noop); diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index e1f3f3a..22225eb 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -93,7 +93,10 @@ using namespace std::chrono_literals; class client { public: - explicit client(boost::asio::io_context& io_context) : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) {} + explicit client(boost::asio::io_context& io_context) + : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) + { + } // Called by the user of the client class to initiate the connection // process. The endpoints will have been obtained using a tcp::resolver. diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index 9e19286..5ebe82f 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ using boost::asio::ip::tcp; using namespace std::chrono_literals; +using message_queue = std::deque< std::string >; constexpr size_t max_length = 65432; constexpr auto timeout_duration = 1s; @@ -25,7 +27,7 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT { public: AsynchronousTCPClient(boost::asio::io_context& io_context, const std::string& host, const std::string& port) - : resolver_(io_context), socket_(io_context), timer_(io_context) + : io_context_(io_context), resolver_(io_context), socket_(io_context), timer_(io_context) { resolver_.async_resolve(host, port, [this](boost::system::error_code ec, const tcp::resolver::results_type& results) @@ -51,6 +53,7 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT }); } + // This function write the message into the msg queue and starts the write actor void write(const std::string& message) { while (!connected_) @@ -63,18 +66,14 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT std::this_thread::sleep_for(timeout_duration); // NOLINT(misc-include-cleaner) } - auto self(shared_from_this()); - boost::asio::async_write(socket_, boost::asio::buffer(message), - [this, self](boost::system::error_code ec, std::size_t /*length*/) + boost::asio::post(io_context_, + [this, message]() { - if (!ec) - { - fmt::print(stderr, "Message sent.\n"); - } - else + bool const write_in_progress = !write_msgs_.empty(); + write_msgs_.push_back(message); + if (!write_in_progress) { - // There are no more endpoints to try. Shut down the client. - stop(); + do_write(); } }); } @@ -92,6 +91,29 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT } private: + void do_write() + { + auto self(shared_from_this()); + boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front()), + [this, self](boost::system::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + fmt::print(stderr, "Message sent.\n"); + write_msgs_.pop_front(); + if (!write_msgs_.empty()) + { + do_write(); + } + } + else + { + // There are no more endpoints to try. Shut down the client. + stop(); + } + }); + } + void do_read() { if (stopped_) @@ -146,10 +168,12 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT }); } + boost::asio::io_context& io_context_; tcp::resolver resolver_; tcp::socket socket_; boost::asio::steady_timer timer_; std::string data_; + message_queue write_msgs_; bool connected_{false}; bool stopped_{false}; }; From d724ccd45566d1266f4023756c551ae48007c4e6 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sat, 15 Mar 2025 18:55:39 +0100 Subject: [PATCH 024/120] Use enable_shared_from_this<> --- CMakeLists.txt | 2 +- async_tcp_client_v20.cpp | 315 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 async_tcp_client_v20.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ad3097..7115e67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,5 +75,5 @@ if(APPLE) target_link_libraries(async_client PUBLIC Boost::asio) endif() -add_executable(async_tcp_client async_tcp_client.cpp) +add_executable(async_tcp_client async_tcp_client_v20.cpp) target_link_libraries(async_tcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) diff --git a/async_tcp_client_v20.cpp b/async_tcp_client_v20.cpp new file mode 100644 index 0000000..275e998 --- /dev/null +++ b/async_tcp_client_v20.cpp @@ -0,0 +1,315 @@ +/*** + * async_tcp_client_v20.cpp + * ~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + ***/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // For std::getline +#include +#include +#include +#include +#include + +using boost::asio::steady_timer; +using boost::asio::ip::tcp; +using namespace std::chrono_literals; + +class client : public std::enable_shared_from_this< client > +{ + public: + explicit client(boost::asio::io_context& io_context) + : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) + { + } + + // Called by the user of the client class to initiate the connection + // process. The endpoints will have been obtained using a tcp::resolver. + void start(tcp::resolver::results_type endpoints) + { + // Start the connect actor. + endpoints_ = std::move(endpoints); + start_connect(endpoints_.begin()); + + // Start the deadline actor. You will note that we're not setting any + // particular deadline here. Instead, the connect and input actors will + // update the deadline prior to each asynchronous operation. + deadline_.async_wait([this](const boost::system::error_code& /*e*/) { check_deadline(); }); + } + + void write() + { + while (!stopped_) + { + std::string message; + std::print("Enter message to send: "); + std::getline(std::cin, message); + + if (message.empty()) + { + continue; + } + + if (message.back() != '\n') + { + message += "\n"; + } + + std::print(stderr, "Sending: {}\n", message); + + // Start an asynchronous operation to send the message. + boost::asio::async_write(socket_, boost::asio::buffer(message), + [this](const boost::system::error_code& error, std::size_t) { handle_write(error); }); + } + } + + // This function terminates all the actors to shut down the connection. It + // may be called by the user of the client class, or by the class itself in + // response to graceful termination or an unrecoverable error. + void stop() + { + stopped_ = true; + boost::system::error_code ignored_error; + socket_.close(ignored_error); + deadline_.cancel(); + heartbeat_timer_.cancel(); + } + + private: + void start_connect(const tcp::resolver::results_type::iterator& endpoint_iter) + { + if (endpoint_iter != endpoints_.end()) + { + std::print("Trying {}:{}...\n", endpoint_iter->endpoint().address().to_string(), endpoint_iter->endpoint().port()); + + // Set a deadline for the connect operation. + deadline_.expires_after(60s); + + // Start the asynchronous connect operation. + socket_.async_connect(endpoint_iter->endpoint(), + [this, endpoint_iter](const boost::system::error_code& error) { handle_connect(error, endpoint_iter); }); + } + else + { + // There are no more endpoints to try. Shut down the client. + stop(); + } + } + + void handle_connect(const boost::system::error_code& error, tcp::resolver::results_type::iterator endpoint_iter) + { + if (stopped_) + { + return; + } + + // The async_connect() function automatically opens the socket at the + // start of the asynchronous operation. If the socket is closed at this + // time then the timeout handler must have run first. + if (!socket_.is_open()) + { + std::print(stderr, "Connect timed out\n"); + + // Try the next available endpoint. + start_connect(++endpoint_iter); + } + + // Check if the connect operation failed before the deadline expired. + else if (error) + { + std::print(stderr, "Error on Connect: {}\n", error.message()); + + // We need to close the socket used in the previous connection + // attempt before starting a new one. + socket_.close(); + + // Try the next available endpoint. + start_connect(++endpoint_iter); + } + + // Otherwise we have successfully established a connection. + else + { + std::print( + "Connected to {}:{}\n", endpoint_iter->endpoint().address().to_string(), endpoint_iter->endpoint().port()); + + // Start the input actor. + start_read(); + + // Start the heartbeat actor. + start_write(); + } + } + + void start_read() + { + if (stopped_) + { + return; + } + + // Set a deadline for the read operation. + deadline_.expires_after(30s); + + auto self(shared_from_this()); + + // Start an asynchronous operation to read a newline-delimited message. + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), '\n', + [this, self](const boost::system::error_code& error, std::size_t n) { handle_read(error, n); }); + } + + void handle_read(const boost::system::error_code& error, std::size_t n) + { + if (stopped_) + { + return; + } + + if (!error) + { + // Extract the newline-delimited message from the buffer. + std::string line = input_buffer_.substr(0, n); + input_buffer_.erase(0, n); + + // Empty messages are heartbeats and so ignored. + if (line.length() > 1) + { + std::print("Received: {}\n", line); + } + else + { + std::print(stderr, "Received: {}\n", "hartbeat"); + } + + start_read(); + } + else + { + std::print(stderr, "Error on receive: {}\n", error.message()); + + stop(); + } + } + + void start_write() + { + if (stopped_) + { + return; + } + + std::string message; + message += "\n"; + std::print(stderr, "Sending: {}\n", "hartbeat"); + + auto self(shared_from_this()); + + // Start an asynchronous operation to send a heartbeat message. + boost::asio::async_write(socket_, boost::asio::buffer(message), + [this, self](const boost::system::error_code& error, std::size_t) { handle_write(error); }); + } + + void handle_write(const boost::system::error_code& error) + { + if (stopped_) + { + return; + } + + if (!error) + { + // Wait 10 seconds before sending the next heartbeat. + std::print(stderr, "Waiting on: {}\n", "hartbeat"); + heartbeat_timer_.expires_after(10s); + heartbeat_timer_.async_wait([this](const boost::system::error_code&) { start_write(); }); + } + else + { + std::print(stderr, "Error on sending heartbeat: {}\n", error.message()); + + stop(); + } + } + + void check_deadline() + { + if (stopped_) + { + return; + } + + // Check whether the deadline has passed. We compare the deadline + // against the current time since a new asynchronous operation may have + // moved the deadline before this actor had a chance to run. + if (deadline_.expiry() <= steady_timer::clock_type::now()) + { + // The deadline has passed. The socket is closed so that any + // outstanding asynchronous operations are cancelled. + socket_.close(); + + // There is no longer an active deadline. The expiry is set to the + // maximum time point so that the actor takes no action until a new + // deadline is set. + deadline_.expires_at(steady_timer::time_point::max()); + } + + auto self(shared_from_this()); + + // Put the actor back to sleep. + deadline_.async_wait([this, self](const boost::system::error_code&) { check_deadline(); }); + } + + bool stopped_{false}; + tcp::resolver::results_type endpoints_; + tcp::socket socket_; + std::string input_buffer_; + steady_timer deadline_; + steady_timer heartbeat_timer_; +}; + +auto main(int argc, char* argv[]) -> int +{ + try + { + if (argc != 3) + { + std::print("Usage: client \n"); + return 1; + } + + boost::asio::io_context io_context; + tcp::resolver resolver(io_context); + + auto c = std::make_shared< client >(io_context); + + c->start(resolver.resolve(argv[1], argv[2])); + + std::thread io_thread([&io_context]() { io_context.run(); }); + + c->write(); + + c->stop(); + io_thread.join(); + } + catch (std::exception& e) + { + std::print("Exception: {}\n", e.what()); + } + + return 0; +} From 2584f7c423816b15abb75f5c850ba4b3f10a4bb2 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 16 Mar 2025 13:10:23 +0100 Subject: [PATCH 025/120] Prevent more clang-tidy warnings Fix gcovr.cfg too --- GNUmakefile | 1 + async_client.cpp | 15 ++++++++++----- blocking_tcp_echo_server.cpp | 7 ++++--- gcovr.cfg | 8 ++++++-- timer.cpp | 4 ++-- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 848d73f..2c6fcbc 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -28,6 +28,7 @@ hicpp-member-init,\ hicpp-named-parameter,\ misc-const-correctness,\ modernize-use-trailing-return-type,\ +performance-avoid-endl,\ performance-unnecessary-value-param,\ readability-braces-around-statements,\ readability-else-after-return,\ diff --git a/async_client.cpp b/async_client.cpp index 8ee0cdb..b77ce02 100644 --- a/async_client.cpp +++ b/async_client.cpp @@ -15,14 +15,17 @@ namespace { -const auto noop = std::bind([] {}); // FIXME: deprecated! convert to std::forward<>; CK +const auto noop = std::bind([] {}); // NOLINT(modernize-avoid-bind) NOTE: deprecated too! CK const std::string delimiter{"\r\n\r\n"}; boost::asio::io_context io_context; boost::asio::ip::tcp::acceptor acceptor(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); boost::asio::ip::tcp::socket socket1(io_context); boost::asio::ip::tcp::socket socket2(io_context); -boost::asio::streambuf streambuf; + +std::string input_buffer_; +auto streambuf = boost::asio::dynamic_buffer(input_buffer_); +; // void do_read(); @@ -30,15 +33,17 @@ void handle_read(boost::system::error_code /*unused*/, std::size_t xfer) { assert(streambuf.size() >= xfer); - std::string const command{buffers_begin(streambuf.data()), buffers_begin(streambuf.data()) + xfer - delimiter.length()}; + std::string const command{input_buffer_.data(), xfer - delimiter.length()}; streambuf.consume(xfer); - // XXX assert(command == "cmd1"); std::cout << "received command: " << command << "\n" << "streambuf contains " << streambuf.size() << " bytes.\n"; - boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); + if (command == "cmd1") + { + boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); + } } } // namespace diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index b7d048e..1d39106 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -25,7 +26,7 @@ using boost::asio::ip::tcp; namespace { -const int max_length = 1024; +constexpr int max_length {1024}; void session(tcp::socket sock) { @@ -33,7 +34,7 @@ void session(tcp::socket sock) { for (;;) { - char data[max_length]; + std::array data{}; boost::system::error_code error; size_t const length = sock.read_some(boost::asio::buffer(data), error); @@ -55,7 +56,7 @@ void session(tcp::socket sock) } } -void server(boost::asio::io_context& io_context, unsigned short port) +void server(boost::asio::io_context& io_context, short port) { tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port)); for (;;) diff --git a/gcovr.cfg b/gcovr.cfg index ec1cf05..53cf6ba 100644 --- a/gcovr.cfg +++ b/gcovr.cfg @@ -1,11 +1,15 @@ root = . search-path = build -filter = src/* +# filter = src -# exclude-directories = tests +exclude-directories = doc +exclude-directories = tests +exclude-directories = coverage exclude-directories = stagedir exclude-directories = .cache +exclude-directories = .direnv +exclude-directories = .venv gcov-ignore-parse-errors = all print-summary = yes diff --git a/timer.cpp b/timer.cpp index 38be13a..234001f 100644 --- a/timer.cpp +++ b/timer.cpp @@ -24,13 +24,13 @@ class printer timer_.async_wait([this](const boost::system::error_code& /*ec*/) { print(); }); } - ~printer() { std::cout << "Final count is " << count_ << std::endl; } + ~printer() { std::cout << "Final count is " << count_ << '\n'; } void print() { if (count_ < kMaxCount) { - std::cout << count_ << std::endl; + std::cout << count_ << '\n'; ++count_; timer_.expires_at(timer_.expiry() + boost::asio::chrono::seconds(1)); From 00f89a5e4834deac57226394b90c71b189216ba5 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 16 Mar 2025 13:39:11 +0100 Subject: [PATCH 026/120] Try to fix heartbeat handling --- async_tcp_client_v20.cpp | 30 ++++++++++++++++-------------- blocking_tcp_echo_server.cpp | 6 +++--- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/async_tcp_client_v20.cpp b/async_tcp_client_v20.cpp index 275e998..d58726a 100644 --- a/async_tcp_client_v20.cpp +++ b/async_tcp_client_v20.cpp @@ -54,12 +54,10 @@ class client : public std::enable_shared_from_this< client > void write() { - while (!stopped_) - { - std::string message; - std::print("Enter message to send: "); - std::getline(std::cin, message); + auto self(shared_from_this()); + for (std::string message; !stopped_ && std::getline(std::cin, message); std::print("Enter message to send: ")) + { if (message.empty()) { continue; @@ -67,14 +65,14 @@ class client : public std::enable_shared_from_this< client > if (message.back() != '\n') { - message += "\n"; + message += '\n'; } std::print(stderr, "Sending: {}\n", message); // Start an asynchronous operation to send the message. boost::asio::async_write(socket_, boost::asio::buffer(message), - [this](const boost::system::error_code& error, std::size_t) { handle_write(error); }); + [this, self](const boost::system::error_code& error, std::size_t) { handle_write(error); }); } } @@ -183,17 +181,17 @@ class client : public std::enable_shared_from_this< client > if (!error) { // Extract the newline-delimited message from the buffer. - std::string line = input_buffer_.substr(0, n); + std::string line = input_buffer_.substr(0, n - 1); // NOTE: w/o '\n' input_buffer_.erase(0, n); // Empty messages are heartbeats and so ignored. - if (line.length() > 1) + if (line.empty()) { - std::print("Received: {}\n", line); + std::print(stderr, "Received: {}\n", "hartbeat"); } else { - std::print(stderr, "Received: {}\n", "hartbeat"); + std::print("Received: {}\n", line); } start_read(); @@ -213,8 +211,7 @@ class client : public std::enable_shared_from_this< client > return; } - std::string message; - message += "\n"; + std::string message{'\n'}; std::print(stderr, "Sending: {}\n", "hartbeat"); auto self(shared_from_this()); @@ -233,8 +230,13 @@ class client : public std::enable_shared_from_this< client > if (!error) { + if (heartbeat_timer_.expiry() <= steady_timer::clock_type::now()) + { + std::print(stderr, "Waiting for next to send: {}\n", "hartbeat"); + } + // Wait 10 seconds before sending the next heartbeat. - std::print(stderr, "Waiting on: {}\n", "hartbeat"); + heartbeat_timer_.cancel(); heartbeat_timer_.expires_after(10s); heartbeat_timer_.async_wait([this](const boost::system::error_code&) { start_write(); }); } diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index 1d39106..c5b04e2 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -8,13 +8,13 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include #include #include #include -#include #include #include #include @@ -26,7 +26,7 @@ using boost::asio::ip::tcp; namespace { -constexpr int max_length {1024}; +constexpr int max_length{1024}; void session(tcp::socket sock) { @@ -34,7 +34,7 @@ void session(tcp::socket sock) { for (;;) { - std::array data{}; + std::array< char, max_length > data{}; boost::system::error_code error; size_t const length = sock.read_some(boost::asio::buffer(data), error); From 4bc35b9505584d53e24ffc416d917b3710759d71 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 16 Mar 2025 14:47:20 +0100 Subject: [PATCH 027/120] Add signal handler to async server --- CMakeLists.txt | 15 +++++++++------ GNUmakefile | 7 ++++++- async_tcp_client_v20.cpp | 4 ++-- async_tcp_echo_server.cpp | 31 ++++++++++++++++++++++++++++++- timer.cpp | 2 +- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7115e67..41da23f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,10 @@ if(ENABLE_TEST_COVERAGE) add_link_options(-fprofile-arcs -ftest-coverage) endif() +# ---- ctest ---- + +enable_testing() + add_executable(async_tcp_echo_server async_tcp_echo_server.cpp) target_link_libraries(async_tcp_echo_server PUBLIC Boost::asio) @@ -57,12 +61,8 @@ target_link_libraries( PUBLIC Boost::asio ) -add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) -target_link_libraries( - blocking_tcp_echo_server - PRIVATE rrcp_helper - PUBLIC Boost::asio -) +# add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) +# target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::asio) add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) target_link_libraries(rrcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) @@ -70,10 +70,13 @@ target_link_libraries(rrcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) if(APPLE) add_executable(timer timer.cpp) target_link_libraries(timer PUBLIC Boost::asio) + add_test(NAME timer COMMAND timer) add_executable(async_client async_client.cpp) target_link_libraries(async_client PUBLIC Boost::asio) + add_test(NAME async_client COMMAND async_client) endif() add_executable(async_tcp_client async_tcp_client_v20.cpp) target_link_libraries(async_tcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) +#XXX add_test(NAME async_tcp_client COMMAND echo | async_tcp_client localhost 8001) diff --git a/GNUmakefile b/GNUmakefile index 2c6fcbc..eed3816 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -11,7 +11,7 @@ all: build ninja -C build distclean: - rm -rf build + rm -rf build coverage/* build: CMakeLists.txt cmake -S . -B $@ @@ -42,6 +42,11 @@ test: all build/async_tcp_echo_server 8000 & cat rrcp.txt | build/rrcp_client localhost 8000 cat rrcp.txt | build/async_tcp_echo_client localhost 8000 + echo | build/async_tcp_client localhost 8001 + cat rrcp.txt | build/blocking_tcp_echo_client localhost 8000 + ctest --test-dir build + -killall async_tcp_echo_server + gcovr format: .clang-format git ls-files ::*.cpp ::*.hpp | xargs clang-format -i diff --git a/async_tcp_client_v20.cpp b/async_tcp_client_v20.cpp index d58726a..ced87c7 100644 --- a/async_tcp_client_v20.cpp +++ b/async_tcp_client_v20.cpp @@ -96,7 +96,7 @@ class client : public std::enable_shared_from_this< client > std::print("Trying {}:{}...\n", endpoint_iter->endpoint().address().to_string(), endpoint_iter->endpoint().port()); // Set a deadline for the connect operation. - deadline_.expires_after(60s); + deadline_.expires_after(6s); // Start the asynchronous connect operation. socket_.async_connect(endpoint_iter->endpoint(), @@ -181,7 +181,7 @@ class client : public std::enable_shared_from_this< client > if (!error) { // Extract the newline-delimited message from the buffer. - std::string line = input_buffer_.substr(0, n - 1); // NOTE: w/o '\n' + std::string line = input_buffer_.substr(0, n - 1); // NOTE: w/o '\n' input_buffer_.erase(0, n); // Empty messages are heartbeats and so ignored. diff --git a/async_tcp_echo_server.cpp b/async_tcp_echo_server.cpp index a8f6a93..d56f9e5 100644 --- a/async_tcp_echo_server.cpp +++ b/async_tcp_echo_server.cpp @@ -8,7 +8,10 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include + #include +#include #include #include #include @@ -62,8 +65,19 @@ class server { public: server(boost::asio::io_context& io_context, short port) - : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)), socket_(io_context) + : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)), socket_(io_context), signals_(io_context) { + // Register to handle the signals that indicate when the server should exit. + // It is safe to register for the same signal multiple times in a program, + // provided all registration for the specified signal is made through Asio. + signals_.add(SIGINT); + signals_.add(SIGTERM); +#if defined(SIGQUIT) + signals_.add(SIGQUIT); +#endif // defined(SIGQUIT) + + do_await_stop(); + do_accept(); } @@ -82,8 +96,23 @@ class server }); } + void do_await_stop() + { + signals_.async_wait( + [this](std::error_code /*ec*/, int /*signo*/) + { + // The server is stopped by cancelling all outstanding asynchronous + // operations. Once all operations have finished the io_context::run() + // call will exit. + acceptor_.close(); + socket_.close(); + // TODO:: connection_manager_.stop_all(); + }); + } + tcp::acceptor acceptor_; tcp::socket socket_; + boost::asio::signal_set signals_; }; auto main(int argc, char* argv[]) -> int diff --git a/timer.cpp b/timer.cpp index 234001f..3e976e4 100644 --- a/timer.cpp +++ b/timer.cpp @@ -33,7 +33,7 @@ class printer std::cout << count_ << '\n'; ++count_; - timer_.expires_at(timer_.expiry() + boost::asio::chrono::seconds(1)); + timer_.expires_at(timer_.expiry() + boost::asio::chrono::milliseconds(100)); // cpp11: timer_.async_wait(std::bind(&printer::print, this)); timer_.async_wait([this](const boost::system::error_code& /*ec*/) { print(); }); } From baa04bbf854171e18274d67c212ab00ebd6a4d44 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 16 Mar 2025 19:00:26 +0100 Subject: [PATCH 028/120] Add Signal and error handling --- CMakeLists.txt | 3 ++- GNUmakefile | 2 +- async_tcp_client_v20.cpp | 1 + async_tcp_echo_server.cpp | 55 ++++++++++++++++++++++++++++++++------- rrcp_client.cpp | 1 + 5 files changed, 51 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 41da23f..57bcb11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,10 +32,11 @@ endif() # ---- code coverage ---- option(ENABLE_TEST_COVERAGE "Compile with test-coverage flags" ON) -if(ENABLE_TEST_COVERAGE) +if(ENABLE_TEST_COVERAGE AND CMAKE_BUILD_TYPE) message(WARNING "ENABLE_TEST_COVERAGE is set!") add_compile_options(-O0 -g -fprofile-arcs -ftest-coverage) add_link_options(-fprofile-arcs -ftest-coverage) + # FIXME: add_compile_definitions(TARGET_CODE_COVERAGE) endif() # ---- ctest ---- diff --git a/GNUmakefile b/GNUmakefile index eed3816..4f6063e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -14,7 +14,7 @@ distclean: rm -rf build coverage/* build: CMakeLists.txt - cmake -S . -B $@ + cmake -S . -B $@ -D CMAKE_BUILD_TYPE=Debug check: all run-clang-tidy -p build *.cpp diff --git a/async_tcp_client_v20.cpp b/async_tcp_client_v20.cpp index ced87c7..14f8b67 100644 --- a/async_tcp_client_v20.cpp +++ b/async_tcp_client_v20.cpp @@ -311,6 +311,7 @@ auto main(int argc, char* argv[]) -> int catch (std::exception& e) { std::print("Exception: {}\n", e.what()); + return 1; } return 0; diff --git a/async_tcp_echo_server.cpp b/async_tcp_echo_server.cpp index d56f9e5..b0450ab 100644 --- a/async_tcp_echo_server.cpp +++ b/async_tcp_echo_server.cpp @@ -19,6 +19,13 @@ #include #include +#ifdef TARGET_CODE_COVERAGE +// Forward declaration of flush api +// extern "C" { +extern void __gcov_flush(); +// } +#endif + using boost::asio::ip::tcp; class session : public std::enable_shared_from_this< session > @@ -41,6 +48,10 @@ class session : public std::enable_shared_from_this< session > { do_write(length); } + else + { + socket_.close(); + } }); } @@ -54,6 +65,10 @@ class session : public std::enable_shared_from_this< session > { do_read(); } + else + { + socket_.close(); + } }); } @@ -72,6 +87,7 @@ class server // provided all registration for the specified signal is made through Asio. signals_.add(SIGINT); signals_.add(SIGTERM); + #if defined(SIGQUIT) signals_.add(SIGQUIT); #endif // defined(SIGQUIT) @@ -90,23 +106,42 @@ class server if (!ec) { std::make_shared< session >(std::move(socket_))->start(); + do_accept(); + } + else + { + acceptor_.close(); + socket_.close(); } - - do_accept(); }); } + // Signal handler definition which flushes profiling data void do_await_stop() { signals_.async_wait( - [this](std::error_code /*ec*/, int /*signo*/) + [this](std::error_code ec, int signo) { - // The server is stopped by cancelling all outstanding asynchronous - // operations. Once all operations have finished the io_context::run() - // call will exit. - acceptor_.close(); - socket_.close(); - // TODO:: connection_manager_.stop_all(); + std::cerr << "Signal handler called for " << signo << "\n"; + if (!ec) + { + // The server is stopped by cancelling all outstanding asynchronous + // operations. Once all operations have finished the io_context::run() + // call will exit. + acceptor_.close(); + socket_.close(); + } + else + { + acceptor_.close(); + socket_.close(); + +#ifdef TARGET_CODE_COVERAGE + __gcov_flush(); +#endif + + exit(0); + } }); } @@ -130,10 +165,12 @@ auto main(int argc, char* argv[]) -> int server const s(io_context, std::strtol(argv[1], nullptr, 10)); io_context.run(); + std::cout << "io_service.run complete, shutdown successful\n"; } catch (std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; + return 1; } return 0; diff --git a/rrcp_client.cpp b/rrcp_client.cpp index a0fe739..1a21522 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -203,6 +203,7 @@ auto main(int argc, char* argv[]) -> int catch (std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; + return 1; } return 0; From 93f338317526ec5b603e4372219916bb4aa88c47 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 16 Mar 2025 19:18:03 +0100 Subject: [PATCH 029/120] Cleanup the code --- async_tcp_client.cpp | 7 ++++--- async_tcp_client_v20.cpp | 8 ++++---- async_tcp_echo_server.cpp | 10 +++++----- blocking_tcp_echo_client.cpp | 5 +---- blocking_tcp_echo_server.cpp | 4 ++-- rrcp_client.cpp | 6 +++--- timer.cpp | 4 ++-- 7 files changed, 21 insertions(+), 23 deletions(-) diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 22225eb..9d19df2 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -308,8 +308,8 @@ auto main(int argc, char* argv[]) -> int { if (argc != 3) { - std::cerr << "Usage: client \n"; - return 1; + std::cerr << "Usage: async_tcp_client \n"; + return EXIT_FAILURE; } boost::asio::io_context io_context; @@ -323,7 +323,8 @@ auto main(int argc, char* argv[]) -> int catch (std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; + return EXIT_FAILURE; } - return 0; + return EXIT_SUCCESS; } diff --git a/async_tcp_client_v20.cpp b/async_tcp_client_v20.cpp index 14f8b67..020ed5e 100644 --- a/async_tcp_client_v20.cpp +++ b/async_tcp_client_v20.cpp @@ -290,8 +290,8 @@ auto main(int argc, char* argv[]) -> int { if (argc != 3) { - std::print("Usage: client \n"); - return 1; + std::print("Usage: {} \n", *argv); + return EXIT_FAILURE; } boost::asio::io_context io_context; @@ -311,8 +311,8 @@ auto main(int argc, char* argv[]) -> int catch (std::exception& e) { std::print("Exception: {}\n", e.what()); - return 1; + return EXIT_FAILURE; } - return 0; + return EXIT_SUCCESS; } diff --git a/async_tcp_echo_server.cpp b/async_tcp_echo_server.cpp index b0450ab..6147b2d 100644 --- a/async_tcp_echo_server.cpp +++ b/async_tcp_echo_server.cpp @@ -30,7 +30,7 @@ using boost::asio::ip::tcp; class session : public std::enable_shared_from_this< session > { - static constexpr int max_length{1014}; + static constexpr int max_length{1024}; public: explicit session(tcp::socket socket) : socket_(std::move(socket)) {} @@ -140,7 +140,7 @@ class server __gcov_flush(); #endif - exit(0); + exit(EXIT_FAILURE); } }); } @@ -157,7 +157,7 @@ auto main(int argc, char* argv[]) -> int if (argc != 2) { std::cerr << "Usage: async_tcp_echo_server \n"; - return 1; + return EXIT_FAILURE; } boost::asio::io_context io_context; @@ -170,8 +170,8 @@ auto main(int argc, char* argv[]) -> int catch (std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; - return 1; + return EXIT_FAILURE; } - return 0; + return EXIT_SUCCESS; } diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index 21b6c40..bc5bc52 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -26,10 +26,7 @@ using boost::asio::ip::tcp; -enum -{ - max_length = 1024 -}; +static constexpr int max_length{1024}; auto main(int argc, char* argv[]) -> int { diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index c5b04e2..c292652 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -76,7 +76,7 @@ auto main(int argc, char* argv[]) -> int if (argc != 2) { std::cerr << "Usage: blocking_tcp_echo_server \n"; - return 1; + return EXIT_FAILURE; } boost::asio::io_context io_context; @@ -88,5 +88,5 @@ auto main(int argc, char* argv[]) -> int std::cerr << "Exception: " << e.what() << "\n"; } - return 0; + return EXIT_SUCCESS; } diff --git a/rrcp_client.cpp b/rrcp_client.cpp index 1a21522..bcc4937 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -148,7 +148,7 @@ auto main(int argc, char* argv[]) -> int if (argc != 3) { std::cerr << "Usage: rrcp_client \n"; - return 1; + return EXIT_FAILURE; } boost::asio::io_context io_context; @@ -203,8 +203,8 @@ auto main(int argc, char* argv[]) -> int catch (std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; - return 1; + return EXIT_FAILURE; } - return 0; + return EXIT_SUCCESS; } diff --git a/timer.cpp b/timer.cpp index 3e976e4..1f6636a 100644 --- a/timer.cpp +++ b/timer.cpp @@ -55,8 +55,8 @@ auto main() -> int catch (const std::exception& e) { std::print("Error: {}\n", e.what()); - return 1; + return EXIT_FAILURE; } - return 0; + return EXIT_SUCCESS; } From 4e30d6c9efc42bc0773d4a5931a39126efc4a1ae Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 16 Mar 2025 19:44:23 +0100 Subject: [PATCH 030/120] Modernize rrcp_helper --- rrcp_helper.cpp | 132 ++++++++++++++++-------------------------------- rrcp_helper.hpp | 7 +-- 2 files changed, 47 insertions(+), 92 deletions(-) diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 6847d4f..de50a10 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -1,125 +1,79 @@ #include "rrcp_helper.hpp" -#include #include +#include #include +#include -constexpr const char ESC = 0x1B; -constexpr const char REPLACE_LF = 0x01; -constexpr const char REPLACE_CR = 0x02; -constexpr const char REPLACE_ESC = 0x03; +constexpr char ESC = 0x1B; +constexpr char REPLACE_LF = 0x01; +constexpr char REPLACE_CR = 0x02; +constexpr char REPLACE_ESC = 0x03; -// constexpr const char SP = 0x20; -// constexpr const char DOT = 0x2E; -// constexpr const char SEMICOLON = 0x3B; -// constexpr const char COMMA = 0x2C; -// constexpr const char SHARP = 0x23; -// constexpr const char DQUOTE = '\"'; -// constexpr const char BACKSLASH = '\\'; -// constexpr const char COLON = 0x3A; - -auto esc2char(const std::string& data) -> std::string +auto esc2char(std::string_view data) -> std::string { std::string message; - size_t const len = data.size(); - char c = 0; - size_t i = 0; - while (i < len) + auto len = data.size(); + for (size_t i = 0; i < len; ++i) { - // get next char - c = data[i]; + char c = data[i]; - // end mark found, the message is complete. - // TODO: Why is the CR not needed anymore? CK - if (c == CR) + if (c == '\r') { - // XXX message += c; return message; } - // An escape is found thus we want to - // replace the escape sequence if (c == ESC) { - // On the ESC should follow an replacement character - // REPLACE_LF... REPLACE_ESC - if (i != (len - 1)) + if (i == len - 1) { - // get next character - i++; - - c = data[i]; - if (REPLACE_LF == c) - { - c = static_cast< char >(LF); - } - else if (REPLACE_CR == c) - { - c = static_cast< char >(CR); - } - else if (REPLACE_ESC == c) - { - c = static_cast< char >(ESC); - } - else - { - std::cerr << "esc2char: Error contains unexpected ESC character!\n"; - return ""; - } + throw std::runtime_error("esc2char: Error - message ends with escape character!"); } - else + + char next = data[++i]; + switch (next) { - std::cerr << "esc2char: Error message ends with escape character!\n"; - return ""; + case REPLACE_LF: + c = '\n'; + break; + case REPLACE_CR: + c = '\r'; + break; + case REPLACE_ESC: + c = ESC; + break; + default: + throw std::runtime_error("esc2char: Error - unexpected ESC character!"); } } - // append current character - message += c; - // continue with next character - i++; + message.push_back(c); } return message; } -auto char2esc(const std::string& data) -> std::string +auto char2esc(std::string_view data) -> std::string { std::string message; - size_t const len = data.size(); - char c = 0; - size_t i = 0; - - while (i < len) + for (char c : data) { - // get next char - c = data[i++]; - // and replace CR and LF and the ESC itself switch (c) { - case LF: - { - message += static_cast< char >(ESC); - message += static_cast< char >(REPLACE_LF); - }; - break; - case CR: - { - message += static_cast< char >(ESC); - message += static_cast< char >(REPLACE_CR); - }; - break; - + case '\n': + message.push_back(ESC); + message.push_back(REPLACE_LF); + break; + case '\r': + message.push_back(ESC); + message.push_back(REPLACE_CR); + break; case ESC: - { - message += static_cast< char >(ESC); - message += static_cast< char >(REPLACE_ESC); - }; - break; + message.push_back(ESC); + message.push_back(REPLACE_ESC); + break; default: - { - message += c; - } - break; + message.push_back(c); + break; } } return message; diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index bd9a577..17f4fe3 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include constexpr const char LF{0x0A}; // \n constexpr const char CR{0x0D}; // \r @@ -14,13 +15,13 @@ constexpr const char CR{0x0D}; // \r * * @return message string like 'M:IBIT SStart' */ -extern auto esc2char(const std::string& data) -> std::string; +extern auto esc2char(const std::string_view data) -> std::string; /** * @brief Replaces LF, CR with Escape sequence * - * @param data: data to send + * @param data: data to send * * @return translated data */ -extern auto char2esc(const std::string& data) -> std::string; +extern auto char2esc(const std::string_view data) -> std::string; From e4b1a82b59a950298d0524cf37031eaae93e7ae0 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 16 Mar 2025 19:59:46 +0100 Subject: [PATCH 031/120] Modernize and format the code again --- GNUmakefile | 2 ++ async_tcp_echo_server.cpp | 3 +-- rrcp_helper.cpp | 4 ++-- rrcp_helper.hpp | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 4f6063e..ace6b02 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -28,8 +28,10 @@ hicpp-member-init,\ hicpp-named-parameter,\ misc-const-correctness,\ modernize-use-trailing-return-type,\ +modernize-deprecated-headers,\ performance-avoid-endl,\ performance-unnecessary-value-param,\ +readability-avoid-const-params-in-decls,\ readability-braces-around-statements,\ readability-else-after-return,\ readability-redundant-member-init,\ diff --git a/async_tcp_echo_server.cpp b/async_tcp_echo_server.cpp index 6147b2d..1a7f99a 100644 --- a/async_tcp_echo_server.cpp +++ b/async_tcp_echo_server.cpp @@ -8,12 +8,11 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include - #include #include #include #include +#include #include #include #include diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index de50a10..274f84b 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -30,7 +30,7 @@ auto esc2char(std::string_view data) -> std::string throw std::runtime_error("esc2char: Error - message ends with escape character!"); } - char next = data[++i]; + char const next = data[++i]; switch (next) { case REPLACE_LF: @@ -55,7 +55,7 @@ auto esc2char(std::string_view data) -> std::string auto char2esc(std::string_view data) -> std::string { std::string message; - for (char c : data) + for (char const c : data) { switch (c) { diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index 17f4fe3..e058ad5 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -15,7 +15,7 @@ constexpr const char CR{0x0D}; // \r * * @return message string like 'M:IBIT SStart' */ -extern auto esc2char(const std::string_view data) -> std::string; +extern auto esc2char(std::string_view data) -> std::string; /** * @brief Replaces LF, CR with Escape sequence @@ -24,4 +24,4 @@ extern auto esc2char(const std::string_view data) -> std::string; * * @return translated data */ -extern auto char2esc(const std::string_view data) -> std::string; +extern auto char2esc(std::string_view data) -> std::string; From a171a77b98b6a75ba92b07d86648fd71ca07655c Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 16 Mar 2025 21:45:43 +0100 Subject: [PATCH 032/120] Add rrcp_async_tcp_client.cpp --- CMakeLists.txt | 6 +- GNUmakefile | 3 +- async_tcp_echo_server.cpp | 2 +- blocking_tcp_echo_server.cpp | 2 +- rrcp_async_tcp_client.cpp | 232 +++++++++++++++++++++++++++++++++++ 5 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 rrcp_async_tcp_client.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 57bcb11..4de46eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,6 @@ if(APPLE) add_test(NAME async_client COMMAND async_client) endif() -add_executable(async_tcp_client async_tcp_client_v20.cpp) -target_link_libraries(async_tcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) -#XXX add_test(NAME async_tcp_client COMMAND echo | async_tcp_client localhost 8001) +add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) +target_link_libraries(rrcp_async_tcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) +# TODO: add_test(NAME rrcp_async_tcp_client COMMAND echo | rrcp_async_tcp_client localhost 8001) diff --git a/GNUmakefile b/GNUmakefile index ace6b02..595a367 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -43,8 +43,9 @@ test: all -killall async_tcp_echo_server build/async_tcp_echo_server 8000 & cat rrcp.txt | build/rrcp_client localhost 8000 + cat rrcp.txt | build/rrcp_async_tcp_client localhost 8000 cat rrcp.txt | build/async_tcp_echo_client localhost 8000 - echo | build/async_tcp_client localhost 8001 + #TODO: echo | build/async_tcp_client localhost 8001 cat rrcp.txt | build/blocking_tcp_echo_client localhost 8000 ctest --test-dir build -killall async_tcp_echo_server diff --git a/async_tcp_echo_server.cpp b/async_tcp_echo_server.cpp index 1a7f99a..fa28de3 100644 --- a/async_tcp_echo_server.cpp +++ b/async_tcp_echo_server.cpp @@ -29,7 +29,7 @@ using boost::asio::ip::tcp; class session : public std::enable_shared_from_this< session > { - static constexpr int max_length{1024}; + static constexpr size_t max_length{1024}; public: explicit session(tcp::socket socket) : socket_(std::move(socket)) {} diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index c292652..594c177 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -26,7 +26,7 @@ using boost::asio::ip::tcp; namespace { -constexpr int max_length{1024}; +constexpr size_t max_length{1024}; void session(tcp::socket sock) { diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp new file mode 100644 index 0000000..91ae3ef --- /dev/null +++ b/rrcp_async_tcp_client.cpp @@ -0,0 +1,232 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr char ESC = 0x1B; +constexpr char REPLACE_LF = 0x01; +constexpr char REPLACE_CR = 0x02; +constexpr char REPLACE_ESC = 0x03; + +using boost::asio::ip::tcp; +using namespace std::chrono_literals; + +auto esc2char(std::string_view data) -> std::string +{ + std::string message; + auto len = data.size(); + for (size_t i = 0; i < len; ++i) + { + char c = data[i]; + + if (c == '\r') + { + return message; + } + + if (c == ESC) + { + if (i == len - 1) + { + throw std::runtime_error("esc2char: Error - message ends with escape character!"); + } + + char next = data[++i]; + switch (next) + { + case REPLACE_LF: + c = '\n'; + break; + case REPLACE_CR: + c = '\r'; + break; + case REPLACE_ESC: + c = ESC; + break; + default: + throw std::runtime_error("esc2char: Error - unexpected ESC character!"); + } + } + + message.push_back(c); + } + return message; +} + +auto char2esc(std::string_view data) -> std::string +{ + std::string message; + for (char c : data) + { + switch (c) + { + case '\n': + message.push_back(ESC); + message.push_back(REPLACE_LF); + break; + case '\r': + message.push_back(ESC); + message.push_back(REPLACE_CR); + break; + case ESC: + message.push_back(ESC); + message.push_back(REPLACE_ESC); + break; + default: + message.push_back(c); + break; + } + } + return message; +} + +class client : public std::enable_shared_from_this< client > +{ + public: + client(boost::asio::io_context& io_context) : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) + { + deadline_.expires_at(boost::asio::steady_timer::time_point::max()); + } + + void start(tcp::resolver::results_type endpoints) + { + boost::asio::async_connect(socket_, endpoints, + [self = shared_from_this()](const boost::system::error_code& ec, const tcp::endpoint&) + { + if (!ec) + { + std::cout << "Connected to server.\n"; + self->read(); + self->start_heartbeat(); + self->check_deadline(); + } + else + { + std::cerr << "Failed to connect: " << ec.message() << '\n'; + } + }); + } + + void write() + { + std::string message; + while (std::getline(std::cin, message)) + { + if (stopped_) return; + + message = '\n' + char2esc(message) + '\r'; + boost::asio::async_write(socket_, boost::asio::buffer(message), + [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) + { + if (ec) + { + std::cerr << "Error sending message: " << ec.message() << '\n'; + self->stop(); + } + }); + deadline_.expires_after(3s); + } + } + + void read() + { + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), '\r', + [self = shared_from_this()](const boost::system::error_code& ec, std::size_t length) + { + if (!ec) + { + std::string line = esc2char(self->input_buffer_.substr(0, length - 1)); + self->input_buffer_.erase(0, length); + // XXX if (line != "GPing") + { + std::cout << "Server: " << line << '\n'; + } + self->read(); + self->deadline_.expires_after(3s); + } + else + { + std::cerr << "Error reading message: " << ec.message() << '\n'; + self->stop(); + } + }); + } + + void start_heartbeat() + { + if (stopped_) return; + + std::string heartbeat_message{'\n' + char2esc("GPing") + '\r'}; + std::cerr << "Send heartbeat: " << heartbeat_message << '\n'; + boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), + [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) + { + if (!ec) + { + self->heartbeat_timer_.expires_after(10s); + self->heartbeat_timer_.async_wait([self](const boost::system::error_code&) { self->start_heartbeat(); }); + } + else + { + std::cerr << "Error sending heartbeat: " << ec.message() << '\n'; + self->stop(); + } + }); + } + + void check_deadline() + { + if (stopped_) return; + + if (deadline_.expiry() <= boost::asio::steady_timer::clock_type::now()) + { + std::cerr << "No response from server, disconnecting...\n"; + stop(); + return; + } + + deadline_.async_wait([self = shared_from_this()](const boost::system::error_code&) { self->check_deadline(); }); + } + + void stop() + { + std::cerr << "stop called, disconnecting...\n"; + stopped_ = true; + boost::system::error_code ec; + socket_.close(ec); + heartbeat_timer_.cancel(); + deadline_.cancel(); + } + + private: + tcp::socket socket_; + boost::asio::steady_timer deadline_, heartbeat_timer_; + std::string input_buffer_; + bool stopped_{false}; +}; + +int main(int argc, char* argv[]) +{ + if (argc != 3) + { + std::cerr << "Usage: " << argv[0] << " \n"; + return 1; + } + + boost::asio::io_context io_context; + tcp::resolver resolver(io_context); + + auto c = std::make_shared< client >(io_context); + c->start(resolver.resolve(argv[1], argv[2])); + + std::thread io_thread([&io_context]() { io_context.run(); }); + c->write(); + c->stop(); + io_thread.join(); + + return 0; +} From ad0a22cf8d68fac8221f3cb3b443fa2ba4a8038c Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Mon, 17 Mar 2025 17:23:06 +0100 Subject: [PATCH 033/120] The final cut Correct error handling Update copyright notice --- CMakeLists.txt | 12 ++- GNUmakefile | 3 +- async_tcp_client.cpp | 61 ++++++----- async_tcp_client_v20.cpp | 8 +- async_tcp_echo_client.cpp | 22 +++- async_tcp_echo_server.cpp | 1 + blocking_tcp_echo_client.cpp | 7 +- blocking_tcp_echo_server.cpp | 1 + rrcp_async_tcp_client.cpp | 192 ++++++++++++++--------------------- rrcp_client.cpp | 1 + rrcp_helper.cpp | 2 +- rrcp_helper.hpp | 10 +- timer.cpp | 1 + 13 files changed, 162 insertions(+), 159 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4de46eb..bce1817 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,12 @@ target_link_libraries( PUBLIC Boost::asio ) +# add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) +# target_link_libraries(async_tcp_client_v20 PUBLIC Boost::asio) + +# add_executable(async_tcp_client async_tcp_client.cpp) +# target_link_libraries(async_tcp_client PUBLIC Boost::asio) + # add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) # target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::asio) @@ -79,5 +85,9 @@ if(APPLE) endif() add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) -target_link_libraries(rrcp_async_tcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) +target_link_libraries( + rrcp_async_tcp_client + PRIVATE rrcp_helper + PUBLIC Boost::asio +) # TODO: add_test(NAME rrcp_async_tcp_client COMMAND echo | rrcp_async_tcp_client localhost 8001) diff --git a/GNUmakefile b/GNUmakefile index 595a367..68bed85 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -45,7 +45,8 @@ test: all cat rrcp.txt | build/rrcp_client localhost 8000 cat rrcp.txt | build/rrcp_async_tcp_client localhost 8000 cat rrcp.txt | build/async_tcp_echo_client localhost 8000 - #TODO: echo | build/async_tcp_client localhost 8001 + -build/async_tcp_echo_client localhost + -echo | build/async_tcp_echo_client localhost 8001 cat rrcp.txt | build/blocking_tcp_echo_client localhost 8000 ctest --test-dir build -killall async_tcp_echo_server diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 9d19df2..33fe80e 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -7,6 +7,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +// Moderniced from Claus Klein and ChatGPT #include #include @@ -20,14 +21,12 @@ #include #include #include +#include #include #include using boost::asio::steady_timer; using boost::asio::ip::tcp; -using std::placeholders::_1; // NOLINT -using std::placeholders::_2; // NOLINT - using namespace std::chrono_literals; // @@ -90,7 +89,7 @@ using namespace std::chrono_literals; // newline character) every 10 seconds. In this example, no deadline is applied // to message sending. // -class client +class client : public std::enable_shared_from_this< client > { public: explicit client(boost::asio::io_context& io_context) @@ -135,9 +134,8 @@ class client deadline_.expires_after(60s); // Start the asynchronous connect operation. - socket_.async_connect(endpoint_iter->endpoint(), std::bind(&client::handle_connect, this, _1, endpoint_iter)); - // FIXME: [this, endpoint_iter](auto && PH1) { - // handle_connect(std::forward(PH1), endpoint_iter); }); + socket_.async_connect(endpoint_iter->endpoint(), + [this, endpoint_iter](const boost::system::error_code& error) { handle_connect(error, endpoint_iter); }); } else { @@ -158,7 +156,7 @@ class client // time then the timeout handler must have run first. if (!socket_.is_open()) { - std::cout << "Connect timed out\n"; + std::cerr << "Connect timed out\n"; // Try the next available endpoint. start_connect(++endpoint_iter); @@ -167,7 +165,7 @@ class client // Check if the connect operation failed before the deadline expired. else if (error) { - std::cout << "Connect error: " << error.message() << "\n"; + std::cerr << "Connect error: " << error.message() << "\n"; // We need to close the socket used in the previous connection // attempt before starting a new one. @@ -192,15 +190,19 @@ class client void start_read() { + if (stopped_) + { + return; + } + // Set a deadline for the read operation. deadline_.expires_after(30s); + auto self(shared_from_this()); + // Start an asynchronous operation to read a newline-delimited message. - boost::asio::async_read_until( - socket_, boost::asio::dynamic_buffer(input_buffer_), '\n', std::bind(&client::handle_read, this, _1, _2)); - // FIXME: [this](auto && PH1, auto && PH2) { - // handle_read(std::forward(PH1), - // std::forward(PH2)); }); + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), '\n', + [this, self](const boost::system::error_code& error, std::size_t n) { handle_read(error, n); }); } void handle_read(const boost::system::error_code& error, std::size_t n) @@ -213,7 +215,7 @@ class client if (!error) { // Extract the newline-delimited message from the buffer. - std::string const line(input_buffer_.substr(0, n - 1)); + std::string const line = input_buffer_.substr(0, n - 1); // NOTE: w/o '\n' input_buffer_.erase(0, n); // Empty messages are heartbeats and so ignored. @@ -226,7 +228,7 @@ class client } else { - std::cout << "Error on receive: " << error.message() << "\n"; + std::cerr << "Error on receive: " << error.message() << "\n"; stop(); } @@ -239,10 +241,14 @@ class client return; } + std::string message{'\n'}; + std::print(stderr, "Sending: {}\n", "hartbeat"); + + auto self(shared_from_this()); + // Start an asynchronous operation to send a heartbeat message. - boost::asio::async_write(socket_, boost::asio::buffer("\n", 1), std::bind(&client::handle_write, this, _1)); - // XXX [this](const boost::system::error_code& /*e*/, PH1) { - // handle_write(std::forward(PH1)); }); + boost::asio::async_write(socket_, boost::asio::buffer(message), + [this, self](const boost::system::error_code& error, std::size_t) { handle_write(error); }); } void handle_write(const boost::system::error_code& error) @@ -256,12 +262,11 @@ class client { // Wait 10 seconds before sending the next heartbeat. heartbeat_timer_.expires_after(10s); - // cpp11: heartbeat_timer_.async_wait(std::bind(&client::start_write, this)); heartbeat_timer_.async_wait([this](const boost::system::error_code& /*e*/) { start_write(); }); } else { - std::cout << "Error on heartbeat: " << error.message() << "\n"; + std::cerr << "Error on heartbeat: " << error.message() << "\n"; stop(); } @@ -289,12 +294,13 @@ class client deadline_.expires_at(steady_timer::time_point::max()); } + auto self(shared_from_this()); + // Put the actor back to sleep. - // cpp11: deadline_.async_wait(std::bind(&client::check_deadline, this)); - deadline_.async_wait([this](const boost::system::error_code& /*e*/) { check_deadline(); }); + deadline_.async_wait([this, self](const boost::system::error_code& /*e*/) { check_deadline(); }); } - bool stopped_ = false; + bool stopped_{false}; tcp::resolver::results_type endpoints_; tcp::socket socket_; std::string input_buffer_; @@ -313,10 +319,11 @@ auto main(int argc, char* argv[]) -> int } boost::asio::io_context io_context; - tcp::resolver r(io_context); - client c(io_context); + tcp::resolver resolver(io_context); + + auto c = std::make_shared< client >(io_context); - c.start(r.resolve(argv[1], argv[2])); + c->start(resolver.resolve(argv[1], argv[2])); io_context.run(); } diff --git a/async_tcp_client_v20.cpp b/async_tcp_client_v20.cpp index 020ed5e..fadb22a 100644 --- a/async_tcp_client_v20.cpp +++ b/async_tcp_client_v20.cpp @@ -6,6 +6,8 @@ * * Distributed under the Boost Software License, Version 1.0. (See accompanying * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * + * Moderniced from Claus Klein and ChatGPT ***/ #include @@ -15,7 +17,7 @@ #include #include #include -#include +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -238,7 +240,7 @@ class client : public std::enable_shared_from_this< client > // Wait 10 seconds before sending the next heartbeat. heartbeat_timer_.cancel(); heartbeat_timer_.expires_after(10s); - heartbeat_timer_.async_wait([this](const boost::system::error_code&) { start_write(); }); + heartbeat_timer_.async_wait([this](const boost::system::error_code& /*e*/) { start_write(); }); } else { @@ -273,7 +275,7 @@ class client : public std::enable_shared_from_this< client > auto self(shared_from_this()); // Put the actor back to sleep. - deadline_.async_wait([this, self](const boost::system::error_code&) { check_deadline(); }); + deadline_.async_wait([this, self](const boost::system::error_code& /*e*/) { check_deadline(); }); } bool stopped_{false}; diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index 5ebe82f..1838f23 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -1,3 +1,15 @@ +/*** + * async_tcp_echo_client.cpp + * ~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * + * Moderniced from Claus Klein and ChatGPT + ***/ + #include #include @@ -137,18 +149,18 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT }); } - boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(data_), CR, + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(data_), STOP, [this, self](boost::system::error_code ec, std::size_t length) { timer_.cancel(); if (!ec) { - std::string response = esc2char(data_.substr(1, length)); // NOTE: w/o LF! + std::string response = esc2char(data_.substr(1, length)); // NOTE: w/o START! // NOTE: data_.erase(0, length); is used instead of data_.clear() because: // - Partial Data Handling: The async_read_until() function reads - // data up to the delimiter (CR) but doesn’t guarantee it consumes + // data up to the delimiter (STOP) but doesn’t guarantee it consumes // all the data in the socket. // There might be extra data left in the buffer after the delimiter! @@ -209,8 +221,8 @@ auto main(int argc, char* argv[]) -> int } std::string command = char2esc(line); - command.insert(0, 1, LF); - command += CR; + command.insert(0, 1, START); + command += STOP; client->write(command); } diff --git a/async_tcp_echo_server.cpp b/async_tcp_echo_server.cpp index fa28de3..8e4efc1 100644 --- a/async_tcp_echo_server.cpp +++ b/async_tcp_echo_server.cpp @@ -7,6 +7,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +// Moderniced from Claus Klein and ChatGPT #include #include diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index bc5bc52..56342c8 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -7,6 +7,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +// Moderniced from Claus Klein and ChatGPT #include #include @@ -60,8 +61,8 @@ auto main(int argc, char* argv[]) -> int // TODO: check boost::system::error_code ec; std::string command = char2esc(line); - command.insert(0, 1, LF); - command += CR; + command.insert(0, 1, START); + command += STOP; boost::asio::write(s, boost::asio::buffer(command.c_str(), command.length())); // TODO: wait for endchar with timeout! @@ -71,7 +72,7 @@ auto main(int argc, char* argv[]) -> int do { - size_t const reply_length = boost::asio::read_until(s, sb2, CR); // NOLINT(clang-analyzer-deadcode.DeadStores) + size_t const reply_length = boost::asio::read_until(s, sb2, STOP); // NOLINT(clang-analyzer-deadcode.DeadStores) std::string const response = esc2char(data); if (response.empty()) { diff --git a/blocking_tcp_echo_server.cpp b/blocking_tcp_echo_server.cpp index 594c177..c0e2602 100644 --- a/blocking_tcp_echo_server.cpp +++ b/blocking_tcp_echo_server.cpp @@ -7,6 +7,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +// Moderniced from Claus Klein and ChatGPT #include #include diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 91ae3ef..2125433 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -1,3 +1,15 @@ +/*** + * rrcp_async_tcp_client.cpp + * ~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * + * Moderniced from Claus Klein and ChatGPT + ***/ + #include #include #include @@ -7,93 +19,25 @@ #include #include -constexpr char ESC = 0x1B; -constexpr char REPLACE_LF = 0x01; -constexpr char REPLACE_CR = 0x02; -constexpr char REPLACE_ESC = 0x03; +#include "rrcp_helper.hpp" using boost::asio::ip::tcp; using namespace std::chrono_literals; -auto esc2char(std::string_view data) -> std::string -{ - std::string message; - auto len = data.size(); - for (size_t i = 0; i < len; ++i) - { - char c = data[i]; - - if (c == '\r') - { - return message; - } - - if (c == ESC) - { - if (i == len - 1) - { - throw std::runtime_error("esc2char: Error - message ends with escape character!"); - } - - char next = data[++i]; - switch (next) - { - case REPLACE_LF: - c = '\n'; - break; - case REPLACE_CR: - c = '\r'; - break; - case REPLACE_ESC: - c = ESC; - break; - default: - throw std::runtime_error("esc2char: Error - unexpected ESC character!"); - } - } - - message.push_back(c); - } - return message; -} - -auto char2esc(std::string_view data) -> std::string -{ - std::string message; - for (char c : data) - { - switch (c) - { - case '\n': - message.push_back(ESC); - message.push_back(REPLACE_LF); - break; - case '\r': - message.push_back(ESC); - message.push_back(REPLACE_CR); - break; - case ESC: - message.push_back(ESC); - message.push_back(REPLACE_ESC); - break; - default: - message.push_back(c); - break; - } - } - return message; -} - -class client : public std::enable_shared_from_this< client > +class rrcp_client : public std::enable_shared_from_this< rrcp_client > { public: - client(boost::asio::io_context& io_context) : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) + explicit rrcp_client(boost::asio::io_context& io_context) + : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) { deadline_.expires_at(boost::asio::steady_timer::time_point::max()); } - void start(tcp::resolver::results_type endpoints) + void start(const tcp::resolver::results_type& endpoints) { + deadline_.expires_after(3s); + check_deadline(); + boost::asio::async_connect(socket_, endpoints, [self = shared_from_this()](const boost::system::error_code& ec, const tcp::endpoint&) { @@ -101,12 +45,12 @@ class client : public std::enable_shared_from_this< client > { std::cout << "Connected to server.\n"; self->read(); - self->start_heartbeat(); - self->check_deadline(); + self->send_heartbeat(); } else { - std::cerr << "Failed to connect: " << ec.message() << '\n'; + // FIXME: this aborts! CK + throw std::runtime_error("Failed to connect: " + ec.message()); } }); } @@ -116,9 +60,12 @@ class client : public std::enable_shared_from_this< client > std::string message; while (std::getline(std::cin, message)) { - if (stopped_) return; + if (stopped_) + { + return; + } - message = '\n' + char2esc(message) + '\r'; + message = START + char2esc(message) + STOP; boost::asio::async_write(socket_, boost::asio::buffer(message), [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) { @@ -128,25 +75,27 @@ class client : public std::enable_shared_from_this< client > self->stop(); } }); + deadline_.expires_after(3s); } } void read() { - boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), '\r', + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), STOP, [self = shared_from_this()](const boost::system::error_code& ec, std::size_t length) { if (!ec) { - std::string line = esc2char(self->input_buffer_.substr(0, length - 1)); + std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); - // XXX if (line != "GPing") + + if (!line.starts_with("gPing")) { - std::cout << "Server: " << line << '\n'; + std::cout << line << '\n'; } self->read(); - self->deadline_.expires_after(3s); + self->deadline_.expires_after(13s); } else { @@ -156,11 +105,25 @@ class client : public std::enable_shared_from_this< client > }); } - void start_heartbeat() + void stop() + { + std::cerr << "stop called, disconnecting...\n"; + stopped_ = true; + boost::system::error_code ec; + socket_.close(ec); + heartbeat_timer_.cancel(); + deadline_.cancel(); + } + + private: + void send_heartbeat() { - if (stopped_) return; + if (stopped_) + { + return; + } - std::string heartbeat_message{'\n' + char2esc("GPing") + '\r'}; + std::string heartbeat_message{START + char2esc("M:Utility GPing") + STOP}; std::cerr << "Send heartbeat: " << heartbeat_message << '\n'; boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) @@ -168,7 +131,7 @@ class client : public std::enable_shared_from_this< client > if (!ec) { self->heartbeat_timer_.expires_after(10s); - self->heartbeat_timer_.async_wait([self](const boost::system::error_code&) { self->start_heartbeat(); }); + self->heartbeat_timer_.async_wait([self](const boost::system::error_code&) { self->send_heartbeat(); }); } else { @@ -180,7 +143,10 @@ class client : public std::enable_shared_from_this< client > void check_deadline() { - if (stopped_) return; + if (stopped_) + { + return; + } if (deadline_.expiry() <= boost::asio::steady_timer::clock_type::now()) { @@ -192,41 +158,41 @@ class client : public std::enable_shared_from_this< client > deadline_.async_wait([self = shared_from_this()](const boost::system::error_code&) { self->check_deadline(); }); } - void stop() - { - std::cerr << "stop called, disconnecting...\n"; - stopped_ = true; - boost::system::error_code ec; - socket_.close(ec); - heartbeat_timer_.cancel(); - deadline_.cancel(); - } - - private: tcp::socket socket_; boost::asio::steady_timer deadline_, heartbeat_timer_; std::string input_buffer_; bool stopped_{false}; }; -int main(int argc, char* argv[]) +auto main(int argc, char* argv[]) -> int { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " \n"; - return 1; + return EXIT_FAILURE; } - boost::asio::io_context io_context; - tcp::resolver resolver(io_context); + try + { + boost::asio::io_context io_context; + tcp::resolver resolver(io_context); + + auto c = std::make_shared< rrcp_client >(io_context); + c->start(resolver.resolve(argv[1], argv[2])); - auto c = std::make_shared< client >(io_context); - c->start(resolver.resolve(argv[1], argv[2])); + std::thread io_thread([&io_context]() { io_context.run(); }); - std::thread io_thread([&io_context]() { io_context.run(); }); - c->write(); - c->stop(); - io_thread.join(); + std::this_thread::sleep_for(3s); // NOLINT(misc-include-cleaner) + c->write(); + + c->stop(); + io_thread.join(); + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + return EXIT_FAILURE; + } - return 0; + return EXIT_SUCCESS; } diff --git a/rrcp_client.cpp b/rrcp_client.cpp index bcc4937..fa51491 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -7,6 +7,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +// Moderniced from Claus Klein and ChatGPT #include #include diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 274f84b..acbe0b1 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -18,7 +18,7 @@ auto esc2char(std::string_view data) -> std::string { char c = data[i]; - if (c == '\r') + if (c == STOP) { return message; } diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index e058ad5..e6ea860 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -3,12 +3,12 @@ #include #include -constexpr const char LF{0x0A}; // \n -constexpr const char CR{0x0D}; // \r +constexpr const char START{0x0A}; // \n +constexpr const char STOP{0x0D}; // \r /** - * @brief Gets the message between message and - * replaces the escape sequences for LF and CR + * @brief Gets the message between message and + * replaces the escape sequences for START and STOP * * * @param data: read from socket @@ -18,7 +18,7 @@ constexpr const char CR{0x0D}; // \r extern auto esc2char(std::string_view data) -> std::string; /** - * @brief Replaces LF, CR with Escape sequence + * @brief Replaces START, STOP with Escape sequence * * @param data: data to send * diff --git a/timer.cpp b/timer.cpp index 1f6636a..31354a3 100644 --- a/timer.cpp +++ b/timer.cpp @@ -7,6 +7,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +// Moderniced from Claus Klein and ChatGPT #include #include From f5b917c38c5170ec37d9962411a34ee20e23fb51 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Mon, 17 Mar 2025 18:13:51 +0100 Subject: [PATCH 034/120] Add more ctests --- CMakeLists.txt | 17 ++++++++++++++++- async_tcp_client.cpp | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bce1817..4f44970 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,8 +43,17 @@ endif() enable_testing() +function(do_test target arg result) + add_test(NAME ${target}${arg} COMMAND ${target} ${arg}) + set_tests_properties( + ${target}${arg} + PROPERTIES PASS_REGULAR_EXPRESSION ${result} + ) +endfunction() + add_executable(async_tcp_echo_server async_tcp_echo_server.cpp) target_link_libraries(async_tcp_echo_server PUBLIC Boost::asio) +do_test(async_tcp_echo_server "" port) add_library(rrcp_helper STATIC rrcp_helper.cpp rrcp_helper.hpp) @@ -54,6 +63,7 @@ target_link_libraries( PRIVATE rrcp_helper PUBLIC Boost::asio fmt::fmt-header-only ) +do_test(async_tcp_echo_client --help Usage) add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) target_link_libraries( @@ -61,18 +71,23 @@ target_link_libraries( PRIVATE rrcp_helper PUBLIC Boost::asio ) +do_test(blocking_tcp_echo_client --help Usage) # add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) # target_link_libraries(async_tcp_client_v20 PUBLIC Boost::asio) +# do_test(async_tcp_client_v20 --help Usage) # add_executable(async_tcp_client async_tcp_client.cpp) # target_link_libraries(async_tcp_client PUBLIC Boost::asio) +# do_test(async_tcp_client --help Usage) # add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) # target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::asio) +#XXX do_test(blocking_tcp_echo_server port Usage) add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) target_link_libraries(rrcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) +do_test(rrcp_client --help Usage) if(APPLE) add_executable(timer timer.cpp) @@ -90,4 +105,4 @@ target_link_libraries( PRIVATE rrcp_helper PUBLIC Boost::asio ) -# TODO: add_test(NAME rrcp_async_tcp_client COMMAND echo | rrcp_async_tcp_client localhost 8001) +do_test(rrcp_async_tcp_client --help Usage) diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 33fe80e..58be689 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -131,7 +131,7 @@ class client : public std::enable_shared_from_this< client > std::cout << "Trying " << endpoint_iter->endpoint() << "...\n"; // Set a deadline for the connect operation. - deadline_.expires_after(60s); + deadline_.expires_after(3s); // Start the asynchronous connect operation. socket_.async_connect(endpoint_iter->endpoint(), @@ -196,7 +196,7 @@ class client : public std::enable_shared_from_this< client > } // Set a deadline for the read operation. - deadline_.expires_after(30s); + deadline_.expires_after(13s); auto self(shared_from_this()); From 4fd771ffb7d8aec6832838475558a16ddf2df969 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 18 Mar 2025 07:48:55 +0100 Subject: [PATCH 035/120] Prevent runtime_error exections --- GNUmakefile | 4 ++++ rrcp_async_tcp_client.cpp | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 68bed85..fe3f8c3 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -41,7 +41,11 @@ readability-use-std-min-max,\ test: all -killall async_tcp_echo_server + -echo | build/async_tcp_echo_client localhost 8000 build/async_tcp_echo_server 8000 & + -(cat rrcp.txt | build/async_tcp_echo_client localhost 8000) & + -killall async_tcp_echo_server + -(echo | build/async_tcp_echo_server 8000) & cat rrcp.txt | build/rrcp_client localhost 8000 cat rrcp.txt | build/rrcp_async_tcp_client localhost 8000 cat rrcp.txt | build/async_tcp_echo_client localhost 8000 diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 2125433..a9b9a34 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -49,8 +49,7 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > } else { - // FIXME: this aborts! CK - throw std::runtime_error("Failed to connect: " + ec.message()); + std::cerr << "Failed to connect: " << ec.message() << '\n'; } }); } @@ -90,12 +89,12 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); - if (!line.starts_with("gPing")) + if (!line.ends_with("Ping")) { std::cout << line << '\n'; } - self->read(); self->deadline_.expires_after(13s); + self->read(); } else { From 796d3d09babfcdc4ea73093c0a04f0c49cae4431 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 18 Mar 2025 09:30:57 +0100 Subject: [PATCH 036/120] Use a write msg queue --- CMakeLists.txt | 4 +- GNUmakefile | 2 +- async_tcp_echo_client.cpp | 14 ++-- rrcp_async_tcp_client.cpp | 133 ++++++++++++++++++++++++++++---------- 4 files changed, 109 insertions(+), 44 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f44970..eb693a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,7 +69,7 @@ add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) target_link_libraries( blocking_tcp_echo_client PRIVATE rrcp_helper - PUBLIC Boost::asio + PUBLIC Boost::asio fmt::fmt-header-only ) do_test(blocking_tcp_echo_client --help Usage) @@ -103,6 +103,6 @@ add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) target_link_libraries( rrcp_async_tcp_client PRIVATE rrcp_helper - PUBLIC Boost::asio + PUBLIC Boost::asio fmt::fmt-header-only ) do_test(rrcp_async_tcp_client --help Usage) diff --git a/GNUmakefile b/GNUmakefile index fe3f8c3..e67e70b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -11,7 +11,7 @@ all: build ninja -C build distclean: - rm -rf build coverage/* + rm -rf build coverage/* *~ ctags build: CMakeLists.txt cmake -S . -B $@ -D CMAKE_BUILD_TYPE=Debug diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index 1838f23..f70fafa 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -192,14 +192,14 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT auto main(int argc, char* argv[]) -> int { - try + if (argc != 3) { - if (argc != 3) - { - fmt::print(stderr, "Usage: async_tcp_echo_client \n"); - return EXIT_FAILURE; - } + fmt::print(stderr, "Usage: {} \n", argv[0]); + return EXIT_FAILURE; + } + try + { boost::asio::io_context io_context; auto client = std::make_shared< AsynchronousTCPClient >(io_context, argv[1], argv[2]); @@ -226,7 +226,7 @@ auto main(int argc, char* argv[]) -> int client->write(command); } - std::this_thread::sleep_for(timeout_duration); // NOLINT(misc-include-cleaner) + std::this_thread::sleep_for(timeout_duration); client->stop(); io_thread.join(); diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index a9b9a34..d3e6431 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -10,9 +10,16 @@ * Moderniced from Claus Klein and ChatGPT ***/ +#include + +#include #include +#include #include #include +#include +#include +#include #include #include #include @@ -23,19 +30,24 @@ using boost::asio::ip::tcp; using namespace std::chrono_literals; +using message_queue = std::deque< std::string >; + +constexpr size_t max_length = 65432; +constexpr auto timeout_duration = 3s; +constexpr auto heartbeat_interval = 10s; class rrcp_client : public std::enable_shared_from_this< rrcp_client > { public: explicit rrcp_client(boost::asio::io_context& io_context) - : socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) + : io_context_(io_context), socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) { deadline_.expires_at(boost::asio::steady_timer::time_point::max()); } void start(const tcp::resolver::results_type& endpoints) { - deadline_.expires_after(3s); + deadline_.expires_after(timeout_duration); check_deadline(); boost::asio::async_connect(socket_, endpoints, @@ -43,40 +55,44 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > { if (!ec) { - std::cout << "Connected to server.\n"; + fmt::print(stderr, "Connected to server.\n"); + self->connected_ = true; self->read(); self->send_heartbeat(); } else { - std::cerr << "Failed to connect: " << ec.message() << '\n'; + fmt::print(stderr, "Failed to connect: {}\n", ec.message()); } }); } - void write() + auto connected() -> bool { return connected_; } + + // This function write the message into the msg queue and starts the write actor + void write(const std::string& message) { - std::string message; - while (std::getline(std::cin, message)) + while (!connected_) { if (stopped_) { return; } + fmt::print(stderr, "Client is not connected yet.\n"); + std::this_thread::sleep_for(timeout_duration); + } - message = START + char2esc(message) + STOP; - boost::asio::async_write(socket_, boost::asio::buffer(message), - [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) + boost::asio::post(io_context_, + [this, message]() + { + bool const write_in_progress = !write_msgs_.empty(); + write_msgs_.push_back(message); + if (!write_in_progress) { - if (ec) - { - std::cerr << "Error sending message: " << ec.message() << '\n'; - self->stop(); - } - }); - - deadline_.expires_after(3s); - } + deadline_.expires_after(timeout_duration); + do_write(); + } + }); } void read() @@ -89,16 +105,16 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); - if (!line.ends_with("Ping")) + if (!line.starts_with("gPing")) { - std::cout << line << '\n'; + fmt::print("{}\n", line); } - self->deadline_.expires_after(13s); + self->deadline_.expires_after(heartbeat_interval + timeout_duration); self->read(); } else { - std::cerr << "Error reading message: " << ec.message() << '\n'; + fmt::print(stderr, "Error reading message: {}\n", ec.message()); self->stop(); } }); @@ -106,8 +122,9 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > void stop() { - std::cerr << "stop called, disconnecting...\n"; + fmt::print(stderr, "stop called, disconnecting...\n"); stopped_ = true; + connected_ = false; boost::system::error_code ec; socket_.close(ec); heartbeat_timer_.cancel(); @@ -115,6 +132,30 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > } private: + void do_write() + { + auto self(shared_from_this()); + boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front()), + [this, self](boost::system::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + fmt::print(stderr, "Message sent.\n"); + write_msgs_.pop_front(); + if (!write_msgs_.empty()) + { + do_write(); + } + } + else + { + // There are no more endpoints to try. Shut down the client. + fmt::print(stderr, "Error writeing message: {}\n", ec.message()); + stop(); + } + }); + } + void send_heartbeat() { if (stopped_) @@ -122,19 +163,19 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > return; } - std::string heartbeat_message{START + char2esc("M:Utility GPing") + STOP}; - std::cerr << "Send heartbeat: " << heartbeat_message << '\n'; + std::string heartbeat_message{START + char2esc("M:Utility GPing\"ÄÖÜ€0ß\"") + STOP}; + fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) { if (!ec) { - self->heartbeat_timer_.expires_after(10s); + self->heartbeat_timer_.expires_after(heartbeat_interval); self->heartbeat_timer_.async_wait([self](const boost::system::error_code&) { self->send_heartbeat(); }); } else { - std::cerr << "Error sending heartbeat: " << ec.message() << '\n'; + fmt::print(stderr, "Error sedning heartbeat: {}\n", ec.message()); self->stop(); } }); @@ -149,7 +190,7 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > if (deadline_.expiry() <= boost::asio::steady_timer::clock_type::now()) { - std::cerr << "No response from server, disconnecting...\n"; + fmt::print(stderr, "No response from server, disconnecting...\n"); stop(); return; } @@ -157,9 +198,13 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > deadline_.async_wait([self = shared_from_this()](const boost::system::error_code&) { self->check_deadline(); }); } + boost::asio::io_context& io_context_; tcp::socket socket_; - boost::asio::steady_timer deadline_, heartbeat_timer_; + boost::asio::steady_timer deadline_; + boost::asio::steady_timer heartbeat_timer_; std::string input_buffer_; + message_queue write_msgs_; + bool connected_{false}; bool stopped_{false}; }; @@ -167,7 +212,7 @@ auto main(int argc, char* argv[]) -> int { if (argc != 3) { - std::cerr << "Usage: " << argv[0] << " \n"; + fmt::print(stderr, "Usage: {} \n", argv[0]); return EXIT_FAILURE; } @@ -181,15 +226,35 @@ auto main(int argc, char* argv[]) -> int std::thread io_thread([&io_context]() { io_context.run(); }); - std::this_thread::sleep_for(3s); // NOLINT(misc-include-cleaner) - c->write(); + std::this_thread::sleep_for(timeout_duration); // NOTE: only for gcov results! CK + + for (std::string line; c->connected() && std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) + { + const std::string::size_type sz = line.find("//"); + if ((sz != std::string::npos)) + { + line.resize(sz); // NOTE: w/o c++ comments + } + + boost::trim_right(line); + if (line.empty()) + { + continue; + } + + std::string command = char2esc(line); + command.insert(0, 1, START); + command += STOP; + + c->write(command); + } c->stop(); io_thread.join(); } catch (std::exception& e) { - std::cerr << "Exception: " << e.what() << "\n"; + fmt::print(stderr, "Exception: {}\n", e.what()); return EXIT_FAILURE; } From bf8e59ab9fd1fea3aaf9cae71a0c30aa920f5cff Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Tue, 18 Mar 2025 20:03:04 +0100 Subject: [PATCH 037/120] Reorder the test data set --- GNUmakefile | 1 + rrcp.txt | 48 +++++++++++++++++++++++---------------- rrcp_async_tcp_client.cpp | 2 +- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index e67e70b..3776a6e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -44,6 +44,7 @@ test: all -echo | build/async_tcp_echo_client localhost 8000 build/async_tcp_echo_server 8000 & -(cat rrcp.txt | build/async_tcp_echo_client localhost 8000) & + sleep 1 -killall async_tcp_echo_server -(echo | build/async_tcp_echo_server 8000) & cat rrcp.txt | build/rrcp_client localhost 8000 diff --git a/rrcp.txt b/rrcp.txt index 7e59c65..9f081a8 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -1,4 +1,12 @@ +M:Utility GInitialInfo"v8.53.2","async",10 M:Radio SString"\rHallo\tWorld\n" +M:WF.FF.Main 123456 T Octet 1 // with optional Message Number: +M:WF.FF.Main 123456 t +M:OBit L:1 123456 GGoState // with optional Logical Address: +M:Audio GAudioVolume // without optionl parts +M:Log SStruct 1,-1,3.14 // multiple parameters + +// The magic part // GET-request TU SET-request TU GET-request TU: GFRQ;MOD SFRQ10000000;MOD13;BW80000 GFRQ;MOD // GET-response TU SET-response TU GET-response TU: @@ -6,49 +14,46 @@ gFRQ18000000;MOD12 s5BW gFRQ18000000;MOD12 // NOTE: // There is an error within the BW command, the complete SET-request TU is cancelled. // The GET-request TU is replied by the corresponding GET-response TU. -M:WF.FF.Main 123456 T Octet 1 // without Logical Address: -M:WF.FF.Main 123456 t -M:Bit L:1 123456 G Octet -M:Audio SOctet // without optionl parts -M:Log SStruct1,-1,3.14 // multiple parameters M:MultilCmd S Octet 1;Long-1;String"Hallo World\n";Struct 1,+1,+3.14 // multiple commands's M:Test 123456 S FREQ123456;MOD12;LOGIN"user","password" G FREQ;MOD;STATUS // multiple TU M:Test gFREQ180000;MOD12 s5BW gFREQ180000;MOD12;STATUS"Running" // TU partly failed -M:eRADIO S FREQUENCY 123456789 -E:12 // MU error -M:RADIO T FREQUENCY 1 -M:RADIO t -M:RADIO d FREQUENCY 123456789 // trap data +// M:eRADIO S FREQUENCY 123456789 +// E:12 // MU error + +M:RADIO T FREQUENCY 1 // register trap +M:RADIO t // trap response OK +M:RADIO d FREQUENCY 123456789 // trap data message M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data -// more samples -M:Utility GInitialInfo"v0.5.9","async",0 + +// The more real samples M:Access GHasControl M:Access THasControl1 M:Access GOwnSession M:Access TOwnSession1 M:Access SReqSession"Monitoring" M:Audio GAudioVolume -M:Audio SAudioVolume"Level 0" M:Audio TAudioVolume1 +M:Audio SAudioVolume"Level 0" +M:Audio TAudioVolume0 M:Control SActPreset0 M:Control GCurrMission -M:Control SCurrMission"testString" M:Control TCurrMission1 +M:Control SCurrMission"testString" +M:Control TCurrMission0 M:Control GCurrWF M:Control TCurrWF1 M:Control GPresetID M:Control TPresetID1 M:Control GTxInhibit -M:Control STxInhibit"Disabled" M:Control TTxInhibit1 +M:Control STxInhibit"Disabled" +M:Control TTxInhibit0 M:Inventory GInvCountCus M:Inventory GInventoryCus0 M:Inventory GInvCount M:Inventory GInventory0 M:IP GOwnAdrvIV"Control" M:IP SOwnAdrvIV"Control","testString","testString" -M:Maintenance SShutdown -M:Maintenance SRestart M:Mission GGlobalAddr M:Mission SGlobalAddr"testString" M:Mission GMissions @@ -58,12 +63,15 @@ M:OBIT TGOState1 M:OBIT GTestErrors M:OBIT GTestIDs M:RxTx GPowerLevel -M:RxTx SPowerLevel"Off" M:RxTx TPowerLevel1 +M:RxTx SPowerLevel"Off" +M:RxTx TPowerLevel0 M:RxTx GVswr M:RxTx TVswr1 M:Utility GBattStatus M:Utility TBattStatus1 M:Utility GErrorText0,"English" -M:Utility GInitialInfo"testString","testString",0 -M:Utility GPing"testString" +M:Utility GInitialInfo"VersionStr","IdString",0 +M:Utility GPing"message" +M:Maintenance SRestart +M:Maintenance SShutdown diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index d3e6431..8e03397 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -227,7 +227,6 @@ auto main(int argc, char* argv[]) -> int std::thread io_thread([&io_context]() { io_context.run(); }); std::this_thread::sleep_for(timeout_duration); // NOTE: only for gcov results! CK - for (std::string line; c->connected() && std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) { const std::string::size_type sz = line.find("//"); @@ -248,6 +247,7 @@ auto main(int argc, char* argv[]) -> int c->write(command); } + std::this_thread::sleep_for(heartbeat_interval); // NOTE: only for gcov results! CK c->stop(); io_thread.join(); From 0088fe6552f8edf92e01cbbd451cfe1e1fa1bac7 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 19 Mar 2025 00:50:42 +0100 Subject: [PATCH 038/120] Add Base64 encoding --- Base64-test.cpp | 136 +++++++++++++++++++++++++++++++++ Base64.cpp | 156 ++++++++++++++++++++++++++++++++++++++ Base64.hpp | 65 ++++++++++++++++ CMakeLists.txt | 28 +++++++ GNUmakefile | 5 +- rrcp_async_tcp_client.cpp | 6 +- 6 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 Base64-test.cpp create mode 100644 Base64.cpp create mode 100644 Base64.hpp diff --git a/Base64-test.cpp b/Base64-test.cpp new file mode 100644 index 0000000..1882781 --- /dev/null +++ b/Base64-test.cpp @@ -0,0 +1,136 @@ +/*** +Additionally, you may want to consider adding more test cases to cover edge cases, such as: + +Null or empty input data +Input data with invalid characters (e.g., non-ASCII characters) +Input data with padding errors (e.g., incorrect number of padding characters) +Input data with encoding errors (e.g., incorrect encoding scheme) +By covering these edge cases, you can ensure that your Base64 class is robust and reliable. +***/ + +#include "Base64.hpp" + +#include + +#include +#include +#include + +using namespace std::string_literals; + +TEST(Base64Test, EmptyString) +{ + RRCP::Common::Base64 base64; + std::string const original = "\0"s; + std::string const encoded = base64.encode(original); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); +} + +TEST(Base64Test, ShortString1) +{ + RRCP::Common::Base64 base64; + std::string const original = "1"; + std::string const encoded = base64.encode(original); + std::println("{}:\t{}", original, encoded); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); +} + +TEST(Base64Test, ShortString2) +{ + RRCP::Common::Base64 base64; + std::string const original = "12"; + std::string const encoded = base64.encode(original); + std::println("{}:\t{}", original, encoded); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); +} + +TEST(Base64Test, ShortString3) +{ + RRCP::Common::Base64 base64; + std::string const original = "123"; + std::string const encoded = base64.encode(original); + std::println("{}:\t{}", original, encoded); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); +} + +TEST(Base64Test, ShortString4) +{ + RRCP::Common::Base64 base64; + std::string const original = "1234"; + std::string const encoded = base64.encode(original); + std::println("{}:\t{}", original, encoded); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); +} + +TEST(Base64Test, MediumString) +{ + RRCP::Common::Base64 base64; + std::string const original = "This is a medium length string."; + std::string const encoded = base64.encode(original); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); +} + +TEST(Base64Test, LongString) +{ + RRCP::Common::Base64 base64; + std::string const original = "This is a very long string that should be encoded and decoded correctly."; + std::string const encoded = base64.encode(original); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); +} + +TEST(Base64Test, BinaryData) +{ + RRCP::Common::Base64 base64; + std::string const original = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"s; + std::string const encoded = base64.encode(original); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); +} + +TEST(Base64Test, NonAsciiString) +{ + RRCP::Common::Base64 base64; + std::string const original = "\xFC@NOs[\xFEVJ\t@\x80\v\xD0\xAA\xF5"; + std::string const encoded = base64.encode(original); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); +} + +#ifdef USE_RANDOM_VALUES +TEST(Base64Test, RandomBinaryData) +{ + std::random_device rd; // a seed source for the random number engine + std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd() + std::uniform_int_distribution<> distrib(0, 255); + + RRCP::Common::Base64 base64; + base64.setLineBreak(true); + + for (size_t i = 0; i < 5; ++i) + { + const std::string::size_type new_cap{64u + i}; + std::string original; + original.reserve(new_cap); + for (size_t j = 0; j < new_cap; ++j) + { + original += static_cast< char >(distrib(gen) % 256); + } + std::string const encoded = base64.encode(original); + std::string const decoded = base64.decode(encoded); + EXPECT_EQ(original, decoded); + } +} +#endif + +auto main(int argc, char **argv) -> int +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/Base64.cpp b/Base64.cpp new file mode 100644 index 0000000..53a5337 --- /dev/null +++ b/Base64.cpp @@ -0,0 +1,156 @@ +#include "Base64.hpp" + +#include +#include +#include + +namespace RRCP::Common +{ + +void Base64::setLineBreak(bool lbrk) { m_encodeWithLinebreak = lbrk; } + +auto Base64::encode(std::string_view data) const -> std::string +{ + if (data.empty()) + { + throw std::invalid_argument("Invalid input data"); + } + + std::string encoded; + size_t linelen = 0; + + // Encode all complete 3 octet blocks + for (size_t i = 0; i < (data.size() / 3); ++i) + { + size_t const pos = 3 * i; + auto i1 = std::uint8_t((data[pos] & 0xfc) >> 2U); + auto i2 = std::uint8_t(((data[pos] & 0x03) << 4U) | ((data[pos + 1] & 0xf0) >> 4U)); + auto i3 = std::uint8_t(((data[pos + 1] & 0x0f) << 2U) | ((data[pos + 2] & 0xfc) >> 6U)); + auto i4 = std::uint8_t((data[pos + 2] & 0x3f)); + assert((i1 < 64) && (i2 < 64) && (i3 < 64) && (i4 < 64)); + + encoded.append(1, m_BaseChars[i1]); + encoded.append(1, m_BaseChars[i2]); + encoded.append(1, m_BaseChars[i3]); + encoded.append(1, m_BaseChars[i4]); + if (m_encodeWithLinebreak) + { + linelen += 4; + if (linelen >= 76) + { + linelen = 0; + encoded.append("\n"); + } + } + } + + // Handle remaining octets + if ((data.size() % 3) == 1) + { + // One octet remaining. + auto i1 = std::uint8_t((data[data.size() - 1] & 0xfc) >> 2U); + auto i2 = std::uint8_t(((data[data.size() - 1] & 0x03) << 4U)); + assert((i1 < 64) && (i2 < 64)); + + encoded.append(1, m_BaseChars[i1]); + encoded.append(1, m_BaseChars[i2]); + encoded.append(2, '='); + } + else if ((data.size() % 3) == 2) + { + // Two octets remaining. + auto i1 = std::uint8_t((data[data.size() - 2] & 0xfc) >> 2U); + auto i2 = std::uint8_t(((data[data.size() - 2] & 0x03) << 4U) | ((data[data.size() - 1] & 0xf0) >> 4U)); + auto i3 = std::uint8_t(((data[data.size() - 1] & 0x0f) << 2U)); + assert((i1 < 64) && (i2 < 64) && (i3 < 64)); + + encoded.append(1, m_BaseChars[i1]); + encoded.append(1, m_BaseChars[i2]); + encoded.append(1, m_BaseChars[i3]); + encoded.append(1, '='); + } + + return encoded; +} + +auto Base64::decode(std::string_view in) -> std::string +{ + std::string decoded; + std::string fourChars; + + // Iterate over the input string + for (char i : in) + { + if (isBase64Char(i) || (i == '=')) + { + fourChars += i; + } + else if ((i != '\n') && (i != '\r')) + { + // Invalid character, throw an exception + throw std::invalid_argument("Invalid Base64 character"); + } + + // If we have four characters, decode them + if (fourChars.size() == 4) + { + std::uint8_t const i1 = base64CharValue(fourChars[0]); + std::uint8_t const i2 = base64CharValue(fourChars[1]); + std::uint8_t const i3 = (fourChars[2] == '=') ? 0 : base64CharValue(fourChars[2]); + std::uint8_t const i4 = (fourChars[3] == '=') ? 0 : base64CharValue(fourChars[3]); + + decoded += static_cast< char >((i1 << 2U) | (i2 >> 4U)); + if (i3 != 0) + { + decoded += static_cast< char >(((i2 << 4U) /* & 0xf0 */) | (i3 >> 2U)); + } + if (i4 != 0) + { + decoded += static_cast< char >(((i3 << 6U) /* & 0xc0 */) | i4); + } + + fourChars.clear(); + } + } + + // Check if there are any remaining characters + if (!fourChars.empty()) + { + throw std::invalid_argument("Invalid Base64 string"); + } + + return decoded; +} + +auto Base64::isBase64Char(char c) const -> bool +{ + return ( + ((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')) || ((c >= '0') && (c <= '9')) || (c == '+') || (c == '/')); +} + +auto Base64::base64CharValue(char c) const -> std::uint8_t +{ + if ((c >= 'A') && (c <= 'Z')) + { + return std::uint8_t(c - 'A'); + } + if ((c >= 'a') && (c <= 'z')) + { + return std::uint8_t(c - 'a' + 26U); + } + if ((c >= '0') && (c <= '9')) + { + return std::uint8_t(c - '0' + 52U); + } + if (c == '+') + { + return 62U; + } + if (c == '/') + { + return 63U; + } + throw std::invalid_argument("Invalid Base64 character"); +} + +} // namespace RRCP::Common diff --git a/Base64.hpp b/Base64.hpp new file mode 100644 index 0000000..a611984 --- /dev/null +++ b/Base64.hpp @@ -0,0 +1,65 @@ +#ifndef BASE64_H +#define BASE64_H + +#include +#include +#include + +namespace RRCP::Common +{ + +class Base64 +{ + public: + /** + * Constructor for the Base64 class. + */ + Base64() = default; + + /** + * Destructor for the Base64 class. + */ + ~Base64() = default; + + /** + * Set the line break flag for encoding. + * @param lbrk If true, the encoded string will have a maximum line length of 80 characters. + */ + void setLineBreak(bool lbrk); + + /** + * Encode binary data to base64. + * @param data The data to be encoded. + * @return The corresponding base64 encoded string. + */ + [[nodiscard]] auto encode(std::string_view data) const -> std::string; + + /** + * Decode a Base64 encoded string. + * @param in The base64 encoded string. + * @return The decoded string. + */ + [[nodiscard]] auto decode(std::string_view in) -> std::string; + + private: + /** + * Check if a character is a valid Base64 character. + * @param c The character to check. + * @return True if the character is a valid Base64 character, false otherwise. + */ + [[nodiscard]] auto isBase64Char(char c) const -> bool; + + /** + * Get the value of a Base64 character. + * @param c The Base64 character. + * @return The value of the character (0-63). + */ + [[nodiscard]] auto base64CharValue(char c) const -> std::uint8_t; + + const std::string_view m_BaseChars{"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"}; + bool m_encodeWithLinebreak{true}; +}; + +} // namespace RRCP::Common + +#endif // BASE64_H diff --git a/CMakeLists.txt b/CMakeLists.txt index eb693a0..845c713 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,3 +106,31 @@ target_link_libraries( PUBLIC Boost::asio fmt::fmt-header-only ) do_test(rrcp_async_tcp_client --help Usage) + +if(APPLE) + # Set up googletest + include(FetchContent) + FetchContent_Declare( + googletest + GIT_TAG v1.16.0 + GIT_REPOSITORY https://github.com/google/googletest.git + FIND_PACKAGE_ARGS 1.16.0 NAMES GTest COMPONENTS gmain_main + EXCLUDE_FROM_ALL + SYSTEM + ) + + # For Windows: Prevent overriding the parent project's compiler/linker settings + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googletest) + + add_library(Base64 STATIC) + target_sources( + Base64 + PRIVATE Base64.cpp + PUBLIC FILE_SET HEADERS FILES Base64.hpp + ) + + add_executable(Base64-test Base64-test.cpp) + target_link_libraries(Base64-test PRIVATE Base64 GTest::gtest_main) + add_test(NAME Base64-test COMMAND Base64-test) +endif() diff --git a/GNUmakefile b/GNUmakefile index 3776a6e..d4fd9fd 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -27,8 +27,11 @@ hicpp-explicit-conversions,\ hicpp-member-init,\ hicpp-named-parameter,\ misc-const-correctness,\ -modernize-use-trailing-return-type,\ modernize-deprecated-headers,\ +modernize-loop-convert,\ +modernize-use-nodiscard,\ +modernize-use-std-print,\ +modernize-use-trailing-return-type,\ performance-avoid-endl,\ performance-unnecessary-value-param,\ readability-avoid-const-params-in-decls,\ diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 8e03397..5786412 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -150,7 +150,7 @@ class rrcp_client : public std::enable_shared_from_this< rrcp_client > else { // There are no more endpoints to try. Shut down the client. - fmt::print(stderr, "Error writeing message: {}\n", ec.message()); + fmt::print(stderr, "Error writing message: {}\n", ec.message()); stop(); } }); @@ -226,7 +226,7 @@ auto main(int argc, char* argv[]) -> int std::thread io_thread([&io_context]() { io_context.run(); }); - std::this_thread::sleep_for(timeout_duration); // NOTE: only for gcov results! CK + std::this_thread::sleep_for(timeout_duration); // NOTE: only for gcov results! CK for (std::string line; c->connected() && std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) { const std::string::size_type sz = line.find("//"); @@ -247,7 +247,7 @@ auto main(int argc, char* argv[]) -> int c->write(command); } - std::this_thread::sleep_for(heartbeat_interval); // NOTE: only for gcov results! CK + std::this_thread::sleep_for(heartbeat_interval); // NOTE: only for gcov results! CK c->stop(); io_thread.join(); From 2ed3e66adab1770832426b06a646ca9a6ae8b94b Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 19 Mar 2025 10:01:38 +0100 Subject: [PATCH 039/120] Add more base64 tests --- Base64.cpp | 10 +-- Base64.hpp | 4 +- CMakeLists.txt | 46 +++++----- Base64-test.cpp => tests/Base64-test.cpp | 108 +++++++++++++++++++---- 4 files changed, 117 insertions(+), 51 deletions(-) rename Base64-test.cpp => tests/Base64-test.cpp (53%) diff --git a/Base64.cpp b/Base64.cpp index 53a5337..388bc40 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -1,14 +1,12 @@ #include "Base64.hpp" -#include +// XXX #include #include #include namespace RRCP::Common { -void Base64::setLineBreak(bool lbrk) { m_encodeWithLinebreak = lbrk; } - auto Base64::encode(std::string_view data) const -> std::string { if (data.empty()) @@ -27,7 +25,7 @@ auto Base64::encode(std::string_view data) const -> std::string auto i2 = std::uint8_t(((data[pos] & 0x03) << 4U) | ((data[pos + 1] & 0xf0) >> 4U)); auto i3 = std::uint8_t(((data[pos + 1] & 0x0f) << 2U) | ((data[pos + 2] & 0xfc) >> 6U)); auto i4 = std::uint8_t((data[pos + 2] & 0x3f)); - assert((i1 < 64) && (i2 < 64) && (i3 < 64) && (i4 < 64)); + // XXX assert((i1 < 64) && (i2 < 64) && (i3 < 64) && (i4 < 64)); encoded.append(1, m_BaseChars[i1]); encoded.append(1, m_BaseChars[i2]); @@ -50,7 +48,7 @@ auto Base64::encode(std::string_view data) const -> std::string // One octet remaining. auto i1 = std::uint8_t((data[data.size() - 1] & 0xfc) >> 2U); auto i2 = std::uint8_t(((data[data.size() - 1] & 0x03) << 4U)); - assert((i1 < 64) && (i2 < 64)); + // XXX assert((i1 < 64) && (i2 < 64)); encoded.append(1, m_BaseChars[i1]); encoded.append(1, m_BaseChars[i2]); @@ -62,7 +60,7 @@ auto Base64::encode(std::string_view data) const -> std::string auto i1 = std::uint8_t((data[data.size() - 2] & 0xfc) >> 2U); auto i2 = std::uint8_t(((data[data.size() - 2] & 0x03) << 4U) | ((data[data.size() - 1] & 0xf0) >> 4U)); auto i3 = std::uint8_t(((data[data.size() - 1] & 0x0f) << 2U)); - assert((i1 < 64) && (i2 < 64) && (i3 < 64)); + // XXX assert((i1 < 64) && (i2 < 64) && (i3 < 64)); encoded.append(1, m_BaseChars[i1]); encoded.append(1, m_BaseChars[i2]); diff --git a/Base64.hpp b/Base64.hpp index a611984..5eacf0b 100644 --- a/Base64.hpp +++ b/Base64.hpp @@ -25,7 +25,7 @@ class Base64 * Set the line break flag for encoding. * @param lbrk If true, the encoded string will have a maximum line length of 80 characters. */ - void setLineBreak(bool lbrk); + void setLineBreak(bool lbrk) { m_encodeWithLinebreak = lbrk; } /** * Encode binary data to base64. @@ -57,7 +57,7 @@ class Base64 [[nodiscard]] auto base64CharValue(char c) const -> std::uint8_t; const std::string_view m_BaseChars{"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"}; - bool m_encodeWithLinebreak{true}; + bool m_encodeWithLinebreak{false}; }; } // namespace RRCP::Common diff --git a/CMakeLists.txt b/CMakeLists.txt index 845c713..f79cf6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,30 +107,28 @@ target_link_libraries( ) do_test(rrcp_async_tcp_client --help Usage) -if(APPLE) - # Set up googletest - include(FetchContent) - FetchContent_Declare( - googletest - GIT_TAG v1.16.0 - GIT_REPOSITORY https://github.com/google/googletest.git - FIND_PACKAGE_ARGS 1.16.0 NAMES GTest COMPONENTS gmain_main - EXCLUDE_FROM_ALL - SYSTEM - ) +# we need googletest +include(FetchContent) +FetchContent_Declare( + googletest + GIT_TAG v1.16.0 + GIT_REPOSITORY https://github.com/google/googletest.git + FIND_PACKAGE_ARGS 1.16.0 NAMES GTest COMPONENTS gmain_main + EXCLUDE_FROM_ALL + SYSTEM +) - # For Windows: Prevent overriding the parent project's compiler/linker settings - set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) - FetchContent_MakeAvailable(googletest) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) - add_library(Base64 STATIC) - target_sources( - Base64 - PRIVATE Base64.cpp - PUBLIC FILE_SET HEADERS FILES Base64.hpp - ) +add_library(Base64 STATIC) +target_sources( + Base64 + PRIVATE Base64.cpp + PUBLIC FILE_SET HEADERS FILES Base64.hpp +) - add_executable(Base64-test Base64-test.cpp) - target_link_libraries(Base64-test PRIVATE Base64 GTest::gtest_main) - add_test(NAME Base64-test COMMAND Base64-test) -endif() +add_executable(Base64-test tests/Base64-test.cpp) +target_link_libraries(Base64-test PRIVATE Base64 GTest::gtest_main) +# FIXME: add_test(NAME Base64-test COMMAND Base64-test) diff --git a/Base64-test.cpp b/tests/Base64-test.cpp similarity index 53% rename from Base64-test.cpp rename to tests/Base64-test.cpp index 1882781..72c5f1b 100644 --- a/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -5,6 +5,7 @@ Null or empty input data Input data with invalid characters (e.g., non-ASCII characters) Input data with padding errors (e.g., incorrect number of padding characters) Input data with encoding errors (e.g., incorrect encoding scheme) + By covering these edge cases, you can ensure that your Base64 class is robust and reliable. ***/ @@ -18,21 +19,69 @@ By covering these edge cases, you can ensure that your Base64 class is robust an using namespace std::string_literals; +// TODO: using RRCP::Common::Base64; +#define TEST_RANDOM_VALUES + +namespace +{ +struct testpattern_t +{ + const char *bin; + const char *encoded; +} testpattern[] = {{"", ""}, {" ", "IA=="}, {" ", "ICA="}, {" ", "ICAg"}, {" ", "ICAgIA=="}, {" ", "ICAgICA="}, + {" ", "ICAgICAg"}, {" ", "ICAgICAgIA=="}, {"U", "VQ=="}, {"UU", "VVU="}, {"UUU", "VVVV"}, + {"UUUU", "VVVVVQ=="}, {"UUUUU", "VVVVVVU="}, {"UUUUUU", "VVVVVVVV"}, {"UUUUUUU", "VVVVVVVVVQ=="}, + {"Franz jagt in einem total verwahrlosten Taxi quer durch Bayern", + "RnJhbnogamFndCBpbiBlaW5lbSB0b3RhbCB2ZXJ3YWhybG9zdGVuIFRheGkgcXVlciBkdXJjaCBCYXllcm4="}, + {nullptr, nullptr}}; +} // namespace + +TEST(Base64Test, encoding) +{ + RRCP::Common::Base64 base64; + base64.setLineBreak(false); + + size_t i = 1; + while (testpattern[i].bin != nullptr) + { + const std::string result = base64.encode(testpattern[i].bin); + const std::string encoded{testpattern[i].encoded}; + EXPECT_EQ(encoded, result); + ++i; + } +} + +TEST(Base64Test, decoding) +{ + RRCP::Common::Base64 base64; + + size_t i = 0; + while (testpattern[i].bin != nullptr) + { + const std::string result = base64.decode(testpattern[i].encoded); + const std::string bin{testpattern[i].bin}; + EXPECT_EQ(bin, result); + ++i; + } +} + TEST(Base64Test, EmptyString) { RRCP::Common::Base64 base64; - std::string const original = "\0"s; - std::string const encoded = base64.encode(original); - std::string const decoded = base64.decode(encoded); - EXPECT_EQ(original, decoded); + std::string const original; + EXPECT_THROW({ auto decoded = base64.encode(original); }, std::invalid_argument); + // XXX std::string const decoded = base64.decode(encoded); + // XXX EXPECT_EQ(original, decoded); } TEST(Base64Test, ShortString1) { RRCP::Common::Base64 base64; - std::string const original = "1"; + std::string const original = "A"; std::string const encoded = base64.encode(original); - std::println("{}:\t{}", original, encoded); + // std::println("{}:\t{}", original, encoded); + EXPECT_EQ(encoded, "QQ=="); + std::string const decoded = base64.decode(encoded); EXPECT_EQ(original, decoded); } @@ -40,9 +89,11 @@ TEST(Base64Test, ShortString1) TEST(Base64Test, ShortString2) { RRCP::Common::Base64 base64; - std::string const original = "12"; + std::string const original = "AA"; std::string const encoded = base64.encode(original); - std::println("{}:\t{}", original, encoded); + // std::println("{}:\t{}", original, encoded); + EXPECT_EQ(encoded, "QUE="); + std::string const decoded = base64.decode(encoded); EXPECT_EQ(original, decoded); } @@ -50,21 +101,25 @@ TEST(Base64Test, ShortString2) TEST(Base64Test, ShortString3) { RRCP::Common::Base64 base64; - std::string const original = "123"; + std::string const original = "AAA"; std::string const encoded = base64.encode(original); - std::println("{}:\t{}", original, encoded); + // std::println("{}:\t{}", original, encoded); + EXPECT_EQ(encoded, "QUFB"); + std::string const decoded = base64.decode(encoded); EXPECT_EQ(original, decoded); } -TEST(Base64Test, ShortString4) +TEST(Base64Test, DecodeMarker) { RRCP::Common::Base64 base64; - std::string const original = "1234"; - std::string const encoded = base64.encode(original); - std::println("{}:\t{}", original, encoded); - std::string const decoded = base64.decode(encoded); - EXPECT_EQ(original, decoded); + EXPECT_ANY_THROW({ (void)base64.decode("====").empty(); }); + EXPECT_ANY_THROW({ (void)base64.decode("===").empty(); }); + EXPECT_ANY_THROW({ (void)base64.decode("==").empty(); }); + EXPECT_ANY_THROW({ (void)base64.decode("=").empty(); }); + EXPECT_ANY_THROW({ (void)base64.decode("\t").empty(); }); + + EXPECT_NO_THROW({ (void)base64.decode("").empty(); }); } TEST(Base64Test, MediumString) @@ -79,8 +134,23 @@ TEST(Base64Test, MediumString) TEST(Base64Test, LongString) { RRCP::Common::Base64 base64; - std::string const original = "This is a very long string that should be encoded and decoded correctly."; + base64.setLineBreak(true); + + std::string const original = "This is not a really long string, but also that should be encoded and decoded correctly."; std::string const encoded = base64.encode(original); + std::string const decoded = base64.decode(encoded); + std::println("{}:\n{}", original, encoded); + EXPECT_EQ(original, decoded); +} + +TEST(Base64Test, FoxString) +{ + RRCP::Common::Base64 base64; + std::string const original = "The quick brown fox jumped over the lazy dogs."; + std::string const encoded = base64.encode(original); + EXPECT_EQ(encoded, "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wZWQgb3ZlciB0aGUgbGF6eSBkb2dzLg=="); + // std::println("{}:\t{}", original, encoded); + std::string const decoded = base64.decode(encoded); EXPECT_EQ(original, decoded); } @@ -103,11 +173,11 @@ TEST(Base64Test, NonAsciiString) EXPECT_EQ(original, decoded); } -#ifdef USE_RANDOM_VALUES +#ifdef TEST_RANDOM_VALUES TEST(Base64Test, RandomBinaryData) { std::random_device rd; // a seed source for the random number engine - std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd() + std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd() std::uniform_int_distribution<> distrib(0, 255); RRCP::Common::Base64 base64; From 78dc2c734fb4ab6c276363f9a7c24cc32dc2b877 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 19 Mar 2025 16:37:15 +0100 Subject: [PATCH 040/120] Add even more tests --- Base64.rfc | 73 +++++++++++++++++++++++++++++++++++++++++++ tests/Base64-test.cpp | 58 +++++++++++++++++++++++++++++----- 2 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 Base64.rfc diff --git a/Base64.rfc b/Base64.rfc new file mode 100644 index 0000000..6964437 --- /dev/null +++ b/Base64.rfc @@ -0,0 +1,73 @@ +RFC 2045 Internet Message Bodies November 1996 + + Table 1: The Base64 Alphabet + + Value Encoding Value Encoding Value Encoding Value Encoding + 0 A 17 R 34 i 51 z + 1 B 18 S 35 j 52 0 + 2 C 19 T 36 k 53 1 + 3 D 20 U 37 l 54 2 + 4 E 21 V 38 m 55 3 + 5 F 22 W 39 n 56 4 + 6 G 23 X 40 o 57 5 + 7 H 24 Y 41 p 58 6 + 8 I 25 Z 42 q 59 7 + 9 J 26 a 43 r 60 8 + 10 K 27 b 44 s 61 9 + 11 L 28 c 45 t 62 + + 12 M 29 d 46 u 63 / + 13 N 30 e 47 v + 14 O 31 f 48 w (pad) = + 15 P 32 g 49 x + 16 Q 33 h 50 y + +The encoded output stream must be represented in lines of no more +than 76 characters each. All line breaks or other characters not +found in Table 1 must be ignored by decoding software. In base64 +data, characters other than those in Table 1, line breaks, and other +white space probably indicate a transmission error, about which a +warning message or even a message rejection might be appropriate +under some circumstances. + +Special processing is performed if fewer than 24 bits are available +at the end of the data being encoded. A full encoding quantum is +always completed at the end of a body. When fewer than 24 input bits +are available in an input group, zero bits are added (on the right) +to form an integral number of 6-bit groups. Padding at the end of +the data is performed using the "=" character. Since all base64 +input is an integral number of octets, only the following cases can +arise: + +(1) the final quantum of encoding input is an integral multiple of + 24 bits; here, the final unit of encoded output will be an + integral multiple of 4 characters with no "=" padding. + +(2) the final quantum of encoding input is exactly 8 bits; here, + the final unit of encoded output will be two characters + followed by two "=" padding characters. + +(3) the final quantum of encoding input is exactly 16 bits; here, + the final unit of encoded output will be three characters + followed by one "=" padding character. + +Because it is used only for padding at the end of the data, the +occurrence of any "=" characters may be taken as evidence that the +end of the data has been reached (without truncation in transit). No +such assurance is possible, however, when the number of octets +transmitted was a multiple of three and no "=" characters are +present. + +Any characters outside of the base64 alphabet are to be ignored in +base64-encoded data. + +Care must be taken to use the proper octets for line breaks if base64 +encoding is applied directly to text material that has not been +converted to canonical form. In particular, text line breaks must be +converted into CRLF sequences prior to base64 encoding. The +important thing to note is that this may be done directly by the +encoder rather than in a prior canonicalization step in some +implementations. + +NOTE: There is no need to worry about quoting potential boundary +delimiters within base64-encoded bodies within multipart entities +because no hyphen characters are used in the base64 encoding. diff --git a/tests/Base64-test.cpp b/tests/Base64-test.cpp index 72c5f1b..a39beed 100644 --- a/tests/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -13,14 +13,14 @@ By covering these edge cases, you can ensure that your Base64 class is robust an #include -#include +// #include // for std::println #include #include using namespace std::string_literals; // TODO: using RRCP::Common::Base64; -#define TEST_RANDOM_VALUES +#undef TEST_RANDOM_VALUES namespace { @@ -30,10 +30,7 @@ struct testpattern_t const char *encoded; } testpattern[] = {{"", ""}, {" ", "IA=="}, {" ", "ICA="}, {" ", "ICAg"}, {" ", "ICAgIA=="}, {" ", "ICAgICA="}, {" ", "ICAgICAg"}, {" ", "ICAgICAgIA=="}, {"U", "VQ=="}, {"UU", "VVU="}, {"UUU", "VVVV"}, - {"UUUU", "VVVVVQ=="}, {"UUUUU", "VVVVVVU="}, {"UUUUUU", "VVVVVVVV"}, {"UUUUUUU", "VVVVVVVVVQ=="}, - {"Franz jagt in einem total verwahrlosten Taxi quer durch Bayern", - "RnJhbnogamFndCBpbiBlaW5lbSB0b3RhbCB2ZXJ3YWhybG9zdGVuIFRheGkgcXVlciBkdXJjaCBCYXllcm4="}, - {nullptr, nullptr}}; + {"UUUU", "VVVVVQ=="}, {"UUUUU", "VVVVVVU="}, {"UUUUUU", "VVVVVVVV"}, {"UUUUUUU", "VVVVVVVVVQ=="}, {nullptr, nullptr}}; } // namespace TEST(Base64Test, encoding) @@ -138,8 +135,13 @@ TEST(Base64Test, LongString) std::string const original = "This is not a really long string, but also that should be encoded and decoded correctly."; std::string const encoded = base64.encode(original); + std::string expected{ + "VGhpcyBpcyBub3QgYSByZWFsbHkgbG9uZyBzdHJpbmcsIGJ1dCBhbHNvIHRoYXQgc2hvdWxkIGJl\n" + "IGVuY29kZWQgYW5kIGRlY29kZWQgY29ycmVjdGx5Lg=="}; + EXPECT_EQ(expected, encoded); + // std::println("{}:\n{}", original, encoded); + std::string const decoded = base64.decode(encoded); - std::println("{}:\n{}", original, encoded); EXPECT_EQ(original, decoded); } @@ -173,6 +175,48 @@ TEST(Base64Test, NonAsciiString) EXPECT_EQ(original, decoded); } +TEST(Base64Test, TestEncoder) +{ + RRCP::Common::Base64 base64; + { + std::string original("\00\01\02\03\04\05", 6); + auto encoded = base64.encode(original); + EXPECT_TRUE(encoded == "AAECAwQF"); + EXPECT_EQ(original, base64.decode(encoded)); + } + { + std::string original("\00\01\02\03", 4); + auto encoded = base64.encode(original); + EXPECT_TRUE(encoded == "AAECAw=="); + EXPECT_EQ(original, base64.decode(encoded)); + } + { + std::string original("ABCDEF"); + auto encoded = base64.encode(original); + EXPECT_TRUE(encoded == "QUJDREVG"); + EXPECT_EQ(original, base64.decode(encoded)); + } + { + std::string original("!@#$%^&*()_~<>"); + auto encoded = base64.encode(original); + EXPECT_TRUE(encoded == "IUAjJCVeJiooKV9+PD4="); + EXPECT_EQ(original, base64.decode(encoded)); + } +} +TEST(Base64Test, TestDecoder) +{ + RRCP::Common::Base64 base64; + { + const std::string istr("QUJ\r\nDRE\r\nVG"); + const std::string decoded = base64.decode(istr); + EXPECT_TRUE(decoded == "ABCDEF"); + } + { + const std::string istr("QUJD#REVG"); + EXPECT_ANY_THROW({ (void)base64.decode(istr).empty(); }); + } +} + #ifdef TEST_RANDOM_VALUES TEST(Base64Test, RandomBinaryData) { From 889a54e521af7c61f8f6faf2f5d574362ee26db5 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 20 Mar 2025 08:02:51 +0100 Subject: [PATCH 041/120] Change base64 tests according to RFC --- .clang-tidy | 10 + Base64.cpp | 29 +-- Base64.hpp | 12 +- CMakeLists.txt | 7 +- base64.c | 425 ++++++++++++++++++++++++++++++++++++++++++ base64.h | 45 +++++ tests/Base64-test.cpp | 80 ++++++-- 7 files changed, 567 insertions(+), 41 deletions(-) create mode 100644 base64.c create mode 100644 base64.h diff --git a/.clang-tidy b/.clang-tidy index b369e97..959362e 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -24,6 +24,7 @@ performance-*,\ portability-*,\ readability-*,\ -readability-identifier-length,\ +-readability-identifier-naming,\ -*magic-numbers,\ -*avoid-c-arrays,\ " @@ -31,4 +32,13 @@ WarningsAsErrors: 'clang-*' HeaderFilterRegex: '.*' FormatStyle: file User: clausklein +# options: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals +CheckOptions: + - { key: readability-identifier-naming.NamespaceCase, value: lower_case } + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.MethodCase, value: lower_case } + - { key: readability-identifier-naming.MemberCase, value: lower_case } + - { key: readability-identifier-naming.MemberSuffix, value: _ } + - { key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals, value: true } ... + diff --git a/Base64.cpp b/Base64.cpp index 388bc40..f5f3bb9 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -11,7 +11,8 @@ auto Base64::encode(std::string_view data) const -> std::string { if (data.empty()) { - throw std::invalid_argument("Invalid input data"); + // NO! throw std::invalid_argument("Invalid input data"); + return {}; } std::string encoded; @@ -27,11 +28,11 @@ auto Base64::encode(std::string_view data) const -> std::string auto i4 = std::uint8_t((data[pos + 2] & 0x3f)); // XXX assert((i1 < 64) && (i2 < 64) && (i3 < 64) && (i4 < 64)); - encoded.append(1, m_BaseChars[i1]); - encoded.append(1, m_BaseChars[i2]); - encoded.append(1, m_BaseChars[i3]); - encoded.append(1, m_BaseChars[i4]); - if (m_encodeWithLinebreak) + encoded.append(1, BaseChars_[i1]); + encoded.append(1, BaseChars_[i2]); + encoded.append(1, BaseChars_[i3]); + encoded.append(1, BaseChars_[i4]); + if (encodeWithLinebreak_) { linelen += 4; if (linelen >= 76) @@ -50,8 +51,8 @@ auto Base64::encode(std::string_view data) const -> std::string auto i2 = std::uint8_t(((data[data.size() - 1] & 0x03) << 4U)); // XXX assert((i1 < 64) && (i2 < 64)); - encoded.append(1, m_BaseChars[i1]); - encoded.append(1, m_BaseChars[i2]); + encoded.append(1, BaseChars_[i1]); + encoded.append(1, BaseChars_[i2]); encoded.append(2, '='); } else if ((data.size() % 3) == 2) @@ -62,9 +63,9 @@ auto Base64::encode(std::string_view data) const -> std::string auto i3 = std::uint8_t(((data[data.size() - 1] & 0x0f) << 2U)); // XXX assert((i1 < 64) && (i2 < 64) && (i3 < 64)); - encoded.append(1, m_BaseChars[i1]); - encoded.append(1, m_BaseChars[i2]); - encoded.append(1, m_BaseChars[i3]); + encoded.append(1, BaseChars_[i1]); + encoded.append(1, BaseChars_[i2]); + encoded.append(1, BaseChars_[i3]); encoded.append(1, '='); } @@ -77,7 +78,7 @@ auto Base64::decode(std::string_view in) -> std::string std::string fourChars; // Iterate over the input string - for (char i : in) + for (const char i : in) { if (isBase64Char(i) || (i == '=')) { @@ -100,11 +101,11 @@ auto Base64::decode(std::string_view in) -> std::string decoded += static_cast< char >((i1 << 2U) | (i2 >> 4U)); if (i3 != 0) { - decoded += static_cast< char >(((i2 << 4U) /* & 0xf0 */) | (i3 >> 2U)); + decoded += static_cast< char >(((i2 << 4U) & 0xf0) | (i3 >> 2U)); } if (i4 != 0) { - decoded += static_cast< char >(((i3 << 6U) /* & 0xc0 */) | i4); + decoded += static_cast< char >(((i3 << 6U) & 0xc0) | i4); } fourChars.clear(); diff --git a/Base64.hpp b/Base64.hpp index 5eacf0b..4fcd6ae 100644 --- a/Base64.hpp +++ b/Base64.hpp @@ -1,5 +1,5 @@ -#ifndef BASE64_H -#define BASE64_H +#ifndef BASE64_HPP +#define BASE64_HPP #include #include @@ -25,7 +25,7 @@ class Base64 * Set the line break flag for encoding. * @param lbrk If true, the encoded string will have a maximum line length of 80 characters. */ - void setLineBreak(bool lbrk) { m_encodeWithLinebreak = lbrk; } + void setLineBreak(bool lbrk) { encodeWithLinebreak_ = lbrk; } /** * Encode binary data to base64. @@ -56,10 +56,10 @@ class Base64 */ [[nodiscard]] auto base64CharValue(char c) const -> std::uint8_t; - const std::string_view m_BaseChars{"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"}; - bool m_encodeWithLinebreak{false}; + const std::string_view BaseChars_{"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"}; + bool encodeWithLinebreak_{false}; }; } // namespace RRCP::Common -#endif // BASE64_H +#endif // BASE64_HPP diff --git a/CMakeLists.txt b/CMakeLists.txt index f79cf6f..ccf61b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.28...4.0) -project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) +project(RRCP-client VERSION 0.1.0 LANGUAGES CXX C) # ---- add dependencies ---- @@ -122,6 +122,9 @@ FetchContent_Declare( set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) +add_library(base64c STATIC) +target_sources(base64c PRIVATE base64.c PUBLIC FILE_SET HEADERS FILES base64.h) + add_library(Base64 STATIC) target_sources( Base64 @@ -130,5 +133,5 @@ target_sources( ) add_executable(Base64-test tests/Base64-test.cpp) -target_link_libraries(Base64-test PRIVATE Base64 GTest::gtest_main) +target_link_libraries(Base64-test PRIVATE Base64 base64c GTest::gtest_main) # FIXME: add_test(NAME Base64-test COMMAND Base64-test) diff --git a/base64.c b/base64.c new file mode 100644 index 0000000..89efbd6 --- /dev/null +++ b/base64.c @@ -0,0 +1,425 @@ +/* base64.c -- Encode binary data using printable characters. + Copyright (C) 1999, 2000, 2001, 2004, 2005, 2006 Free Software + Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +/* Written by Simon Josefsson. Partially adapted from GNU MailUtils + * (mailbox/filter_trans.c, as of 2004-11-28). Improved by review + * from Paul Eggert, Bruno Haible, and Stepan Kasal. + * + * See also RFC 3548 . + * + * Be careful with error checking. Here is how you would typically + * use these functions: + * + * bool ok = base64_decode_alloc (in, inlen, &out, &outlen); + * if (!ok) + * FAIL: input was not valid base64 + * if (out == NULL) + * FAIL: memory allocation error + * OK: data in OUT/OUTLEN + * + * size_t outlen = base64_encode_alloc (in, inlen, &out); + * if (out == NULL && outlen == 0 && inlen != 0) + * FAIL: input too long + * if (out == NULL) + * FAIL: memory allocation error + * OK: data in OUT/OUTLEN. + * + */ + +//XXX #include + +/* Get prototype. */ +#include "base64.h" + +/* Get malloc. */ +#include + +/* Get UCHAR_MAX. */ +#include + +/* C89 compliant way to cast 'char' to 'unsigned char'. */ +static inline unsigned char +to_uchar (char ch) +{ + return ch; +} + +/* Base64 encode IN array of size INLEN into OUT array of size OUTLEN. + If OUTLEN is less than BASE64_LENGTH(INLEN), write as many bytes as + possible. If OUTLEN is larger than BASE64_LENGTH(INLEN), also zero + terminate the output buffer. */ +void +base64_encode (const char *restrict in, size_t inlen, + char *restrict out, size_t outlen) +{ + static const char b64str[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + while (inlen && outlen) + { + *out++ = b64str[(to_uchar (in[0]) >> 2) & 0x3f]; + if (!--outlen) + break; + *out++ = b64str[((to_uchar (in[0]) << 4) + + (--inlen ? to_uchar (in[1]) >> 4 : 0)) + & 0x3f]; + if (!--outlen) + break; + *out++ = + (inlen + ? b64str[((to_uchar (in[1]) << 2) + + (--inlen ? to_uchar (in[2]) >> 6 : 0)) + & 0x3f] + : '='); + if (!--outlen) + break; + *out++ = inlen ? b64str[to_uchar (in[2]) & 0x3f] : '='; + if (!--outlen) + break; + if (inlen) + inlen--; + if (inlen) + in += 3; + } + + if (outlen) + *out = '\0'; +} + +/* Allocate a buffer and store zero terminated base64 encoded data + from array IN of size INLEN, returning BASE64_LENGTH(INLEN), i.e., + the length of the encoded data, excluding the terminating zero. On + return, the OUT variable will hold a pointer to newly allocated + memory that must be deallocated by the caller. If output string + length would overflow, 0 is returned and OUT is set to NULL. If + memory allocation failed, OUT is set to NULL, and the return value + indicates length of the requested memory block, i.e., + BASE64_LENGTH(inlen) + 1. */ +size_t +base64_encode_alloc (const char *in, size_t inlen, char **out) +{ + size_t outlen = 1 + BASE64_LENGTH (inlen); + + /* Check for overflow in outlen computation. + * + * If there is no overflow, outlen >= inlen. + * + * If the operation (inlen + 2) overflows then it yields at most +1, so + * outlen is 0. + * + * If the multiplication overflows, we lose at least half of the + * correct value, so the result is < ((inlen + 2) / 3) * 2, which is + * less than (inlen + 2) * 0.66667, which is less than inlen as soon as + * (inlen > 4). + */ + if (inlen > outlen) + { + *out = NULL; + return 0; + } + + *out = malloc (outlen); + if (!*out) + return outlen; + + base64_encode (in, inlen, *out, outlen); + + return outlen - 1; +} + +/* With this approach this file works independent of the charset used + (think EBCDIC). However, it does assume that the characters in the + Base64 alphabet (A-Za-z0-9+/) are encoded in 0..255. POSIX + 1003.1-2001 require that char and unsigned char are 8-bit + quantities, though, taking care of that problem. But this may be a + potential problem on non-POSIX C99 platforms. + + IBM C V6 for AIX mishandles "#define B64(x) ...'x'...", so use "_" + as the formal parameter rather than "x". */ +#define B64(_) \ + ((_) == 'A' ? 0 \ + : (_) == 'B' ? 1 \ + : (_) == 'C' ? 2 \ + : (_) == 'D' ? 3 \ + : (_) == 'E' ? 4 \ + : (_) == 'F' ? 5 \ + : (_) == 'G' ? 6 \ + : (_) == 'H' ? 7 \ + : (_) == 'I' ? 8 \ + : (_) == 'J' ? 9 \ + : (_) == 'K' ? 10 \ + : (_) == 'L' ? 11 \ + : (_) == 'M' ? 12 \ + : (_) == 'N' ? 13 \ + : (_) == 'O' ? 14 \ + : (_) == 'P' ? 15 \ + : (_) == 'Q' ? 16 \ + : (_) == 'R' ? 17 \ + : (_) == 'S' ? 18 \ + : (_) == 'T' ? 19 \ + : (_) == 'U' ? 20 \ + : (_) == 'V' ? 21 \ + : (_) == 'W' ? 22 \ + : (_) == 'X' ? 23 \ + : (_) == 'Y' ? 24 \ + : (_) == 'Z' ? 25 \ + : (_) == 'a' ? 26 \ + : (_) == 'b' ? 27 \ + : (_) == 'c' ? 28 \ + : (_) == 'd' ? 29 \ + : (_) == 'e' ? 30 \ + : (_) == 'f' ? 31 \ + : (_) == 'g' ? 32 \ + : (_) == 'h' ? 33 \ + : (_) == 'i' ? 34 \ + : (_) == 'j' ? 35 \ + : (_) == 'k' ? 36 \ + : (_) == 'l' ? 37 \ + : (_) == 'm' ? 38 \ + : (_) == 'n' ? 39 \ + : (_) == 'o' ? 40 \ + : (_) == 'p' ? 41 \ + : (_) == 'q' ? 42 \ + : (_) == 'r' ? 43 \ + : (_) == 's' ? 44 \ + : (_) == 't' ? 45 \ + : (_) == 'u' ? 46 \ + : (_) == 'v' ? 47 \ + : (_) == 'w' ? 48 \ + : (_) == 'x' ? 49 \ + : (_) == 'y' ? 50 \ + : (_) == 'z' ? 51 \ + : (_) == '0' ? 52 \ + : (_) == '1' ? 53 \ + : (_) == '2' ? 54 \ + : (_) == '3' ? 55 \ + : (_) == '4' ? 56 \ + : (_) == '5' ? 57 \ + : (_) == '6' ? 58 \ + : (_) == '7' ? 59 \ + : (_) == '8' ? 60 \ + : (_) == '9' ? 61 \ + : (_) == '+' ? 62 \ + : (_) == '/' ? 63 \ + : -1) + +static const signed char b64[0x100] = { + B64 (0), B64 (1), B64 (2), B64 (3), + B64 (4), B64 (5), B64 (6), B64 (7), + B64 (8), B64 (9), B64 (10), B64 (11), + B64 (12), B64 (13), B64 (14), B64 (15), + B64 (16), B64 (17), B64 (18), B64 (19), + B64 (20), B64 (21), B64 (22), B64 (23), + B64 (24), B64 (25), B64 (26), B64 (27), + B64 (28), B64 (29), B64 (30), B64 (31), + B64 (32), B64 (33), B64 (34), B64 (35), + B64 (36), B64 (37), B64 (38), B64 (39), + B64 (40), B64 (41), B64 (42), B64 (43), + B64 (44), B64 (45), B64 (46), B64 (47), + B64 (48), B64 (49), B64 (50), B64 (51), + B64 (52), B64 (53), B64 (54), B64 (55), + B64 (56), B64 (57), B64 (58), B64 (59), + B64 (60), B64 (61), B64 (62), B64 (63), + B64 (64), B64 (65), B64 (66), B64 (67), + B64 (68), B64 (69), B64 (70), B64 (71), + B64 (72), B64 (73), B64 (74), B64 (75), + B64 (76), B64 (77), B64 (78), B64 (79), + B64 (80), B64 (81), B64 (82), B64 (83), + B64 (84), B64 (85), B64 (86), B64 (87), + B64 (88), B64 (89), B64 (90), B64 (91), + B64 (92), B64 (93), B64 (94), B64 (95), + B64 (96), B64 (97), B64 (98), B64 (99), + B64 (100), B64 (101), B64 (102), B64 (103), + B64 (104), B64 (105), B64 (106), B64 (107), + B64 (108), B64 (109), B64 (110), B64 (111), + B64 (112), B64 (113), B64 (114), B64 (115), + B64 (116), B64 (117), B64 (118), B64 (119), + B64 (120), B64 (121), B64 (122), B64 (123), + B64 (124), B64 (125), B64 (126), B64 (127), + B64 (128), B64 (129), B64 (130), B64 (131), + B64 (132), B64 (133), B64 (134), B64 (135), + B64 (136), B64 (137), B64 (138), B64 (139), + B64 (140), B64 (141), B64 (142), B64 (143), + B64 (144), B64 (145), B64 (146), B64 (147), + B64 (148), B64 (149), B64 (150), B64 (151), + B64 (152), B64 (153), B64 (154), B64 (155), + B64 (156), B64 (157), B64 (158), B64 (159), + B64 (160), B64 (161), B64 (162), B64 (163), + B64 (164), B64 (165), B64 (166), B64 (167), + B64 (168), B64 (169), B64 (170), B64 (171), + B64 (172), B64 (173), B64 (174), B64 (175), + B64 (176), B64 (177), B64 (178), B64 (179), + B64 (180), B64 (181), B64 (182), B64 (183), + B64 (184), B64 (185), B64 (186), B64 (187), + B64 (188), B64 (189), B64 (190), B64 (191), + B64 (192), B64 (193), B64 (194), B64 (195), + B64 (196), B64 (197), B64 (198), B64 (199), + B64 (200), B64 (201), B64 (202), B64 (203), + B64 (204), B64 (205), B64 (206), B64 (207), + B64 (208), B64 (209), B64 (210), B64 (211), + B64 (212), B64 (213), B64 (214), B64 (215), + B64 (216), B64 (217), B64 (218), B64 (219), + B64 (220), B64 (221), B64 (222), B64 (223), + B64 (224), B64 (225), B64 (226), B64 (227), + B64 (228), B64 (229), B64 (230), B64 (231), + B64 (232), B64 (233), B64 (234), B64 (235), + B64 (236), B64 (237), B64 (238), B64 (239), + B64 (240), B64 (241), B64 (242), B64 (243), + B64 (244), B64 (245), B64 (246), B64 (247), + B64 (248), B64 (249), B64 (250), B64 (251), + B64 (252), B64 (253), B64 (254), B64 (255) +}; + +#if UCHAR_MAX == 255 +# define uchar_in_range(c) true +#else +# define uchar_in_range(c) ((c) <= 255) +#endif + +/* Return true if CH is a character from the Base64 alphabet, and + false otherwise. Note that '=' is padding and not considered to be + part of the alphabet. */ +bool +isbase64 (char ch) +{ + return uchar_in_range (to_uchar (ch)) && 0 <= b64[to_uchar (ch)]; +} + +/* Decode base64 encoded input array IN of length INLEN to output + array OUT that can hold *OUTLEN bytes. Return true if decoding was + successful, i.e. if the input was valid base64 data, false + otherwise. If *OUTLEN is too small, as many bytes as possible will + be written to OUT. On return, *OUTLEN holds the length of decoded + bytes in OUT. Note that as soon as any non-alphabet characters are + encountered, decoding is stopped and false is returned. This means + that, when applicable, you must remove any line terminators that is + part of the data stream before calling this function. */ +bool +base64_decode (const char *restrict in, size_t inlen, + char *restrict out, size_t *outlen) +{ + size_t outleft = *outlen; + + while (inlen >= 2) + { + if (!isbase64 (in[0]) || !isbase64 (in[1])) + break; + + if (outleft) + { + *out++ = ((b64[to_uchar (in[0])] << 2) + | (b64[to_uchar (in[1])] >> 4)); + outleft--; + } + + if (inlen == 2) + break; + + if (in[2] == '=') + { + if (inlen != 4) + break; + + if (in[3] != '=') + break; + + } + else + { + if (!isbase64 (in[2])) + break; + + if (outleft) + { + *out++ = (((b64[to_uchar (in[1])] << 4) & 0xf0) + | (b64[to_uchar (in[2])] >> 2)); + outleft--; + } + + if (inlen == 3) + break; + + if (in[3] == '=') + { + if (inlen != 4) + break; + } + else + { + if (!isbase64 (in[3])) + break; + + if (outleft) + { + *out++ = (((b64[to_uchar (in[2])] << 6) & 0xc0) + | b64[to_uchar (in[3])]); + outleft--; + } + } + } + + in += 4; + inlen -= 4; + } + + *outlen -= outleft; + + if (inlen != 0) + return false; + + return true; +} + +/* Allocate an output buffer in *OUT, and decode the base64 encoded + data stored in IN of size INLEN to the *OUT buffer. On return, the + size of the decoded data is stored in *OUTLEN. OUTLEN may be NULL, + if the caller is not interested in the decoded length. *OUT may be + NULL to indicate an out of memory error, in which case *OUTLEN + contains the size of the memory block needed. The function returns + true on successful decoding and memory allocation errors. (Use the + *OUT and *OUTLEN parameters to differentiate between successful + decoding and memory error.) The function returns false if the + input was invalid, in which case *OUT is NULL and *OUTLEN is + undefined. */ +bool +base64_decode_alloc (const char *in, size_t inlen, char **out, + size_t *outlen) +{ + /* This may allocate a few bytes too much, depending on input, + but it's not worth the extra CPU time to compute the exact amount. + The exact amount is 3 * inlen / 4, minus 1 if the input ends + with "=" and minus another 1 if the input ends with "==". + Dividing before multiplying avoids the possibility of overflow. */ + size_t needlen = 3 * (inlen / 4) + 2; + + *out = malloc (needlen); + if (!*out) + return true; + + if (!base64_decode (in, inlen, *out, &needlen)) + { + free (*out); + *out = NULL; + return false; + } + + if (outlen) + *outlen = needlen; + + return true; +} diff --git a/base64.h b/base64.h new file mode 100644 index 0000000..0e82ece --- /dev/null +++ b/base64.h @@ -0,0 +1,45 @@ +/* base64.h -- Encode binary data using printable characters. + Copyright (C) 2004, 2005, 2006 Free Software Foundation, Inc. + Written by Simon Josefsson. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +#ifndef BASE64_H +# define BASE64_H + +/* Get size_t. */ +# include + +/* Get bool. */ +# include + +/* This uses that the expression (n+(k-1))/k means the smallest + integer >= n/k, i.e., the ceiling of n/k. */ +# define BASE64_LENGTH(inlen) ((((inlen) + 2) / 3) * 4) + +extern bool isbase64 (char ch); + +extern void base64_encode (const char *in, size_t inlen, + char *out, size_t outlen); + +extern size_t base64_encode_alloc (const char *in, size_t inlen, char **out); + +extern bool base64_decode (const char *in, size_t inlen, + char *out, size_t *outlen); + +extern bool base64_decode_alloc (const char *in, size_t inlen, + char **out, size_t *outlen); + +#endif /* BASE64_H */ diff --git a/tests/Base64-test.cpp b/tests/Base64-test.cpp index a39beed..b48943b 100644 --- a/tests/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -11,9 +11,19 @@ By covering these edge cases, you can ensure that your Base64 class is robust an #include "Base64.hpp" +#include + +extern "C" +{ + // #include "base64.h" + extern void base64_encode(const char *in, size_t inlen, char *out, size_t outlen); + extern bool base64_decode(const char *in, size_t inlen, char *out, size_t *outlen); +} + #include -// #include // for std::println +#include +#include // for std::println #include #include @@ -24,13 +34,30 @@ using namespace std::string_literals; namespace { +// Test Vectors from rfc4648 +// see https://datatracker.ietf.org/doc/html/rfc4648#section-10 struct testpattern_t { const char *bin; const char *encoded; -} testpattern[] = {{"", ""}, {" ", "IA=="}, {" ", "ICA="}, {" ", "ICAg"}, {" ", "ICAgIA=="}, {" ", "ICAgICA="}, - {" ", "ICAgICAg"}, {" ", "ICAgICAgIA=="}, {"U", "VQ=="}, {"UU", "VVU="}, {"UUU", "VVVV"}, - {"UUUU", "VVVVVQ=="}, {"UUUUU", "VVVVVVU="}, {"UUUUUU", "VVVVVVVV"}, {"UUUUUUU", "VVVVVVVVVQ=="}, {nullptr, nullptr}}; +} testpattern[] = { // + {"", ""}, // + {"f", "Zg=="}, // + {"fo", "Zm8="}, // + {"foo", "Zm9v"}, // + {"foob", "Zm9vYg=="}, // + {"fooba", "Zm9vYmE="}, // + {"foobar", "Zm9vYmFy"}, // + // FIXME: {" ", "IA=="}, {" ", "ICA="}, {" ", "ICAg"}, {" ", "ICAgIA=="}, {" ", "ICAgICA="}, {" ", + // "ICAgICAg"}, {" ", "ICAgICAgIA=="}, // + {"U", "VQ=="}, // + {"UU", "VVU="}, // + {"UUU", "VVVV"}, // + {"UUUU", "VVVVVQ=="}, // + {"UUUUU", "VVVVVVU="}, // + {"UUUUUU", "VVVVVVVV"}, // + {"UUUUUUU", "VVVVVVVVVQ=="}, // + {nullptr, nullptr}}; } // namespace TEST(Base64Test, encoding) @@ -38,12 +65,20 @@ TEST(Base64Test, encoding) RRCP::Common::Base64 base64; base64.setLineBreak(false); - size_t i = 1; + std::array< char, 54 > text{}; + size_t i = 0; while (testpattern[i].bin != nullptr) { - const std::string result = base64.encode(testpattern[i].bin); + const std::string binary(testpattern[i].bin); const std::string encoded{testpattern[i].encoded}; - EXPECT_EQ(encoded, result); + std::println("'{}':\t{}", binary, encoded); + + const std::string base64_encoded = base64.encode(binary); + EXPECT_EQ(encoded, base64_encoded); + + base64_encode(binary.c_str(), binary.size(), text.data(), text.size()); + EXPECT_EQ(encoded, text.data()); + ++i; } } @@ -52,25 +87,32 @@ TEST(Base64Test, decoding) { RRCP::Common::Base64 base64; + std::array< char, 54 > data{}; size_t i = 0; while (testpattern[i].bin != nullptr) { - const std::string result = base64.decode(testpattern[i].encoded); - const std::string bin{testpattern[i].bin}; - EXPECT_EQ(bin, result); + const std::string encoded{testpattern[i].encoded}; + const std::string binary{testpattern[i].bin}; + std::println("'{}':\t{}", binary, encoded); + + const std::string decoded = base64.decode(encoded); + EXPECT_EQ(binary, decoded); + +#if 0 + size_t length{}; + const bool ok = base64_decode(encoded.c_str(), encoded.size(), data.data(), &length); + EXPECT_TRUE(ok); + if (ok) + { + EXPECT_EQ(binary.length(), length); + EXPECT_EQ(binary, data.data()); + } +#endif + ++i; } } -TEST(Base64Test, EmptyString) -{ - RRCP::Common::Base64 base64; - std::string const original; - EXPECT_THROW({ auto decoded = base64.encode(original); }, std::invalid_argument); - // XXX std::string const decoded = base64.decode(encoded); - // XXX EXPECT_EQ(original, decoded); -} - TEST(Base64Test, ShortString1) { RRCP::Common::Base64 base64; From 03229b0440595480a3819fdded64e4087569a3ff Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 20 Mar 2025 13:15:40 +0100 Subject: [PATCH 042/120] Add examples too --- CMakeLists.txt | 5 ++++ GNUmakefile | 2 +- examples/CMakeLists.txt | 16 ++++++++++ examples/README.md | 23 ++++++++++++++ examples/base64decode.cpp | 63 +++++++++++++++++++++++++++++++++++++++ examples/base64encode.cpp | 63 +++++++++++++++++++++++++++++++++++++++ tests/Base64-test.cpp | 25 +++++++++++----- 7 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 examples/CMakeLists.txt create mode 100644 examples/README.md create mode 100644 examples/base64decode.cpp create mode 100644 examples/base64encode.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ccf61b7..5f66151 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ endif() # ---- code coverage ---- +option(BUILD_EXAMPLES "Compile examples too" ON) option(ENABLE_TEST_COVERAGE "Compile with test-coverage flags" ON) if(ENABLE_TEST_COVERAGE AND CMAKE_BUILD_TYPE) message(WARNING "ENABLE_TEST_COVERAGE is set!") @@ -135,3 +136,7 @@ target_sources( add_executable(Base64-test tests/Base64-test.cpp) target_link_libraries(Base64-test PRIVATE Base64 base64c GTest::gtest_main) # FIXME: add_test(NAME Base64-test COMMAND Base64-test) + +if(APPLE AND BUILD_EXAMPLES) + add_subdirectory(examples) +endif() diff --git a/GNUmakefile b/GNUmakefile index d4fd9fd..aeaf0a1 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -62,7 +62,7 @@ test: all format: .clang-format git ls-files ::*.cpp ::*.hpp | xargs clang-format -i - gersemi -i CMakeLists.txt + git ls-files ::*CMakeLists.txt | xargs gersemi -i # These rules keep make from trying to use the match-anything rule below # to rebuild the makefiles--ouch! diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..2230fcf --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.28...4.0) + +project(Base64-examples VERSION 0.1.0 LANGUAGES CXX) + +find_package(Poco 1.14 COMPONENTS Foundation REQUIRED) + +add_executable(base64decode base64decode.cpp) +target_link_libraries(base64decode PUBLIC Poco::Foundation) + +add_executable(base64encode base64encode.cpp) +target_link_libraries(base64encode PUBLIC Poco::Foundation) + +if(DEFINED do_test) + do_test(base64decode --help stdin) + do_test(base64encode --help stdin) +endif() diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..205d64f --- /dev/null +++ b/examples/README.md @@ -0,0 +1,23 @@ +# Usage examples + +## Build with cmake + +cmake -S . -G build -G Ninja +cd build +ninja + +## Tests + + echo '!@#$%^&*()_~<>' > base64.dat + cat base64.dat | ./base64encode - > base64.txt + cat base64.dat | ./base64encode - | ./base64decode - | diff base64.dat - + +hexdump -C base64.dat + + 00000000 21 40 23 24 25 5e 26 2a 28 29 5f 7e 3c 3e 0a |!@#$%^&*()_~<>.| + 0000000f + +cat base64.txt + + IUAjJCVeJiooKV9+PD4K + diff --git a/examples/base64decode.cpp b/examples/base64decode.cpp new file mode 100644 index 0000000..b38309c --- /dev/null +++ b/examples/base64decode.cpp @@ -0,0 +1,63 @@ +// +// base64decode.cpp +// +// This sample demonstrates the Base64Decoder and StreamCopier classes. +// +// Copyright (c) 2004-2006, Applied Informatics Software Engineering GmbH. +// and Contributors. +// +// SPDX-License-Identifier: BSL-1.0 +// + +#include +#include + +#include "Poco/Base64Decoder.h" +#include "Poco/StreamCopier.h" + +using Poco::Base64Decoder; +using Poco::StreamCopier; + +int main(int argc, char** argv) +{ + if (argc < 2) + { + std::cout << "usage: " << argv[0] << ": " << std::endl + << " read base64-encoded , decode it and write the result to " << std::endl + << " read from stdin if is '-', decode it and write the result to stdout" << std::endl; + return 1; + } + + if (argv[1] == std::string("-")) + { + Base64Decoder decoder(std::cin); + StreamCopier::copyStream(decoder, std::cout); + } + else + { + std::ifstream istr(argv[1]); + if (!istr) + { + std::cerr << "cannot open input file: " << argv[1] << std::endl; + return 2; + } + + std::ofstream ostr(argv[2], std::ios::binary); + if (!ostr) + { + std::cerr << "cannot open output file: " << argv[2] << std::endl; + return 3; + } + + Base64Decoder decoder(istr); + StreamCopier::copyStream(decoder, ostr); + + if (!ostr) + { + std::cerr << "error writing output file: " << argv[2] << std::endl; + return 4; + } + } + + return 0; +} diff --git a/examples/base64encode.cpp b/examples/base64encode.cpp new file mode 100644 index 0000000..698195b --- /dev/null +++ b/examples/base64encode.cpp @@ -0,0 +1,63 @@ +// +// base64encode.cpp +// +// This sample demonstrates the Base64Encoder and StreamCopier classes. +// +// Copyright (c) 2004-2006, Applied Informatics Software Engineering GmbH. +// and Contributors. +// +// SPDX-License-Identifier: BSL-1.0 +// + +#include +#include + +#include "Poco/Base64Encoder.h" +#include "Poco/StreamCopier.h" + +using Poco::Base64Encoder; +using Poco::StreamCopier; + +int main(int argc, char** argv) +{ + if (argc < 2) + { + std::cout << "usage: " << argv[0] << ": " << std::endl + << " read , base64-encode it and write the result to " << std::endl + << " read from stdin if is '-', decode it and write the result to stdout" << std::endl; + return 1; + } + + if (argv[1] == std::string("-")) + { + Base64Encoder encoder(std::cout); + StreamCopier::copyStream(std::cin, encoder); + } + else + { + std::ifstream istr(argv[1], std::ios::binary); + if (!istr) + { + std::cerr << "cannot open input file: " << argv[1] << std::endl; + return 2; + } + + std::ofstream ostr(argv[2]); + if (!ostr) + { + std::cerr << "cannot open output file: " << argv[2] << std::endl; + return 3; + } + + Base64Encoder encoder(ostr); + StreamCopier::copyStream(istr, encoder); + + if (!ostr) + { + std::cerr << "error writing output file: " << argv[2] << std::endl; + return 4; + } + } + + return 0; +} diff --git a/tests/Base64-test.cpp b/tests/Base64-test.cpp index b48943b..904024c 100644 --- a/tests/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -29,7 +29,9 @@ extern "C" using namespace std::string_literals; -// TODO: using RRCP::Common::Base64; +// TODO(CK): using RRCP::Common::Base64; + +// FIXME: aktivate and fix! CK #undef TEST_RANDOM_VALUES namespace @@ -48,8 +50,13 @@ struct testpattern_t {"foob", "Zm9vYg=="}, // {"fooba", "Zm9vYmE="}, // {"foobar", "Zm9vYmFy"}, // - // FIXME: {" ", "IA=="}, {" ", "ICA="}, {" ", "ICAg"}, {" ", "ICAgIA=="}, {" ", "ICAgICA="}, {" ", - // "ICAgICAg"}, {" ", "ICAgICAgIA=="}, // + {" ", "IA=="}, // 1 space + {" ", "ICA="}, // 2 spaces + {" ", "ICAg"}, // 3 spaces + {" ", "ICAgIA=="}, // 4 spaces + {" ", "ICAgICA="}, // 5 spaces + {" ", "ICAgICAg"}, // 6 spaces + {" ", "ICAgICAgIA=="}, // 7 spaces {"U", "VQ=="}, // {"UU", "VVU="}, // {"UUU", "VVVV"}, // @@ -223,25 +230,27 @@ TEST(Base64Test, TestEncoder) { std::string original("\00\01\02\03\04\05", 6); auto encoded = base64.encode(original); - EXPECT_TRUE(encoded == "AAECAwQF"); + EXPECT_EQ(encoded, "AAECAwQF"); EXPECT_EQ(original, base64.decode(encoded)); } { std::string original("\00\01\02\03", 4); auto encoded = base64.encode(original); - EXPECT_TRUE(encoded == "AAECAw=="); + EXPECT_EQ(encoded, "AAECAw=="); EXPECT_EQ(original, base64.decode(encoded)); } { std::string original("ABCDEF"); auto encoded = base64.encode(original); - EXPECT_TRUE(encoded == "QUJDREVG"); + EXPECT_EQ(encoded, "QUJDREVG"); EXPECT_EQ(original, base64.decode(encoded)); } { std::string original("!@#$%^&*()_~<>"); + std::string expected{"IUAjJCVeJiooKV9+PD4K"}; auto encoded = base64.encode(original); - EXPECT_TRUE(encoded == "IUAjJCVeJiooKV9+PD4="); + // FIXME: EXPECT_EQ(encoded, "IUAjJCVeJiooKV9+PD4="); + EXPECT_EQ(encoded, expected); EXPECT_EQ(original, base64.decode(encoded)); } } @@ -251,7 +260,7 @@ TEST(Base64Test, TestDecoder) { const std::string istr("QUJ\r\nDRE\r\nVG"); const std::string decoded = base64.decode(istr); - EXPECT_TRUE(decoded == "ABCDEF"); + EXPECT_EQ(decoded, "ABCDEF"); } { const std::string istr("QUJD#REVG"); From d67e4adb58ae848b6cfb30157029194047ae8f2b Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 20 Mar 2025 13:30:52 +0100 Subject: [PATCH 043/120] Fix format in Readme Fix typos too --- examples/README.md | 8 ++++---- tests/Base64-test.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/README.md b/examples/README.md index 205d64f..6e8bbb7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,10 +1,10 @@ # Usage examples -## Build with cmake +## Build with CMake -cmake -S . -G build -G Ninja -cd build -ninja + cmake -S . -G build -G Ninja + cd build + ninja ## Tests diff --git a/tests/Base64-test.cpp b/tests/Base64-test.cpp index 904024c..247ba73 100644 --- a/tests/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -31,7 +31,7 @@ using namespace std::string_literals; // TODO(CK): using RRCP::Common::Base64; -// FIXME: aktivate and fix! CK +// FIXME: activate and fix! CK #undef TEST_RANDOM_VALUES namespace From 798db5b8b9f30970a8fe9e37a33ab3ce4506ce39 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 20 Mar 2025 13:44:54 +0100 Subject: [PATCH 044/120] Move base64 reference docu into tests --- Base64.rfc => tests/base64.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) rename Base64.rfc => tests/base64.md (93%) diff --git a/Base64.rfc b/tests/base64.md similarity index 93% rename from Base64.rfc rename to tests/base64.md index 6964437..a9f0cf5 100644 --- a/Base64.rfc +++ b/tests/base64.md @@ -1,6 +1,6 @@ -RFC 2045 Internet Message Bodies November 1996 +## Parts of RFC 2045 Internet Message Bodies, November 1996 - Table 1: The Base64 Alphabet +### Table 1: The Base64 Alphabet Value Encoding Value Encoding Value Encoding Value Encoding 0 A 17 R 34 i 51 z @@ -68,6 +68,12 @@ important thing to note is that this may be done directly by the encoder rather than in a prior canonicalization step in some implementations. -NOTE: There is no need to worry about quoting potential boundary +#### NOTE: + +There is no need to worry about quoting potential boundary delimiters within base64-encoded bodies within multipart entities because no hyphen characters are used in the base64 encoding. + +## see too rfc4648 + +https://datatracker.ietf.org/doc/html/rfc4648#section-4 From 4bd1aa21526a6b59711b659e9c378b0b0adcc007 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 20 Mar 2025 19:44:18 +0100 Subject: [PATCH 045/120] Changes after review --- CMakeLists.txt | 30 +++++++++++++++--------------- async_tcp_client.cpp | 13 ++++--------- async_tcp_client_v20.cpp | 21 ++++++++------------- examples/CMakeLists.txt | 8 +++++--- examples/base64decode.cpp | 2 +- examples/base64encode.cpp | 2 +- rrcp_client.cpp | 2 +- 7 files changed, 35 insertions(+), 43 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f66151..899cfd6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(RRCP-client VERSION 0.1.0 LANGUAGES CXX C) # ---- add dependencies ---- -find_package(Boost 1.87 COMPONENTS asio REQUIRED HINTS $ENV{HOME}/.local) +find_package(Boost 1.87 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) # ---- default settings ---- @@ -53,7 +53,7 @@ function(do_test target arg result) endfunction() add_executable(async_tcp_echo_server async_tcp_echo_server.cpp) -target_link_libraries(async_tcp_echo_server PUBLIC Boost::asio) +target_link_libraries(async_tcp_echo_server PUBLIC Boost::headers) do_test(async_tcp_echo_server "" port) add_library(rrcp_helper STATIC rrcp_helper.cpp rrcp_helper.hpp) @@ -62,7 +62,7 @@ add_executable(async_tcp_echo_client async_tcp_echo_client.cpp) target_link_libraries( async_tcp_echo_client PRIVATE rrcp_helper - PUBLIC Boost::asio fmt::fmt-header-only + PUBLIC Boost::headers fmt::fmt-header-only ) do_test(async_tcp_echo_client --help Usage) @@ -70,33 +70,33 @@ add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) target_link_libraries( blocking_tcp_echo_client PRIVATE rrcp_helper - PUBLIC Boost::asio fmt::fmt-header-only + PUBLIC Boost::headers fmt::fmt-header-only ) do_test(blocking_tcp_echo_client --help Usage) -# add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) -# target_link_libraries(async_tcp_client_v20 PUBLIC Boost::asio) -# do_test(async_tcp_client_v20 --help Usage) +add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) +target_link_libraries(async_tcp_client_v20 PUBLIC Boost::headers) +do_test(async_tcp_client_v20 --help Usage) -# add_executable(async_tcp_client async_tcp_client.cpp) -# target_link_libraries(async_tcp_client PUBLIC Boost::asio) -# do_test(async_tcp_client --help Usage) +add_executable(async_tcp_client async_tcp_client.cpp) +target_link_libraries(async_tcp_client PUBLIC Boost::headers) +do_test(async_tcp_client --help Usage) # add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) -# target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::asio) +# target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::headers) #XXX do_test(blocking_tcp_echo_server port Usage) add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) -target_link_libraries(rrcp_client PRIVATE rrcp_helper PUBLIC Boost::asio) +target_link_libraries(rrcp_client PRIVATE rrcp_helper PUBLIC Boost::headers) do_test(rrcp_client --help Usage) if(APPLE) add_executable(timer timer.cpp) - target_link_libraries(timer PUBLIC Boost::asio) + target_link_libraries(timer PUBLIC Boost::headers) add_test(NAME timer COMMAND timer) add_executable(async_client async_client.cpp) - target_link_libraries(async_client PUBLIC Boost::asio) + target_link_libraries(async_client PUBLIC Boost::headers) add_test(NAME async_client COMMAND async_client) endif() @@ -104,7 +104,7 @@ add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) target_link_libraries( rrcp_async_tcp_client PRIVATE rrcp_helper - PUBLIC Boost::asio fmt::fmt-header-only + PUBLIC Boost::headers fmt::fmt-header-only ) do_test(rrcp_async_tcp_client --help Usage) diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 58be689..359d2b6 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -198,11 +198,10 @@ class client : public std::enable_shared_from_this< client > // Set a deadline for the read operation. deadline_.expires_after(13s); - auto self(shared_from_this()); - // Start an asynchronous operation to read a newline-delimited message. boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), '\n', - [this, self](const boost::system::error_code& error, std::size_t n) { handle_read(error, n); }); + [self = shared_from_this()](const boost::system::error_code& error, std::size_t n) + { self->handle_read(error, n); }); } void handle_read(const boost::system::error_code& error, std::size_t n) @@ -244,11 +243,9 @@ class client : public std::enable_shared_from_this< client > std::string message{'\n'}; std::print(stderr, "Sending: {}\n", "hartbeat"); - auto self(shared_from_this()); - // Start an asynchronous operation to send a heartbeat message. boost::asio::async_write(socket_, boost::asio::buffer(message), - [this, self](const boost::system::error_code& error, std::size_t) { handle_write(error); }); + [self = shared_from_this()](const boost::system::error_code& error, std::size_t) { self->handle_write(error); }); } void handle_write(const boost::system::error_code& error) @@ -294,10 +291,8 @@ class client : public std::enable_shared_from_this< client > deadline_.expires_at(steady_timer::time_point::max()); } - auto self(shared_from_this()); - // Put the actor back to sleep. - deadline_.async_wait([this, self](const boost::system::error_code& /*e*/) { check_deadline(); }); + deadline_.async_wait([self = shared_from_this()](const boost::system::error_code& /*e*/) { self->check_deadline(); }); } bool stopped_{false}; diff --git a/async_tcp_client_v20.cpp b/async_tcp_client_v20.cpp index fadb22a..db3050c 100644 --- a/async_tcp_client_v20.cpp +++ b/async_tcp_client_v20.cpp @@ -74,7 +74,7 @@ class client : public std::enable_shared_from_this< client > // Start an asynchronous operation to send the message. boost::asio::async_write(socket_, boost::asio::buffer(message), - [this, self](const boost::system::error_code& error, std::size_t) { handle_write(error); }); + [self](const boost::system::error_code& error, std::size_t) { self->handle_write(error); }); } } @@ -98,7 +98,7 @@ class client : public std::enable_shared_from_this< client > std::print("Trying {}:{}...\n", endpoint_iter->endpoint().address().to_string(), endpoint_iter->endpoint().port()); // Set a deadline for the connect operation. - deadline_.expires_after(6s); + deadline_.expires_after(3s); // Start the asynchronous connect operation. socket_.async_connect(endpoint_iter->endpoint(), @@ -164,13 +164,12 @@ class client : public std::enable_shared_from_this< client > } // Set a deadline for the read operation. - deadline_.expires_after(30s); - - auto self(shared_from_this()); + deadline_.expires_after(13s); // Start an asynchronous operation to read a newline-delimited message. boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), '\n', - [this, self](const boost::system::error_code& error, std::size_t n) { handle_read(error, n); }); + [self = shared_from_this()](const boost::system::error_code& error, std::size_t n) + { self->handle_read(error, n); }); } void handle_read(const boost::system::error_code& error, std::size_t n) @@ -183,7 +182,7 @@ class client : public std::enable_shared_from_this< client > if (!error) { // Extract the newline-delimited message from the buffer. - std::string line = input_buffer_.substr(0, n - 1); // NOTE: w/o '\n' + std::string const line = input_buffer_.substr(0, n - 1); // NOTE: w/o '\n' input_buffer_.erase(0, n); // Empty messages are heartbeats and so ignored. @@ -216,11 +215,9 @@ class client : public std::enable_shared_from_this< client > std::string message{'\n'}; std::print(stderr, "Sending: {}\n", "hartbeat"); - auto self(shared_from_this()); - // Start an asynchronous operation to send a heartbeat message. boost::asio::async_write(socket_, boost::asio::buffer(message), - [this, self](const boost::system::error_code& error, std::size_t) { handle_write(error); }); + [self = shared_from_this()](const boost::system::error_code& error, std::size_t) { self->handle_write(error); }); } void handle_write(const boost::system::error_code& error) @@ -272,10 +269,8 @@ class client : public std::enable_shared_from_this< client > deadline_.expires_at(steady_timer::time_point::max()); } - auto self(shared_from_this()); - // Put the actor back to sleep. - deadline_.async_wait([this, self](const boost::system::error_code& /*e*/) { check_deadline(); }); + deadline_.async_wait([self = shared_from_this()](const boost::system::error_code& /*e*/) { self->check_deadline(); }); } bool stopped_{false}; diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2230fcf..78eee4c 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -10,7 +10,9 @@ target_link_libraries(base64decode PUBLIC Poco::Foundation) add_executable(base64encode base64encode.cpp) target_link_libraries(base64encode PUBLIC Poco::Foundation) -if(DEFINED do_test) - do_test(base64decode --help stdin) - do_test(base64encode --help stdin) +if(PROJECT_IS_TOP_LEVEL) + return() endif() + +do_test(base64decode --help input) +do_test(base64encode --help input) diff --git a/examples/base64decode.cpp b/examples/base64decode.cpp index b38309c..37c8a90 100644 --- a/examples/base64decode.cpp +++ b/examples/base64decode.cpp @@ -22,7 +22,7 @@ int main(int argc, char** argv) { if (argc < 2) { - std::cout << "usage: " << argv[0] << ": " << std::endl + std::cerr << "usage: " << argv[0] << ": " << std::endl << " read base64-encoded , decode it and write the result to " << std::endl << " read from stdin if is '-', decode it and write the result to stdout" << std::endl; return 1; diff --git a/examples/base64encode.cpp b/examples/base64encode.cpp index 698195b..2e761ab 100644 --- a/examples/base64encode.cpp +++ b/examples/base64encode.cpp @@ -22,7 +22,7 @@ int main(int argc, char** argv) { if (argc < 2) { - std::cout << "usage: " << argv[0] << ": " << std::endl + std::cerr << "usage: " << argv[0] << ": " << std::endl << " read , base64-encode it and write the result to " << std::endl << " read from stdin if is '-', decode it and write the result to stdout" << std::endl; return 1; diff --git a/rrcp_client.cpp b/rrcp_client.cpp index fa51491..cd9acd0 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -161,7 +161,7 @@ auto main(int argc, char* argv[]) -> int std::thread t([&io_context]() { io_context.run(); }); //================================================================ - std::string binary = "\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"s; + std::string binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"s}; std::cerr << binary.length() << ' ' << std::quoted(binary) << '\n'; auto quoted = char2esc(binary); std::cerr << quoted.length() << ' ' << std::quoted(quoted) << '\n'; From 0359352c7ff74159b8ca6cbfcfcffec36abb3d80 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 21 Mar 2025 12:58:17 +0100 Subject: [PATCH 046/120] Use Boost::beast::base64 --- Base64.cpp | 66 ++++++++++++++++++++++++++++++++++++++++++- CMakeLists.txt | 19 +++++++++---- tests/Base64-test.cpp | 6 ++-- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/Base64.cpp b/Base64.cpp index f5f3bb9..11f461b 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -1,9 +1,56 @@ #include "Base64.hpp" +#define USE_BOOST_BEAST +#ifdef USE_BOOST_BEAST +// with homebrew /usr/local/include/boost/beast/core/detail/base64.hpp +// /Users/clausklein/.local/include/boost/beast/core/detail/base64.hpp +#include +#endif + // XXX #include #include #include +#ifdef USE_BOOST_BEAST + +namespace +{ + +using boost::beast::detail::base64::decode; +using boost::beast::detail::base64::decoded_size; +using boost::beast::detail::base64::encode; +using boost::beast::detail::base64::encoded_size; + +class base64 +{ + public: + static auto base64_encode(std::uint8_t const* data, std::size_t len) -> std::string + { + std::string dest; + dest.resize(encoded_size(len)); + dest.resize(encode(&dest[0], data, len)); + return dest; + } + + static auto base64_encode(std::string_view s) -> std::string + { + return base64_encode(reinterpret_cast< std::uint8_t const* >(s.data()), s.size()); + } + + static auto base64_decode(std::string_view data) -> std::string + { + std::string dest; + dest.resize(decoded_size(data.size())); + auto const result = decode(&dest[0], data.data(), data.size()); + dest.resize(result.first); + return dest; + } +}; + +} // namespace + +#endif + namespace RRCP::Common { @@ -11,10 +58,15 @@ auto Base64::encode(std::string_view data) const -> std::string { if (data.empty()) { - // NO! throw std::invalid_argument("Invalid input data"); return {}; } +#ifdef USE_BOOST_BEAST + + return base64::base64_encode(data); + +#else + std::string encoded; size_t linelen = 0; @@ -70,10 +122,18 @@ auto Base64::encode(std::string_view data) const -> std::string } return encoded; + +#endif } auto Base64::decode(std::string_view in) -> std::string { +#ifdef USE_BOOST_BEAST + + return base64::base64_decode(in); + +#else + std::string decoded; std::string fourChars; @@ -119,8 +179,11 @@ auto Base64::decode(std::string_view in) -> std::string } return decoded; + +#endif } +#ifndef USE_BOOST_BEAST auto Base64::isBase64Char(char c) const -> bool { return ( @@ -151,5 +214,6 @@ auto Base64::base64CharValue(char c) const -> std::uint8_t } throw std::invalid_argument("Invalid Base64 character"); } +#endif } // namespace RRCP::Common diff --git a/CMakeLists.txt b/CMakeLists.txt index 899cfd6..77d9330 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,16 @@ cmake_minimum_required(VERSION 3.28...4.0) -project(RRCP-client VERSION 0.1.0 LANGUAGES CXX C) +project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) # ---- add dependencies ---- -find_package(Boost 1.87 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) +find_package( + Boost + 1.87 + COMPONENTS headers beast + REQUIRED + HINTS $ENV{HOME}/.local +) find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) # ---- default settings ---- @@ -123,8 +129,8 @@ FetchContent_Declare( set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) -add_library(base64c STATIC) -target_sources(base64c PRIVATE base64.c PUBLIC FILE_SET HEADERS FILES base64.h) +# add_library(base64c STATIC) +# target_sources(base64c PRIVATE base64.c PUBLIC FILE_SET HEADERS FILES base64.h) add_library(Base64 STATIC) target_sources( @@ -132,10 +138,11 @@ target_sources( PRIVATE Base64.cpp PUBLIC FILE_SET HEADERS FILES Base64.hpp ) +target_link_libraries(Base64 PUBLIC Boost::beast) add_executable(Base64-test tests/Base64-test.cpp) -target_link_libraries(Base64-test PRIVATE Base64 base64c GTest::gtest_main) -# FIXME: add_test(NAME Base64-test COMMAND Base64-test) +target_link_libraries(Base64-test PRIVATE Base64 GTest::gtest_main) +add_test(NAME Base64-test COMMAND Base64-test) if(APPLE AND BUILD_EXAMPLES) add_subdirectory(examples) diff --git a/tests/Base64-test.cpp b/tests/Base64-test.cpp index 247ba73..b6dcd84 100644 --- a/tests/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -16,8 +16,6 @@ By covering these edge cases, you can ensure that your Base64 class is robust an extern "C" { // #include "base64.h" - extern void base64_encode(const char *in, size_t inlen, char *out, size_t outlen); - extern bool base64_decode(const char *in, size_t inlen, char *out, size_t *outlen); } #include @@ -83,8 +81,8 @@ TEST(Base64Test, encoding) const std::string base64_encoded = base64.encode(binary); EXPECT_EQ(encoded, base64_encoded); - base64_encode(binary.c_str(), binary.size(), text.data(), text.size()); - EXPECT_EQ(encoded, text.data()); + // base64_encode(binary.c_str(), binary.size(), text.data(), text.size()); + // EXPECT_EQ(encoded, text.data()); ++i; } From 2f477a03c20d750c77576b325127eed5478dacc0 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 21 Mar 2025 17:56:54 +0100 Subject: [PATCH 047/120] Remove WS from base64 strings before decode --- Base64.cpp | 23 +++++++++++++-- GNUmakefile | 6 +++- tests/Base64-test.cpp | 69 +++++++++++++++++++------------------------ 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/Base64.cpp b/Base64.cpp index 11f461b..fea0d0b 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -3,11 +3,12 @@ #define USE_BOOST_BEAST #ifdef USE_BOOST_BEAST // with homebrew /usr/local/include/boost/beast/core/detail/base64.hpp +// and its impl. /usr/local/include/boost/beast/core/detail/base64.ipp // /Users/clausklein/.local/include/boost/beast/core/detail/base64.hpp #include #endif -// XXX #include +#include #include #include @@ -23,7 +24,15 @@ using boost::beast::detail::base64::encoded_size; class base64 { - public: + // Function to remove all whitespace characters from a std::string + static auto remove_whitespace(std::string_view input) -> std::string + { + std::string result{input.data(), input.length()}; + result.erase( + std::remove_if(result.begin(), result.end(), [](unsigned char c) { return std::isspace(c); }), result.end()); + return result; + } + static auto base64_encode(std::uint8_t const* data, std::size_t len) -> std::string { std::string dest; @@ -32,6 +41,7 @@ class base64 return dest; } + public: static auto base64_encode(std::string_view s) -> std::string { return base64_encode(reinterpret_cast< std::uint8_t const* >(s.data()), s.size()); @@ -41,7 +51,10 @@ class base64 { std::string dest; dest.resize(decoded_size(data.size())); - auto const result = decode(&dest[0], data.data(), data.size()); + + // TODO(CK): remove first at least all "\n\r" or better all non printable chars! + std::string striped = remove_whitespace(data); + auto const result = decode(&dest[0], striped.data(), striped.size()); dest.resize(result.first); return dest; } @@ -49,6 +62,10 @@ class base64 } // namespace +#else + +// XXX #include + #endif namespace RRCP::Common diff --git a/GNUmakefile b/GNUmakefile index aeaf0a1..2d5525f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -10,7 +10,11 @@ MAKEFLAGS+= --warn-undefined-variables all: build ninja -C build -distclean: +clean: build + - ninja -C $< $@ + - find $< -name '*.gcda' -delete + +distclean: clean rm -rf build coverage/* *~ ctags build: CMakeLists.txt diff --git a/tests/Base64-test.cpp b/tests/Base64-test.cpp index b6dcd84..a2cbd45 100644 --- a/tests/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -11,8 +11,6 @@ By covering these edge cases, you can ensure that your Base64 class is robust an #include "Base64.hpp" -#include - extern "C" { // #include "base64.h" @@ -27,13 +25,13 @@ extern "C" using namespace std::string_literals; -// TODO(CK): using RRCP::Common::Base64; +using RRCP::Common::Base64; -// FIXME: activate and fix! CK -#undef TEST_RANDOM_VALUES +#define TEST_RANDOM_VALUES namespace { + // Test Vectors from rfc4648 // see https://datatracker.ietf.org/doc/html/rfc4648#section-10 struct testpattern_t @@ -63,11 +61,12 @@ struct testpattern_t {"UUUUUU", "VVVVVVVV"}, // {"UUUUUUU", "VVVVVVVVVQ=="}, // {nullptr, nullptr}}; + } // namespace TEST(Base64Test, encoding) { - RRCP::Common::Base64 base64; + Base64 base64; base64.setLineBreak(false); std::array< char, 54 > text{}; @@ -81,16 +80,13 @@ TEST(Base64Test, encoding) const std::string base64_encoded = base64.encode(binary); EXPECT_EQ(encoded, base64_encoded); - // base64_encode(binary.c_str(), binary.size(), text.data(), text.size()); - // EXPECT_EQ(encoded, text.data()); - ++i; } } TEST(Base64Test, decoding) { - RRCP::Common::Base64 base64; + Base64 base64; std::array< char, 54 > data{}; size_t i = 0; @@ -103,24 +99,13 @@ TEST(Base64Test, decoding) const std::string decoded = base64.decode(encoded); EXPECT_EQ(binary, decoded); -#if 0 - size_t length{}; - const bool ok = base64_decode(encoded.c_str(), encoded.size(), data.data(), &length); - EXPECT_TRUE(ok); - if (ok) - { - EXPECT_EQ(binary.length(), length); - EXPECT_EQ(binary, data.data()); - } -#endif - ++i; } } TEST(Base64Test, ShortString1) { - RRCP::Common::Base64 base64; + Base64 base64; std::string const original = "A"; std::string const encoded = base64.encode(original); // std::println("{}:\t{}", original, encoded); @@ -132,7 +117,7 @@ TEST(Base64Test, ShortString1) TEST(Base64Test, ShortString2) { - RRCP::Common::Base64 base64; + Base64 base64; std::string const original = "AA"; std::string const encoded = base64.encode(original); // std::println("{}:\t{}", original, encoded); @@ -144,7 +129,7 @@ TEST(Base64Test, ShortString2) TEST(Base64Test, ShortString3) { - RRCP::Common::Base64 base64; + Base64 base64; std::string const original = "AAA"; std::string const encoded = base64.encode(original); // std::println("{}:\t{}", original, encoded); @@ -154,9 +139,10 @@ TEST(Base64Test, ShortString3) EXPECT_EQ(original, decoded); } +#ifdef TEST_INVALID_VALUES TEST(Base64Test, DecodeMarker) { - RRCP::Common::Base64 base64; + Base64 base64; EXPECT_ANY_THROW({ (void)base64.decode("====").empty(); }); EXPECT_ANY_THROW({ (void)base64.decode("===").empty(); }); EXPECT_ANY_THROW({ (void)base64.decode("==").empty(); }); @@ -165,10 +151,11 @@ TEST(Base64Test, DecodeMarker) EXPECT_NO_THROW({ (void)base64.decode("").empty(); }); } +#endif TEST(Base64Test, MediumString) { - RRCP::Common::Base64 base64; + Base64 base64; std::string const original = "This is a medium length string."; std::string const encoded = base64.encode(original); std::string const decoded = base64.decode(encoded); @@ -177,13 +164,13 @@ TEST(Base64Test, MediumString) TEST(Base64Test, LongString) { - RRCP::Common::Base64 base64; - base64.setLineBreak(true); + Base64 base64; + // XXX base64.setLineBreak(true); std::string const original = "This is not a really long string, but also that should be encoded and decoded correctly."; std::string const encoded = base64.encode(original); std::string expected{ - "VGhpcyBpcyBub3QgYSByZWFsbHkgbG9uZyBzdHJpbmcsIGJ1dCBhbHNvIHRoYXQgc2hvdWxkIGJl\n" + "VGhpcyBpcyBub3QgYSByZWFsbHkgbG9uZyBzdHJpbmcsIGJ1dCBhbHNvIHRoYXQgc2hvdWxkIGJl" "IGVuY29kZWQgYW5kIGRlY29kZWQgY29ycmVjdGx5Lg=="}; EXPECT_EQ(expected, encoded); // std::println("{}:\n{}", original, encoded); @@ -194,7 +181,7 @@ TEST(Base64Test, LongString) TEST(Base64Test, FoxString) { - RRCP::Common::Base64 base64; + Base64 base64; std::string const original = "The quick brown fox jumped over the lazy dogs."; std::string const encoded = base64.encode(original); EXPECT_EQ(encoded, "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wZWQgb3ZlciB0aGUgbGF6eSBkb2dzLg=="); @@ -206,7 +193,7 @@ TEST(Base64Test, FoxString) TEST(Base64Test, BinaryData) { - RRCP::Common::Base64 base64; + Base64 base64; std::string const original = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"s; std::string const encoded = base64.encode(original); std::string const decoded = base64.decode(encoded); @@ -215,16 +202,18 @@ TEST(Base64Test, BinaryData) TEST(Base64Test, NonAsciiString) { - RRCP::Common::Base64 base64; + Base64 base64; std::string const original = "\xFC@NOs[\xFEVJ\t@\x80\v\xD0\xAA\xF5"; std::string const encoded = base64.encode(original); + // std::println("'{}':\t{}", original, encoded); + std::string const decoded = base64.decode(encoded); EXPECT_EQ(original, decoded); } TEST(Base64Test, TestEncoder) { - RRCP::Common::Base64 base64; + Base64 base64; { std::string original("\00\01\02\03\04\05", 6); auto encoded = base64.encode(original); @@ -245,16 +234,19 @@ TEST(Base64Test, TestEncoder) } { std::string original("!@#$%^&*()_~<>"); - std::string expected{"IUAjJCVeJiooKV9+PD4K"}; + std::string expected{"IUAjJCVeJiooKV9+PD4="}; auto encoded = base64.encode(original); - // FIXME: EXPECT_EQ(encoded, "IUAjJCVeJiooKV9+PD4="); + std::println("'{}':\t{}", original, encoded); + EXPECT_EQ(encoded, expected); EXPECT_EQ(original, base64.decode(encoded)); } } + +#ifdef TEST_INVALID_VALUES TEST(Base64Test, TestDecoder) { - RRCP::Common::Base64 base64; + Base64 base64; { const std::string istr("QUJ\r\nDRE\r\nVG"); const std::string decoded = base64.decode(istr); @@ -265,6 +257,7 @@ TEST(Base64Test, TestDecoder) EXPECT_ANY_THROW({ (void)base64.decode(istr).empty(); }); } } +#endif #ifdef TEST_RANDOM_VALUES TEST(Base64Test, RandomBinaryData) @@ -273,8 +266,8 @@ TEST(Base64Test, RandomBinaryData) std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd() std::uniform_int_distribution<> distrib(0, 255); - RRCP::Common::Base64 base64; - base64.setLineBreak(true); + Base64 base64; + // XXX base64.setLineBreak(true); for (size_t i = 0; i < 5; ++i) { From 52309a9d3ddbed0c11bb3e728180104a6f1d851d Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 21 Mar 2025 18:29:20 +0100 Subject: [PATCH 048/120] Use std::ranges if available --- Base64.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Base64.cpp b/Base64.cpp index fea0d0b..7363147 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -8,11 +8,14 @@ #include #endif +#ifdef USE_BOOST_BEAST + #include -#include -#include +#include -#ifdef USE_BOOST_BEAST +#ifdef __cpp_lib_ranges +#include +#endif namespace { @@ -24,7 +27,8 @@ using boost::beast::detail::base64::encoded_size; class base64 { - // Function to remove all whitespace characters from a std::string +#ifndef __cpp_lib_ranges + // Function to remove all whitespace characters from a std::string (C++17) static auto remove_whitespace(std::string_view input) -> std::string { std::string result{input.data(), input.length()}; @@ -32,6 +36,14 @@ class base64 std::remove_if(result.begin(), result.end(), [](unsigned char c) { return std::isspace(c); }), result.end()); return result; } +#else + // Function to remove all whitespace characters from a std::string_view (C++20) + static auto remove_whitespace(std::string_view input) -> std::string + { + auto filtered = input | std::views::filter([](unsigned char c) { return !std::isspace(c); }); + return std::string(filtered.begin(), filtered.end()); + } +#endif static auto base64_encode(std::uint8_t const* data, std::size_t len) -> std::string { @@ -65,6 +77,7 @@ class base64 #else // XXX #include +#include #endif From 09ed3e054202bde159579f5918250f35d14216c2 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Tue, 25 Mar 2025 12:38:58 +0100 Subject: [PATCH 049/120] Prevent build errors on debian --- CMakeLists.txt | 54 ++++++++++++++++++++++++------------------- async_tcp_client.cpp | 2 +- gcovr.cfg | 5 ++-- tests/Base64-test.cpp | 21 +++++++++-------- 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 77d9330..6f35b94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,13 +4,7 @@ project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) # ---- add dependencies ---- -find_package( - Boost - 1.87 - COMPONENTS headers beast - REQUIRED - HINTS $ENV{HOME}/.local -) +find_package(Boost 1.87 COMPONENTS asio beast REQUIRED HINTS $ENV{HOME}/.local) find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) # ---- default settings ---- @@ -59,7 +53,7 @@ function(do_test target arg result) endfunction() add_executable(async_tcp_echo_server async_tcp_echo_server.cpp) -target_link_libraries(async_tcp_echo_server PUBLIC Boost::headers) +target_link_libraries(async_tcp_echo_server PUBLIC Boost::asio) do_test(async_tcp_echo_server "" port) add_library(rrcp_helper STATIC rrcp_helper.cpp rrcp_helper.hpp) @@ -68,7 +62,7 @@ add_executable(async_tcp_echo_client async_tcp_echo_client.cpp) target_link_libraries( async_tcp_echo_client PRIVATE rrcp_helper - PUBLIC Boost::headers fmt::fmt-header-only + PUBLIC Boost::asio fmt::fmt-header-only ) do_test(async_tcp_echo_client --help Usage) @@ -76,33 +70,42 @@ add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) target_link_libraries( blocking_tcp_echo_client PRIVATE rrcp_helper - PUBLIC Boost::headers fmt::fmt-header-only + PUBLIC Boost::asio fmt::fmt-header-only ) do_test(blocking_tcp_echo_client --help Usage) -add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) -target_link_libraries(async_tcp_client_v20 PUBLIC Boost::headers) -do_test(async_tcp_client_v20 --help Usage) +if(APPLE) + add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) + target_link_libraries( + async_tcp_client_v20 + PUBLIC Boost::asio fmt::fmt-header-only + ) + do_test(async_tcp_client_v20 --help Usage) -add_executable(async_tcp_client async_tcp_client.cpp) -target_link_libraries(async_tcp_client PUBLIC Boost::headers) -do_test(async_tcp_client --help Usage) + add_executable(async_tcp_client async_tcp_client.cpp) + target_link_libraries(async_tcp_client PUBLIC Boost::asio fmt::fmt-header-only) + do_test(async_tcp_client --help Usage) -# add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) -# target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::headers) -#XXX do_test(blocking_tcp_echo_server port Usage) + # add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) + # target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::asio) + #XXX do_test(blocking_tcp_echo_server port Usage) +endif() add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) -target_link_libraries(rrcp_client PRIVATE rrcp_helper PUBLIC Boost::headers) +target_link_libraries( + rrcp_client + PRIVATE rrcp_helper + PUBLIC Boost::asio fmt::fmt-header-only +) do_test(rrcp_client --help Usage) if(APPLE) add_executable(timer timer.cpp) - target_link_libraries(timer PUBLIC Boost::headers) + target_link_libraries(timer PUBLIC Boost::asio fmt::fmt-header-only) add_test(NAME timer COMMAND timer) add_executable(async_client async_client.cpp) - target_link_libraries(async_client PUBLIC Boost::headers) + target_link_libraries(async_client PUBLIC Boost::asio fmt::fmt-header-only) add_test(NAME async_client COMMAND async_client) endif() @@ -110,7 +113,7 @@ add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) target_link_libraries( rrcp_async_tcp_client PRIVATE rrcp_helper - PUBLIC Boost::headers fmt::fmt-header-only + PUBLIC Boost::asio fmt::fmt-header-only ) do_test(rrcp_async_tcp_client --help Usage) @@ -141,7 +144,10 @@ target_sources( target_link_libraries(Base64 PUBLIC Boost::beast) add_executable(Base64-test tests/Base64-test.cpp) -target_link_libraries(Base64-test PRIVATE Base64 GTest::gtest_main) +target_link_libraries( + Base64-test + PRIVATE Base64 GTest::gtest_main fmt::fmt-header-only +) add_test(NAME Base64-test COMMAND Base64-test) if(APPLE AND BUILD_EXAMPLES) diff --git a/async_tcp_client.cpp b/async_tcp_client.cpp index 359d2b6..ad35df6 100644 --- a/async_tcp_client.cpp +++ b/async_tcp_client.cpp @@ -241,7 +241,7 @@ class client : public std::enable_shared_from_this< client > } std::string message{'\n'}; - std::print(stderr, "Sending: {}\n", "hartbeat"); + std::cerr << "Sending: heartbeat\n"; // Start an asynchronous operation to send a heartbeat message. boost::asio::async_write(socket_, boost::asio::buffer(message), diff --git a/gcovr.cfg b/gcovr.cfg index 53cf6ba..e4f0171 100644 --- a/gcovr.cfg +++ b/gcovr.cfg @@ -3,10 +3,11 @@ search-path = build # filter = src -exclude-directories = doc -exclude-directories = tests +exclude-directories = build/_deps exclude-directories = coverage +exclude-directories = doc exclude-directories = stagedir +exclude-directories = tests exclude-directories = .cache exclude-directories = .direnv exclude-directories = .venv diff --git a/tests/Base64-test.cpp b/tests/Base64-test.cpp index a2cbd45..475c664 100644 --- a/tests/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -16,10 +16,11 @@ extern "C" // #include "base64.h" } +#include #include #include -#include // for std::println +// XXX #include // for std::println #include #include @@ -75,7 +76,7 @@ TEST(Base64Test, encoding) { const std::string binary(testpattern[i].bin); const std::string encoded{testpattern[i].encoded}; - std::println("'{}':\t{}", binary, encoded); + fmt::println("'{}':\t{}", binary, encoded); const std::string base64_encoded = base64.encode(binary); EXPECT_EQ(encoded, base64_encoded); @@ -94,7 +95,7 @@ TEST(Base64Test, decoding) { const std::string encoded{testpattern[i].encoded}; const std::string binary{testpattern[i].bin}; - std::println("'{}':\t{}", binary, encoded); + fmt::println("'{}':\t{}", binary, encoded); const std::string decoded = base64.decode(encoded); EXPECT_EQ(binary, decoded); @@ -108,7 +109,7 @@ TEST(Base64Test, ShortString1) Base64 base64; std::string const original = "A"; std::string const encoded = base64.encode(original); - // std::println("{}:\t{}", original, encoded); + // fmt::println("{}:\t{}", original, encoded); EXPECT_EQ(encoded, "QQ=="); std::string const decoded = base64.decode(encoded); @@ -120,7 +121,7 @@ TEST(Base64Test, ShortString2) Base64 base64; std::string const original = "AA"; std::string const encoded = base64.encode(original); - // std::println("{}:\t{}", original, encoded); + // fmt::println("{}:\t{}", original, encoded); EXPECT_EQ(encoded, "QUE="); std::string const decoded = base64.decode(encoded); @@ -132,7 +133,7 @@ TEST(Base64Test, ShortString3) Base64 base64; std::string const original = "AAA"; std::string const encoded = base64.encode(original); - // std::println("{}:\t{}", original, encoded); + // fmt::println("{}:\t{}", original, encoded); EXPECT_EQ(encoded, "QUFB"); std::string const decoded = base64.decode(encoded); @@ -173,7 +174,7 @@ TEST(Base64Test, LongString) "VGhpcyBpcyBub3QgYSByZWFsbHkgbG9uZyBzdHJpbmcsIGJ1dCBhbHNvIHRoYXQgc2hvdWxkIGJl" "IGVuY29kZWQgYW5kIGRlY29kZWQgY29ycmVjdGx5Lg=="}; EXPECT_EQ(expected, encoded); - // std::println("{}:\n{}", original, encoded); + // fmt::println("{}:\n{}", original, encoded); std::string const decoded = base64.decode(encoded); EXPECT_EQ(original, decoded); @@ -185,7 +186,7 @@ TEST(Base64Test, FoxString) std::string const original = "The quick brown fox jumped over the lazy dogs."; std::string const encoded = base64.encode(original); EXPECT_EQ(encoded, "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wZWQgb3ZlciB0aGUgbGF6eSBkb2dzLg=="); - // std::println("{}:\t{}", original, encoded); + // fmt::println("{}:\t{}", original, encoded); std::string const decoded = base64.decode(encoded); EXPECT_EQ(original, decoded); @@ -205,7 +206,7 @@ TEST(Base64Test, NonAsciiString) Base64 base64; std::string const original = "\xFC@NOs[\xFEVJ\t@\x80\v\xD0\xAA\xF5"; std::string const encoded = base64.encode(original); - // std::println("'{}':\t{}", original, encoded); + // fmt::println("'{}':\t{}", original, encoded); std::string const decoded = base64.decode(encoded); EXPECT_EQ(original, decoded); @@ -236,7 +237,7 @@ TEST(Base64Test, TestEncoder) std::string original("!@#$%^&*()_~<>"); std::string expected{"IUAjJCVeJiooKV9+PD4="}; auto encoded = base64.encode(original); - std::println("'{}':\t{}", original, encoded); + fmt::println("'{}':\t{}", original, encoded); EXPECT_EQ(encoded, expected); EXPECT_EQ(original, base64.decode(encoded)); From 5a38c2dcaa77d22b14de85e07f63c0e53442cae4 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 25 Mar 2025 17:54:30 +0100 Subject: [PATCH 050/120] Increase test coverage --- CMakeLists.txt | 13 ++++++++----- GNUmakefile | 2 +- examples/CMakeLists.txt | 10 ++++++++++ examples/README.md | 2 ++ examples/test.sh | 18 ++++++++++++++++++ gcovr.cfg | 3 ++- 6 files changed, 41 insertions(+), 7 deletions(-) create mode 100755 examples/test.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f35b94..797c953 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,7 @@ target_link_libraries( ) do_test(blocking_tcp_echo_client --help Usage) -if(APPLE) +if(APPLE AND NOT ENABLE_TEST_COVERAGE) add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) target_link_libraries( async_tcp_client_v20 @@ -83,12 +83,15 @@ if(APPLE) do_test(async_tcp_client_v20 --help Usage) add_executable(async_tcp_client async_tcp_client.cpp) - target_link_libraries(async_tcp_client PUBLIC Boost::asio fmt::fmt-header-only) + target_link_libraries( + async_tcp_client + PUBLIC Boost::asio fmt::fmt-header-only + ) do_test(async_tcp_client --help Usage) - # add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) - # target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::asio) - #XXX do_test(blocking_tcp_echo_server port Usage) + add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) + target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::asio) + # FIXME: do_test(blocking_tcp_echo_server port Usage) endif() add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) diff --git a/GNUmakefile b/GNUmakefile index 2d5525f..2c539bc 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -14,7 +14,7 @@ clean: build - ninja -C $< $@ - find $< -name '*.gcda' -delete -distclean: clean +distclean: # XXX clean rm -rf build coverage/* *~ ctags build: CMakeLists.txt diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 78eee4c..47b4488 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,12 +4,22 @@ project(Base64-examples VERSION 0.1.0 LANGUAGES CXX) find_package(Poco 1.14 COMPONENTS Foundation REQUIRED) +enable_testing() + add_executable(base64decode base64decode.cpp) target_link_libraries(base64decode PUBLIC Poco::Foundation) add_executable(base64encode base64encode.cpp) target_link_libraries(base64encode PUBLIC Poco::Foundation) +if(UNIX) + add_test( + NAME base64-test + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test.sh + WORKING_DIRECTORY ${CMAKE__CURRENT_BINARY_DIR} + ) +endif() + if(PROJECT_IS_TOP_LEVEL) return() endif() diff --git a/examples/README.md b/examples/README.md index 6e8bbb7..043b5cb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,6 +11,8 @@ echo '!@#$%^&*()_~<>' > base64.dat cat base64.dat | ./base64encode - > base64.txt cat base64.dat | ./base64encode - | ./base64decode - | diff base64.dat - + ./base64encode base64.dat base64.txt + ./base64decode base64.txt base64.dat hexdump -C base64.dat diff --git a/examples/test.sh b/examples/test.sh new file mode 100755 index 0000000..a24f45a --- /dev/null +++ b/examples/test.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -x +set -u +set -e + +echo '!@#$%^&*()_~<>' > base64.dat +cat base64.dat | ./base64encode - > base64.txt +cat base64.dat | ./base64encode - | ./base64decode - | diff base64.dat - +./base64encode 2>&1 | grep -w usage +./base64decode 2>&1 | grep -w usage +rm -rf tmp +./base64encode base64.txt tmp/output_file 2>&1 | grep -w output_file +./base64decode base64.dat tmp/output_file 2>&1 | grep -w output_file +./base64encode base64.dat base64-out.txt +./base64decode base64.txt base64-out.dat +diff -u base64.txt base64-out.txt +diff -u base64.dat base64-out.dat diff --git a/gcovr.cfg b/gcovr.cfg index e4f0171..c8b1812 100644 --- a/gcovr.cfg +++ b/gcovr.cfg @@ -3,11 +3,12 @@ search-path = build # filter = src +exclude = tests + exclude-directories = build/_deps exclude-directories = coverage exclude-directories = doc exclude-directories = stagedir -exclude-directories = tests exclude-directories = .cache exclude-directories = .direnv exclude-directories = .venv From 4317190c11302c7063561dbf0fa1df27052e8776 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 30 Mar 2025 15:42:12 +0200 Subject: [PATCH 051/120] Create RRCP::async_rrcp_client class --- CMakeLists.txt | 6 +- async_rrcp_client.hpp | 218 +++++++++++++++++++++++++++++++++++ async_tcp_echo_client.cpp | 2 + blocking_tcp_echo_client.cpp | 2 + rrcp_async_tcp_client.cpp | 191 +----------------------------- rrcp_helper.cpp | 4 +- rrcp_helper.hpp | 5 + rrcp_message.hpp | 2 + 8 files changed, 240 insertions(+), 190 deletions(-) create mode 100644 async_rrcp_client.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 797c953..7d5e7be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,7 +112,11 @@ if(APPLE) add_test(NAME async_client COMMAND async_client) endif() -add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) +add_executable( + rrcp_async_tcp_client + rrcp_async_tcp_client.cpp + async_rrcp_client.hpp +) target_link_libraries( rrcp_async_tcp_client PRIVATE rrcp_helper diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp new file mode 100644 index 0000000..529adfc --- /dev/null +++ b/async_rrcp_client.hpp @@ -0,0 +1,218 @@ +#pragma once + +/*** + * async_rrcp_client.hpp + * ~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * + * Moderniced from Claus Klein and ChatGPT + ***/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rrcp_helper.hpp" + +namespace RRCP +{ + +using boost::asio::ip::tcp; +using namespace std::chrono_literals; +using message_queue = std::deque< std::string >; + +constexpr size_t max_length = 65432; +constexpr auto timeout_duration = 3s; +constexpr auto heartbeat_interval = 10s; + +class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client > +{ + public: + explicit async_rrcp_client(boost::asio::io_context& io_context) + : io_context_(io_context), socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) + { + deadline_.expires_at(boost::asio::steady_timer::time_point::max()); + } + + void start(const tcp::resolver::results_type& endpoints) + { + deadline_.expires_after(timeout_duration); + check_deadline(); + + boost::asio::async_connect(socket_, endpoints, + [self = shared_from_this()](const boost::system::error_code& ec, const tcp::endpoint&) + { + if (!ec) + { + fmt::print(stderr, "Connected to server.\n"); + self->connected_ = true; + self->read(); + self->send_heartbeat(); + } + else + { + fmt::print(stderr, "Failed to connect: {}\n", ec.message()); + } + }); + } + + auto connected() -> bool { return connected_; } + + // This function write the message into the msg queue and starts the write actor + void write(const std::string& message) + { + while (!connected_) + { + if (stopped_) + { + return; + } + fmt::print(stderr, "Client is not connected yet.\n"); + std::this_thread::sleep_for(timeout_duration); + } + + boost::asio::post(io_context_, + [this, message]() + { + bool const write_in_progress = !write_msgs_.empty(); + write_msgs_.push_back(message); + if (!write_in_progress) + { + deadline_.expires_after(timeout_duration); + do_write(); + } + }); + } + + void read() + { + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), STOP, + [self = shared_from_this()](const boost::system::error_code& ec, std::size_t length) + { + if (!ec) + { + std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP + self->input_buffer_.erase(0, length); + + if (!line.starts_with("gPing")) + { + fmt::print("{}\n", line); + } + self->deadline_.expires_after(heartbeat_interval + timeout_duration); + self->read(); + } + else + { + fmt::print(stderr, "Error reading message: {}\n", ec.message()); + self->stop(); + } + }); + } + + void stop() + { + fmt::print(stderr, "stop called, disconnecting...\n"); + stopped_ = true; + connected_ = false; + boost::system::error_code ec; + socket_.close(ec); + heartbeat_timer_.cancel(); + deadline_.cancel(); + } + + private: + void do_write() + { + auto self(shared_from_this()); + boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front()), + [this, self](boost::system::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + fmt::print(stderr, "Message sent.\n"); + write_msgs_.pop_front(); + if (!write_msgs_.empty()) + { + do_write(); + } + } + else + { + // There are no more endpoints to try. Shut down the client. + fmt::print(stderr, "Error writing message: {}\n", ec.message()); + stop(); + } + }); + } + + void send_heartbeat() + { + if (stopped_) + { + return; + } + + std::string heartbeat_message{START + char2esc("M:Utility GPing\"ÄÖÜ€0ß\"") + STOP}; + fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); + boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), + [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) + { + if (!ec) + { + self->heartbeat_timer_.expires_after(heartbeat_interval); + self->heartbeat_timer_.async_wait([self](const boost::system::error_code&) { self->send_heartbeat(); }); + } + else + { + fmt::print(stderr, "Error sedning heartbeat: {}\n", ec.message()); + self->stop(); + } + }); + } + + void check_deadline() + { + if (stopped_) + { + return; + } + + if (deadline_.expiry() <= boost::asio::steady_timer::clock_type::now()) + { + fmt::print(stderr, "No response from server, disconnecting...\n"); + stop(); + return; + } + + deadline_.async_wait([self = shared_from_this()](const boost::system::error_code&) { self->check_deadline(); }); + } + + boost::asio::io_context& io_context_; + tcp::socket socket_; + boost::asio::steady_timer deadline_; + boost::asio::steady_timer heartbeat_timer_; + std::string input_buffer_; + message_queue write_msgs_; + bool connected_{false}; + bool stopped_{false}; +}; + +} // namespace RRCP diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index f70fafa..a339031 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -32,6 +32,8 @@ using boost::asio::ip::tcp; using namespace std::chrono_literals; using message_queue = std::deque< std::string >; +using namespace RRCP; + constexpr size_t max_length = 65432; constexpr auto timeout_duration = 1s; diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index 56342c8..2e350e0 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -33,6 +33,8 @@ auto main(int argc, char* argv[]) -> int { try { + using namespace RRCP; + if (argc != 3) { std::cerr << "Usage: blocking_tcp_echo_client \n"; diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 5786412..9a42b95 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -13,200 +13,15 @@ #include #include -#include -#include -#include #include #include -#include #include #include #include #include -#include #include -#include "rrcp_helper.hpp" - -using boost::asio::ip::tcp; -using namespace std::chrono_literals; -using message_queue = std::deque< std::string >; - -constexpr size_t max_length = 65432; -constexpr auto timeout_duration = 3s; -constexpr auto heartbeat_interval = 10s; - -class rrcp_client : public std::enable_shared_from_this< rrcp_client > -{ - public: - explicit rrcp_client(boost::asio::io_context& io_context) - : io_context_(io_context), socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) - { - deadline_.expires_at(boost::asio::steady_timer::time_point::max()); - } - - void start(const tcp::resolver::results_type& endpoints) - { - deadline_.expires_after(timeout_duration); - check_deadline(); - - boost::asio::async_connect(socket_, endpoints, - [self = shared_from_this()](const boost::system::error_code& ec, const tcp::endpoint&) - { - if (!ec) - { - fmt::print(stderr, "Connected to server.\n"); - self->connected_ = true; - self->read(); - self->send_heartbeat(); - } - else - { - fmt::print(stderr, "Failed to connect: {}\n", ec.message()); - } - }); - } - - auto connected() -> bool { return connected_; } - - // This function write the message into the msg queue and starts the write actor - void write(const std::string& message) - { - while (!connected_) - { - if (stopped_) - { - return; - } - fmt::print(stderr, "Client is not connected yet.\n"); - std::this_thread::sleep_for(timeout_duration); - } - - boost::asio::post(io_context_, - [this, message]() - { - bool const write_in_progress = !write_msgs_.empty(); - write_msgs_.push_back(message); - if (!write_in_progress) - { - deadline_.expires_after(timeout_duration); - do_write(); - } - }); - } - - void read() - { - boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), STOP, - [self = shared_from_this()](const boost::system::error_code& ec, std::size_t length) - { - if (!ec) - { - std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP - self->input_buffer_.erase(0, length); - - if (!line.starts_with("gPing")) - { - fmt::print("{}\n", line); - } - self->deadline_.expires_after(heartbeat_interval + timeout_duration); - self->read(); - } - else - { - fmt::print(stderr, "Error reading message: {}\n", ec.message()); - self->stop(); - } - }); - } - - void stop() - { - fmt::print(stderr, "stop called, disconnecting...\n"); - stopped_ = true; - connected_ = false; - boost::system::error_code ec; - socket_.close(ec); - heartbeat_timer_.cancel(); - deadline_.cancel(); - } - - private: - void do_write() - { - auto self(shared_from_this()); - boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front()), - [this, self](boost::system::error_code ec, std::size_t /*length*/) - { - if (!ec) - { - fmt::print(stderr, "Message sent.\n"); - write_msgs_.pop_front(); - if (!write_msgs_.empty()) - { - do_write(); - } - } - else - { - // There are no more endpoints to try. Shut down the client. - fmt::print(stderr, "Error writing message: {}\n", ec.message()); - stop(); - } - }); - } - - void send_heartbeat() - { - if (stopped_) - { - return; - } - - std::string heartbeat_message{START + char2esc("M:Utility GPing\"ÄÖÜ€0ß\"") + STOP}; - fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); - boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), - [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) - { - if (!ec) - { - self->heartbeat_timer_.expires_after(heartbeat_interval); - self->heartbeat_timer_.async_wait([self](const boost::system::error_code&) { self->send_heartbeat(); }); - } - else - { - fmt::print(stderr, "Error sedning heartbeat: {}\n", ec.message()); - self->stop(); - } - }); - } - - void check_deadline() - { - if (stopped_) - { - return; - } - - if (deadline_.expiry() <= boost::asio::steady_timer::clock_type::now()) - { - fmt::print(stderr, "No response from server, disconnecting...\n"); - stop(); - return; - } - - deadline_.async_wait([self = shared_from_this()](const boost::system::error_code&) { self->check_deadline(); }); - } - - boost::asio::io_context& io_context_; - tcp::socket socket_; - boost::asio::steady_timer deadline_; - boost::asio::steady_timer heartbeat_timer_; - std::string input_buffer_; - message_queue write_msgs_; - bool connected_{false}; - bool stopped_{false}; -}; +#include "async_rrcp_client.hpp" auto main(int argc, char* argv[]) -> int { @@ -218,10 +33,12 @@ auto main(int argc, char* argv[]) -> int try { + using namespace RRCP; + boost::asio::io_context io_context; tcp::resolver resolver(io_context); - auto c = std::make_shared< rrcp_client >(io_context); + auto c = std::make_shared< async_rrcp_client >(io_context); c->start(resolver.resolve(argv[1], argv[2])); std::thread io_thread([&io_context]() { io_context.run(); }); diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index acbe0b1..3108d08 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -10,7 +10,7 @@ constexpr char REPLACE_LF = 0x01; constexpr char REPLACE_CR = 0x02; constexpr char REPLACE_ESC = 0x03; -auto esc2char(std::string_view data) -> std::string +auto RRCP::esc2char(std::string_view data) -> std::string { std::string message; auto len = data.size(); @@ -52,7 +52,7 @@ auto esc2char(std::string_view data) -> std::string return message; } -auto char2esc(std::string_view data) -> std::string +auto RRCP::char2esc(std::string_view data) -> std::string { std::string message; for (char const c : data) diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index e6ea860..1d9cf21 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -3,6 +3,9 @@ #include #include +namespace RRCP +{ + constexpr const char START{0x0A}; // \n constexpr const char STOP{0x0D}; // \r @@ -25,3 +28,5 @@ extern auto esc2char(std::string_view data) -> std::string; * @return translated data */ extern auto char2esc(std::string_view data) -> std::string; + +} // namespace RRCP diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 355fc17..d49f88b 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -21,6 +21,8 @@ #include "rrcp_helper.hpp" +using namespace RRCP; + class rrcp_message { public: From 62ad71610cd5799bf7da95f2241801a5d831125b Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 30 Mar 2025 15:58:30 +0200 Subject: [PATCH 052/120] Fix more clang-tidy warnings --- .clang-tidy | 5 ++--- Base64.cpp | 4 ++-- GNUmakefile | 2 ++ async_rrcp_client.hpp | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 959362e..10b88b0 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -7,9 +7,8 @@ cert-*,\ clang-analyzer-*,\ -clang-analyzer-unix.BlockInCriticalSection,\ cppcoreguidelines-*,\ --cppcoreguidelines-avoid-do-while,\ --cppcoreguidelines-macro-to-enum,\ --cppcoreguidelines-macro-usage,\ +-cppcoreguidelines-avoid-*,\ +-cppcoreguidelines-macro-*,\ -cppcoreguidelines-owning-memory,\ -cppcoreguidelines-pro-bounds-pointer-arithmetic,\ hicpp-*,\ diff --git a/Base64.cpp b/Base64.cpp index 7363147..25350b8 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -49,7 +49,7 @@ class base64 { std::string dest; dest.resize(encoded_size(len)); - dest.resize(encode(&dest[0], data, len)); + dest.resize(encode(dest.data(), data, len)); return dest; } @@ -66,7 +66,7 @@ class base64 // TODO(CK): remove first at least all "\n\r" or better all non printable chars! std::string striped = remove_whitespace(data); - auto const result = decode(&dest[0], striped.data(), striped.size()); + auto const result = decode(dest.data(), striped.data(), striped.size()); dest.resize(result.first); return dest; } diff --git a/GNUmakefile b/GNUmakefile index 2c539bc..591f8d0 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -40,7 +40,9 @@ performance-avoid-endl,\ performance-unnecessary-value-param,\ readability-avoid-const-params-in-decls,\ readability-braces-around-statements,\ +readability-container-data-pointer,\ readability-else-after-return,\ +readability-make-member-function-const,\ readability-redundant-member-init,\ readability-use-std-min-max,\ ' \ diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 529adfc..238b90b 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -74,7 +74,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client }); } - auto connected() -> bool { return connected_; } + auto connected() const -> bool { return connected_; } // This function write the message into the msg queue and starts the write actor void write(const std::string& message) From 6da233c38589b454998f05ba4c71396b84fd5c37 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 30 Mar 2025 19:17:45 +0200 Subject: [PATCH 053/120] Build static lib rrcp_helper using c++17 only --- CMakeLists.txt | 115 +++++++++++++++++++---------------- async_rrcp_client.hpp | 3 +- blocking_tcp_echo_client.cpp | 7 +-- examples/CMakeLists.txt | 2 +- rrcp_message.hpp | 12 ++-- timer.cpp | 4 +- 6 files changed, 78 insertions(+), 65 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d5e7be..997b852 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,15 +1,15 @@ -cmake_minimum_required(VERSION 3.28...4.0) +cmake_minimum_required(VERSION 3.25...4.0) project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) # ---- add dependencies ---- -find_package(Boost 1.87 COMPONENTS asio beast REQUIRED HINTS $ENV{HOME}/.local) +find_package(Boost 1.71 COMPONENTS asio beast REQUIRED HINTS $ENV{HOME}/.local) find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) # ---- default settings ---- -set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -31,9 +31,14 @@ endif() # ---- code coverage ---- -option(BUILD_EXAMPLES "Compile examples too" ON) -option(ENABLE_TEST_COVERAGE "Compile with test-coverage flags" ON) -if(ENABLE_TEST_COVERAGE AND CMAKE_BUILD_TYPE) +option(BUILD_TESTING "Build ctest" ${PROJECT_IS_TOP_LEVEL}) +option(BUILD_EXAMPLES "Compile examples too" ${PROJECT_IS_TOP_LEVEL}) +option( + ENABLE_TEST_COVERAGE + "Compile with test-coverage flags" + ${PROJECT_IS_TOP_LEVEL} +) +if(ENABLE_TEST_COVERAGE AND CMAKE_BUILD_TYPE STREQUAL Debug) message(WARNING "ENABLE_TEST_COVERAGE is set!") add_compile_options(-O0 -g -fprofile-arcs -ftest-coverage) add_link_options(-fprofile-arcs -ftest-coverage) @@ -45,18 +50,25 @@ endif() enable_testing() function(do_test target arg result) - add_test(NAME ${target}${arg} COMMAND ${target} ${arg}) - set_tests_properties( - ${target}${arg} - PROPERTIES PASS_REGULAR_EXPRESSION ${result} - ) + if(BUILD_TESTING) + add_test(NAME ${target}${arg} COMMAND ${target} ${arg}) + set_tests_properties( + ${target}${arg} + PROPERTIES PASS_REGULAR_EXPRESSION ${result} + ) + endif() endfunction() add_executable(async_tcp_echo_server async_tcp_echo_server.cpp) target_link_libraries(async_tcp_echo_server PUBLIC Boost::asio) do_test(async_tcp_echo_server "" port) -add_library(rrcp_helper STATIC rrcp_helper.cpp rrcp_helper.hpp) +add_library(rrcp_helper STATIC) +target_sources( + rrcp_helper + PRIVATE rrcp_helper.cpp + PUBLIC FILE_SET HEADERS FILES async_rrcp_client.hpp rrcp_helper.hpp +) add_executable(async_tcp_echo_client async_tcp_echo_client.cpp) target_link_libraries( @@ -94,15 +106,15 @@ if(APPLE AND NOT ENABLE_TEST_COVERAGE) # FIXME: do_test(blocking_tcp_echo_server port Usage) endif() -add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) -target_link_libraries( - rrcp_client - PRIVATE rrcp_helper - PUBLIC Boost::asio fmt::fmt-header-only -) -do_test(rrcp_client --help Usage) +if(APPLE AND BUILD_TESTING) + add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) + target_link_libraries( + rrcp_client + PRIVATE rrcp_helper + PUBLIC Boost::asio fmt::fmt-header-only + ) + do_test(rrcp_client --help Usage) -if(APPLE) add_executable(timer timer.cpp) target_link_libraries(timer PUBLIC Boost::asio fmt::fmt-header-only) add_test(NAME timer COMMAND timer) @@ -112,11 +124,7 @@ if(APPLE) add_test(NAME async_client COMMAND async_client) endif() -add_executable( - rrcp_async_tcp_client - rrcp_async_tcp_client.cpp - async_rrcp_client.hpp -) +add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) target_link_libraries( rrcp_async_tcp_client PRIVATE rrcp_helper @@ -124,38 +132,39 @@ target_link_libraries( ) do_test(rrcp_async_tcp_client --help Usage) -# we need googletest -include(FetchContent) -FetchContent_Declare( - googletest - GIT_TAG v1.16.0 - GIT_REPOSITORY https://github.com/google/googletest.git - FIND_PACKAGE_ARGS 1.16.0 NAMES GTest COMPONENTS gmain_main - EXCLUDE_FROM_ALL - SYSTEM -) +if(BUILD_TESTING) + # we need googletest + include(FetchContent) + FetchContent_Declare( + googletest + GIT_TAG v1.16.0 + GIT_REPOSITORY https://github.com/google/googletest.git + FIND_PACKAGE_ARGS 1.16.0 NAMES GTest COMPONENTS gmain_main + EXCLUDE_FROM_ALL + SYSTEM + ) -# For Windows: Prevent overriding the parent project's compiler/linker settings -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(googletest) + # For Windows: Prevent overriding the parent project's compiler/linker settings + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googletest) -# add_library(base64c STATIC) -# target_sources(base64c PRIVATE base64.c PUBLIC FILE_SET HEADERS FILES base64.h) + # add_library(base64c STATIC) + # NO! target_sources(base64c PRIVATE base64.c PUBLIC FILE_SET HEADERS FILES base64.h) -add_library(Base64 STATIC) -target_sources( - Base64 - PRIVATE Base64.cpp - PUBLIC FILE_SET HEADERS FILES Base64.hpp -) -target_link_libraries(Base64 PUBLIC Boost::beast) + target_sources( + rrcp_helper + PRIVATE Base64.cpp + PUBLIC FILE_SET HEADERS FILES Base64.hpp + ) + target_link_libraries(rrcp_helper PUBLIC Boost::beast) -add_executable(Base64-test tests/Base64-test.cpp) -target_link_libraries( - Base64-test - PRIVATE Base64 GTest::gtest_main fmt::fmt-header-only -) -add_test(NAME Base64-test COMMAND Base64-test) + add_executable(Base64-test tests/Base64-test.cpp) + target_link_libraries( + Base64-test + PRIVATE rrcp_helper GTest::gtest_main fmt::fmt-header-only + ) + add_test(NAME Base64-test COMMAND Base64-test) +endif() if(APPLE AND BUILD_EXAMPLES) add_subdirectory(examples) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 238b90b..8cb8e2f 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -14,6 +14,7 @@ #include +#include // for starts_with #include #include #include @@ -112,7 +113,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); - if (!line.starts_with("gPing")) + if (!boost::algorithm::starts_with(line, "gPing")) { fmt::print("{}\n", line); } diff --git a/blocking_tcp_echo_client.cpp b/blocking_tcp_echo_client.cpp index 2e350e0..5920ef7 100644 --- a/blocking_tcp_echo_client.cpp +++ b/blocking_tcp_echo_client.cpp @@ -9,7 +9,7 @@ // // Moderniced from Claus Klein and ChatGPT -#include +#include // for trim_right #include #include #include @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -61,13 +60,13 @@ auto main(int argc, char* argv[]) -> int continue; } - // TODO: check boost::system::error_code ec; + // TODO(CK): check boost::system::error_code ec; std::string command = char2esc(line); command.insert(0, 1, START); command += STOP; boost::asio::write(s, boost::asio::buffer(command.c_str(), command.length())); - // TODO: wait for endchar with timeout! + // TODO(CK): wait for endchar with timeout! std::string data; boost::asio::dynamic_string_buffer< char, std::string::traits_type, std::string::allocator_type > const sb2 = boost::asio::dynamic_buffer(data, max_length); diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 47b4488..0b38381 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.28...4.0) +cmake_minimum_required(VERSION 3.25...4.0) project(Base64-examples VERSION 0.1.0 LANGUAGES CXX) diff --git a/rrcp_message.hpp b/rrcp_message.hpp index d49f88b..35b1568 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -11,6 +11,8 @@ #ifndef RRCP_MESSAGE_HPP #define RRCP_MESSAGE_HPP +#include + #include #include #include @@ -31,14 +33,14 @@ class rrcp_message rrcp_message() = default; - // TODO: or better const &std::string_view? + // TODO(CK): or better const &std::string_view? [[nodiscard]] auto data() const -> const char* { return data_.data(); } auto data() -> char* { return data_.data(); } [[nodiscard]] auto length() const -> std::size_t { return header_length + msg_length_; } - // TODO: or better const &std::string_view? + // TODO(CK): or better const &std::string_view? [[nodiscard]] auto body() const -> const char* { return data_.data() + header_length; } auto body() -> char* { return data_.data() + header_length; } @@ -54,7 +56,7 @@ class rrcp_message auto decode_body() -> bool { - // TODO: or better const &std::string_view? + // TODO(CK): or better const &std::string_view? auto result = esc2char(std::string(body(), msg_length_)); if (result.length() != msg_length_) { @@ -83,7 +85,7 @@ class rrcp_message void encode_body() { - // TODO: or better const &std::string_view? + // TODO(CK): or better const &std::string_view? auto msg = char2esc(std::string(body(), msg_length_)); if (msg.length() != msg_length_) { @@ -94,7 +96,7 @@ class rrcp_message void encode_header() { - std::string header = std::format("{:04x}", static_cast< uint16_t >(msg_length_)); + std::string header = fmt::format("{:04x}", static_cast< uint16_t >(msg_length_)); std::memcpy(data_.data(), header.data(), header_length); } diff --git a/timer.cpp b/timer.cpp index 31354a3..75be5c9 100644 --- a/timer.cpp +++ b/timer.cpp @@ -9,6 +9,8 @@ // // Moderniced from Claus Klein and ChatGPT +#include + #include #include #include @@ -55,7 +57,7 @@ auto main() -> int } catch (const std::exception& e) { - std::print("Error: {}\n", e.what()); + fmt::print("Error: {}\n", e.what()); return EXIT_FAILURE; } From 36e7d28a891773aea0162ce229ab8b4b07d6e55a Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 30 Mar 2025 23:30:21 +0200 Subject: [PATCH 054/120] Link only Boost::headers --- CMakeLists.txt | 57 ++++++++++++++++++++---------------------------- rrcp_message.hpp | 11 +++++----- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 997b852..61adb38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,8 +4,12 @@ project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) # ---- add dependencies ---- -find_package(Boost 1.71 COMPONENTS asio beast REQUIRED HINTS $ENV{HOME}/.local) -find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) +if(NOT TARGET Boost::headers) + find_package(Boost 1.71 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) +endif() +if(NOT TARGET fmt::fmt-header-only) + find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) +endif() # ---- default settings ---- @@ -60,7 +64,7 @@ function(do_test target arg result) endfunction() add_executable(async_tcp_echo_server async_tcp_echo_server.cpp) -target_link_libraries(async_tcp_echo_server PUBLIC Boost::asio) +target_link_libraries(async_tcp_echo_server PUBLIC Boost::headers) do_test(async_tcp_echo_server "" port) add_library(rrcp_helper STATIC) @@ -69,67 +73,55 @@ target_sources( PRIVATE rrcp_helper.cpp PUBLIC FILE_SET HEADERS FILES async_rrcp_client.hpp rrcp_helper.hpp ) +target_link_libraries(rrcp_helper PUBLIC Boost::headers fmt::fmt-header-only) add_executable(async_tcp_echo_client async_tcp_echo_client.cpp) -target_link_libraries( - async_tcp_echo_client - PRIVATE rrcp_helper - PUBLIC Boost::asio fmt::fmt-header-only -) +target_link_libraries(async_tcp_echo_client PRIVATE rrcp_helper) do_test(async_tcp_echo_client --help Usage) add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) -target_link_libraries( - blocking_tcp_echo_client - PRIVATE rrcp_helper - PUBLIC Boost::asio fmt::fmt-header-only -) +target_link_libraries(blocking_tcp_echo_client PRIVATE rrcp_helper) do_test(blocking_tcp_echo_client --help Usage) if(APPLE AND NOT ENABLE_TEST_COVERAGE) add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) target_link_libraries( async_tcp_client_v20 - PUBLIC Boost::asio fmt::fmt-header-only + PUBLIC Boost::headers fmt::fmt-header-only ) do_test(async_tcp_client_v20 --help Usage) add_executable(async_tcp_client async_tcp_client.cpp) target_link_libraries( async_tcp_client - PUBLIC Boost::asio fmt::fmt-header-only + PUBLIC Boost::headers fmt::fmt-header-only ) do_test(async_tcp_client --help Usage) add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) - target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::asio) + target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::headers) # FIXME: do_test(blocking_tcp_echo_server port Usage) endif() if(APPLE AND BUILD_TESTING) add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) - target_link_libraries( - rrcp_client - PRIVATE rrcp_helper - PUBLIC Boost::asio fmt::fmt-header-only - ) + target_link_libraries(rrcp_client PRIVATE rrcp_helper) do_test(rrcp_client --help Usage) add_executable(timer timer.cpp) - target_link_libraries(timer PUBLIC Boost::asio fmt::fmt-header-only) + target_link_libraries(timer PUBLIC Boost::headers fmt::fmt-header-only) add_test(NAME timer COMMAND timer) add_executable(async_client async_client.cpp) - target_link_libraries(async_client PUBLIC Boost::asio fmt::fmt-header-only) + target_link_libraries( + async_client + PUBLIC Boost::headers fmt::fmt-header-only + ) add_test(NAME async_client COMMAND async_client) endif() add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) -target_link_libraries( - rrcp_async_tcp_client - PRIVATE rrcp_helper - PUBLIC Boost::asio fmt::fmt-header-only -) +target_link_libraries(rrcp_async_tcp_client PRIVATE rrcp_helper) do_test(rrcp_async_tcp_client --help Usage) if(BUILD_TESTING) @@ -156,13 +148,12 @@ if(BUILD_TESTING) PRIVATE Base64.cpp PUBLIC FILE_SET HEADERS FILES Base64.hpp ) - target_link_libraries(rrcp_helper PUBLIC Boost::beast) - - add_executable(Base64-test tests/Base64-test.cpp) target_link_libraries( - Base64-test - PRIVATE rrcp_helper GTest::gtest_main fmt::fmt-header-only + rrcp_helper # Not needed: PUBLIC Boost::beast ) + + add_executable(Base64-test tests/Base64-test.cpp) + target_link_libraries(Base64-test PRIVATE rrcp_helper GTest::gtest_main) add_test(NAME Base64-test COMMAND Base64-test) endif() diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 35b1568..66f6328 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -7,6 +7,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +// Moderniced from Claus Klein and ChatGPT #ifndef RRCP_MESSAGE_HPP #define RRCP_MESSAGE_HPP @@ -14,12 +15,10 @@ #include #include -#include #include #include -#include #include -// #include +// XXX #include #include "rrcp_helper.hpp" @@ -51,7 +50,7 @@ class rrcp_message { msg_length_ = new_length; msg_length_ = std::min(msg_length_, max_msg_length); - std::cerr << "body_length(" << msg_length_ << ")\n"; + fmt::print(stderr, "body_length({})\n", msg_length_); } auto decode_body() -> bool @@ -60,7 +59,7 @@ class rrcp_message auto result = esc2char(std::string(body(), msg_length_)); if (result.length() != msg_length_) { - std::cerr << result << '\n'; + fmt::print(stderr, "{}\n", result); body_length(result.length()); std::memcpy(body(), result.c_str(), msg_length_); @@ -75,7 +74,7 @@ class rrcp_message msg_length_ = std::stoul(header, nullptr, 16); if (msg_length_ > max_msg_length) { - std::cerr << "Invalid msg_length!\n"; + fmt::print(stderr, "Invalid msg_length!\n"); msg_length_ = 0; return false; From 159c780ae49999407d1715a7d6c3c9c78541e4f0 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Mon, 31 Mar 2025 00:22:41 +0200 Subject: [PATCH 055/120] Little more modernize --- .clang-tidy | 2 ++ GNUmakefile | 3 +-- async_rrcp_client.hpp | 4 ++-- async_tcp_echo_client.cpp | 2 +- rrcp_client.cpp | 2 +- rrcp_message.hpp | 3 +++ 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 10b88b0..589486b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -8,11 +8,13 @@ clang-analyzer-*,\ -clang-analyzer-unix.BlockInCriticalSection,\ cppcoreguidelines-*,\ -cppcoreguidelines-avoid-*,\ +-cppcoreguidelines-init-variables,\ -cppcoreguidelines-macro-*,\ -cppcoreguidelines-owning-memory,\ -cppcoreguidelines-pro-bounds-pointer-arithmetic,\ hicpp-*,\ misc-*,\ +-misc-const-correctness,\ -misc-include-cleaner,\ -misc-no-recursion,\ modernize-*,\ diff --git a/GNUmakefile b/GNUmakefile index 591f8d0..f6a7e22 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -26,11 +26,9 @@ check: all fix: all run-clang-tidy -p build -fix \ -checks='-*,\ -cppcoreguidelines-init-variables,\ hicpp-explicit-conversions,\ hicpp-member-init,\ hicpp-named-parameter,\ -misc-const-correctness,\ modernize-deprecated-headers,\ modernize-loop-convert,\ modernize-use-nodiscard,\ @@ -44,6 +42,7 @@ readability-container-data-pointer,\ readability-else-after-return,\ readability-make-member-function-const,\ readability-redundant-member-init,\ +readability-simplify-boolean-expr,\ readability-use-std-min-max,\ ' \ *.cpp diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 8cb8e2f..288e9c5 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -75,7 +75,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client }); } - auto connected() const -> bool { return connected_; } + [[nodiscard]] auto connected() const -> bool { return connected_; } // This function write the message into the msg queue and starts the write actor void write(const std::string& message) @@ -93,7 +93,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client boost::asio::post(io_context_, [this, message]() { - bool const write_in_progress = !write_msgs_.empty(); + bool const write_in_progress{!write_msgs_.empty()}; write_msgs_.push_back(message); if (!write_in_progress) { diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index a339031..e9af686 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -83,7 +83,7 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT boost::asio::post(io_context_, [this, message]() { - bool const write_in_progress = !write_msgs_.empty(); + bool const write_in_progress{!write_msgs_.empty()}; write_msgs_.push_back(message); if (!write_in_progress) { diff --git a/rrcp_client.cpp b/rrcp_client.cpp index cd9acd0..3a8d858 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -50,7 +50,7 @@ class rrcp_client boost::asio::post(io_context_, [this, msg]() { - bool const write_in_progress = !write_msgs_.empty(); + bool const write_in_progress{!write_msgs_.empty()}; write_msgs_.push_back(msg); if (!write_in_progress) { diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 66f6328..dcd366a 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -72,6 +72,8 @@ class rrcp_message { const std::string header(data_.data(), header_length); msg_length_ = std::stoul(header, nullptr, 16); + + // NOLINTNEXTLINE(readability-simplify-boolean-expr) if (msg_length_ > max_msg_length) { fmt::print(stderr, "Invalid msg_length!\n"); @@ -79,6 +81,7 @@ class rrcp_message msg_length_ = 0; return false; } + return true; } From 3f06b4a74468b0abed1fe4b2b903f66642490ad5 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Mon, 31 Mar 2025 21:13:17 +0200 Subject: [PATCH 056/120] Use read msg queue --- CMakeLists.txt | 2 ++ async_rrcp_client.hpp | 80 ++++++++++++++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 61adb38..0a560cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,8 @@ project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) # ---- add dependencies ---- +find_package(Threads) + if(NOT TARGET Boost::headers) find_package(Boost 1.71 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) endif() diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 288e9c5..3b20edb 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -65,7 +65,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { fmt::print(stderr, "Connected to server.\n"); self->connected_ = true; - self->read(); + self->do_read(); self->send_heartbeat(); } else @@ -90,20 +90,69 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::this_thread::sleep_for(timeout_duration); } + std::string msg_id = fmt::format("{:d} ", (++msg_id_)); + std::string msg = msg_id + message; + fmt::print(stderr, "write_msgs_.push_back({})\n", msg); + boost::asio::post(io_context_, - [this, message]() + [this, msg]() { bool const write_in_progress{!write_msgs_.empty()}; - write_msgs_.push_back(message); + write_msgs_.push_back(msg); if (!write_in_progress) { deadline_.expires_after(timeout_duration); do_write(); } }); + + auto result = read(msg_id); + } + + auto read(const std::string& msg_id) -> std::string + { + std::string message; + + do + { + boost::asio::post(io_context_, + [this, &message]() + { + if (!read_msgs_.empty()) + { + message = read_msgs_.front(); + read_msgs_.pop_front(); + } + }); + + if (message.length()) + { + fmt::print(stderr, "read_msgs_.front({})\n", message); + if (!boost::algorithm::starts_with(message, msg_id)) + { + fmt::print("{}\n", message); + break; + } + } + std::this_thread::sleep_for(500ms); + } while (!stopped_); + + return message; } - void read() + void stop() + { + fmt::print(stderr, "stop called, disconnecting...\n"); + stopped_ = true; + connected_ = false; + boost::system::error_code ec; + socket_.close(ec); + heartbeat_timer_.cancel(); + deadline_.cancel(); + } + + private: + void do_read() { boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), STOP, [self = shared_from_this()](const boost::system::error_code& ec, std::size_t length) @@ -115,10 +164,11 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (!boost::algorithm::starts_with(line, "gPing")) { - fmt::print("{}\n", line); + fmt::print(stderr, "{}\n", line); + self->read_msgs_.push_back(line); } self->deadline_.expires_after(heartbeat_interval + timeout_duration); - self->read(); + self->do_read(); } else { @@ -128,18 +178,6 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client }); } - void stop() - { - fmt::print(stderr, "stop called, disconnecting...\n"); - stopped_ = true; - connected_ = false; - boost::system::error_code ec; - socket_.close(ec); - heartbeat_timer_.cancel(); - deadline_.cancel(); - } - - private: void do_write() { auto self(shared_from_this()); @@ -148,7 +186,6 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { if (!ec) { - fmt::print(stderr, "Message sent.\n"); write_msgs_.pop_front(); if (!write_msgs_.empty()) { @@ -171,7 +208,8 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client return; } - std::string heartbeat_message{START + char2esc("M:Utility GPing\"ÄÖÜ€0ß\"") + STOP}; + // FIXME: std::string heartbeat_message{START + char2esc("M:Utility GPing\"ÄÖÜ€0ß\"") + STOP}; + std::string heartbeat_message{START + char2esc("gPing\"ÄÖÜ€0ß\"") + STOP}; fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) @@ -211,7 +249,9 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client boost::asio::steady_timer deadline_; boost::asio::steady_timer heartbeat_timer_; std::string input_buffer_; + message_queue read_msgs_; message_queue write_msgs_; + uint16_t msg_id_{1000}; bool connected_{false}; bool stopped_{false}; }; From 5cfb6217fdc94a3d3fda42cb6351b431745b1045 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 1 Apr 2025 07:50:30 +0200 Subject: [PATCH 057/120] Add message number handling --- async_rrcp_client.hpp | 66 +++++++++++++++++++++++++-------------- async_tcp_echo_client.cpp | 2 +- gcovr.cfg | 2 +- rrcp.txt | 34 +++++++++++--------- rrcp_async_tcp_client.cpp | 11 ++++--- rrcp_helper.hpp | 22 +++++++++++++ 6 files changed, 93 insertions(+), 44 deletions(-) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 3b20edb..477b757 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -77,7 +77,10 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client [[nodiscard]] auto connected() const -> bool { return connected_; } - // This function write the message into the msg queue and starts the write actor + // This function write the message into the send msg queue and starts the write actor + // + // TODO(CK): should have to input strings: the MIB name and the command string! + // void write(const std::string& message) { while (!connected_) @@ -90,15 +93,22 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::this_thread::sleep_for(timeout_duration); } - std::string msg_id = fmt::format("{:d} ", (++msg_id_)); - std::string msg = msg_id + message; + // TODO(CK): prevent to use the msg_id for trap commands! + std::string msg_id = fmt::format("{:d}", (++msg_id_)); + std::string msg = insertAfterFirstWord(message, msg_id); + + // TRACE: fmt::print(stderr, "write_msgs_.push_back({})\n", msg); + std::string command = char2esc(msg); + command.insert(0, 1, START); + command += STOP; + boost::asio::post(io_context_, - [this, msg]() + [this, command]() { bool const write_in_progress{!write_msgs_.empty()}; - write_msgs_.push_back(msg); + write_msgs_.push_back(command); if (!write_in_progress) { deadline_.expires_after(timeout_duration); @@ -106,38 +116,44 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } }); - auto result = read(msg_id); + auto response = read(msg_id); + fmt::print("{}\n", response); } + // This function try to read the response message from the receive msg queue auto read(const std::string& msg_id) -> std::string { - std::string message; + std::string response; do { boost::asio::post(io_context_, - [this, &message]() + [this, &response]() { if (!read_msgs_.empty()) { - message = read_msgs_.front(); + response = read_msgs_.front(); read_msgs_.pop_front(); } }); - if (message.length()) + if (response.length()) { - fmt::print(stderr, "read_msgs_.front({})\n", message); - if (!boost::algorithm::starts_with(message, msg_id)) + // DEBUG: + fmt::print(stderr, "read_msgs_.front({})\n", response); + + // FIXME: if (boost::algorithm::starts_with(response, msg_id)) + auto pos = response.find(msg_id); + if(pos != std::string::npos) { - fmt::print("{}\n", message); + // FIXME: response = response.substr(0, pos - 1) break; } } std::this_thread::sleep_for(500ms); } while (!stopped_); - return message; + return response; } void stop() @@ -162,8 +178,14 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); - if (!boost::algorithm::starts_with(line, "gPing")) + if (boost::algorithm::starts_with(line, "d")) { + // Handle trap data messages + fmt::print("{}\n", line); + } + else if (!boost::algorithm::starts_with(line, "gPing")) + { + // Other responses the trap and ping messages fmt::print(stderr, "{}\n", line); self->read_msgs_.push_back(line); } @@ -180,23 +202,22 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client void do_write() { - auto self(shared_from_this()); boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front()), - [this, self](boost::system::error_code ec, std::size_t /*length*/) + [self = shared_from_this()](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { - write_msgs_.pop_front(); - if (!write_msgs_.empty()) + self->write_msgs_.pop_front(); + if (!self->write_msgs_.empty()) { - do_write(); + self->do_write(); } } else { // There are no more endpoints to try. Shut down the client. fmt::print(stderr, "Error writing message: {}\n", ec.message()); - stop(); + self->stop(); } }); } @@ -208,8 +229,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client return; } - // FIXME: std::string heartbeat_message{START + char2esc("M:Utility GPing\"ÄÖÜ€0ß\"") + STOP}; - std::string heartbeat_message{START + char2esc("gPing\"ÄÖÜ€0ß\"") + STOP}; + std::string heartbeat_message{START + char2esc("M:Utility GPing\"async client\"") + STOP}; fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) diff --git a/async_tcp_echo_client.cpp b/async_tcp_echo_client.cpp index e9af686..ae5a5d4 100644 --- a/async_tcp_echo_client.cpp +++ b/async_tcp_echo_client.cpp @@ -216,7 +216,7 @@ auto main(int argc, char* argv[]) -> int line.resize(sz); // NOTE: w/o c++ comments } - boost::trim_right(line); + boost::trim(line); if (line.empty()) { continue; diff --git a/gcovr.cfg b/gcovr.cfg index c8b1812..cb00781 100644 --- a/gcovr.cfg +++ b/gcovr.cfg @@ -1,7 +1,7 @@ root = . search-path = build -# filter = src +filter = *rrcp* exclude = tests diff --git a/rrcp.txt b/rrcp.txt index 9f081a8..24181b6 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -1,31 +1,37 @@ M:Utility GInitialInfo"v8.53.2","async",10 M:Radio SString"\rHallo\tWorld\n" -M:WF.FF.Main 123456 T Octet 1 // with optional Message Number: -M:WF.FF.Main 123456 t -M:OBit L:1 123456 GGoState // with optional Logical Address: + +// NOTE: M:WF.FF.Main 123456 T Octet 1 // with optional Message Number: +// NOTE: M:WF.FF.Main 123456 t +// NOTE: M:OBit L:1 123456 GGoState // with optional Logical Address: + M:Audio GAudioVolume // without optionl parts -M:Log SStruct 1,-1,3.14 // multiple parameters +// NOTE: M:Log SStruct 1,-1,3.14 // multiple parameters // The magic part // GET-request TU SET-request TU GET-request TU: -GFRQ;MOD SFRQ10000000;MOD13;BW80000 GFRQ;MOD +// NOTE: GFRQ;MOD SFRQ10000000;MOD13;BW80000 GFRQ;MOD +// // GET-response TU SET-response TU GET-response TU: -gFRQ18000000;MOD12 s5BW gFRQ18000000;MOD12 +// NOTE: gFRQ18000000;MOD12 s5BW gFRQ18000000;MOD12 + // NOTE: // There is an error within the BW command, the complete SET-request TU is cancelled. // The GET-request TU is replied by the corresponding GET-response TU. -M:MultilCmd S Octet 1;Long-1;String"Hallo World\n";Struct 1,+1,+3.14 // multiple commands's -M:Test 123456 S FREQ123456;MOD12;LOGIN"user","password" G FREQ;MOD;STATUS // multiple TU -M:Test gFREQ180000;MOD12 s5BW gFREQ180000;MOD12;STATUS"Running" // TU partly failed +// NOTE: M:MultilCmd S Octet 1;Long-1;String"Hallo World\n";Struct 1,+1,+3.14 // multiple commands's +// NOTE: M:Test 123456 S FREQ123456;MOD12;LOGIN"user","password" G FREQ;MOD;STATUS // multiple TU +// NOTE: M:Test gFREQ180000;MOD12 s5BW gFREQ180000;MOD12;STATUS"Running" // TU partly failed // M:eRADIO S FREQUENCY 123456789 // E:12 // MU error -M:RADIO T FREQUENCY 1 // register trap -M:RADIO t // trap response OK -M:RADIO d FREQUENCY 123456789 // trap data message -M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data +// NOTE: M:RADIO T FREQUENCY 1 // register trap +// NOTE: M:RADIO t // trap response OK +// NOTE: M:RADIO d FREQUENCY 123456789 // trap data message +// NOTE: M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data -// The more real samples +// +// The more real samples: +// M:Access GHasControl M:Access THasControl1 M:Access GOwnSession diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 9a42b95..65611ea 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -52,17 +52,18 @@ auto main(int argc, char* argv[]) -> int line.resize(sz); // NOTE: w/o c++ comments } - boost::trim_right(line); + boost::trim(line); if (line.empty()) { continue; } - std::string command = char2esc(line); - command.insert(0, 1, START); - command += STOP; + if (boost::algorithm::starts_with(line, "E:")) + { + continue; + } - c->write(command); + c->write(line); } std::this_thread::sleep_for(heartbeat_interval); // NOTE: only for gcov results! CK diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index 1d9cf21..f4656f0 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -29,4 +29,26 @@ extern auto esc2char(std::string_view data) -> std::string; */ extern auto char2esc(std::string_view data) -> std::string; +// Here’s a C++17 function that inserts a given string after the first word in an input string, +// where words are separated by WS +inline std::string insertAfterFirstWord(const std::string& input, const std::string& toInsert) +{ + // XXX size_t firstSpace = input.find_first_of(" \t\n\r"); // Find first whitespace + size_t firstSpace = input.find_first_of(" \t"); // Find first whitespace + if (firstSpace == std::string::npos) + { + return input; // No spaces found, return original string + } + + size_t nextNonSpace = input.find_first_not_of(" \t\n", firstSpace); + + // If there's no second word, just append toInsert after the first word + if (nextNonSpace == std::string::npos) + { + return input + " " + toInsert; + } + + return input.substr(0, firstSpace + 1) + toInsert + " " + input.substr(nextNonSpace); +} + } // namespace RRCP From e85a7e47f98da44c4c3fa25950d64a9f8469781a Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 1 Apr 2025 13:38:37 +0200 Subject: [PATCH 058/120] Make it works for Get, Set, and Trap commands --- async_rrcp_client.hpp | 45 ++++++++++++++++++++++++++++----------- gcovr.cfg | 3 ++- rrcp.txt | 26 ++++++++++++++++++++++ rrcp_async_tcp_client.cpp | 3 ++- rrcp_helper.hpp | 13 +++++------ 5 files changed, 69 insertions(+), 21 deletions(-) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 477b757..0b70484 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -15,6 +15,7 @@ #include #include // for starts_with +#include // for trim_left #include #include #include @@ -77,29 +78,37 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client [[nodiscard]] auto connected() const -> bool { return connected_; } - // This function write the message into the send msg queue and starts the write actor + // This function write the message into the send msg queue and starts the write actor. + // It wait for the response message and return this. // // TODO(CK): should have to input strings: the MIB name and the command string! // - void write(const std::string& message) + [[nodiscard]] auto write(const std::string& message) -> std::string { while (!connected_) { if (stopped_) { - return; + return {}; } fmt::print(stderr, "Client is not connected yet.\n"); std::this_thread::sleep_for(timeout_duration); } - // TODO(CK): prevent to use the msg_id for trap commands! - std::string msg_id = fmt::format("{:d}", (++msg_id_)); + // Insert the next message number for Set/Get request. + // But prevent to insert the msg_id for Trap commands! + std::string msg_id; + auto trap_cmd = message.find(" T"); + if (trap_cmd == std::string::npos) + { + msg_id = fmt::format("{:d}", (++msg_id_)); + } std::string msg = insertAfterFirstWord(message, msg_id); // TRACE: fmt::print(stderr, "write_msgs_.push_back({})\n", msg); + // Create the RRCP message frame std::string command = char2esc(msg); command.insert(0, 1, START); command += STOP; @@ -116,8 +125,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } }); - auto response = read(msg_id); - fmt::print("{}\n", response); + return read(msg_id); } // This function try to read the response message from the receive msg queue @@ -142,15 +150,25 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client // DEBUG: fmt::print(stderr, "read_msgs_.front({})\n", response); - // FIXME: if (boost::algorithm::starts_with(response, msg_id)) + // FIXME: wrong order for error responses like this: "E:2 10001" auto pos = response.find(msg_id); - if(pos != std::string::npos) + if (pos != std::string::npos) + { + // Remove the inserted message number for Set/Get responses. + if (boost::algorithm::starts_with(response, msg_id)) + { + response = response.substr(pos + msg_id.length()); + boost::trim_left(response); + } + break; + } + + if (boost::algorithm::starts_with(response, "E:")) { - // FIXME: response = response.substr(0, pos - 1) break; } } - std::this_thread::sleep_for(500ms); + std::this_thread::sleep_for(250ms); } while (!stopped_); return response; @@ -181,7 +199,8 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (boost::algorithm::starts_with(line, "d")) { // Handle trap data messages - fmt::print("{}\n", line); + fmt::print(stderr, "Ignored trap data: {}\n", line); + // TODO(CK): fmt::print("{}\n", line); } else if (!boost::algorithm::starts_with(line, "gPing")) { @@ -271,7 +290,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::string input_buffer_; message_queue read_msgs_; message_queue write_msgs_; - uint16_t msg_id_{1000}; + uint16_t msg_id_{10000}; bool connected_{false}; bool stopped_{false}; }; diff --git a/gcovr.cfg b/gcovr.cfg index cb00781..a8a1235 100644 --- a/gcovr.cfg +++ b/gcovr.cfg @@ -1,7 +1,8 @@ root = . search-path = build -filter = *rrcp* +filter = rrcp* +filter = async_rrcp_client.hpp exclude = tests diff --git a/rrcp.txt b/rrcp.txt index 24181b6..bf8be5f 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -29,6 +29,32 @@ M:Audio GAudioVolume // without optionl parts // NOTE: M:RADIO d FREQUENCY 123456789 // trap data message // NOTE: M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data +// XXX Test output: +// XXX Enter command: write_msgs_.push_back(M:Mission 1032 GGlobalAddr) +// XXX 1032 gGlobalAddr"SDHR-RGA" +// XXX read_msgs_.front(1032 gGlobalAddr"SDHR-RGA") +// XXX 1032 gGlobalAddr"SDHR-RGA" +// XXX Enter command: write_msgs_.push_back(M:Mission 1033 SGlobalAddr"testString") +// XXX 1033 s0GlobalAddr +// XXX read_msgs_.front(1033 s0GlobalAddr) +// XXX 1033 s0GlobalAddr +// XXX Enter command: write_msgs_.push_back(M:Mission 1034 GMissions) +// XXX 1034 gMissions1,"ExWF_Test","Original" +// XXX read_msgs_.front(1034 gMissions1,"ExWF_Test","Original") +// XXX 1034 gMissions1,"ExWF_Test","Original" +// XXX Enter command: write_msgs_.push_back(M:Mission 1035 GPresets0,1) +// XXX 1035 gPresets5,1,"Mission","ExWF","RN1",2,"Mission","ExWF","RN2",3,"Mission","ExWF","RN3",4,"Mission","ExWF","RN4",5,"Mission","ExWF","RN5" +// XXX read_msgs_.front(1035 gPresets5,1,"Mission","ExWF","RN1",2,"Mission","ExWF","RN2",3,"Mission","ExWF","RN3",4,"Mission","ExWF","RN4",5,"Mission","ExWF","RN5") +// XXX 1035 gPresets5,1,"Mission","ExWF","RN1",2,"Mission","ExWF","RN2",3,"Mission","ExWF","RN3",4,"Mission","ExWF","RN4",5,"Mission","ExWF","RN5" +// XXX Enter command: write_msgs_.push_back(M:OBIT 1036 GGOState) +// XXX 1036 g0GOState +// XXX read_msgs_.front(1036 g0GOState) +// XXX 1036 g0GOState +// XXX Enter command: write_msgs_.push_back(M:OBIT 1037 TGOState1) +// XXX 1037 t0GOState +// XXX Send heartbeat: +// XXX M:Utility GPing"async client" + // // The more real samples: // diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 65611ea..97dbd50 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -63,7 +63,8 @@ auto main(int argc, char* argv[]) -> int continue; } - c->write(line); + const auto response = c->write(line); + fmt::print("{}\n", response); } std::this_thread::sleep_for(heartbeat_interval); // NOTE: only for gcov results! CK diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index f4656f0..b100710 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -33,21 +33,22 @@ extern auto char2esc(std::string_view data) -> std::string; // where words are separated by WS inline std::string insertAfterFirstWord(const std::string& input, const std::string& toInsert) { - // XXX size_t firstSpace = input.find_first_of(" \t\n\r"); // Find first whitespace size_t firstSpace = input.find_first_of(" \t"); // Find first whitespace - if (firstSpace == std::string::npos) + if (toInsert.empty() || (firstSpace == std::string::npos)) { return input; // No spaces found, return original string } - size_t nextNonSpace = input.find_first_not_of(" \t\n", firstSpace); - - // If there's no second word, just append toInsert after the first word + // NOTE: Only if Set/Get command request, NOT for Trap commands! + size_t nextNonSpace = input.find_first_of("SG", firstSpace); if (nextNonSpace == std::string::npos) { - return input + " " + toInsert; + // If there's no second valid command, just return the input! + // XXX return input + " " + toInsert; + return input; } + // If there's valid command, just append toInsert after the first word return input.substr(0, firstSpace + 1) + toInsert + " " + input.substr(nextNonSpace); } From 3f72929a878d77c7bb9d940ebc6cc61df7625fa7 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 1 Apr 2025 14:00:26 +0200 Subject: [PATCH 059/120] Fix typo --- async_rrcp_client.hpp | 2 +- rrcp_helper.hpp | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 0b70484..4ace535 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -260,7 +260,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } else { - fmt::print(stderr, "Error sedning heartbeat: {}\n", ec.message()); + fmt::print(stderr, "Error sending heartbeat: {}\n", ec.message()); self->stop(); } }); diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index b100710..249ee75 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -33,8 +33,13 @@ extern auto char2esc(std::string_view data) -> std::string; // where words are separated by WS inline std::string insertAfterFirstWord(const std::string& input, const std::string& toInsert) { + if (toInsert.empty()) + { + return input; // Nothing to do + } + size_t firstSpace = input.find_first_of(" \t"); // Find first whitespace - if (toInsert.empty() || (firstSpace == std::string::npos)) + if (firstSpace == std::string::npos) { return input; // No spaces found, return original string } From d19be7d0140fe9ffe5c12b80bb85389323a8c0c8 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 1 Apr 2025 14:09:20 +0200 Subject: [PATCH 060/120] Fix more readability clang-tidy isssues --- GNUmakefile | 4 +++- async_rrcp_client.hpp | 2 +- rrcp_helper.hpp | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index f6a7e22..ff6bf0d 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -25,7 +25,7 @@ check: all fix: all run-clang-tidy -p build -fix \ - -checks='-*,\ + -checks='-*,\ hicpp-explicit-conversions,\ hicpp-member-init,\ hicpp-named-parameter,\ @@ -39,7 +39,9 @@ performance-unnecessary-value-param,\ readability-avoid-const-params-in-decls,\ readability-braces-around-statements,\ readability-container-data-pointer,\ +readability-container-size-empty,\ readability-else-after-return,\ +readability-implicit-bool-conversion,\ readability-make-member-function-const,\ readability-redundant-member-init,\ readability-simplify-boolean-expr,\ diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 4ace535..8696f13 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -145,7 +145,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } }); - if (response.length()) + if (!response.empty()) { // DEBUG: fmt::print(stderr, "read_msgs_.front({})\n", response); diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index 249ee75..de8c135 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -31,7 +31,7 @@ extern auto char2esc(std::string_view data) -> std::string; // Here’s a C++17 function that inserts a given string after the first word in an input string, // where words are separated by WS -inline std::string insertAfterFirstWord(const std::string& input, const std::string& toInsert) +inline auto insertAfterFirstWord(const std::string& input, const std::string& toInsert) -> std::string { if (toInsert.empty()) { From e2237d51353fe16913858f9edbd68cd85bbfeabb Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Tue, 1 Apr 2025 19:16:09 +0200 Subject: [PATCH 061/120] Cleanup and create log file --- CMakeLists.txt | 8 +- async_rrcp_client.hpp | 22 ++--- rrcp.txt | 7 +- rrcp_helper.hpp | 4 +- rrcp_message.hpp | 2 +- test.log | 213 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 237 insertions(+), 19 deletions(-) create mode 100644 test.log diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a560cb..55ca52d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,11 +105,11 @@ if(APPLE AND NOT ENABLE_TEST_COVERAGE) # FIXME: do_test(blocking_tcp_echo_server port Usage) endif() -if(APPLE AND BUILD_TESTING) - add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) - target_link_libraries(rrcp_client PRIVATE rrcp_helper) - do_test(rrcp_client --help Usage) +add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) +target_link_libraries(rrcp_client PRIVATE rrcp_helper) +do_test(rrcp_client --help Usage) +if(APPLE AND BUILD_TESTING) add_executable(timer timer.cpp) target_link_libraries(timer PUBLIC Boost::headers fmt::fmt-header-only) add_test(NAME timer COMMAND timer) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 8696f13..72a0b99 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -101,12 +101,13 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client auto trap_cmd = message.find(" T"); if (trap_cmd == std::string::npos) { - msg_id = fmt::format("{:d}", (++msg_id_)); + msg_id_ = ++msg_id_ % INVALID_ID; + msg_id = fmt::format("{}", (msg_id_)); } std::string msg = insertAfterFirstWord(message, msg_id); - // TRACE: - fmt::print(stderr, "write_msgs_.push_back({})\n", msg); + // DEBUG: + fmt::print("write_msgs_.push_back({})\n", msg); // Create the RRCP message frame std::string command = char2esc(msg); @@ -147,10 +148,9 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (!response.empty()) { - // DEBUG: - fmt::print(stderr, "read_msgs_.front({})\n", response); + // DEBUG: fmt::print("read_msgs_.front({})\n", response); - // FIXME: wrong order for error responses like this: "E:2 10001" + // NOTE: other order for error responses like this: "E:2 10001" auto pos = response.find(msg_id); if (pos != std::string::npos) { @@ -168,7 +168,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client break; } } - std::this_thread::sleep_for(250ms); + std::this_thread::sleep_for(125ms); } while (!stopped_); return response; @@ -176,7 +176,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client void stop() { - fmt::print(stderr, "stop called, disconnecting...\n"); + fmt::print(stderr, "Stoped, disconnecting ...\n"); stopped_ = true; connected_ = false; boost::system::error_code ec; @@ -200,7 +200,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { // Handle trap data messages fmt::print(stderr, "Ignored trap data: {}\n", line); - // TODO(CK): fmt::print("{}\n", line); + fmt::print("{}\n", line); } else if (!boost::algorithm::starts_with(line, "gPing")) { @@ -275,7 +275,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (deadline_.expiry() <= boost::asio::steady_timer::clock_type::now()) { - fmt::print(stderr, "No response from server, disconnecting...\n"); + fmt::print(stderr, "No response from server, stopping ...\n"); stop(); return; } @@ -290,7 +290,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::string input_buffer_; message_queue read_msgs_; message_queue write_msgs_; - uint16_t msg_id_{10000}; + int msg_id_{10000}; bool connected_{false}; bool stopped_{false}; }; diff --git a/rrcp.txt b/rrcp.txt index bf8be5f..243b1cc 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -1,5 +1,7 @@ M:Utility GInitialInfo"v8.53.2","async",10 + M:Radio SString"\rHallo\tWorld\n" +E:2 10002 // MU error // NOTE: M:WF.FF.Main 123456 T Octet 1 // with optional Message Number: // NOTE: M:WF.FF.Main 123456 t @@ -21,8 +23,9 @@ M:Audio GAudioVolume // without optionl parts // NOTE: M:MultilCmd S Octet 1;Long-1;String"Hallo World\n";Struct 1,+1,+3.14 // multiple commands's // NOTE: M:Test 123456 S FREQ123456;MOD12;LOGIN"user","password" G FREQ;MOD;STATUS // multiple TU // NOTE: M:Test gFREQ180000;MOD12 s5BW gFREQ180000;MOD12;STATUS"Running" // TU partly failed -// M:eRADIO S FREQUENCY 123456789 -// E:12 // MU error + +M:eRADIO S FREQUENCY 123456789 +E:12 // MU error // NOTE: M:RADIO T FREQUENCY 1 // register trap // NOTE: M:RADIO t // trap response OK diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index de8c135..1b888ab 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -8,6 +8,8 @@ namespace RRCP constexpr const char START{0x0A}; // \n constexpr const char STOP{0x0D}; // \r +constexpr const int INVALID_ID{16777216}; // valid range is 0 to 2**24 - 1 +constexpr const size_t MAX_MU_LENGTH{65432}; /** * @brief Gets the message between message and @@ -35,7 +37,7 @@ inline auto insertAfterFirstWord(const std::string& input, const std::string& to { if (toInsert.empty()) { - return input; // Nothing to do + return input; // Nothing to do } size_t firstSpace = input.find_first_of(" \t"); // Find first whitespace diff --git a/rrcp_message.hpp b/rrcp_message.hpp index dcd366a..af7e64a 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -28,7 +28,7 @@ class rrcp_message { public: static constexpr std::size_t header_length = 4; - static constexpr std::size_t max_msg_length = 65432; + static constexpr std::size_t max_msg_length = MAX_MU_LENGTH; rrcp_message() = default; diff --git a/test.log b/test.log new file mode 100644 index 0000000..534e6ea --- /dev/null +++ b/test.log @@ -0,0 +1,213 @@ +# +# cat rrcp.txt | build/rrcp_async_tcp_client localhost 8001 | tee test.log +# +write_msgs_.push_back(M:Utility 10001 GInitialInfo"v8.53.2","async",10) +gInitialInfo"08.43.00 SVFuA" +write_msgs_.push_back(M:Radio 10002 SString"\rHallo\tWorld\n") +E:2 10002 +write_msgs_.push_back(M:Audio 10003 GAudioVolume) +gAudioVolume"Level 0" +write_msgs_.push_back(M:eRADIO 10004 S FREQUENCY 123456789) +E:2 10004 +write_msgs_.push_back(M:Access 10005 GHasControl) +gHasControl1 +write_msgs_.push_back(M:Access THasControl1) +dHasControl1 +t +write_msgs_.push_back(M:Access 10006 GOwnSession) +gOwnSession"Monitoring" +write_msgs_.push_back(M:Access TOwnSession1) +dOwnSession"Monitoring" +t +write_msgs_.push_back(M:Access 10007 SReqSession"Monitoring") +s +write_msgs_.push_back(M:Audio 10008 GAudioVolume) +gAudioVolume"Level 0" +write_msgs_.push_back(M:Audio TAudioVolume1) +dAudioVolume"Level 0" +t +write_msgs_.push_back(M:Audio 10009 SAudioVolume"Level 0") +dAudioVolume"Level 2" +s +write_msgs_.push_back(M:Audio TAudioVolume0) +t +write_msgs_.push_back(M:Control 10010 SActPreset0) +s +write_msgs_.push_back(M:Control 10011 GCurrMission) +gCurrMission"testString" +write_msgs_.push_back(M:Control TCurrMission1) +dCurrMission"Mission1" +dCurrMission"Mission1" +t +write_msgs_.push_back(M:Control 10012 SCurrMission"testString") +s +write_msgs_.push_back(M:Control TCurrMission0) +t +write_msgs_.push_back(M:Control 10013 GCurrWF) +gCurrWF"ExWF" +write_msgs_.push_back(M:Control TCurrWF1) +dCurrWF"ExWF" +dOwnSession"Advanced" +dHasControl1 +t +write_msgs_.push_back(M:Control 10014 GPresetID) +gPresetID0,"Leer Preset" +write_msgs_.push_back(M:Control TPresetID1) +dPresetID4,"RN4" +t +write_msgs_.push_back(M:Control 10015 GTxInhibit) +gTxInhibit"None" +write_msgs_.push_back(M:Control TTxInhibit1) +dTxInhibit"None" +dOwnSession"Monitoring" +dHasControl0 +dPresetID5,"RN5" +dTxInhibit"None" +t +write_msgs_.push_back(M:Control 10016 STxInhibit"Disabled") +s +write_msgs_.push_back(M:Control TTxInhibit0) +t +write_msgs_.push_back(M:Inventory 10017 GInvCountCus) +gInvCountCus1,"GG" +write_msgs_.push_back(M:Inventory 10018 GInventoryCus0) +dPresetID6,"RN6" +gInventoryCus"GG, , , , , , , , , , ,","" +write_msgs_.push_back(M:Inventory 10019 GInvCount) +gInvCount1 +write_msgs_.push_back(M:Inventory 10020 GInventory0) +gInventory"GG, , , , , , , , , , ,","" +write_msgs_.push_back(M:IP 10021 GOwnAdrvIV"Control") +gOwnAdrvIV"127.0.0.1/8","" +write_msgs_.push_back(M:IP 10022 SOwnAdrvIV"Control","testString","testString") +dPresetID7,"RN7" +s0OwnAdrvIV +write_msgs_.push_back(M:Mission 10023 GGlobalAddr) +gGlobalAddr"RGA1123581321" +write_msgs_.push_back(M:Mission 10024 SGlobalAddr"testString") +s0GlobalAddr +write_msgs_.push_back(M:Mission 10025 GMissions) +gMissions1,"testString","Original" +write_msgs_.push_back(M:Mission 10026 GPresets0,1) +dPresetID8,"RN8" +dCurrWF"" +gPresets0 +write_msgs_.push_back(M:OBIT 10027 GGOState) +gGOState"GO" +write_msgs_.push_back(M:OBIT TGOState1) +dGOState"GO" +t +write_msgs_.push_back(M:OBIT 10028 GTestErrors) +gTestErrors0 +write_msgs_.push_back(M:OBIT 10029 GTestIDs) +dPresetID9,"RN9" +dGOState"NOGO" +dCurrWF"ExWF" +gTestIDs4,0,1,2,42 +write_msgs_.push_back(M:RxTx 10030 GPowerLevel) +gPowerLevel"Off" +write_msgs_.push_back(M:RxTx TPowerLevel1) +dPowerLevel"Low" +t +write_msgs_.push_back(M:RxTx 10031 SPowerLevel"Off") +s +write_msgs_.push_back(M:RxTx TPowerLevel0) +dPresetID10,"RN10" +dGOState"GO" +dCurrWF"" +t +write_msgs_.push_back(M:RxTx 10032 GVswr) +gVswr0 +write_msgs_.push_back(M:RxTx TVswr1) +dVswr0 +t +write_msgs_.push_back(M:Utility 10033 GBattStatus) +gBattStatus"Broken",0 +write_msgs_.push_back(M:Utility TBattStatus1) +dBattStatus"Charging",11 +dPresetID11,"RN11" +dGOState"NOGO" +dCurrWF"ExWF" +dBattStatus"Charging",12 +dVswr33 +t +write_msgs_.push_back(M:Utility 10034 GErrorText0,"English") +gErrorText"Unknown ErrorId 0","Error" +write_msgs_.push_back(M:Utility 10035 GInitialInfo"VersionStr","IdString",0) +gInitialInfo"08.43.00 SVFuA" +write_msgs_.push_back(M:Utility 10036 GPing"message") +gPing"message" +write_msgs_.push_back(M:Maintenance 10037 SRestart) +dPresetID12,"RN12" +dGOState"GO" +dCurrWF"" +dBattStatus"Charging",13 +dVswr36 +dPresetID13,"RN13" +dGOState"NOGO" +dCurrWF"ExWF" +dBattStatus"Charging",14 +dVswr39 +dPresetID14,"RN14" +dGOState"GO" +dCurrWF"" +dBattStatus"Charging",15 +dVswr42 +dPresetID15,"RN15" +dGOState"NOGO" +dCurrWF"ExWF" +dBattStatus"Charging",16 +dVswr45 +dPresetID16,"RN16" +dGOState"GO" +dCurrWF"" +dBattStatus"Charging",17 +dVswr48 +s +write_msgs_.push_back(M:Maintenance 10038 SShutdown) +dCurrWF"ExWF" +dCurrWF"ExWF" +dPresetID0,"RN0" +dBattStatus"Charging",1 +dVswr0 +s +dPresetID1,"RN1" +dBattStatus"Charging",2 +dVswr3 +dPresetID2,"RN2" +dBattStatus"Charging",3 +dVswr6 +dPresetID3,"RN3" +dBattStatus"Charging",4 +dVswr9 +dOwnSession"Advanced" +dHasControl1 +dPresetID4,"RN4" +dBattStatus"Charging",5 +dVswr12 +dOwnSession"Monitoring" +dHasControl0 +dPresetID5,"RN5" +dBattStatus"Charging",6 +dVswr15 +dPresetID6,"RN6" +dBattStatus"Charging",7 +dVswr18 +dPresetID7,"RN7" +dBattStatus"Charging",8 +dVswr21 +dPresetID8,"RN8" +dGOState"GO" +dCurrWF"" +dBattStatus"Charging",9 +dVswr24 +dPresetID9,"RN9" +dGOState"NOGO" +dCurrWF"ExWF" +dBattStatus"Charging",10 +dVswr27 +dPresetID10,"RN10" +dGOState"GO" +dCurrWF"" +dBattStatus"Charging",11 +dVswr30 From 0eb25f77bb9fc8524db90167a18413cf81da6bc8 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 2 Apr 2025 08:43:06 +0200 Subject: [PATCH 062/120] Be less verbose --- async_rrcp_client.hpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 72a0b99..6acca61 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -64,7 +64,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { if (!ec) { - fmt::print(stderr, "Connected to server.\n"); + fmt::print(stderr, "Connected to server.\n"); // TRACE self->connected_ = true; self->do_read(); self->send_heartbeat(); @@ -91,7 +91,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { return {}; } - fmt::print(stderr, "Client is not connected yet.\n"); + fmt::print(stderr, "Client is not connected yet.\n"); // TRACE std::this_thread::sleep_for(timeout_duration); } @@ -106,8 +106,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } std::string msg = insertAfterFirstWord(message, msg_id); - // DEBUG: - fmt::print("write_msgs_.push_back({})\n", msg); + // DEBUG: fmt::print("write_msgs_.push_back({})\n", msg); // Create the RRCP message frame std::string command = char2esc(msg); @@ -160,6 +159,11 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client response = response.substr(pos + msg_id.length()); boost::trim_left(response); } + else + { + response = response.substr(0, pos); + boost::trim_right(response); + } break; } @@ -196,16 +200,16 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); - if (boost::algorithm::starts_with(line, "d")) + if (boost::algorithm::starts_with(line, "d")) // Trap data message { // Handle trap data messages - fmt::print(stderr, "Ignored trap data: {}\n", line); + fmt::print(stderr, "Ignored trap data: {}\n", line); // WARNING fmt::print("{}\n", line); } else if (!boost::algorithm::starts_with(line, "gPing")) { - // Other responses the trap and ping messages - fmt::print(stderr, "{}\n", line); + // Other responses than Trap and Ping messages + fmt::print(stderr, "{}\n", line); // TRACE self->read_msgs_.push_back(line); } self->deadline_.expires_after(heartbeat_interval + timeout_duration); @@ -249,7 +253,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } std::string heartbeat_message{START + char2esc("M:Utility GPing\"async client\"") + STOP}; - fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); + fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); // TRACE boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) { From 0e689d1bf9b424e8a9d5a619e5c88193a9ea82ca Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Wed, 2 Apr 2025 10:17:49 +0200 Subject: [PATCH 063/120] Add a missing note and fix a typo --- async_rrcp_client.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 6acca61..1fba32b 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -167,6 +167,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client break; } + // NOTE: This is an Error response with or w/o a valid msg_id! if (boost::algorithm::starts_with(response, "E:")) { break; @@ -180,7 +181,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client void stop() { - fmt::print(stderr, "Stoped, disconnecting ...\n"); + fmt::print(stderr, "Stopped, disconnecting ...\n"); stopped_ = true; connected_ = false; boost::system::error_code ec; From 842b4f7e0c387efae2855e023ce89099a67334ed Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 3 Apr 2025 14:02:00 +0200 Subject: [PATCH 064/120] Separate examples, tests, and sources --- CMakeLists.txt | 35 +-- async_rrcp_client.hpp | 8 +- examples/CMakeLists.txt | 28 +++ .../async_tcp_client.cpp | 0 .../async_tcp_client_v20.cpp | 0 .../async_tcp_echo_client.cpp | 0 .../async_tcp_echo_server.cpp | 0 .../blocking_tcp_echo_client.cpp | 0 .../blocking_tcp_echo_server.cpp | 0 gcovr.cfg | 6 +- rrcp.txt | 54 +---- test.log | 213 ------------------ base64.c => tests/base64.c | 0 base64.h => tests/base64.h | 0 14 files changed, 51 insertions(+), 293 deletions(-) rename async_tcp_client.cpp => examples/async_tcp_client.cpp (100%) rename async_tcp_client_v20.cpp => examples/async_tcp_client_v20.cpp (100%) rename async_tcp_echo_client.cpp => examples/async_tcp_echo_client.cpp (100%) rename async_tcp_echo_server.cpp => examples/async_tcp_echo_server.cpp (100%) rename blocking_tcp_echo_client.cpp => examples/blocking_tcp_echo_client.cpp (100%) rename blocking_tcp_echo_server.cpp => examples/blocking_tcp_echo_server.cpp (100%) delete mode 100644 test.log rename base64.c => tests/base64.c (100%) rename base64.h => tests/base64.h (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 55ca52d..990eb7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,6 @@ project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) # ---- add dependencies ---- find_package(Threads) - if(NOT TARGET Boost::headers) find_package(Boost 1.71 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) endif() @@ -65,7 +64,7 @@ function(do_test target arg result) endif() endfunction() -add_executable(async_tcp_echo_server async_tcp_echo_server.cpp) +add_executable(async_tcp_echo_server examples/async_tcp_echo_server.cpp) target_link_libraries(async_tcp_echo_server PUBLIC Boost::headers) do_test(async_tcp_echo_server "" port) @@ -77,34 +76,14 @@ target_sources( ) target_link_libraries(rrcp_helper PUBLIC Boost::headers fmt::fmt-header-only) -add_executable(async_tcp_echo_client async_tcp_echo_client.cpp) +add_executable(async_tcp_echo_client examples/async_tcp_echo_client.cpp) target_link_libraries(async_tcp_echo_client PRIVATE rrcp_helper) do_test(async_tcp_echo_client --help Usage) -add_executable(blocking_tcp_echo_client blocking_tcp_echo_client.cpp) +add_executable(blocking_tcp_echo_client examples/blocking_tcp_echo_client.cpp) target_link_libraries(blocking_tcp_echo_client PRIVATE rrcp_helper) do_test(blocking_tcp_echo_client --help Usage) -if(APPLE AND NOT ENABLE_TEST_COVERAGE) - add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) - target_link_libraries( - async_tcp_client_v20 - PUBLIC Boost::headers fmt::fmt-header-only - ) - do_test(async_tcp_client_v20 --help Usage) - - add_executable(async_tcp_client async_tcp_client.cpp) - target_link_libraries( - async_tcp_client - PUBLIC Boost::headers fmt::fmt-header-only - ) - do_test(async_tcp_client --help Usage) - - add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) - target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::headers) - # FIXME: do_test(blocking_tcp_echo_server port Usage) -endif() - add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) target_link_libraries(rrcp_client PRIVATE rrcp_helper) do_test(rrcp_client --help Usage) @@ -143,12 +122,16 @@ if(BUILD_TESTING) FetchContent_MakeAvailable(googletest) # add_library(base64c STATIC) - # NO! target_sources(base64c PRIVATE base64.c PUBLIC FILE_SET HEADERS FILES base64.h) + # NO! target_sources(base64c PRIVATE tests/base64.c + # PUBLIC FILE_SET HEADERS BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/tests FILES tests/base64.h) target_sources( rrcp_helper PRIVATE Base64.cpp - PUBLIC FILE_SET HEADERS FILES Base64.hpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} + FILES Base64.hpp ) target_link_libraries( rrcp_helper # Not needed: PUBLIC Boost::beast diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 1fba32b..e259b60 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -64,7 +64,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { if (!ec) { - fmt::print(stderr, "Connected to server.\n"); // TRACE + fmt::print(stderr, "Connected to server.\n"); // TRACE self->connected_ = true; self->do_read(); self->send_heartbeat(); @@ -91,7 +91,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { return {}; } - fmt::print(stderr, "Client is not connected yet.\n"); // TRACE + fmt::print(stderr, "Client is not connected yet.\n"); // TRACE std::this_thread::sleep_for(timeout_duration); } @@ -201,7 +201,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); - if (boost::algorithm::starts_with(line, "d")) // Trap data message + if (boost::algorithm::starts_with(line, "d")) // Trap data message { // Handle trap data messages fmt::print(stderr, "Ignored trap data: {}\n", line); // WARNING @@ -210,7 +210,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client else if (!boost::algorithm::starts_with(line, "gPing")) { // Other responses than Trap and Ping messages - fmt::print(stderr, "{}\n", line); // TRACE + fmt::print(stderr, "{}\n", line); // TRACE self->read_msgs_.push_back(line); } self->deadline_.expires_after(heartbeat_interval + timeout_duration); diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 0b38381..e7dd4ea 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,6 +3,14 @@ cmake_minimum_required(VERSION 3.25...4.0) project(Base64-examples VERSION 0.1.0 LANGUAGES CXX) find_package(Poco 1.14 COMPONENTS Foundation REQUIRED) +find_package(Threads) + +if(NOT TARGET Boost::headers) + find_package(Boost 1.71 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) +endif() +if(NOT TARGET fmt::fmt-header-only) + find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) +endif() enable_testing() @@ -26,3 +34,23 @@ endif() do_test(base64decode --help input) do_test(base64encode --help input) + +if(APPLE AND NOT ENABLE_TEST_COVERAGE) + add_executable(async_tcp_client_v20 async_tcp_client_v20.cpp) + target_link_libraries( + async_tcp_client_v20 + PUBLIC Boost::headers fmt::fmt-header-only + ) + do_test(async_tcp_client_v20 --help Usage) + + add_executable(async_tcp_client async_tcp_client.cpp) + target_link_libraries( + async_tcp_client + PUBLIC Boost::headers fmt::fmt-header-only + ) + do_test(async_tcp_client --help Usage) + + add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) + target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::headers) + # FIXME: do_test(blocking_tcp_echo_server port Usage) +endif() diff --git a/async_tcp_client.cpp b/examples/async_tcp_client.cpp similarity index 100% rename from async_tcp_client.cpp rename to examples/async_tcp_client.cpp diff --git a/async_tcp_client_v20.cpp b/examples/async_tcp_client_v20.cpp similarity index 100% rename from async_tcp_client_v20.cpp rename to examples/async_tcp_client_v20.cpp diff --git a/async_tcp_echo_client.cpp b/examples/async_tcp_echo_client.cpp similarity index 100% rename from async_tcp_echo_client.cpp rename to examples/async_tcp_echo_client.cpp diff --git a/async_tcp_echo_server.cpp b/examples/async_tcp_echo_server.cpp similarity index 100% rename from async_tcp_echo_server.cpp rename to examples/async_tcp_echo_server.cpp diff --git a/blocking_tcp_echo_client.cpp b/examples/blocking_tcp_echo_client.cpp similarity index 100% rename from blocking_tcp_echo_client.cpp rename to examples/blocking_tcp_echo_client.cpp diff --git a/blocking_tcp_echo_server.cpp b/examples/blocking_tcp_echo_server.cpp similarity index 100% rename from blocking_tcp_echo_server.cpp rename to examples/blocking_tcp_echo_server.cpp diff --git a/gcovr.cfg b/gcovr.cfg index a8a1235..63bfed4 100644 --- a/gcovr.cfg +++ b/gcovr.cfg @@ -1,8 +1,10 @@ root = . search-path = build -filter = rrcp* -filter = async_rrcp_client.hpp +filter = examples/* +filter = Base64* +filter = rrcp_* +filter = async_* exclude = tests diff --git a/rrcp.txt b/rrcp.txt index 243b1cc..5938001 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -1,63 +1,21 @@ -M:Utility GInitialInfo"v8.53.2","async",10 +M:Utility GInitialInfo"v0.8.15","async client",10 -M:Radio SString"\rHallo\tWorld\n" -E:2 10002 // MU error +// with optional Message Number +M:Radio 10002 SString"\rHallo\tWorld\n" +// NOTE: w/o MibName! E:2 10002 // MU error -// NOTE: M:WF.FF.Main 123456 T Octet 1 // with optional Message Number: +// NOTE: M:WF.FF.Main 123456 T Octet 1 // NOTE: M:WF.FF.Main 123456 t -// NOTE: M:OBit L:1 123456 GGoState // with optional Logical Address: +// NOTE: M:OBit L:1 123456 GGoState // with optional Logical Address M:Audio GAudioVolume // without optionl parts // NOTE: M:Log SStruct 1,-1,3.14 // multiple parameters -// The magic part -// GET-request TU SET-request TU GET-request TU: -// NOTE: GFRQ;MOD SFRQ10000000;MOD13;BW80000 GFRQ;MOD -// -// GET-response TU SET-response TU GET-response TU: -// NOTE: gFRQ18000000;MOD12 s5BW gFRQ18000000;MOD12 - -// NOTE: -// There is an error within the BW command, the complete SET-request TU is cancelled. -// The GET-request TU is replied by the corresponding GET-response TU. -// NOTE: M:MultilCmd S Octet 1;Long-1;String"Hallo World\n";Struct 1,+1,+3.14 // multiple commands's -// NOTE: M:Test 123456 S FREQ123456;MOD12;LOGIN"user","password" G FREQ;MOD;STATUS // multiple TU -// NOTE: M:Test gFREQ180000;MOD12 s5BW gFREQ180000;MOD12;STATUS"Running" // TU partly failed - -M:eRADIO S FREQUENCY 123456789 -E:12 // MU error - // NOTE: M:RADIO T FREQUENCY 1 // register trap // NOTE: M:RADIO t // trap response OK // NOTE: M:RADIO d FREQUENCY 123456789 // trap data message // NOTE: M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data -// XXX Test output: -// XXX Enter command: write_msgs_.push_back(M:Mission 1032 GGlobalAddr) -// XXX 1032 gGlobalAddr"SDHR-RGA" -// XXX read_msgs_.front(1032 gGlobalAddr"SDHR-RGA") -// XXX 1032 gGlobalAddr"SDHR-RGA" -// XXX Enter command: write_msgs_.push_back(M:Mission 1033 SGlobalAddr"testString") -// XXX 1033 s0GlobalAddr -// XXX read_msgs_.front(1033 s0GlobalAddr) -// XXX 1033 s0GlobalAddr -// XXX Enter command: write_msgs_.push_back(M:Mission 1034 GMissions) -// XXX 1034 gMissions1,"ExWF_Test","Original" -// XXX read_msgs_.front(1034 gMissions1,"ExWF_Test","Original") -// XXX 1034 gMissions1,"ExWF_Test","Original" -// XXX Enter command: write_msgs_.push_back(M:Mission 1035 GPresets0,1) -// XXX 1035 gPresets5,1,"Mission","ExWF","RN1",2,"Mission","ExWF","RN2",3,"Mission","ExWF","RN3",4,"Mission","ExWF","RN4",5,"Mission","ExWF","RN5" -// XXX read_msgs_.front(1035 gPresets5,1,"Mission","ExWF","RN1",2,"Mission","ExWF","RN2",3,"Mission","ExWF","RN3",4,"Mission","ExWF","RN4",5,"Mission","ExWF","RN5") -// XXX 1035 gPresets5,1,"Mission","ExWF","RN1",2,"Mission","ExWF","RN2",3,"Mission","ExWF","RN3",4,"Mission","ExWF","RN4",5,"Mission","ExWF","RN5" -// XXX Enter command: write_msgs_.push_back(M:OBIT 1036 GGOState) -// XXX 1036 g0GOState -// XXX read_msgs_.front(1036 g0GOState) -// XXX 1036 g0GOState -// XXX Enter command: write_msgs_.push_back(M:OBIT 1037 TGOState1) -// XXX 1037 t0GOState -// XXX Send heartbeat: -// XXX M:Utility GPing"async client" - // // The more real samples: // diff --git a/test.log b/test.log deleted file mode 100644 index 534e6ea..0000000 --- a/test.log +++ /dev/null @@ -1,213 +0,0 @@ -# -# cat rrcp.txt | build/rrcp_async_tcp_client localhost 8001 | tee test.log -# -write_msgs_.push_back(M:Utility 10001 GInitialInfo"v8.53.2","async",10) -gInitialInfo"08.43.00 SVFuA" -write_msgs_.push_back(M:Radio 10002 SString"\rHallo\tWorld\n") -E:2 10002 -write_msgs_.push_back(M:Audio 10003 GAudioVolume) -gAudioVolume"Level 0" -write_msgs_.push_back(M:eRADIO 10004 S FREQUENCY 123456789) -E:2 10004 -write_msgs_.push_back(M:Access 10005 GHasControl) -gHasControl1 -write_msgs_.push_back(M:Access THasControl1) -dHasControl1 -t -write_msgs_.push_back(M:Access 10006 GOwnSession) -gOwnSession"Monitoring" -write_msgs_.push_back(M:Access TOwnSession1) -dOwnSession"Monitoring" -t -write_msgs_.push_back(M:Access 10007 SReqSession"Monitoring") -s -write_msgs_.push_back(M:Audio 10008 GAudioVolume) -gAudioVolume"Level 0" -write_msgs_.push_back(M:Audio TAudioVolume1) -dAudioVolume"Level 0" -t -write_msgs_.push_back(M:Audio 10009 SAudioVolume"Level 0") -dAudioVolume"Level 2" -s -write_msgs_.push_back(M:Audio TAudioVolume0) -t -write_msgs_.push_back(M:Control 10010 SActPreset0) -s -write_msgs_.push_back(M:Control 10011 GCurrMission) -gCurrMission"testString" -write_msgs_.push_back(M:Control TCurrMission1) -dCurrMission"Mission1" -dCurrMission"Mission1" -t -write_msgs_.push_back(M:Control 10012 SCurrMission"testString") -s -write_msgs_.push_back(M:Control TCurrMission0) -t -write_msgs_.push_back(M:Control 10013 GCurrWF) -gCurrWF"ExWF" -write_msgs_.push_back(M:Control TCurrWF1) -dCurrWF"ExWF" -dOwnSession"Advanced" -dHasControl1 -t -write_msgs_.push_back(M:Control 10014 GPresetID) -gPresetID0,"Leer Preset" -write_msgs_.push_back(M:Control TPresetID1) -dPresetID4,"RN4" -t -write_msgs_.push_back(M:Control 10015 GTxInhibit) -gTxInhibit"None" -write_msgs_.push_back(M:Control TTxInhibit1) -dTxInhibit"None" -dOwnSession"Monitoring" -dHasControl0 -dPresetID5,"RN5" -dTxInhibit"None" -t -write_msgs_.push_back(M:Control 10016 STxInhibit"Disabled") -s -write_msgs_.push_back(M:Control TTxInhibit0) -t -write_msgs_.push_back(M:Inventory 10017 GInvCountCus) -gInvCountCus1,"GG" -write_msgs_.push_back(M:Inventory 10018 GInventoryCus0) -dPresetID6,"RN6" -gInventoryCus"GG, , , , , , , , , , ,","" -write_msgs_.push_back(M:Inventory 10019 GInvCount) -gInvCount1 -write_msgs_.push_back(M:Inventory 10020 GInventory0) -gInventory"GG, , , , , , , , , , ,","" -write_msgs_.push_back(M:IP 10021 GOwnAdrvIV"Control") -gOwnAdrvIV"127.0.0.1/8","" -write_msgs_.push_back(M:IP 10022 SOwnAdrvIV"Control","testString","testString") -dPresetID7,"RN7" -s0OwnAdrvIV -write_msgs_.push_back(M:Mission 10023 GGlobalAddr) -gGlobalAddr"RGA1123581321" -write_msgs_.push_back(M:Mission 10024 SGlobalAddr"testString") -s0GlobalAddr -write_msgs_.push_back(M:Mission 10025 GMissions) -gMissions1,"testString","Original" -write_msgs_.push_back(M:Mission 10026 GPresets0,1) -dPresetID8,"RN8" -dCurrWF"" -gPresets0 -write_msgs_.push_back(M:OBIT 10027 GGOState) -gGOState"GO" -write_msgs_.push_back(M:OBIT TGOState1) -dGOState"GO" -t -write_msgs_.push_back(M:OBIT 10028 GTestErrors) -gTestErrors0 -write_msgs_.push_back(M:OBIT 10029 GTestIDs) -dPresetID9,"RN9" -dGOState"NOGO" -dCurrWF"ExWF" -gTestIDs4,0,1,2,42 -write_msgs_.push_back(M:RxTx 10030 GPowerLevel) -gPowerLevel"Off" -write_msgs_.push_back(M:RxTx TPowerLevel1) -dPowerLevel"Low" -t -write_msgs_.push_back(M:RxTx 10031 SPowerLevel"Off") -s -write_msgs_.push_back(M:RxTx TPowerLevel0) -dPresetID10,"RN10" -dGOState"GO" -dCurrWF"" -t -write_msgs_.push_back(M:RxTx 10032 GVswr) -gVswr0 -write_msgs_.push_back(M:RxTx TVswr1) -dVswr0 -t -write_msgs_.push_back(M:Utility 10033 GBattStatus) -gBattStatus"Broken",0 -write_msgs_.push_back(M:Utility TBattStatus1) -dBattStatus"Charging",11 -dPresetID11,"RN11" -dGOState"NOGO" -dCurrWF"ExWF" -dBattStatus"Charging",12 -dVswr33 -t -write_msgs_.push_back(M:Utility 10034 GErrorText0,"English") -gErrorText"Unknown ErrorId 0","Error" -write_msgs_.push_back(M:Utility 10035 GInitialInfo"VersionStr","IdString",0) -gInitialInfo"08.43.00 SVFuA" -write_msgs_.push_back(M:Utility 10036 GPing"message") -gPing"message" -write_msgs_.push_back(M:Maintenance 10037 SRestart) -dPresetID12,"RN12" -dGOState"GO" -dCurrWF"" -dBattStatus"Charging",13 -dVswr36 -dPresetID13,"RN13" -dGOState"NOGO" -dCurrWF"ExWF" -dBattStatus"Charging",14 -dVswr39 -dPresetID14,"RN14" -dGOState"GO" -dCurrWF"" -dBattStatus"Charging",15 -dVswr42 -dPresetID15,"RN15" -dGOState"NOGO" -dCurrWF"ExWF" -dBattStatus"Charging",16 -dVswr45 -dPresetID16,"RN16" -dGOState"GO" -dCurrWF"" -dBattStatus"Charging",17 -dVswr48 -s -write_msgs_.push_back(M:Maintenance 10038 SShutdown) -dCurrWF"ExWF" -dCurrWF"ExWF" -dPresetID0,"RN0" -dBattStatus"Charging",1 -dVswr0 -s -dPresetID1,"RN1" -dBattStatus"Charging",2 -dVswr3 -dPresetID2,"RN2" -dBattStatus"Charging",3 -dVswr6 -dPresetID3,"RN3" -dBattStatus"Charging",4 -dVswr9 -dOwnSession"Advanced" -dHasControl1 -dPresetID4,"RN4" -dBattStatus"Charging",5 -dVswr12 -dOwnSession"Monitoring" -dHasControl0 -dPresetID5,"RN5" -dBattStatus"Charging",6 -dVswr15 -dPresetID6,"RN6" -dBattStatus"Charging",7 -dVswr18 -dPresetID7,"RN7" -dBattStatus"Charging",8 -dVswr21 -dPresetID8,"RN8" -dGOState"GO" -dCurrWF"" -dBattStatus"Charging",9 -dVswr24 -dPresetID9,"RN9" -dGOState"NOGO" -dCurrWF"ExWF" -dBattStatus"Charging",10 -dVswr27 -dPresetID10,"RN10" -dGOState"GO" -dCurrWF"" -dBattStatus"Charging",11 -dVswr30 diff --git a/base64.c b/tests/base64.c similarity index 100% rename from base64.c rename to tests/base64.c diff --git a/base64.h b/tests/base64.h similarity index 100% rename from base64.h rename to tests/base64.h From f0a09b0aae47bed17e14808e5c2b33959661a49e Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 3 Apr 2025 14:38:21 +0200 Subject: [PATCH 065/120] Prepare refactory --- CMakeLists.txt | 11 +++++++++++ async_rrcp_client.hpp | 25 +++++++++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 990eb7f..4f9a1d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,10 +64,14 @@ function(do_test target arg result) endif() endfunction() +# ---- server needed for tests ---- + add_executable(async_tcp_echo_server examples/async_tcp_echo_server.cpp) target_link_libraries(async_tcp_echo_server PUBLIC Boost::headers) do_test(async_tcp_echo_server "" port) +# ---- rrcp class sources and helpers as a library ---- + add_library(rrcp_helper STATIC) target_sources( rrcp_helper @@ -89,10 +93,12 @@ target_link_libraries(rrcp_client PRIVATE rrcp_helper) do_test(rrcp_client --help Usage) if(APPLE AND BUILD_TESTING) + # TODO(CK): mv to examples too! add_executable(timer timer.cpp) target_link_libraries(timer PUBLIC Boost::headers fmt::fmt-header-only) add_test(NAME timer COMMAND timer) + # TODO(CK): rm this stupid example? add_executable(async_client async_client.cpp) target_link_libraries( async_client @@ -101,11 +107,16 @@ if(APPLE AND BUILD_TESTING) add_test(NAME async_client COMMAND async_client) endif() +# ---- rrcp client class usage examples main ---- + add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) target_link_libraries(rrcp_async_tcp_client PRIVATE rrcp_helper) do_test(rrcp_async_tcp_client --help Usage) +# TODO(CK): mv this part to tests/CMakeLists.txt if(BUILD_TESTING) + # XXX add_subdirectory(tests) + # we need googletest include(FetchContent) FetchContent_Declare( diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index e259b60..07be25f 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -95,6 +95,8 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::this_thread::sleep_for(timeout_duration); } + // TODO(CK): refactory to helper class + //========================== RRCP ============================ // Insert the next message number for Set/Get request. // But prevent to insert the msg_id for Trap commands! std::string msg_id; @@ -106,12 +108,13 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } std::string msg = insertAfterFirstWord(message, msg_id); - // DEBUG: fmt::print("write_msgs_.push_back({})\n", msg); + // DEBUG: fmt::print("rrcp MU to send({})\n", msg); // Create the RRCP message frame std::string command = char2esc(msg); command.insert(0, 1, START); command += STOP; + //========================== END ============================ boost::asio::post(io_context_, [this, command]() @@ -147,31 +150,36 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (!response.empty()) { - // DEBUG: fmt::print("read_msgs_.front({})\n", response); - // NOTE: other order for error responses like this: "E:2 10001" + // TODO(CK): refactory to helper class + //========================== RRCP ============================ + // DEBUG: fmt::print("RRCP MU received({})\n", response); + // NOTE: different order for error responses like this: "E:2 10001" auto pos = response.find(msg_id); if (pos != std::string::npos) { // Remove the inserted message number for Set/Get responses. if (boost::algorithm::starts_with(response, msg_id)) { + // NOTE: This is an Command response with msg_id! response = response.substr(pos + msg_id.length()); boost::trim_left(response); } else { + // NOTE: This is an Error response with msg_id! response = response.substr(0, pos); boost::trim_right(response); } break; } - // NOTE: This is an Error response with or w/o a valid msg_id! if (boost::algorithm::starts_with(response, "E:")) { break; } + //========================== END ============================ + } std::this_thread::sleep_for(125ms); } while (!stopped_); @@ -198,9 +206,13 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { if (!ec) { + + //========================== RRCP ============================ std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); + // TODO(CK): refactory to helper class + //========================== RRCP ============================ if (boost::algorithm::starts_with(line, "d")) // Trap data message { // Handle trap data messages @@ -213,6 +225,8 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client fmt::print(stderr, "{}\n", line); // TRACE self->read_msgs_.push_back(line); } + //========================== END ============================ + self->deadline_.expires_after(heartbeat_interval + timeout_duration); self->do_read(); } @@ -253,7 +267,10 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client return; } + //========================== RRCP ============================ std::string heartbeat_message{START + char2esc("M:Utility GPing\"async client\"") + STOP}; + //========================== END ============================ + fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); // TRACE boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) From 5133ad01d03d43ad2707a354ccc799f1be98b548 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 3 Apr 2025 19:59:39 +0200 Subject: [PATCH 066/120] Finish refactory --- Base64.cpp | 2 +- Base64.hpp | 4 +-- GNUmakefile | 1 + async_rrcp_client.hpp | 61 +++++-------------------------- rrcp_helper.cpp | 84 +++++++++++++++++++++++++++++++++++++++++++ rrcp_helper.hpp | 34 +++++------------- 6 files changed, 105 insertions(+), 81 deletions(-) diff --git a/Base64.cpp b/Base64.cpp index 25350b8..f0e4459 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -84,7 +84,7 @@ class base64 namespace RRCP::Common { -auto Base64::encode(std::string_view data) const -> std::string +auto Base64::encode(std::string_view data) -> std::string { if (data.empty()) { diff --git a/Base64.hpp b/Base64.hpp index 4fcd6ae..c88fa71 100644 --- a/Base64.hpp +++ b/Base64.hpp @@ -32,14 +32,14 @@ class Base64 * @param data The data to be encoded. * @return The corresponding base64 encoded string. */ - [[nodiscard]] auto encode(std::string_view data) const -> std::string; + [[nodiscard]] static auto encode(std::string_view data) -> std::string; /** * Decode a Base64 encoded string. * @param in The base64 encoded string. * @return The decoded string. */ - [[nodiscard]] auto decode(std::string_view in) -> std::string; + [[nodiscard]] static auto decode(std::string_view in) -> std::string; private: /** diff --git a/GNUmakefile b/GNUmakefile index ff6bf0d..05fd6d6 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -40,6 +40,7 @@ readability-avoid-const-params-in-decls,\ readability-braces-around-statements,\ readability-container-data-pointer,\ readability-container-size-empty,\ +readability-convert-member-functions-to-static,\ readability-else-after-return,\ readability-implicit-bool-conversion,\ readability-make-member-function-const,\ diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 07be25f..d76d9a3 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -15,7 +15,7 @@ #include #include // for starts_with -#include // for trim_left +#include // for trim_left, trim_right #include #include #include @@ -81,7 +81,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client // This function write the message into the send msg queue and starts the write actor. // It wait for the response message and return this. // - // TODO(CK): should have to input strings: the MIB name and the command string! + // TODO(CK): we should have two input strings: the MIB name and the command string! // [[nodiscard]] auto write(const std::string& message) -> std::string { @@ -95,26 +95,8 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::this_thread::sleep_for(timeout_duration); } - // TODO(CK): refactory to helper class - //========================== RRCP ============================ - // Insert the next message number for Set/Get request. - // But prevent to insert the msg_id for Trap commands! - std::string msg_id; - auto trap_cmd = message.find(" T"); - if (trap_cmd == std::string::npos) - { - msg_id_ = ++msg_id_ % INVALID_ID; - msg_id = fmt::format("{}", (msg_id_)); - } - std::string msg = insertAfterFirstWord(message, msg_id); - - // DEBUG: fmt::print("rrcp MU to send({})\n", msg); - - // Create the RRCP message frame - std::string command = char2esc(msg); - command.insert(0, 1, START); - command += STOP; - //========================== END ============================ + std::string msg_id_str; + auto command = RRCP::create_command_msg(message, msg_id_str, msg_id_); boost::asio::post(io_context_, [this, command]() @@ -128,7 +110,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } }); - return read(msg_id); + return read(msg_id_str); } // This function try to read the response message from the receive msg queue @@ -150,36 +132,11 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (!response.empty()) { - - // TODO(CK): refactory to helper class - //========================== RRCP ============================ - // DEBUG: fmt::print("RRCP MU received({})\n", response); - // NOTE: different order for error responses like this: "E:2 10001" - auto pos = response.find(msg_id); - if (pos != std::string::npos) - { - // Remove the inserted message number for Set/Get responses. - if (boost::algorithm::starts_with(response, msg_id)) - { - // NOTE: This is an Command response with msg_id! - response = response.substr(pos + msg_id.length()); - boost::trim_left(response); - } - else - { - // NOTE: This is an Error response with msg_id! - response = response.substr(0, pos); - boost::trim_right(response); - } - break; - } - // NOTE: This is an Error response with or w/o a valid msg_id! - if (boost::algorithm::starts_with(response, "E:")) + // helper which returns true if the msg with matching msg_id was found + if (RRCP::find_response_msg(response, msg_id)) { break; } - //========================== END ============================ - } std::this_thread::sleep_for(125ms); } while (!stopped_); @@ -206,12 +163,12 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { if (!ec) { - //========================== RRCP ============================ std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); + //========================== END ============================ - // TODO(CK): refactory to helper class + // TODO(CK): maby refactored to helper class? //========================== RRCP ============================ if (boost::algorithm::starts_with(line, "d")) // Trap data message { diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 3108d08..5cd79e9 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -1,5 +1,9 @@ #include "rrcp_helper.hpp" +#include + +#include // for starts_with +#include // for trim_left, trim_right #include #include #include @@ -78,3 +82,83 @@ auto RRCP::char2esc(std::string_view data) -> std::string } return message; } + +auto RRCP::insertAfterFirstWord(const std::string& input, const std::string& toInsert) -> std::string +{ + if (toInsert.empty()) + { + return input; // Nothing to do + } + + size_t firstSpace = input.find_first_of(" \t"); // Find first whitespace + if (firstSpace == std::string::npos) + { + return input; // No spaces found, return original string + } + + // NOTE: Only if Set/Get command request, NOT for Trap commands! + size_t nextNonSpace = input.find_first_of("SG", firstSpace); + if (nextNonSpace == std::string::npos) + { + // If there's no second valid command, just return the input! + // XXX return input + " " + toInsert; + return input; + } + + // If there's valid command, just append toInsert after the first word + return input.substr(0, firstSpace + 1) + toInsert + " " + input.substr(nextNonSpace); +} + +auto RRCP::find_response_msg(std::string& response, const std::string& msg_id) -> bool +{ + // DEBUG: fmt::print("RRCP MU received({})\n", response); + // NOTE: different order for error responses like this: "E:2 10001" + auto pos = response.find(msg_id); + if (pos != std::string::npos) + { + // Remove the inserted message number for Set/Get responses. + if (boost::algorithm::starts_with(response, msg_id)) + { + // NOTE: This is an Command response with msg_id! + response = response.substr(pos + msg_id.length()); + boost::trim_left(response); + } + else + { + // NOTE: This is an Error response with msg_id! + response = response.substr(0, pos); + boost::trim_right(response); + } + return true; + } + + // NOTE: This is an Error response with or w/o a valid msg_id! + if (boost::algorithm::starts_with(response, "E:")) + { + return true; + } + + return false; +} + +extern auto RRCP::create_command_msg(const std::string& message, std::string& msg_id_str, int& msg_id) -> std::string +{ + // Insert the next message number for Set/Get request. + // But prevent to insert the msg_id for Trap commands! + auto trap_cmd = message.find(" T"); + if (trap_cmd == std::string::npos) + { + msg_id = ++msg_id % INVALID_ID; + msg_id_str = fmt::format("{}", (msg_id)); + } + std::string msg = insertAfterFirstWord(message, msg_id_str); + + // DEBUG: fmt::print("rrcp MU to send({})\n", msg); + + // Create the RRCP message frame + std::string command = char2esc(msg); + command.insert(0, 1, START); + command += STOP; + + return command; +} diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index 1b888ab..babc4b6 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -31,32 +31,14 @@ extern auto esc2char(std::string_view data) -> std::string; */ extern auto char2esc(std::string_view data) -> std::string; -// Here’s a C++17 function that inserts a given string after the first word in an input string, +// A C++17 function that inserts a given string after the first word in an input string, // where words are separated by WS -inline auto insertAfterFirstWord(const std::string& input, const std::string& toInsert) -> std::string -{ - if (toInsert.empty()) - { - return input; // Nothing to do - } - - size_t firstSpace = input.find_first_of(" \t"); // Find first whitespace - if (firstSpace == std::string::npos) - { - return input; // No spaces found, return original string - } - - // NOTE: Only if Set/Get command request, NOT for Trap commands! - size_t nextNonSpace = input.find_first_of("SG", firstSpace); - if (nextNonSpace == std::string::npos) - { - // If there's no second valid command, just return the input! - // XXX return input + " " + toInsert; - return input; - } - - // If there's valid command, just append toInsert after the first word - return input.substr(0, firstSpace + 1) + toInsert + " " + input.substr(nextNonSpace); -} +extern auto insertAfterFirstWord(const std::string& input, const std::string& toInsert) -> std::string; + +// helper which returns true if the msg with matching msg_id was found +extern auto find_response_msg(std::string& response, const std::string& msg_id) -> bool; + +// helper which returns the command msg with next valid msg_id inserted if needed +extern auto create_command_msg(const std::string& message, std::string& msg_id_str, int& msg_id) -> std::string; } // namespace RRCP From badd25523f056989f0532c36001497cac3275e6b Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 4 Apr 2025 10:15:06 +0200 Subject: [PATCH 067/120] Use Boost::ut framwork --- CMakeLists.txt | 48 +++++++------------------------------------ tests/CMakeLists.txt | 49 ++++++++++++++++++++++++++++++++++++++++++++ tests/RRCP-test.cpp | 41 ++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/RRCP-test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f9a1d8..efb741c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,10 +14,6 @@ endif() # ---- default settings ---- -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if(APPLE) execute_process( @@ -26,14 +22,20 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") COMMAND_ECHO STDOUT ) string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) + set(CMAKE_CXX_STANDARD 23) elseif(LINUX) set(LLVM_PREFIX $ENV{LLVM_ROOT}) endif() add_compile_options(-fexperimental-library) add_link_options(-L${LLVM_PREFIX}/lib/c++ -lc++experimental) +else() + set(CMAKE_CXX_STANDARD 17) endif() +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + # ---- code coverage ---- option(BUILD_TESTING "Build ctest" ${PROJECT_IS_TOP_LEVEL}) @@ -113,44 +115,8 @@ add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) target_link_libraries(rrcp_async_tcp_client PRIVATE rrcp_helper) do_test(rrcp_async_tcp_client --help Usage) -# TODO(CK): mv this part to tests/CMakeLists.txt if(BUILD_TESTING) - # XXX add_subdirectory(tests) - - # we need googletest - include(FetchContent) - FetchContent_Declare( - googletest - GIT_TAG v1.16.0 - GIT_REPOSITORY https://github.com/google/googletest.git - FIND_PACKAGE_ARGS 1.16.0 NAMES GTest COMPONENTS gmain_main - EXCLUDE_FROM_ALL - SYSTEM - ) - - # For Windows: Prevent overriding the parent project's compiler/linker settings - set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) - FetchContent_MakeAvailable(googletest) - - # add_library(base64c STATIC) - # NO! target_sources(base64c PRIVATE tests/base64.c - # PUBLIC FILE_SET HEADERS BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/tests FILES tests/base64.h) - - target_sources( - rrcp_helper - PRIVATE Base64.cpp - PUBLIC - FILE_SET HEADERS - BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} - FILES Base64.hpp - ) - target_link_libraries( - rrcp_helper # Not needed: PUBLIC Boost::beast - ) - - add_executable(Base64-test tests/Base64-test.cpp) - target_link_libraries(Base64-test PRIVATE rrcp_helper GTest::gtest_main) - add_test(NAME Base64-test COMMAND Base64-test) + add_subdirectory(tests) endif() if(APPLE AND BUILD_EXAMPLES) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..e8a6df4 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,49 @@ +include(FetchContent) + +# we use boost::ut +FetchContent_Declare( + ut + GIT_TAG v2.3.1 + GIT_REPOSITORY https://github.com/boost-ext/ut.git + FIND_PACKAGE_ARGS 2.3.1 NAMES ut + EXCLUDE_FROM_ALL + SYSTEM +) + +# TODO(CK): we stil needs googletest too! But change it to Boost::ut +FetchContent_Declare( + googletest + GIT_TAG v1.16.0 + GIT_REPOSITORY https://github.com/google/googletest.git + FIND_PACKAGE_ARGS 1.16.0 NAMES GTest COMPONENTS gmain_main + EXCLUDE_FROM_ALL + SYSTEM +) + +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest ut) + +# add_library(base64c STATIC) +# NO! target_sources(base64c PRIVATE base64.c +# PUBLIC FILE_SET HEADERS BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} FILES base64.h) + +target_sources( + rrcp_helper + PRIVATE ${CMAKE_SOURCE_DIR}/Base64.cpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_SOURCE_DIR} + FILES ${CMAKE_SOURCE_DIR}/Base64.hpp +) +target_link_libraries( + rrcp_helper # Not needed: PUBLIC Boost::beast +) + +add_executable(RRCP-test RRCP-test.cpp) +target_link_libraries(RRCP-test PRIVATE rrcp_helper Boost::ut) +add_test(NAME RRCP-test COMMAND RRCP-test) + +add_executable(Base64-test Base64-test.cpp) +target_link_libraries(Base64-test PRIVATE rrcp_helper GTest::gtest_main) +add_test(NAME Base64-test COMMAND Base64-test) diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp new file mode 100644 index 0000000..c9fba4c --- /dev/null +++ b/tests/RRCP-test.cpp @@ -0,0 +1,41 @@ +#include // import boost.ut; +#include // use std::quoted +#include + +#include "rrcp_helper.hpp" + +namespace ut = boost::ut; + +ut::suite errors = [] +{ + using namespace ut; + using namespace std::string_literals; + + "throws"_test = [] { expect(throws([] { throw 0; })); }; + + "doesn't throw"_test = [] { expect(nothrow([] {})); }; + + "basic_quoteing"_test = [] + { + std::string binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"s}; + auto quoted = RRCP::char2esc(binary); + + // NOTE: std::quoted works only with std::stringstream +#if defined(BOOST_UT_HAS_FORMAT) && defined(FIXME) + std::ostringstream binary_bin; + binary_bin << std::quoted(binary); + ut::log("{} {}\n", binary.length(), binary_bin.str()); + + std::ostringstream quoted_bin; + quoted_bin << std::quoted(quoted); + ut::log("{} {}\n", quoted.length(), quoted_bin.str()); +#endif + + expect(binary == RRCP::esc2char(quoted)); + expect(binary.length() < quoted.length()); + expect(binary.length() == 28); + expect(quoted.length() == 33); + }; +}; + +int main() {} From e6e338fa7e5f1212c7bd1d8be4613174a808e80d Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 4 Apr 2025 12:14:01 +0200 Subject: [PATCH 068/120] Add more tests --- tests/RRCP-test.cpp | 86 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index c9fba4c..a6f8113 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -1,6 +1,8 @@ #include // import boost.ut; #include // use std::quoted #include +#include +#include #include "rrcp_helper.hpp" @@ -9,15 +11,91 @@ namespace ut = boost::ut; ut::suite errors = [] { using namespace ut; - using namespace std::string_literals; + using namespace std::literals; - "throws"_test = [] { expect(throws([] { throw 0; })); }; + "find_response_msg"_test = [] + { + constexpr std::string_view expected{"gGoState"sv}; + const std::string message{"123456 gGoState"}; + std::string result{message}; + auto found = RRCP::find_response_msg(result, "123456"); + expect(found); + expect(expected == result); + }; + + "find_error_response_msg"_test = [] + { + constexpr std::string_view expected{"E:10"sv}; + const std::string message{"E:10 123456"}; + std::string result{message}; + auto found = RRCP::find_response_msg(result, "123456"); + expect(found); + expect(expected == result); + }; + + "find_no_response_msg"_test = [] + { + constexpr std::string_view expected{"d NoGo"sv}; + const std::string message{"d NoGo"}; + std::string result{message}; + auto found = RRCP::find_response_msg(result, "123456"); + expect(!found); + expect(expected == result); + }; + + "insertAfterFirstWord"_test = [] + { + const std::string command{"M:test GGoState"}; + constexpr std::string_view expected{"M:test 123456 GGoState"sv}; + auto result = RRCP::insertAfterFirstWord(command, "123456"); + expect(expected == result); + }; + + "insertEmptyStringAfterFirstWord"_test = [] + { + constexpr std::string_view expected{"M:test GGoState"sv}; + const std::string command{expected}; + auto result = RRCP::insertAfterFirstWord(command, ""); + expect(expected == result); + }; + + "insertAfterSingleWord"_test = [] + { + constexpr std::string_view expected{"E:10"sv}; + const std::string message{expected}; + auto result = RRCP::insertAfterFirstWord(message, "123456"); + expect(expected == result); + }; + + "wrong_quoted"_test = [] + { + expect(throws( + [] + { + constexpr std::string_view wrong_quoted{"\n\x1b\004\r"sv}; + auto result = RRCP::esc2char(wrong_quoted); + })); + }; + + "empty_str"_test = [] + { + expect(nothrow( + [] + { + auto result = RRCP::esc2char(""); + expect(result.empty()); + })); + }; + + "single_esc_msg"_test = [] { expect(throws([] { auto result = RRCP::esc2char("\x1b\rSINGLE_ESC"); })); }; + + "esc_as_last_msg"_test = [] { expect(throws([] { auto result = RRCP::esc2char("ESC_AS_LAST\x1b"); })); }; - "doesn't throw"_test = [] { expect(nothrow([] {})); }; + "to_short_msg"_test = [] { expect(throws([] { auto result = RRCP::esc2char("\x1b\0"s); })); }; "basic_quoteing"_test = [] { - std::string binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"s}; + constexpr std::string_view binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; auto quoted = RRCP::char2esc(binary); // NOTE: std::quoted works only with std::stringstream From 35a8a72529b41d25e22ea4f7bd9fc35f53daf3b3 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Fri, 4 Apr 2025 15:03:39 +0200 Subject: [PATCH 069/120] Cleanup unit test --- tests/RRCP-test.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index a6f8113..e5c572c 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -33,20 +33,22 @@ ut::suite errors = [] expect(expected == result); }; - "find_no_response_msg"_test = [] + "doNotfind_response_msg"_test = [] { constexpr std::string_view expected{"d NoGo"sv}; - const std::string message{"d NoGo"}; + const std::string message{expected}; std::string result{message}; auto found = RRCP::find_response_msg(result, "123456"); expect(!found); expect(expected == result); }; + // ============================================================ + "insertAfterFirstWord"_test = [] { - const std::string command{"M:test GGoState"}; constexpr std::string_view expected{"M:test 123456 GGoState"sv}; + const std::string command{"M:test GGoState"}; auto result = RRCP::insertAfterFirstWord(command, "123456"); expect(expected == result); }; @@ -59,7 +61,15 @@ ut::suite errors = [] expect(expected == result); }; - "insertAfterSingleWord"_test = [] + "doNotInsertBeforeTrapCmd"_test = [] + { + constexpr std::string_view expected{"M:test TGoState1"sv}; + const std::string command{expected}; + auto result = RRCP::insertAfterFirstWord(command, ""); + expect(expected == result); + }; + + "doNotInsertAfterSingleWord"_test = [] { constexpr std::string_view expected{"E:10"sv}; const std::string message{expected}; @@ -67,6 +77,8 @@ ut::suite errors = [] expect(expected == result); }; + // ============================================================ + "wrong_quoted"_test = [] { expect(throws( @@ -114,6 +126,7 @@ ut::suite errors = [] expect(binary.length() == 28); expect(quoted.length() == 33); }; + }; int main() {} From 8ca2128a74ed7d997924f44e89a77ec13f880a6b Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Fri, 4 Apr 2025 15:42:58 +0200 Subject: [PATCH 070/120] Add missing test --- tests/CMakeLists.txt | 2 +- tests/RRCP-test.cpp | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e8a6df4..164506d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,7 +10,7 @@ FetchContent_Declare( SYSTEM ) -# TODO(CK): we stil needs googletest too! But change it to Boost::ut +# TODO(CK): We still use googletest too! But will be changed to Boost::ut FetchContent_Declare( googletest GIT_TAG v1.16.0 diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index e5c572c..ae9e5de 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -23,6 +23,16 @@ ut::suite errors = [] expect(expected == result); }; + "doNotfind_error_response_msg"_test = [] + { + constexpr std::string_view expected{"E:1"sv}; + const std::string message{"E:1"}; + std::string result{message}; + auto found = RRCP::find_response_msg(result, "0815"); + expect(found); + expect(expected == result); + }; + "find_error_response_msg"_test = [] { constexpr std::string_view expected{"E:10"sv}; @@ -53,7 +63,7 @@ ut::suite errors = [] expect(expected == result); }; - "insertEmptyStringAfterFirstWord"_test = [] + "doNotInsertAnEmpty"_test = [] { constexpr std::string_view expected{"M:test GGoState"sv}; const std::string command{expected}; @@ -79,6 +89,19 @@ ut::suite errors = [] // ============================================================ + "create_command_msg"_test = [] + { + constexpr std::string_view expected{"\nM:test 1 SGoState1\r"sv}; + const std::string command{"M:test SGoState1"}; + std::string msg_id_str; + int counter{RRCP::INVALID_ID}; + auto result = RRCP::create_command_msg(command, msg_id_str, counter); + expect(1 == counter); + expect(expected == result); + }; + + // ============================================================ + "wrong_quoted"_test = [] { expect(throws( @@ -126,7 +149,6 @@ ut::suite errors = [] expect(binary.length() == 28); expect(quoted.length() == 33); }; - }; int main() {} From c61da3dcedfb33204ceb25fefc0aa10833ca5201 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Fri, 4 Apr 2025 15:44:20 +0200 Subject: [PATCH 071/120] Use better test name --- tests/RRCP-test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index ae9e5de..9128f7e 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -63,7 +63,7 @@ ut::suite errors = [] expect(expected == result); }; - "doNotInsertAnEmpty"_test = [] + "doNotInsertAnEmptyString"_test = [] { constexpr std::string_view expected{"M:test GGoState"sv}; const std::string command{expected}; From 0a49b6fbaa3a33fd6959778bd634f84302068f34 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 4 Apr 2025 21:29:20 +0200 Subject: [PATCH 072/120] Use std::quoted() and ut::log() --- gcovr.cfg | 11 ++++++----- tests/RRCP-test.cpp | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/gcovr.cfg b/gcovr.cfg index 63bfed4..02ce318 100644 --- a/gcovr.cfg +++ b/gcovr.cfg @@ -1,14 +1,15 @@ root = . search-path = build -filter = examples/* filter = Base64* -filter = rrcp_* +filter = RRCP* filter = async_* +filter = examples/* +filter = rrcp_* +filter = tests/* +# exclude = tests -exclude = tests - -exclude-directories = build/_deps +# exclude-directories = build/_deps exclude-directories = coverage exclude-directories = doc exclude-directories = stagedir diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index 9128f7e..46bb054 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -21,6 +21,7 @@ ut::suite errors = [] auto found = RRCP::find_response_msg(result, "123456"); expect(found); expect(expected == result); + ut::log("{} == {}\n", result, expected); }; "doNotfind_error_response_msg"_test = [] @@ -31,6 +32,7 @@ ut::suite errors = [] auto found = RRCP::find_response_msg(result, "0815"); expect(found); expect(expected == result); + ut::log("{} == {}\n", result, expected); }; "find_error_response_msg"_test = [] @@ -41,6 +43,7 @@ ut::suite errors = [] auto found = RRCP::find_response_msg(result, "123456"); expect(found); expect(expected == result); + ut::log("{} == {}\n", result, expected); }; "doNotfind_response_msg"_test = [] @@ -51,6 +54,7 @@ ut::suite errors = [] auto found = RRCP::find_response_msg(result, "123456"); expect(!found); expect(expected == result); + ut::log("{} == {}\n", result, expected); }; // ============================================================ @@ -61,6 +65,7 @@ ut::suite errors = [] const std::string command{"M:test GGoState"}; auto result = RRCP::insertAfterFirstWord(command, "123456"); expect(expected == result); + ut::log("{} == {}\n", result, expected); }; "doNotInsertAnEmptyString"_test = [] @@ -69,6 +74,7 @@ ut::suite errors = [] const std::string command{expected}; auto result = RRCP::insertAfterFirstWord(command, ""); expect(expected == result); + ut::log("{} == {}\n", result, expected); }; "doNotInsertBeforeTrapCmd"_test = [] @@ -77,6 +83,7 @@ ut::suite errors = [] const std::string command{expected}; auto result = RRCP::insertAfterFirstWord(command, ""); expect(expected == result); + ut::log("{} == {}\n", result, expected); }; "doNotInsertAfterSingleWord"_test = [] @@ -85,19 +92,24 @@ ut::suite errors = [] const std::string message{expected}; auto result = RRCP::insertAfterFirstWord(message, "123456"); expect(expected == result); + ut::log("{} == {}\n", result, expected); }; // ============================================================ "create_command_msg"_test = [] { - constexpr std::string_view expected{"\nM:test 1 SGoState1\r"sv}; - const std::string command{"M:test SGoState1"}; + constexpr std::string_view expected{"\nM:RxTx 1 SPowerLevel\"Off\"\r"sv}; + const std::string command{R"(M:RxTx SPowerLevel"Off")"}; std::string msg_id_str; int counter{RRCP::INVALID_ID}; auto result = RRCP::create_command_msg(command, msg_id_str, counter); expect(1 == counter); expect(expected == result); + + std::ostringstream quoted; + quoted << std::quoted(result.substr(1, result.length() - 1)); // NOTE: w/o START STOP + ut::log("{} == {}\n", "RRCP MU", quoted.str()); }; // ============================================================ From 4406d5975f7850f8bb98f6613998dbcf17e8e061 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sat, 5 Apr 2025 09:56:37 +0200 Subject: [PATCH 073/120] Remove stupit sample --- CMakeLists.txt | 8 ------ async_client.cpp | 63 ------------------------------------------------ gcovr.cfg | 4 +-- rrcp_helper.cpp | 2 +- 4 files changed, 3 insertions(+), 74 deletions(-) delete mode 100644 async_client.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index efb741c..c044442 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,14 +99,6 @@ if(APPLE AND BUILD_TESTING) add_executable(timer timer.cpp) target_link_libraries(timer PUBLIC Boost::headers fmt::fmt-header-only) add_test(NAME timer COMMAND timer) - - # TODO(CK): rm this stupid example? - add_executable(async_client async_client.cpp) - target_link_libraries( - async_client - PUBLIC Boost::headers fmt::fmt-header-only - ) - add_test(NAME async_client COMMAND async_client) endif() # ---- rrcp client class usage examples main ---- diff --git a/async_client.cpp b/async_client.cpp deleted file mode 100644 index b77ce02..0000000 --- a/async_client.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -const auto noop = std::bind([] {}); // NOLINT(modernize-avoid-bind) NOTE: deprecated too! CK -const std::string delimiter{"\r\n\r\n"}; - -boost::asio::io_context io_context; -boost::asio::ip::tcp::acceptor acceptor(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); -boost::asio::ip::tcp::socket socket1(io_context); -boost::asio::ip::tcp::socket socket2(io_context); - -std::string input_buffer_; -auto streambuf = boost::asio::dynamic_buffer(input_buffer_); -; - -// void do_read(); - -void handle_read(boost::system::error_code /*unused*/, std::size_t xfer) -{ - assert(streambuf.size() >= xfer); - - std::string const command{input_buffer_.data(), xfer - delimiter.length()}; - - streambuf.consume(xfer); - - std::cout << "received command: " << command << "\n" - << "streambuf contains " << streambuf.size() << " bytes.\n"; - - if (command == "cmd1") - { - boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); - } -} - -} // namespace - -auto main() -> int -{ - acceptor.async_accept(socket1, noop); - socket2.async_connect(acceptor.local_endpoint(), noop); - io_context.run(); - io_context.restart(); - - boost::asio::write(socket1, boost::asio::buffer("cmd1" + delimiter)); - boost::asio::write(socket1, boost::asio::buffer("cmd2" + delimiter)); - boost::asio::async_read_until(socket2, streambuf, delimiter, handle_read); - - io_context.run(); -} diff --git a/gcovr.cfg b/gcovr.cfg index 02ce318..c00744f 100644 --- a/gcovr.cfg +++ b/gcovr.cfg @@ -6,8 +6,8 @@ filter = RRCP* filter = async_* filter = examples/* filter = rrcp_* -filter = tests/* -# exclude = tests +# filter = tests/* +exclude = tests # exclude-directories = build/_deps exclude-directories = coverage diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 5cd79e9..b4ed1a0 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -135,7 +135,7 @@ auto RRCP::find_response_msg(std::string& response, const std::string& msg_id) - // NOTE: This is an Error response with or w/o a valid msg_id! if (boost::algorithm::starts_with(response, "E:")) { - return true; + return true; // return, this may be a response to an Trap command? } return false; From 4d68eef0ead5ea9166afd184d7da3b2ff6413217 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 6 Apr 2025 11:41:21 +0200 Subject: [PATCH 074/120] Start to refactory the rrcp_message class too --- Base64.cpp | 2 +- GNUmakefile | 1 + rrcp_helper.cpp | 2 +- rrcp_message.hpp | 113 ++++++++++++++++++++++++++++++++++++-------- tests/RRCP-test.cpp | 33 ++++++++++++- 5 files changed, 127 insertions(+), 24 deletions(-) diff --git a/Base64.cpp b/Base64.cpp index f0e4459..eb18ae3 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -41,7 +41,7 @@ class base64 static auto remove_whitespace(std::string_view input) -> std::string { auto filtered = input | std::views::filter([](unsigned char c) { return !std::isspace(c); }); - return std::string(filtered.begin(), filtered.end()); + return {filtered.begin(), filtered.end()}; } #endif diff --git a/GNUmakefile b/GNUmakefile index 05fd6d6..1871c6e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -31,6 +31,7 @@ hicpp-member-init,\ hicpp-named-parameter,\ modernize-deprecated-headers,\ modernize-loop-convert,\ +modernize-return-braced-init-list,\ modernize-use-nodiscard,\ modernize-use-std-print,\ modernize-use-trailing-return-type,\ diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index b4ed1a0..38d9b41 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -135,7 +135,7 @@ auto RRCP::find_response_msg(std::string& response, const std::string& msg_id) - // NOTE: This is an Error response with or w/o a valid msg_id! if (boost::algorithm::starts_with(response, "E:")) { - return true; // return, this may be a response to an Trap command? + return true; // return, this may be a response to an Trap command? } return false; diff --git a/rrcp_message.hpp b/rrcp_message.hpp index af7e64a..2b2aef4 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -18,34 +18,96 @@ #include #include #include -// XXX #include +#include #include "rrcp_helper.hpp" using namespace RRCP; +/** + * @class rrcp_message + * @brief A message class for handling RRCP protocol messages with a 4-byte header indicating message length. + */ class rrcp_message { public: - static constexpr std::size_t header_length = 4; - static constexpr std::size_t max_msg_length = MAX_MU_LENGTH; + static constexpr std::size_t header_length = 4; ///< Fixed length of the message header. + static constexpr std::size_t max_msg_length = MAX_MU_LENGTH; ///< Maximum allowed message body length. + /// Default constructor. rrcp_message() = default; - // TODO(CK): or better const &std::string_view? - [[nodiscard]] auto data() const -> const char* { return data_.data(); } - - auto data() -> char* { return data_.data(); } - + /** + * @brief Get a const pointer to the raw data buffer. + * @return Pointer to the beginning of the message data. + */ + [[nodiscard]] auto data() const -> const char* { return data(); } + + /** + * @brief Get a pointer to the raw data buffer. + * @return Pointer to the beginning of the message data. + */ + [[nodiscard]] auto data() -> char* { return data_.data(); } + + /** + * @brief Get the total length of the message (header + body). + * @return Message length in bytes. + */ [[nodiscard]] auto length() const -> std::size_t { return header_length + msg_length_; } - // TODO(CK): or better const &std::string_view? - [[nodiscard]] auto body() const -> const char* { return data_.data() + header_length; } - - auto body() -> char* { return data_.data() + header_length; } - + /** + * @brief Get a string view of the full message data. + * @return View of the message data. + */ + [[nodiscard]] auto get_data() const -> std::string_view { return {data(), length()}; } + + /** + * @brief Get a const pointer to the message body. + * @return Pointer to the message body. + */ + [[nodiscard]] auto body() const -> const char* { return body(); } + + /** + * @brief Get a pointer to the message body. + * @return Pointer to the message body. + */ + [[nodiscard]] auto body() -> char* { return data_.data() + header_length; } + + /** + * @brief Get the length of the message body. + * @return Length of the body in bytes. + */ [[nodiscard]] auto body_length() const -> std::size_t { return msg_length_; } + /** + * @brief Get a string view of the message body. + * @return View of the message body. + */ + [[nodiscard]] auto get_body() const -> std::string_view { return {body(), body_length()}; } + + /** + * @brief Get the decoded message as a string. + * @return Decoded string from the message body. + */ + [[nodiscard]] auto get_msg() const -> std::string { return esc2char(std::string(body(), body_length())); } + + /** + * @brief Set the message body from a string view. Encodes it and sets length. + * @param msg The message to store. + * @return True if header was successfully decoded after setting the message. + */ + [[nodiscard]] auto set_msg(std::string_view msg) -> bool + { + std::string data = char2esc(std::string(msg.data(), msg.length())); + body_length(data.length()); + std::memcpy(body(), data.c_str(), data.length()); + return decode_header(); + } + + /** + * @brief Set the message body length, constrained by the max message length. + * @param new_length New length to assign. + */ void body_length(std::size_t new_length) { msg_length_ = new_length; @@ -53,10 +115,13 @@ class rrcp_message fmt::print(stderr, "body_length({})\n", msg_length_); } + /** + * @brief Decode the message body, converting escaped characters. + * @return True if decoding was successful and not empty. + */ auto decode_body() -> bool { - // TODO(CK): or better const &std::string_view? - auto result = esc2char(std::string(body(), msg_length_)); + const std::string result = esc2char(std::string(body(), msg_length_)); if (result.length() != msg_length_) { fmt::print(stderr, "{}\n", result); @@ -68,12 +133,15 @@ class rrcp_message return !result.empty(); } + /** + * @brief Decode the message header to extract the body length. + * @return True if header is valid, false otherwise. + */ auto decode_header() -> bool { const std::string header(data_.data(), header_length); msg_length_ = std::stoul(header, nullptr, 16); - // NOLINTNEXTLINE(readability-simplify-boolean-expr) if (msg_length_ > max_msg_length) { fmt::print(stderr, "Invalid msg_length!\n"); @@ -85,10 +153,12 @@ class rrcp_message return true; } + /** + * @brief Encode the message body by escaping special characters. + */ void encode_body() { - // TODO(CK): or better const &std::string_view? - auto msg = char2esc(std::string(body(), msg_length_)); + std::string msg = char2esc(std::string(body(), msg_length_)); if (msg.length() != msg_length_) { body_length(msg.length()); @@ -96,6 +166,9 @@ class rrcp_message } } + /** + * @brief Encode the message header with the body length in hexadecimal. + */ void encode_header() { std::string header = fmt::format("{:04x}", static_cast< uint16_t >(msg_length_)); @@ -103,8 +176,8 @@ class rrcp_message } private: - std::array< char, header_length + max_msg_length > data_{}; - std::size_t msg_length_{0}; + std::array< char, header_length + max_msg_length > data_{}; ///< Internal buffer for message data. + std::size_t msg_length_{0}; ///< Length of the message body. }; #endif // RRCP_MESSAGE_HPP diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index 46bb054..2edadda 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -5,6 +5,7 @@ #include #include "rrcp_helper.hpp" +#include "rrcp_message.hpp" namespace ut = boost::ut; @@ -108,7 +109,7 @@ ut::suite errors = [] expect(expected == result); std::ostringstream quoted; - quoted << std::quoted(result.substr(1, result.length() - 1)); // NOTE: w/o START STOP + quoted << std::quoted(result.substr(1, result.length() - 1)); // NOTE: w/o START STOP ut::log("{} == {}\n", "RRCP MU", quoted.str()); }; @@ -146,7 +147,7 @@ ut::suite errors = [] auto quoted = RRCP::char2esc(binary); // NOTE: std::quoted works only with std::stringstream -#if defined(BOOST_UT_HAS_FORMAT) && defined(FIXME) +#if defined(BOOST_UT_HAS_FORMAT) && defined(FIXME) // FIXME! std::ostringstream binary_bin; binary_bin << std::quoted(binary); ut::log("{} {}\n", binary.length(), binary_bin.str()); @@ -161,6 +162,34 @@ ut::suite errors = [] expect(binary.length() == 28); expect(quoted.length() == 33); }; + + // ============================================================ + + "rrcp_message"_test = [] + { + constexpr std::string_view command{"Hallo Server"}; + constexpr std::string_view binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; + + rrcp_message msg; + msg.body_length(MAX_MU_LENGTH); + expect(msg.length() == MAX_MU_LENGTH + 4); + msg.encode_body(); + expect(msg.body_length() == MAX_MU_LENGTH); + + msg.encode_header(); + expect(msg.length() == MAX_MU_LENGTH + 4); + + expect(msg.set_msg(command)); + // FIXME: expect(msg.body_length() == command.length()); + + expect(msg.set_msg(binary)); + // FIXME: expect(msg.body_length() == 33); + + // FIXME: auto result = msg.get_msg(); + // FIXME: expect(command == result); + }; + + // ============================================================ }; int main() {} From cc46ed63df7cd7fdd90ecaf27bc9fc45c52df330 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 6 Apr 2025 11:52:51 +0200 Subject: [PATCH 075/120] Continue to refactory the rrcp_message class --- rrcp_message.hpp | 142 +++++++++++++++++++++++++++++++---------------- 1 file changed, 94 insertions(+), 48 deletions(-) diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 2b2aef4..4840280 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -2,12 +2,9 @@ // rrcp_message.hpp // ~~~~~~~~~~~~~~~~ // -// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// +// Copyright (c) 2003-2024 Christopher M. Kohlhoff // Moderniced from Claus Klein and ChatGPT +// #ifndef RRCP_MESSAGE_HPP #define RRCP_MESSAGE_HPP @@ -15,6 +12,7 @@ #include #include +#include #include #include #include @@ -26,106 +24,142 @@ using namespace RRCP; /** * @class rrcp_message - * @brief A message class for handling RRCP protocol messages with a 4-byte header indicating message length. + * @brief Encapsulates an RRCP message with methods for encoding, decoding, and accessing message content. + * + * Each message consists of a 4-byte header (hex-encoded length) and an escaped string payload. */ class rrcp_message { public: - static constexpr std::size_t header_length = 4; ///< Fixed length of the message header. - static constexpr std::size_t max_msg_length = MAX_MU_LENGTH; ///< Maximum allowed message body length. + /// Number of bytes used for the fixed-size header. + static constexpr std::size_t header_length = 4; + + /// Maximum message body length in bytes. + static constexpr std::size_t max_msg_length = MAX_MU_LENGTH; - /// Default constructor. + /** + * @brief Default constructor. + */ rrcp_message() = default; /** - * @brief Get a const pointer to the raw data buffer. - * @return Pointer to the beginning of the message data. + * @brief Construct a message from a string view. + * @param msg The message content. + * + * The message is encoded and the header is generated. Validity is tracked. */ - [[nodiscard]] auto data() const -> const char* { return data(); } + explicit rrcp_message(std::string_view msg) + { + valid_ = set_msg(msg); + if (valid_) + { + encode_header(); + } + } /** - * @brief Get a pointer to the raw data buffer. - * @return Pointer to the beginning of the message data. + * @brief Checks if the message is in a valid state. + * @return True if valid, false otherwise. + */ + [[nodiscard]] auto is_valid() const -> bool { return valid_; } + + /** + * @brief Returns a const pointer to the start of the raw message buffer. + * @return Pointer to buffer (includes header and body). + */ + [[nodiscard]] auto data() const -> const char* { return data_.data(); } + + /** + * @brief Returns a mutable pointer to the start of the raw message buffer. + * @return Pointer to buffer (includes header and body). */ [[nodiscard]] auto data() -> char* { return data_.data(); } /** - * @brief Get the total length of the message (header + body). - * @return Message length in bytes. + * @brief Returns the total length of the message (header + body). + * @return Total length in bytes. */ [[nodiscard]] auto length() const -> std::size_t { return header_length + msg_length_; } /** - * @brief Get a string view of the full message data. - * @return View of the message data. + * @brief Returns a view over the full message buffer. + * @return Message as a std::string_view. */ [[nodiscard]] auto get_data() const -> std::string_view { return {data(), length()}; } /** - * @brief Get a const pointer to the message body. - * @return Pointer to the message body. + * @brief Returns a const pointer to the message body. + * @return Pointer to the body (excludes header). */ - [[nodiscard]] auto body() const -> const char* { return body(); } + [[nodiscard]] auto body() const -> const char* { return data_.data() + header_length; } /** - * @brief Get a pointer to the message body. - * @return Pointer to the message body. + * @brief Returns a mutable pointer to the message body. + * @return Pointer to the body (excludes header). */ [[nodiscard]] auto body() -> char* { return data_.data() + header_length; } /** - * @brief Get the length of the message body. - * @return Length of the body in bytes. + * @brief Returns the length of the message body in bytes. + * @return Length of the body. */ [[nodiscard]] auto body_length() const -> std::size_t { return msg_length_; } /** - * @brief Get a string view of the message body. - * @return View of the message body. + * @brief Returns a view of the message body. + * @return Message body as a std::string_view. */ [[nodiscard]] auto get_body() const -> std::string_view { return {body(), body_length()}; } /** - * @brief Get the decoded message as a string. - * @return Decoded string from the message body. + * @brief Returns the decoded message string (after escaping is removed). + * @return Decoded message. */ [[nodiscard]] auto get_msg() const -> std::string { return esc2char(std::string(body(), body_length())); } /** - * @brief Set the message body from a string view. Encodes it and sets length. - * @param msg The message to store. - * @return True if header was successfully decoded after setting the message. + * @brief Sets the message body using the input string, escaping it as needed. + * @param msg The input message. + * @return True if header decoding is successful, false otherwise. */ [[nodiscard]] auto set_msg(std::string_view msg) -> bool { - std::string data = char2esc(std::string(msg.data(), msg.length())); + std::string data = char2esc(std::string{msg}); body_length(data.length()); - std::memcpy(body(), data.c_str(), data.length()); + + if (msg_length_ > max_msg_length) + { + return false; + } + + std::memcpy(body(), data.c_str(), msg_length_); return decode_header(); } /** - * @brief Set the message body length, constrained by the max message length. - * @param new_length New length to assign. + * @brief Sets the body length, clamping it to the maximum allowed length. + * @param new_length Desired length of body. */ void body_length(std::size_t new_length) { - msg_length_ = new_length; - msg_length_ = std::min(msg_length_, max_msg_length); + msg_length_ = std::min(new_length, max_msg_length); +#ifdef DEBUG fmt::print(stderr, "body_length({})\n", msg_length_); +#endif } /** - * @brief Decode the message body, converting escaped characters. - * @return True if decoding was successful and not empty. + * @brief Decodes the escaped content in the body. + * @return True if decoding succeeds, false if result is empty. */ auto decode_body() -> bool { const std::string result = esc2char(std::string(body(), msg_length_)); if (result.length() != msg_length_) { +#ifdef DEBUG fmt::print(stderr, "{}\n", result); - +#endif body_length(result.length()); std::memcpy(body(), result.c_str(), msg_length_); } @@ -134,8 +168,8 @@ class rrcp_message } /** - * @brief Decode the message header to extract the body length. - * @return True if header is valid, false otherwise. + * @brief Parses the 4-byte header to determine body length. + * @return True if the header is valid, false if the length is out of bounds. */ auto decode_header() -> bool { @@ -144,8 +178,9 @@ class rrcp_message if (msg_length_ > max_msg_length) { +#ifdef DEBUG fmt::print(stderr, "Invalid msg_length!\n"); - +#endif msg_length_ = 0; return false; } @@ -154,7 +189,7 @@ class rrcp_message } /** - * @brief Encode the message body by escaping special characters. + * @brief Encodes the body to escaped format and adjusts body length. */ void encode_body() { @@ -167,7 +202,7 @@ class rrcp_message } /** - * @brief Encode the message header with the body length in hexadecimal. + * @brief Encodes the message header from the current body length. */ void encode_header() { @@ -175,9 +210,20 @@ class rrcp_message std::memcpy(data_.data(), header.data(), header_length); } + /** + * @brief Clears the message buffer and resets internal state. + */ + void clear() + { + msg_length_ = 0; + valid_ = false; + data_.fill('\0'); + } + private: - std::array< char, header_length + max_msg_length > data_{}; ///< Internal buffer for message data. - std::size_t msg_length_{0}; ///< Length of the message body. + std::array< char, header_length + max_msg_length > data_{}; ///< Internal buffer for message (header + body). + std::size_t msg_length_{0}; ///< Length of message body. + bool valid_{false}; ///< Flag indicating whether the message is valid. }; #endif // RRCP_MESSAGE_HPP From 0e47b1c50dc4276223ddeb5343c2d92447095ed8 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 6 Apr 2025 18:44:40 +0200 Subject: [PATCH 076/120] Add more tests --- rrcp_message.hpp | 22 ++++++++++++++++------ tests/RRCP-test.cpp | 19 ++++++++++++------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 4840280..6eece16 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -51,9 +51,9 @@ class rrcp_message explicit rrcp_message(std::string_view msg) { valid_ = set_msg(msg); - if (valid_) + if (!valid_) { - encode_header(); + clear(); } } @@ -120,7 +120,7 @@ class rrcp_message /** * @brief Sets the message body using the input string, escaping it as needed. * @param msg The input message. - * @return True if header decoding is successful, false otherwise. + * @return True if header encoding is successful, false otherwise. */ [[nodiscard]] auto set_msg(std::string_view msg) -> bool { @@ -129,11 +129,16 @@ class rrcp_message if (msg_length_ > max_msg_length) { +#ifdef DEBUG + fmt::print(stderr, "{}: {} to long!\n", __func__, msg_length_); +#endif + clear(); return false; } std::memcpy(body(), data.c_str(), msg_length_); - return decode_header(); + encode_header(); + return valid_; } /** @@ -154,11 +159,12 @@ class rrcp_message */ auto decode_body() -> bool { + // TODO(CK): only if (msg_length != 0 && not yet done)! const std::string result = esc2char(std::string(body(), msg_length_)); if (result.length() != msg_length_) { #ifdef DEBUG - fmt::print(stderr, "{}\n", result); + fmt::print(stderr, "{}: {}\n", __func__, result); #endif body_length(result.length()); std::memcpy(body(), result.c_str(), msg_length_); @@ -173,13 +179,14 @@ class rrcp_message */ auto decode_header() -> bool { + // TODO(CK): only if msg_length != 0 const std::string header(data_.data(), header_length); msg_length_ = std::stoul(header, nullptr, 16); if (msg_length_ > max_msg_length) { #ifdef DEBUG - fmt::print(stderr, "Invalid msg_length!\n"); + fmt::print(stderr, "{}: Invalid msg_length {}!\n", __func__, header); #endif msg_length_ = 0; return false; @@ -193,6 +200,7 @@ class rrcp_message */ void encode_body() { + // TODO(CK): only if (msg_length != 0 && not yet done)! std::string msg = char2esc(std::string(body(), msg_length_)); if (msg.length() != msg_length_) { @@ -206,8 +214,10 @@ class rrcp_message */ void encode_header() { + // TODO(CK): only if msg_length != 0 std::string header = fmt::format("{:04x}", static_cast< uint16_t >(msg_length_)); std::memcpy(data_.data(), header.data(), header_length); + valid_ = true; } /** diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index 2edadda..44dc372 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -4,6 +4,8 @@ #include #include +#define DEBUG + #include "rrcp_helper.hpp" #include "rrcp_message.hpp" @@ -180,13 +182,16 @@ ut::suite errors = [] expect(msg.length() == MAX_MU_LENGTH + 4); expect(msg.set_msg(command)); - // FIXME: expect(msg.body_length() == command.length()); - - expect(msg.set_msg(binary)); - // FIXME: expect(msg.body_length() == 33); - - // FIXME: auto result = msg.get_msg(); - // FIXME: expect(command == result); + expect(msg.is_valid()); + expect(msg.body_length() == command.length()); + auto result = msg.get_msg(); + expect(command == result); + + rrcp_message msg2(binary); + expect(msg2.is_valid()); + expect(msg2.body_length() == 33); + result = msg2.get_msg(); + expect(binary == result); }; // ============================================================ From afb5662538d3af3c27dc1283d2f3b07d3da004af Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 6 Apr 2025 19:41:22 +0200 Subject: [PATCH 077/120] Tested with docker and ubuntu-20.04 --- CMakeLists.txt | 31 +++++++++++++++++++++++++------ GNUmakefile | 2 +- tests/CMakeLists.txt | 16 ++++++++++++++-- tests/RRCP-test.cpp | 18 ++++++++++++++++++ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c044442..647e6e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,13 +4,23 @@ project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) # ---- add dependencies ---- +include(FetchContent) + find_package(Threads) if(NOT TARGET Boost::headers) find_package(Boost 1.71 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) endif() -if(NOT TARGET fmt::fmt-header-only) - find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) -endif() + +FetchContent_Declare( + fmt + GIT_TAG 11.1.4 + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + FIND_PACKAGE_ARGS 11.1.4 NAMES fmt + EXCLUDE_FROM_ALL + SYSTEM +) + +FetchContent_MakeAvailable(fmt) # ---- default settings ---- @@ -69,7 +79,10 @@ endfunction() # ---- server needed for tests ---- add_executable(async_tcp_echo_server examples/async_tcp_echo_server.cpp) -target_link_libraries(async_tcp_echo_server PUBLIC Boost::headers) +target_link_libraries( + async_tcp_echo_server + PUBLIC Threads::Threads Boost::headers +) do_test(async_tcp_echo_server "" port) # ---- rrcp class sources and helpers as a library ---- @@ -80,7 +93,10 @@ target_sources( PRIVATE rrcp_helper.cpp PUBLIC FILE_SET HEADERS FILES async_rrcp_client.hpp rrcp_helper.hpp ) -target_link_libraries(rrcp_helper PUBLIC Boost::headers fmt::fmt-header-only) +target_link_libraries( + rrcp_helper + PUBLIC Threads::Threads Boost::headers fmt::fmt-header-only +) add_executable(async_tcp_echo_client examples/async_tcp_echo_client.cpp) target_link_libraries(async_tcp_echo_client PRIVATE rrcp_helper) @@ -97,7 +113,10 @@ do_test(rrcp_client --help Usage) if(APPLE AND BUILD_TESTING) # TODO(CK): mv to examples too! add_executable(timer timer.cpp) - target_link_libraries(timer PUBLIC Boost::headers fmt::fmt-header-only) + target_link_libraries( + timer + PUBLIC Threads::Threads Boost::headers fmt::fmt-header-only + ) add_test(NAME timer COMMAND timer) endif() diff --git a/GNUmakefile b/GNUmakefile index 1871c6e..7968f8b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -18,7 +18,7 @@ distclean: # XXX clean rm -rf build coverage/* *~ ctags build: CMakeLists.txt - cmake -S . -B $@ -D CMAKE_BUILD_TYPE=Debug + cmake -S . -B $@ -G Ninja -D CMAKE_BUILD_TYPE=Debug # --fresh check: all run-clang-tidy -p build *.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 164506d..9016d8c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,14 @@ include(FetchContent) +FetchContent_Declare( + fmt + GIT_TAG 11.1.4 + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + FIND_PACKAGE_ARGS 11.1.4 NAMES fmt + EXCLUDE_FROM_ALL + SYSTEM +) + # we use boost::ut FetchContent_Declare( ut @@ -22,7 +31,7 @@ FetchContent_Declare( # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(googletest ut) +FetchContent_MakeAvailable(fmt googletest ut) # add_library(base64c STATIC) # NO! target_sources(base64c PRIVATE base64.c @@ -37,7 +46,10 @@ target_sources( FILES ${CMAKE_SOURCE_DIR}/Base64.hpp ) target_link_libraries( - rrcp_helper # Not needed: PUBLIC Boost::beast + rrcp_helper + PUBLIC + fmt::fmt-header-only + Boost::headers # Not needed: PUBLIC Boost::beast ) add_executable(RRCP-test RRCP-test.cpp) diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index 44dc372..38f0580 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -24,7 +24,9 @@ ut::suite errors = [] auto found = RRCP::find_response_msg(result, "123456"); expect(found); expect(expected == result); +#if defined(BOOST_UT_HAS_FORMAT) ut::log("{} == {}\n", result, expected); +#endif }; "doNotfind_error_response_msg"_test = [] @@ -35,7 +37,9 @@ ut::suite errors = [] auto found = RRCP::find_response_msg(result, "0815"); expect(found); expect(expected == result); +#if defined(BOOST_UT_HAS_FORMAT) ut::log("{} == {}\n", result, expected); +#endif }; "find_error_response_msg"_test = [] @@ -46,7 +50,9 @@ ut::suite errors = [] auto found = RRCP::find_response_msg(result, "123456"); expect(found); expect(expected == result); +#if defined(BOOST_UT_HAS_FORMAT) ut::log("{} == {}\n", result, expected); +#endif }; "doNotfind_response_msg"_test = [] @@ -57,7 +63,9 @@ ut::suite errors = [] auto found = RRCP::find_response_msg(result, "123456"); expect(!found); expect(expected == result); +#if defined(BOOST_UT_HAS_FORMAT) ut::log("{} == {}\n", result, expected); +#endif }; // ============================================================ @@ -68,7 +76,9 @@ ut::suite errors = [] const std::string command{"M:test GGoState"}; auto result = RRCP::insertAfterFirstWord(command, "123456"); expect(expected == result); +#if defined(BOOST_UT_HAS_FORMAT) ut::log("{} == {}\n", result, expected); +#endif }; "doNotInsertAnEmptyString"_test = [] @@ -77,7 +87,9 @@ ut::suite errors = [] const std::string command{expected}; auto result = RRCP::insertAfterFirstWord(command, ""); expect(expected == result); +#if defined(BOOST_UT_HAS_FORMAT) ut::log("{} == {}\n", result, expected); +#endif }; "doNotInsertBeforeTrapCmd"_test = [] @@ -86,7 +98,9 @@ ut::suite errors = [] const std::string command{expected}; auto result = RRCP::insertAfterFirstWord(command, ""); expect(expected == result); +#if defined(BOOST_UT_HAS_FORMAT) ut::log("{} == {}\n", result, expected); +#endif }; "doNotInsertAfterSingleWord"_test = [] @@ -95,7 +109,9 @@ ut::suite errors = [] const std::string message{expected}; auto result = RRCP::insertAfterFirstWord(message, "123456"); expect(expected == result); +#if defined(BOOST_UT_HAS_FORMAT) ut::log("{} == {}\n", result, expected); +#endif }; // ============================================================ @@ -112,7 +128,9 @@ ut::suite errors = [] std::ostringstream quoted; quoted << std::quoted(result.substr(1, result.length() - 1)); // NOTE: w/o START STOP +#if defined(BOOST_UT_HAS_FORMAT) ut::log("{} == {}\n", "RRCP MU", quoted.str()); +#endif }; // ============================================================ From f06df4fea014a51dcd9e815ec0fc6fea49304a8c Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Mon, 7 Apr 2025 19:44:46 +0200 Subject: [PATCH 078/120] Add more tests --- rrcp_message.hpp | 2 +- tests/RRCP-test.cpp | 50 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/rrcp_message.hpp b/rrcp_message.hpp index 6eece16..b9c789c 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -127,7 +127,7 @@ class rrcp_message std::string data = char2esc(std::string{msg}); body_length(data.length()); - if (msg_length_ > max_msg_length) + if (data.length() > max_msg_length) { #ifdef DEBUG fmt::print(stderr, "{}: {} to long!\n", __func__, msg_length_); diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index 38f0580..5ded5a4 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -187,28 +187,64 @@ ut::suite errors = [] "rrcp_message"_test = [] { - constexpr std::string_view command{"Hallo Server"}; - constexpr std::string_view binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; - rrcp_message msg; msg.body_length(MAX_MU_LENGTH); expect(msg.length() == MAX_MU_LENGTH + 4); msg.encode_body(); expect(msg.body_length() == MAX_MU_LENGTH); + // XXX expect(msg.is_valid()); + expect(msg.decode_body()); msg.encode_header(); expect(msg.length() == MAX_MU_LENGTH + 4); + expect(msg.decode_header()); + + msg.clear(); + expect(!msg.is_valid()); + expect(msg.get_body().empty()); + expect(msg.get_data().length() == 4); + }; + + "rrcp_message_empty"_test = [] + { + rrcp_message msg; + msg.body_length(0); + expect(msg.length() == 4); + msg.encode_body(); + expect(msg.body_length() == 0); + expect(!msg.is_valid()); + expect(!msg.decode_body()); + + // FIXME: expect(nothrow([&] {msg.decode_header();} )); + }; + "rrcp_message_to_long"_test = [] + { + const std::string invalid(MAX_MU_LENGTH, '\n'); + rrcp_message msg(invalid); + expect(!msg.is_valid()); + expect(!msg.set_msg(invalid)); + }; + + "rrcp_message_text"_test = [] + { + constexpr std::string_view command{"Hallo Server"}; + rrcp_message msg; expect(msg.set_msg(command)); expect(msg.is_valid()); expect(msg.body_length() == command.length()); + expect(msg.body() == command); auto result = msg.get_msg(); expect(command == result); + }; - rrcp_message msg2(binary); - expect(msg2.is_valid()); - expect(msg2.body_length() == 33); - result = msg2.get_msg(); + "rrcp_message_binary"_test = [] + { + constexpr std::string_view binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; + rrcp_message msg(binary); + expect(msg.is_valid()); + expect(msg.body_length() == 33); + auto result = msg.get_msg(); expect(binary == result); }; From 5db00649bb263ff9f62ad72c354081565aad3037 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Mon, 14 Apr 2025 14:54:04 +0200 Subject: [PATCH 079/120] Use boost::signals2 --- async_rrcp_client.hpp | 12 +++++++++--- rrcp_async_tcp_client.cpp | 8 ++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index d76d9a3..f6d799b 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -25,9 +25,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -76,6 +78,9 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client }); } + using signal_string_type = boost::signals2::signal< void(std::string) >; + void register_trap_hander(std::function< void(std::string) > handler) { trap_handler_.connect(handler); } + [[nodiscard]] auto connected() const -> bool { return connected_; } // This function write the message into the send msg queue and starts the write actor. @@ -164,7 +169,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (!ec) { //========================== RRCP ============================ - std::string const line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP + std::string line = esc2char(self->input_buffer_.substr(1, length - 1)); // w/o START, STOP self->input_buffer_.erase(0, length); //========================== END ============================ @@ -173,8 +178,8 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (boost::algorithm::starts_with(line, "d")) // Trap data message { // Handle trap data messages - fmt::print(stderr, "Ignored trap data: {}\n", line); // WARNING - fmt::print("{}\n", line); + fmt::print(stderr, "trap data: {}\n", line); // TRACE + self->trap_handler_(line); } else if (!boost::algorithm::starts_with(line, "gPing")) { @@ -272,6 +277,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client int msg_id_{10000}; bool connected_{false}; bool stopped_{false}; + signal_string_type trap_handler_; }; } // namespace RRCP diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 97dbd50..388d68d 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -23,6 +23,13 @@ #include "async_rrcp_client.hpp" +namespace +{ + +void print(std::string msg) { fmt::print("{}\n", msg); } + +} // namespace + auto main(int argc, char* argv[]) -> int { if (argc != 3) @@ -39,6 +46,7 @@ auto main(int argc, char* argv[]) -> int tcp::resolver resolver(io_context); auto c = std::make_shared< async_rrcp_client >(io_context); + c->register_trap_hander(&print); c->start(resolver.resolve(argv[1], argv[2])); std::thread io_thread([&io_context]() { io_context.run(); }); From 894e3d3bbe87dbac0e1122936619924f99e5cdc8 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Mon, 14 Apr 2025 17:33:32 +0200 Subject: [PATCH 080/120] Fix typos --- .clang-tidy | 1 + CMakeLists.txt | 2 +- async_rrcp_client.hpp | 7 ++++--- rrcp_async_tcp_client.cpp | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 589486b..e49b50d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -11,6 +11,7 @@ cppcoreguidelines-*,\ -cppcoreguidelines-init-variables,\ -cppcoreguidelines-macro-*,\ -cppcoreguidelines-owning-memory,\ +-cppcoreguidelines-prefer-member-initializer,\ -cppcoreguidelines-pro-bounds-pointer-arithmetic,\ hicpp-*,\ misc-*,\ diff --git a/CMakeLists.txt b/CMakeLists.txt index 647e6e8..2fbdd1e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) find_package(Threads) if(NOT TARGET Boost::headers) - find_package(Boost 1.71 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) + find_package(Boost 1.71 COMPONENTS headers signals2 REQUIRED HINTS $ENV{HOME}/.local) endif() FetchContent_Declare( diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index f6d799b..bd9c1e4 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -41,7 +41,6 @@ namespace RRCP using boost::asio::ip::tcp; using namespace std::chrono_literals; -using message_queue = std::deque< std::string >; constexpr size_t max_length = 65432; constexpr auto timeout_duration = 3s; @@ -49,6 +48,9 @@ constexpr auto heartbeat_interval = 10s; class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client > { + using message_queue = std::deque< std::string >; + using signal_string_type = boost::signals2::signal< void(std::string) >; + public: explicit async_rrcp_client(boost::asio::io_context& io_context) : io_context_(io_context), socket_(io_context), deadline_(io_context), heartbeat_timer_(io_context) @@ -78,8 +80,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client }); } - using signal_string_type = boost::signals2::signal< void(std::string) >; - void register_trap_hander(std::function< void(std::string) > handler) { trap_handler_.connect(handler); } + void register_trap_handler(const std::function< void(std::string) >& handler) { trap_handler_.connect(handler); } [[nodiscard]] auto connected() const -> bool { return connected_; } diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 388d68d..ccaac89 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -46,7 +46,7 @@ auto main(int argc, char* argv[]) -> int tcp::resolver resolver(io_context); auto c = std::make_shared< async_rrcp_client >(io_context); - c->register_trap_hander(&print); + c->register_trap_handler(&print); c->start(resolver.resolve(argv[1], argv[2])); std::thread io_thread([&io_context]() { io_context.run(); }); From d4e04fc6618c0e4b8c77e4ed80d081f62e17aa36 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Tue, 15 Apr 2025 12:50:40 +0200 Subject: [PATCH 081/120] Change readability-identifier-naming rules --- .clang-tidy | 19 +++-- Base64.cpp | 12 +-- Base64.hpp | 20 ++--- CMakeLists.txt | 10 ++- async_rrcp_client.hpp | 28 +++--- examples/async_tcp_echo_client.cpp | 2 +- examples/blocking_tcp_echo_client.cpp | 2 +- rrcp_async_tcp_client.cpp | 6 +- rrcp_client.cpp | 2 +- rrcp_helper.cpp | 10 +-- rrcp_helper.hpp | 4 +- rrcp_message.hpp | 24 +++--- tests/Base64-test.cpp | 100 +++++++++++----------- tests/RRCP-test.cpp | 118 +++++++++++++------------- 14 files changed, 183 insertions(+), 174 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index e49b50d..ecd16a2 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -25,8 +25,8 @@ performance-*,\ -performance-enum-size,\ portability-*,\ readability-*,\ --readability-identifier-length,\ --readability-identifier-naming,\ +readability-identifier-length,\ +readability-identifier-naming,\ -*magic-numbers,\ -*avoid-c-arrays,\ " @@ -36,11 +36,14 @@ FormatStyle: file User: clausklein # options: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals CheckOptions: - - { key: readability-identifier-naming.NamespaceCase, value: lower_case } - - { key: readability-identifier-naming.ClassCase, value: CamelCase } - - { key: readability-identifier-naming.MethodCase, value: lower_case } - - { key: readability-identifier-naming.MemberCase, value: lower_case } - - { key: readability-identifier-naming.MemberSuffix, value: _ } - - { key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals, value: true } + - { key: readability-identifier-naming.NamespaceCase, value: lower_case } + - { key: readability-identifier-naming.ClassCase, value: lower_case } + - { key: readability-identifier-naming.MethodCase, value: lower_case } + - { key: readability-identifier-naming.MemberCase, value: lower_case } + - { key: readability-identifier-naming.MemberSuffix, value: _ } + - { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE } + - { key: readability-identifier-length.MinimumVariableNameLength, value: 2 } + - { key: readability-identifier-length.MinimumParameterNameLength, value: 1 } + - { key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals, value: true } ... diff --git a/Base64.cpp b/Base64.cpp index eb18ae3..643a568 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -81,10 +81,10 @@ class base64 #endif -namespace RRCP::Common +namespace rrcp::common { -auto Base64::encode(std::string_view data) -> std::string +auto base64::encode(std::string_view data) -> std::string { if (data.empty()) { @@ -93,7 +93,7 @@ auto Base64::encode(std::string_view data) -> std::string #ifdef USE_BOOST_BEAST - return base64::base64_encode(data); + return ::base64::base64_encode(data); #else @@ -156,11 +156,11 @@ auto Base64::encode(std::string_view data) -> std::string #endif } -auto Base64::decode(std::string_view in) -> std::string +auto base64::decode(std::string_view in) -> std::string { #ifdef USE_BOOST_BEAST - return base64::base64_decode(in); + return ::base64::base64_decode(in); #else @@ -246,4 +246,4 @@ auto Base64::base64CharValue(char c) const -> std::uint8_t } #endif -} // namespace RRCP::Common +} // namespace rrcp::common diff --git a/Base64.hpp b/Base64.hpp index c88fa71..60a5849 100644 --- a/Base64.hpp +++ b/Base64.hpp @@ -5,27 +5,27 @@ #include #include -namespace RRCP::Common +namespace rrcp::common { -class Base64 +class base64 { public: /** * Constructor for the Base64 class. */ - Base64() = default; + base64() = default; /** * Destructor for the Base64 class. */ - ~Base64() = default; + ~base64() = default; /** * Set the line break flag for encoding. * @param lbrk If true, the encoded string will have a maximum line length of 80 characters. */ - void setLineBreak(bool lbrk) { encodeWithLinebreak_ = lbrk; } + void set_line_break(bool lbrk) { encode_with_linebreak_ = lbrk; } /** * Encode binary data to base64. @@ -47,19 +47,19 @@ class Base64 * @param c The character to check. * @return True if the character is a valid Base64 character, false otherwise. */ - [[nodiscard]] auto isBase64Char(char c) const -> bool; + [[nodiscard]] auto is_base64_char(char c) const -> bool; /** * Get the value of a Base64 character. * @param c The Base64 character. * @return The value of the character (0-63). */ - [[nodiscard]] auto base64CharValue(char c) const -> std::uint8_t; + [[nodiscard]] auto base64_char_value(char c) const -> std::uint8_t; - const std::string_view BaseChars_{"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"}; - bool encodeWithLinebreak_{false}; + const std::string_view base_chars_{"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"}; + bool encode_with_linebreak_{false}; }; -} // namespace RRCP::Common +} // namespace rrcp::common #endif // BASE64_HPP diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fbdd1e..24c7298 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,16 +8,22 @@ include(FetchContent) find_package(Threads) if(NOT TARGET Boost::headers) - find_package(Boost 1.71 COMPONENTS headers signals2 REQUIRED HINTS $ENV{HOME}/.local) + find_package( + Boost + 1.71 + COMPONENTS headers signals2 + REQUIRED + HINTS $ENV{HOME}/.local + ) endif() FetchContent_Declare( fmt GIT_TAG 11.1.4 GIT_REPOSITORY https://github.com/fmtlib/fmt.git - FIND_PACKAGE_ARGS 11.1.4 NAMES fmt EXCLUDE_FROM_ALL SYSTEM + FIND_PACKAGE_ARGS 11.1.4 NAMES fmt HINTS $ENV{HOME}/.local ) FetchContent_MakeAvailable(fmt) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index bd9c1e4..ddd72b6 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -36,20 +36,20 @@ #include "rrcp_helper.hpp" -namespace RRCP +namespace rrcp { using boost::asio::ip::tcp; using namespace std::chrono_literals; -constexpr size_t max_length = 65432; -constexpr auto timeout_duration = 3s; -constexpr auto heartbeat_interval = 10s; +constexpr size_t MAX_LENGTH = 65432; +constexpr auto TIMEOUT_DURATION = 3s; +constexpr auto HEARTBEAT_INTERVAL = 10s; class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client > { - using message_queue = std::deque< std::string >; - using signal_string_type = boost::signals2::signal< void(std::string) >; + using message_queue = std::deque< std::string >; + using signal_string_type = boost::signals2::signal< void(std::string) >; public: explicit async_rrcp_client(boost::asio::io_context& io_context) @@ -60,7 +60,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client void start(const tcp::resolver::results_type& endpoints) { - deadline_.expires_after(timeout_duration); + deadline_.expires_after(TIMEOUT_DURATION); check_deadline(); boost::asio::async_connect(socket_, endpoints, @@ -98,11 +98,11 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client return {}; } fmt::print(stderr, "Client is not connected yet.\n"); // TRACE - std::this_thread::sleep_for(timeout_duration); + std::this_thread::sleep_for(TIMEOUT_DURATION); } std::string msg_id_str; - auto command = RRCP::create_command_msg(message, msg_id_str, msg_id_); + auto command = rrcp::create_command_msg(message, msg_id_str, msg_id_); boost::asio::post(io_context_, [this, command]() @@ -111,7 +111,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client write_msgs_.push_back(command); if (!write_in_progress) { - deadline_.expires_after(timeout_duration); + deadline_.expires_after(TIMEOUT_DURATION); do_write(); } }); @@ -139,7 +139,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (!response.empty()) { // helper which returns true if the msg with matching msg_id was found - if (RRCP::find_response_msg(response, msg_id)) + if (rrcp::find_response_msg(response, msg_id)) { break; } @@ -190,7 +190,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } //========================== END ============================ - self->deadline_.expires_after(heartbeat_interval + timeout_duration); + self->deadline_.expires_after(HEARTBEAT_INTERVAL + TIMEOUT_DURATION); self->do_read(); } else @@ -240,7 +240,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { if (!ec) { - self->heartbeat_timer_.expires_after(heartbeat_interval); + self->heartbeat_timer_.expires_after(HEARTBEAT_INTERVAL); self->heartbeat_timer_.async_wait([self](const boost::system::error_code&) { self->send_heartbeat(); }); } else @@ -281,4 +281,4 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client signal_string_type trap_handler_; }; -} // namespace RRCP +} // namespace rrcp diff --git a/examples/async_tcp_echo_client.cpp b/examples/async_tcp_echo_client.cpp index ae5a5d4..ecea0a9 100644 --- a/examples/async_tcp_echo_client.cpp +++ b/examples/async_tcp_echo_client.cpp @@ -32,7 +32,7 @@ using boost::asio::ip::tcp; using namespace std::chrono_literals; using message_queue = std::deque< std::string >; -using namespace RRCP; +using namespace rrcp; constexpr size_t max_length = 65432; constexpr auto timeout_duration = 1s; diff --git a/examples/blocking_tcp_echo_client.cpp b/examples/blocking_tcp_echo_client.cpp index 5920ef7..2f26f6d 100644 --- a/examples/blocking_tcp_echo_client.cpp +++ b/examples/blocking_tcp_echo_client.cpp @@ -32,7 +32,7 @@ auto main(int argc, char* argv[]) -> int { try { - using namespace RRCP; + using namespace rrcp; if (argc != 3) { diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index ccaac89..aa836fe 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -40,7 +40,7 @@ auto main(int argc, char* argv[]) -> int try { - using namespace RRCP; + using namespace rrcp; boost::asio::io_context io_context; tcp::resolver resolver(io_context); @@ -51,7 +51,7 @@ auto main(int argc, char* argv[]) -> int std::thread io_thread([&io_context]() { io_context.run(); }); - std::this_thread::sleep_for(timeout_duration); // NOTE: only for gcov results! CK + std::this_thread::sleep_for(TIMEOUT_DURATION); // NOTE: only for gcov results! CK for (std::string line; c->connected() && std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) { const std::string::size_type sz = line.find("//"); @@ -74,7 +74,7 @@ auto main(int argc, char* argv[]) -> int const auto response = c->write(line); fmt::print("{}\n", response); } - std::this_thread::sleep_for(heartbeat_interval); // NOTE: only for gcov results! CK + std::this_thread::sleep_for(HEARTBEAT_INTERVAL); // NOTE: only for gcov results! CK c->stop(); io_thread.join(); diff --git a/rrcp_client.cpp b/rrcp_client.cpp index 3a8d858..a78dbea 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -80,7 +80,7 @@ class rrcp_client void do_read_header() { - boost::asio::async_read(socket_, boost::asio::buffer(read_msg_.data(), rrcp_message::header_length), + boost::asio::async_read(socket_, boost::asio::buffer(read_msg_.data(), rrcp_message::HEADER_LENGTH), [this](boost::system::error_code ec, std::size_t /*length*/) { if (!ec && read_msg_.decode_header()) diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 38d9b41..98210e9 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -14,7 +14,7 @@ constexpr char REPLACE_LF = 0x01; constexpr char REPLACE_CR = 0x02; constexpr char REPLACE_ESC = 0x03; -auto RRCP::esc2char(std::string_view data) -> std::string +auto rrcp::esc2char(std::string_view data) -> std::string { std::string message; auto len = data.size(); @@ -56,7 +56,7 @@ auto RRCP::esc2char(std::string_view data) -> std::string return message; } -auto RRCP::char2esc(std::string_view data) -> std::string +auto rrcp::char2esc(std::string_view data) -> std::string { std::string message; for (char const c : data) @@ -83,7 +83,7 @@ auto RRCP::char2esc(std::string_view data) -> std::string return message; } -auto RRCP::insertAfterFirstWord(const std::string& input, const std::string& toInsert) -> std::string +auto rrcp::insertAfterFirstWord(const std::string& input, const std::string& toInsert) -> std::string { if (toInsert.empty()) { @@ -109,7 +109,7 @@ auto RRCP::insertAfterFirstWord(const std::string& input, const std::string& toI return input.substr(0, firstSpace + 1) + toInsert + " " + input.substr(nextNonSpace); } -auto RRCP::find_response_msg(std::string& response, const std::string& msg_id) -> bool +auto rrcp::find_response_msg(std::string& response, const std::string& msg_id) -> bool { // DEBUG: fmt::print("RRCP MU received({})\n", response); // NOTE: different order for error responses like this: "E:2 10001" @@ -141,7 +141,7 @@ auto RRCP::find_response_msg(std::string& response, const std::string& msg_id) - return false; } -extern auto RRCP::create_command_msg(const std::string& message, std::string& msg_id_str, int& msg_id) -> std::string +extern auto rrcp::create_command_msg(const std::string& message, std::string& msg_id_str, int& msg_id) -> std::string { // Insert the next message number for Set/Get request. // But prevent to insert the msg_id for Trap commands! diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index babc4b6..13fad00 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -3,7 +3,7 @@ #include #include -namespace RRCP +namespace rrcp { constexpr const char START{0x0A}; // \n @@ -41,4 +41,4 @@ extern auto find_response_msg(std::string& response, const std::string& msg_id) // helper which returns the command msg with next valid msg_id inserted if needed extern auto create_command_msg(const std::string& message, std::string& msg_id_str, int& msg_id) -> std::string; -} // namespace RRCP +} // namespace rrcp diff --git a/rrcp_message.hpp b/rrcp_message.hpp index b9c789c..239050b 100644 --- a/rrcp_message.hpp +++ b/rrcp_message.hpp @@ -20,7 +20,7 @@ #include "rrcp_helper.hpp" -using namespace RRCP; +using namespace rrcp; /** * @class rrcp_message @@ -32,10 +32,10 @@ class rrcp_message { public: /// Number of bytes used for the fixed-size header. - static constexpr std::size_t header_length = 4; + static constexpr std::size_t HEADER_LENGTH = 4; /// Maximum message body length in bytes. - static constexpr std::size_t max_msg_length = MAX_MU_LENGTH; + static constexpr std::size_t MAX_MSG_LENGTH = MAX_MU_LENGTH; /** * @brief Default constructor. @@ -79,7 +79,7 @@ class rrcp_message * @brief Returns the total length of the message (header + body). * @return Total length in bytes. */ - [[nodiscard]] auto length() const -> std::size_t { return header_length + msg_length_; } + [[nodiscard]] auto length() const -> std::size_t { return HEADER_LENGTH + msg_length_; } /** * @brief Returns a view over the full message buffer. @@ -91,13 +91,13 @@ class rrcp_message * @brief Returns a const pointer to the message body. * @return Pointer to the body (excludes header). */ - [[nodiscard]] auto body() const -> const char* { return data_.data() + header_length; } + [[nodiscard]] auto body() const -> const char* { return data_.data() + HEADER_LENGTH; } /** * @brief Returns a mutable pointer to the message body. * @return Pointer to the body (excludes header). */ - [[nodiscard]] auto body() -> char* { return data_.data() + header_length; } + [[nodiscard]] auto body() -> char* { return data_.data() + HEADER_LENGTH; } /** * @brief Returns the length of the message body in bytes. @@ -127,7 +127,7 @@ class rrcp_message std::string data = char2esc(std::string{msg}); body_length(data.length()); - if (data.length() > max_msg_length) + if (data.length() > MAX_MSG_LENGTH) { #ifdef DEBUG fmt::print(stderr, "{}: {} to long!\n", __func__, msg_length_); @@ -147,7 +147,7 @@ class rrcp_message */ void body_length(std::size_t new_length) { - msg_length_ = std::min(new_length, max_msg_length); + msg_length_ = std::min(new_length, MAX_MSG_LENGTH); #ifdef DEBUG fmt::print(stderr, "body_length({})\n", msg_length_); #endif @@ -180,10 +180,10 @@ class rrcp_message auto decode_header() -> bool { // TODO(CK): only if msg_length != 0 - const std::string header(data_.data(), header_length); + const std::string header(data_.data(), HEADER_LENGTH); msg_length_ = std::stoul(header, nullptr, 16); - if (msg_length_ > max_msg_length) + if (msg_length_ > MAX_MSG_LENGTH) { #ifdef DEBUG fmt::print(stderr, "{}: Invalid msg_length {}!\n", __func__, header); @@ -216,7 +216,7 @@ class rrcp_message { // TODO(CK): only if msg_length != 0 std::string header = fmt::format("{:04x}", static_cast< uint16_t >(msg_length_)); - std::memcpy(data_.data(), header.data(), header_length); + std::memcpy(data_.data(), header.data(), HEADER_LENGTH); valid_ = true; } @@ -231,7 +231,7 @@ class rrcp_message } private: - std::array< char, header_length + max_msg_length > data_{}; ///< Internal buffer for message (header + body). + std::array< char, HEADER_LENGTH + MAX_MSG_LENGTH > data_{}; ///< Internal buffer for message (header + body). std::size_t msg_length_{0}; ///< Length of message body. bool valid_{false}; ///< Flag indicating whether the message is valid. }; diff --git a/tests/Base64-test.cpp b/tests/Base64-test.cpp index 475c664..ebb21bf 100644 --- a/tests/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -6,7 +6,7 @@ Input data with invalid characters (e.g., non-ASCII characters) Input data with padding errors (e.g., incorrect number of padding characters) Input data with encoding errors (e.g., incorrect encoding scheme) -By covering these edge cases, you can ensure that your Base64 class is robust and reliable. +By covering these edge cases, you can ensure that your base64 class is robust and reliable. ***/ #include "Base64.hpp" @@ -26,7 +26,7 @@ extern "C" using namespace std::string_literals; -using RRCP::Common::Base64; +using rrcp::common::base64; #define TEST_RANDOM_VALUES @@ -37,8 +37,8 @@ namespace // see https://datatracker.ietf.org/doc/html/rfc4648#section-10 struct testpattern_t { - const char *bin; - const char *encoded; + const char *bin_; + const char *encoded_; } testpattern[] = { // {"", ""}, // {"f", "Zg=="}, // @@ -67,18 +67,18 @@ struct testpattern_t TEST(Base64Test, encoding) { - Base64 base64; - base64.setLineBreak(false); + base64 base64; + base64.set_line_break(false); std::array< char, 54 > text{}; size_t i = 0; - while (testpattern[i].bin != nullptr) + while (testpattern[i].bin_ != nullptr) { - const std::string binary(testpattern[i].bin); - const std::string encoded{testpattern[i].encoded}; + const std::string binary(testpattern[i].bin_); + const std::string encoded{testpattern[i].encoded_}; fmt::println("'{}':\t{}", binary, encoded); - const std::string base64_encoded = base64.encode(binary); + const std::string base64_encoded = rrcp::common::base64::encode(binary); EXPECT_EQ(encoded, base64_encoded); ++i; @@ -87,17 +87,17 @@ TEST(Base64Test, encoding) TEST(Base64Test, decoding) { - Base64 base64; + base64 base64; std::array< char, 54 > data{}; size_t i = 0; - while (testpattern[i].bin != nullptr) + while (testpattern[i].bin_ != nullptr) { - const std::string encoded{testpattern[i].encoded}; - const std::string binary{testpattern[i].bin}; + const std::string encoded{testpattern[i].encoded_}; + const std::string binary{testpattern[i].bin_}; fmt::println("'{}':\t{}", binary, encoded); - const std::string decoded = base64.decode(encoded); + const std::string decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(binary, decoded); ++i; @@ -106,44 +106,44 @@ TEST(Base64Test, decoding) TEST(Base64Test, ShortString1) { - Base64 base64; + base64 base64; std::string const original = "A"; - std::string const encoded = base64.encode(original); + std::string const encoded = rrcp::common::base64::encode(original); // fmt::println("{}:\t{}", original, encoded); EXPECT_EQ(encoded, "QQ=="); - std::string const decoded = base64.decode(encoded); + std::string const decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(original, decoded); } TEST(Base64Test, ShortString2) { - Base64 base64; + base64 base64; std::string const original = "AA"; - std::string const encoded = base64.encode(original); + std::string const encoded = rrcp::common::base64::encode(original); // fmt::println("{}:\t{}", original, encoded); EXPECT_EQ(encoded, "QUE="); - std::string const decoded = base64.decode(encoded); + std::string const decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(original, decoded); } TEST(Base64Test, ShortString3) { - Base64 base64; + base64 base64; std::string const original = "AAA"; - std::string const encoded = base64.encode(original); + std::string const encoded = rrcp::common::base64::encode(original); // fmt::println("{}:\t{}", original, encoded); EXPECT_EQ(encoded, "QUFB"); - std::string const decoded = base64.decode(encoded); + std::string const decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(original, decoded); } #ifdef TEST_INVALID_VALUES TEST(Base64Test, DecodeMarker) { - Base64 base64; + base64 base64; EXPECT_ANY_THROW({ (void)base64.decode("====").empty(); }); EXPECT_ANY_THROW({ (void)base64.decode("===").empty(); }); EXPECT_ANY_THROW({ (void)base64.decode("==").empty(); }); @@ -156,87 +156,87 @@ TEST(Base64Test, DecodeMarker) TEST(Base64Test, MediumString) { - Base64 base64; + base64 base64; std::string const original = "This is a medium length string."; - std::string const encoded = base64.encode(original); - std::string const decoded = base64.decode(encoded); + std::string const encoded = rrcp::common::base64::encode(original); + std::string const decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(original, decoded); } TEST(Base64Test, LongString) { - Base64 base64; + base64 base64; // XXX base64.setLineBreak(true); std::string const original = "This is not a really long string, but also that should be encoded and decoded correctly."; - std::string const encoded = base64.encode(original); + std::string const encoded = rrcp::common::base64::encode(original); std::string expected{ "VGhpcyBpcyBub3QgYSByZWFsbHkgbG9uZyBzdHJpbmcsIGJ1dCBhbHNvIHRoYXQgc2hvdWxkIGJl" "IGVuY29kZWQgYW5kIGRlY29kZWQgY29ycmVjdGx5Lg=="}; EXPECT_EQ(expected, encoded); // fmt::println("{}:\n{}", original, encoded); - std::string const decoded = base64.decode(encoded); + std::string const decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(original, decoded); } TEST(Base64Test, FoxString) { - Base64 base64; + base64 base64; std::string const original = "The quick brown fox jumped over the lazy dogs."; - std::string const encoded = base64.encode(original); + std::string const encoded = rrcp::common::base64::encode(original); EXPECT_EQ(encoded, "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wZWQgb3ZlciB0aGUgbGF6eSBkb2dzLg=="); // fmt::println("{}:\t{}", original, encoded); - std::string const decoded = base64.decode(encoded); + std::string const decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(original, decoded); } TEST(Base64Test, BinaryData) { - Base64 base64; + base64 base64; std::string const original = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"s; - std::string const encoded = base64.encode(original); - std::string const decoded = base64.decode(encoded); + std::string const encoded = rrcp::common::base64::encode(original); + std::string const decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(original, decoded); } TEST(Base64Test, NonAsciiString) { - Base64 base64; + base64 base64; std::string const original = "\xFC@NOs[\xFEVJ\t@\x80\v\xD0\xAA\xF5"; - std::string const encoded = base64.encode(original); + std::string const encoded = rrcp::common::base64::encode(original); // fmt::println("'{}':\t{}", original, encoded); - std::string const decoded = base64.decode(encoded); + std::string const decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(original, decoded); } TEST(Base64Test, TestEncoder) { - Base64 base64; + base64 base64; { std::string original("\00\01\02\03\04\05", 6); - auto encoded = base64.encode(original); + auto encoded = rrcp::common::base64::encode(original); EXPECT_EQ(encoded, "AAECAwQF"); EXPECT_EQ(original, base64.decode(encoded)); } { std::string original("\00\01\02\03", 4); - auto encoded = base64.encode(original); + auto encoded = rrcp::common::base64::encode(original); EXPECT_EQ(encoded, "AAECAw=="); EXPECT_EQ(original, base64.decode(encoded)); } { std::string original("ABCDEF"); - auto encoded = base64.encode(original); + auto encoded = rrcp::common::base64::encode(original); EXPECT_EQ(encoded, "QUJDREVG"); EXPECT_EQ(original, base64.decode(encoded)); } { std::string original("!@#$%^&*()_~<>"); std::string expected{"IUAjJCVeJiooKV9+PD4="}; - auto encoded = base64.encode(original); + auto encoded = rrcp::common::base64::encode(original); fmt::println("'{}':\t{}", original, encoded); EXPECT_EQ(encoded, expected); @@ -247,7 +247,7 @@ TEST(Base64Test, TestEncoder) #ifdef TEST_INVALID_VALUES TEST(Base64Test, TestDecoder) { - Base64 base64; + base64 base64; { const std::string istr("QUJ\r\nDRE\r\nVG"); const std::string decoded = base64.decode(istr); @@ -267,20 +267,20 @@ TEST(Base64Test, RandomBinaryData) std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd() std::uniform_int_distribution<> distrib(0, 255); - Base64 base64; + base64 base64; // XXX base64.setLineBreak(true); for (size_t i = 0; i < 5; ++i) { - const std::string::size_type new_cap{64u + i}; + const std::string::size_type new_cap{64U + i}; std::string original; original.reserve(new_cap); for (size_t j = 0; j < new_cap; ++j) { original += static_cast< char >(distrib(gen) % 256); } - std::string const encoded = base64.encode(original); - std::string const decoded = base64.decode(encoded); + std::string const encoded = rrcp::common::base64::encode(original); + std::string const decoded = rrcp::common::base64::decode(encoded); EXPECT_EQ(original, decoded); } } diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index 5ded5a4..332ea3f 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -18,53 +18,53 @@ ut::suite errors = [] "find_response_msg"_test = [] { - constexpr std::string_view expected{"gGoState"sv}; + constexpr std::string_view EXPECTED{"gGoState"sv}; const std::string message{"123456 gGoState"}; std::string result{message}; - auto found = RRCP::find_response_msg(result, "123456"); + auto found = rrcp::find_response_msg(result, "123456"); expect(found); - expect(expected == result); + expect(EXPECTED == result); #if defined(BOOST_UT_HAS_FORMAT) - ut::log("{} == {}\n", result, expected); + ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotfind_error_response_msg"_test = [] { - constexpr std::string_view expected{"E:1"sv}; + constexpr std::string_view EXPECTED{"E:1"sv}; const std::string message{"E:1"}; std::string result{message}; - auto found = RRCP::find_response_msg(result, "0815"); + auto found = rrcp::find_response_msg(result, "0815"); expect(found); - expect(expected == result); + expect(EXPECTED == result); #if defined(BOOST_UT_HAS_FORMAT) - ut::log("{} == {}\n", result, expected); + ut::log("{} == {}\n", result, EXPECTED); #endif }; "find_error_response_msg"_test = [] { - constexpr std::string_view expected{"E:10"sv}; + constexpr std::string_view EXPECTED{"E:10"sv}; const std::string message{"E:10 123456"}; std::string result{message}; - auto found = RRCP::find_response_msg(result, "123456"); + auto found = rrcp::find_response_msg(result, "123456"); expect(found); - expect(expected == result); + expect(EXPECTED == result); #if defined(BOOST_UT_HAS_FORMAT) - ut::log("{} == {}\n", result, expected); + ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotfind_response_msg"_test = [] { - constexpr std::string_view expected{"d NoGo"sv}; - const std::string message{expected}; + constexpr std::string_view EXPECTED{"d NoGo"sv}; + const std::string message{EXPECTED}; std::string result{message}; - auto found = RRCP::find_response_msg(result, "123456"); + auto found = rrcp::find_response_msg(result, "123456"); expect(!found); - expect(expected == result); + expect(EXPECTED == result); #if defined(BOOST_UT_HAS_FORMAT) - ut::log("{} == {}\n", result, expected); + ut::log("{} == {}\n", result, EXPECTED); #endif }; @@ -72,45 +72,45 @@ ut::suite errors = [] "insertAfterFirstWord"_test = [] { - constexpr std::string_view expected{"M:test 123456 GGoState"sv}; + constexpr std::string_view EXPECTED{"M:test 123456 GGoState"sv}; const std::string command{"M:test GGoState"}; - auto result = RRCP::insertAfterFirstWord(command, "123456"); - expect(expected == result); + auto result = rrcp::insertAfterFirstWord(command, "123456"); + expect(EXPECTED == result); #if defined(BOOST_UT_HAS_FORMAT) - ut::log("{} == {}\n", result, expected); + ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotInsertAnEmptyString"_test = [] { - constexpr std::string_view expected{"M:test GGoState"sv}; - const std::string command{expected}; - auto result = RRCP::insertAfterFirstWord(command, ""); - expect(expected == result); + constexpr std::string_view EXPECTED{"M:test GGoState"sv}; + const std::string command{EXPECTED}; + auto result = rrcp::insertAfterFirstWord(command, ""); + expect(EXPECTED == result); #if defined(BOOST_UT_HAS_FORMAT) - ut::log("{} == {}\n", result, expected); + ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotInsertBeforeTrapCmd"_test = [] { - constexpr std::string_view expected{"M:test TGoState1"sv}; - const std::string command{expected}; - auto result = RRCP::insertAfterFirstWord(command, ""); - expect(expected == result); + constexpr std::string_view EXPECTED{"M:test TGoState1"sv}; + const std::string command{EXPECTED}; + auto result = rrcp::insertAfterFirstWord(command, ""); + expect(EXPECTED == result); #if defined(BOOST_UT_HAS_FORMAT) - ut::log("{} == {}\n", result, expected); + ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotInsertAfterSingleWord"_test = [] { - constexpr std::string_view expected{"E:10"sv}; - const std::string message{expected}; - auto result = RRCP::insertAfterFirstWord(message, "123456"); - expect(expected == result); + constexpr std::string_view EXPECTED{"E:10"sv}; + const std::string message{EXPECTED}; + auto result = rrcp::insertAfterFirstWord(message, "123456"); + expect(EXPECTED == result); #if defined(BOOST_UT_HAS_FORMAT) - ut::log("{} == {}\n", result, expected); + ut::log("{} == {}\n", result, EXPECTED); #endif }; @@ -118,13 +118,13 @@ ut::suite errors = [] "create_command_msg"_test = [] { - constexpr std::string_view expected{"\nM:RxTx 1 SPowerLevel\"Off\"\r"sv}; + constexpr std::string_view EXPECTED{"\nM:RxTx 1 SPowerLevel\"Off\"\r"sv}; const std::string command{R"(M:RxTx SPowerLevel"Off")"}; std::string msg_id_str; - int counter{RRCP::INVALID_ID}; - auto result = RRCP::create_command_msg(command, msg_id_str, counter); + int counter{rrcp::INVALID_ID}; + auto result = rrcp::create_command_msg(command, msg_id_str, counter); expect(1 == counter); - expect(expected == result); + expect(EXPECTED == result); std::ostringstream quoted; quoted << std::quoted(result.substr(1, result.length() - 1)); // NOTE: w/o START STOP @@ -140,8 +140,8 @@ ut::suite errors = [] expect(throws( [] { - constexpr std::string_view wrong_quoted{"\n\x1b\004\r"sv}; - auto result = RRCP::esc2char(wrong_quoted); + constexpr std::string_view WRONG_QUOTED{"\n\x1b\004\r"sv}; + auto result = rrcp::esc2char(WRONG_QUOTED); })); }; @@ -150,21 +150,21 @@ ut::suite errors = [] expect(nothrow( [] { - auto result = RRCP::esc2char(""); + auto result = rrcp::esc2char(""); expect(result.empty()); })); }; - "single_esc_msg"_test = [] { expect(throws([] { auto result = RRCP::esc2char("\x1b\rSINGLE_ESC"); })); }; + "single_esc_msg"_test = [] { expect(throws([] { auto result = rrcp::esc2char("\x1b\rSINGLE_ESC"); })); }; - "esc_as_last_msg"_test = [] { expect(throws([] { auto result = RRCP::esc2char("ESC_AS_LAST\x1b"); })); }; + "esc_as_last_msg"_test = [] { expect(throws([] { auto result = rrcp::esc2char("ESC_AS_LAST\x1b"); })); }; - "to_short_msg"_test = [] { expect(throws([] { auto result = RRCP::esc2char("\x1b\0"s); })); }; + "to_short_msg"_test = [] { expect(throws([] { auto result = rrcp::esc2char("\x1b\0"s); })); }; "basic_quoteing"_test = [] { - constexpr std::string_view binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; - auto quoted = RRCP::char2esc(binary); + constexpr std::string_view BINARY{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; + auto quoted = rrcp::char2esc(BINARY); // NOTE: std::quoted works only with std::stringstream #if defined(BOOST_UT_HAS_FORMAT) && defined(FIXME) // FIXME! @@ -177,9 +177,9 @@ ut::suite errors = [] ut::log("{} {}\n", quoted.length(), quoted_bin.str()); #endif - expect(binary == RRCP::esc2char(quoted)); - expect(binary.length() < quoted.length()); - expect(binary.length() == 28); + expect(BINARY == rrcp::esc2char(quoted)); + expect(BINARY.length() < quoted.length()); + expect(BINARY.length() == 28); expect(quoted.length() == 33); }; @@ -228,24 +228,24 @@ ut::suite errors = [] "rrcp_message_text"_test = [] { - constexpr std::string_view command{"Hallo Server"}; + constexpr std::string_view COMMAND{"Hallo Server"}; rrcp_message msg; - expect(msg.set_msg(command)); + expect(msg.set_msg(COMMAND)); expect(msg.is_valid()); - expect(msg.body_length() == command.length()); - expect(msg.body() == command); + expect(msg.body_length() == COMMAND.length()); + expect(msg.body() == COMMAND); auto result = msg.get_msg(); - expect(command == result); + expect(COMMAND == result); }; "rrcp_message_binary"_test = [] { - constexpr std::string_view binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; - rrcp_message msg(binary); + constexpr std::string_view BINARY{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; + rrcp_message msg(BINARY); expect(msg.is_valid()); expect(msg.body_length() == 33); auto result = msg.get_msg(); - expect(binary == result); + expect(BINARY == result); }; // ============================================================ From 4564c6b9051ffc3f46f83745ada03ea6ac629efe Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 13 May 2025 08:39:52 +0200 Subject: [PATCH 082/120] Add rrcp ANTLR4 grammer --- docs/.gitignore | 7 ++ docs/GNUmakefile | 20 ++++++ docs/rrcp-generate.md | 63 +++++++++++++++++ docs/rrcp.g4 | 152 ++++++++++++++++++++++++++++++++++++++++++ docs/rrcp.py | 20 ++++++ docs/rrcp.txt | 27 ++++++++ 6 files changed, 289 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/GNUmakefile create mode 100644 docs/rrcp-generate.md create mode 100644 docs/rrcp.g4 create mode 100755 docs/rrcp.py create mode 100644 docs/rrcp.txt diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..8c31f4c --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,7 @@ +*.interp +*.tokens +*Parser.* +*Lexer.* +*Listener.* +*.class +__pycache__ diff --git a/docs/GNUmakefile b/docs/GNUmakefile new file mode 100644 index 0000000..6dfc4b4 --- /dev/null +++ b/docs/GNUmakefile @@ -0,0 +1,20 @@ +PYTHONPATH:=$(CURDIR):${PYTHONPATH} +export PYTHONPATH + +ANTLR4:=java -jar ${ANTLR_HOME}/antlr-4.13.2-complete.jar + +.PHONY: all test format clean +all: rrcpParser.py rrcpLexer.py + +rrcpParser.py rrcpLexer.py: rrcp.g4 + $(ANTLR4) -Dlanguage=Python3 $< + +format: all + black *.py + +test: format + pygrun --trace rrcp rrcp rrcp.txt + # pygrun --trace rrcp rrcp TestSamples.txt + +clean: + $(RM) -r *.interp *.tokens *Parser.* *Lexer.* *Listener.* *.class *~ __pycache__ diff --git a/docs/rrcp-generate.md b/docs/rrcp-generate.md new file mode 100644 index 0000000..aea4c65 --- /dev/null +++ b/docs/rrcp-generate.md @@ -0,0 +1,63 @@ +# RRCP with ANTLR + +To test the ANTLR grammar with Python, you'll need to follow several steps to set up your environment, generate the parser +and lexer, and then write a Python script to test the grammar. Below are the detailed steps to accomplish this: + +## Step 1: Install ANTLR + +### Download ANTLR: + +Download the ANTLR jar file from the [ANTLR](https://www.antlr.org/download.html) website. + +### Set Up ANTLR in Your Environment: + +You can place the ANTLR jar file in a directory of your choice. +For example, let's say you place it in `~/antlr/antlr-4.13.0-complete.jar`. + +Set Up Environment Variables (Optional but recommended)! + +You can set an environment variable for ANTLR. Add the following lines to your `~/.bashrc` or `~/.bash_profile` (or +equivalent for your shell): + + export ANTLR_HOME=~/antlr + export CLASSPATH="$ANTLR_HOME/antlr-4.13.0-complete.jar:$CLASSPATH" + alias antlr4='java -jar $ANTLR_HOME/antlr-4.13.0-complete.jar' + alias grun='java org.antlr.v4.gui.TestRig' + +Reload your shell: + + source ~/.bashrc # or source ~/.bash_profile + +## Step 2: Install Python and Required Libraries + +### Install Python. + +Make sure you have Python 3 installed. You can check by running: + + python3 --version + +### Install ANTLR4 Python Runtime. + +Use pip to install the ANTLR4 runtime for Python: + + pip install antlr4-python3-runtime + +## Step 3: Generate Lexer and Parser + +### Create a File for Your Grammar. + +Save your ANTLR grammar in a file named `rrcp.g4`. + +### Generate the Lexer and Parser. + +Run the following command in the terminal to generate the lexer and parser: + + antlr4 rrcp.g4 -Dlanguage=Python3 + +This will generate several Python files in the same directory, including `rrcpLexer.py`, `rrcpParser.py`, and others. + +## Step 4: Write a Test Script + +see [rrcp.py](rrcp.py) + +and [GNUmakefile](GNUmakefile) diff --git a/docs/rrcp.g4 b/docs/rrcp.g4 new file mode 100644 index 0000000..9ae3beb --- /dev/null +++ b/docs/rrcp.g4 @@ -0,0 +1,152 @@ +// +// Remote Radio Control Protocol (RRCP) +// +// Define a grammar called rrcp +grammar rrcp; + +// +// rules: +// + +//TODO(CK): rrcp: (LF MU CR)+ EOF ; +rrcp : (Newline* MU Newline?)+ EOF ; + +//TODO(CK): MU: TU (SP TU)* ; +// NOTE: with optional LineComment for test +MU + : MIB_PATH SP Optional? TU (SP TU)* LineComment? + | Optional? RESP_TU (SP RESP_TU)* LineComment? + | MuErrorStatus SP? Optional? LineComment? + | SP? LineComment + ; + +MIB_PATH: + 'M:' ALPHANUM ('.' ALPHANUM)* + ; + +// NOTE: the LogicalAddress must not used with RESP_TU? +fragment +Optional: + (LogicalAddress SP)? (MessageID SP)? + ; + +LogicalAddress: + 'L:' NUM + ; + +MessageID: + NUM + ; + +TU + : REQ SP* CU // with optional space before CU! + ; + +RESP_TU + : RESP SP* TuErrorStatus CMD + | RESP SP* CU + | ACK // NOTE: trap or set response without CU! + ; + +REQ: 'G' | 'S' | 'T' ; + +RESP: 'g' | 's' | 'd' ; + +ACK: [ts] ; + +MuErrorStatus: + 'E:' NUM + ; + +TuErrorStatus: + NUM + ; + +CU + : CMD PU? (';' CMD PU?)* + ; + +CMD: + ALPHA + ; + +fragment +PU: + SP* PARAMETER (',' PARAMETER)* // with optional space after command! + ; + +PARAMETER + : STRING + | INT + | FLOAT + | BINARY + ; + +BINARY + : '#' NUM ':' Base64Digit+ + ; + +fragment +Base64Digit + : [0-9a-zA-Z+/=] + ; + +//XXX STRING : '"'~('"')*'"' ; // NOTE: without EscapeSequence +STRING : StringLiteral ; + +fragment +EscapeSequence + : SimpleEscapeSequence + ; + +fragment +SimpleEscapeSequence + : '\\' ['"?abfnrtv\\] + ; + +StringLiteral + : '"' SCharSequence? '"' + ; + +fragment +SCharSequence + : SChar+ + ; + +fragment +SChar + : ~["\\\n] + | EscapeSequence + | '\\\n' // Added line + ; + +Newline + : [\n] + -> skip + ; + +LineComment + : '//' ~[\n]* + -> skip + ; + +//====================================================== +fragment +NUM : [0-9]+ ; // match unsigned decimal numbers +fragment +INT : [+-]?[0-9]+ ; // signed decimal numbers +fragment +FLOAT : [+-]?[0-9]+'.'[0-9]+ ; // rational numbers +fragment +ALPHA : [a-zA-Z]+ ; // match alpha identifiers +fragment +ALPHANUM : [0-9a-zA-Z]+ ; // match alphaNum words +//XXX NO! WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines +SP : ' ' -> skip ; +//TODO(CK): CR : '\n' ; # 0x0d +//TODO(CK): LF : '\r' ; # 0x0a +//====================================================== + +// +// vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 syntax=antlr +// diff --git a/docs/rrcp.py b/docs/rrcp.py new file mode 100755 index 0000000..7ad1464 --- /dev/null +++ b/docs/rrcp.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import sys +from antlr4 import * +from rrcpLexer import rrcpLexer +from rrcpParser import rrcpParser + + +def main(argv): + input = FileStream(argv[1]) + lexer = rrcpLexer(input) + stream = CommonTokenStream(lexer) + parser = rrcpParser(stream) + tree = parser.rrcp() // startRule + + print(tree.toStringTree(recog=parser)) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/docs/rrcp.txt b/docs/rrcp.txt new file mode 100644 index 0000000..2eed4d3 --- /dev/null +++ b/docs/rrcp.txt @@ -0,0 +1,27 @@ +// GET-request TU SET-request TU GET-request TU: +M:Test GFRQ;MOD SFRQ10000000;MOD13;BW80000 GFRQ;MOD + +// GET-response TU SET-response TU GET-response TU: +gFRQ18000000;MOD12 sBW gFRQ18000000;MOD12 +// NOTE: +// There is an error within the BW command, the complete SET-request TU is cancelled. +// The GET-request TU is replied by the corresponding GET-response TU. + +M:WF.FF.Main 123456 T Octet 1 // TRAP command with optional message number, but without Logical Address: +123456 t // TRAP acknowlage +M:Bit L:1 123456 G Octet +M:Audio SOctet // SET without optionl parts +M:Radio SString"\rHallo\t\"World\"\n" // quoted string with escape chars +M:Log SStruct1,-1,3.14 // multiple parameters + +M:MultilCmd S Octet 1;Long-1;String"Hallo World\r\n";Struct 1,+1,+3.14 // multiple commands's + +M:Test 123456 S FREQ123456;MOD12;BW80000 G FREQ;MOD;STATUS // multiple TU with GET and SET! +123456 gFREQ180000;MOD12 s5BW gFREQ180000;MOD12;STATUS"Running" // TU partly failed! + +M:RADIO S FREQUENCY 123456789 +E:12 // MU error +M:RADIO 112368 T FREQUENCY 1 +112368 t +112368 d FREQUENCY 123456789 // trap data +M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data From 40a17251966a774b3457035ae63992f9820a868d Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 13 May 2025 08:47:29 +0200 Subject: [PATCH 083/120] Add more rrcp test samples --- docs/GNUmakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/GNUmakefile b/docs/GNUmakefile index 6dfc4b4..a5badf1 100644 --- a/docs/GNUmakefile +++ b/docs/GNUmakefile @@ -14,7 +14,7 @@ format: all test: format pygrun --trace rrcp rrcp rrcp.txt - # pygrun --trace rrcp rrcp TestSamples.txt + pygrun --trace rrcp rrcp ../rrcp.txt clean: $(RM) -r *.interp *.tokens *Parser.* *Lexer.* *Listener.* *.class *~ __pycache__ From 142aad6e1a3615199bfd9f0bfb33581baaaeb7ab Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Tue, 13 May 2025 09:26:13 +0200 Subject: [PATCH 084/120] Make it works under ubuntu too --- docs/.gitignore | 1 + docs/GNUmakefile | 4 ++++ docs/rrcp-generate.md | 2 ++ 3 files changed, 7 insertions(+) diff --git a/docs/.gitignore b/docs/.gitignore index 8c31f4c..cc8cd48 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,4 @@ +*.jar *.interp *.tokens *Parser.* diff --git a/docs/GNUmakefile b/docs/GNUmakefile index a5badf1..fb7b583 100644 --- a/docs/GNUmakefile +++ b/docs/GNUmakefile @@ -1,6 +1,10 @@ PYTHONPATH:=$(CURDIR):${PYTHONPATH} export PYTHONPATH +ANTLR_HOME?=$(CURDIR) +CLASSPATH:="${ANTLR_HOME}/antlr-4.13.0-complete.jar:${CLASSPATH}" +export CLASSPATH + ANTLR4:=java -jar ${ANTLR_HOME}/antlr-4.13.2-complete.jar .PHONY: all test format clean diff --git a/docs/rrcp-generate.md b/docs/rrcp-generate.md index aea4c65..abc5cae 100644 --- a/docs/rrcp-generate.md +++ b/docs/rrcp-generate.md @@ -9,6 +9,8 @@ and lexer, and then write a Python script to test the grammar. Below are the det Download the ANTLR jar file from the [ANTLR](https://www.antlr.org/download.html) website. + wget https://www.antlr.org/download/antlr-4.13.2-complete.jar + ### Set Up ANTLR in Your Environment: You can place the ANTLR jar file in a directory of your choice. From 69e6b15262edec945b0e8ade70df45d16de980e2 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Tue, 13 May 2025 10:00:28 +0200 Subject: [PATCH 085/120] Add new rrcp command samples --- .gitignore | 1 + CMakeLists.txt | 6 +++--- rrcp.txt | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 75bfe7c..5e8d57b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ coverage/* +tags .*swp *.log diff --git a/CMakeLists.txt b/CMakeLists.txt index 24c7298..e596569 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,11 +19,11 @@ endif() FetchContent_Declare( fmt - GIT_TAG 11.1.4 + GIT_TAG 11.2.0 GIT_REPOSITORY https://github.com/fmtlib/fmt.git EXCLUDE_FROM_ALL SYSTEM - FIND_PACKAGE_ARGS 11.1.4 NAMES fmt HINTS $ENV{HOME}/.local + FIND_PACKAGE_ARGS 11.2.0 NAMES fmt HINTS $ENV{HOME}/.local ) FetchContent_MakeAvailable(fmt) @@ -34,7 +34,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if(APPLE) execute_process( OUTPUT_VARIABLE LLVM_PREFIX - COMMAND brew --prefix llvm@19 + COMMAND brew --prefix llvm@20 COMMAND_ECHO STDOUT ) string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) diff --git a/rrcp.txt b/rrcp.txt index 5938001..693629c 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -55,12 +55,17 @@ M:OBIT GGOState M:OBIT TGOState1 M:OBIT GTestErrors M:OBIT GTestIDs +M:OBIT TErrorEvent1 M:RxTx GPowerLevel M:RxTx TPowerLevel1 M:RxTx SPowerLevel"Off" M:RxTx TPowerLevel0 M:RxTx GVswr M:RxTx TVswr1 +M:RxTx GVswrThres +M:RxTx TVswrThres1 +M:RxTx GVswrThresLev +M:RxTx SVswrThresLev10 M:Utility GBattStatus M:Utility TBattStatus1 M:Utility GErrorText0,"English" From a84a93b5a952d10b7d37fae9ef8e4da6ed6e0ad8 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Tue, 13 May 2025 11:11:05 +0200 Subject: [PATCH 086/120] Update fmt version to v11.2.0 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e596569..6848a9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25...4.0) -project(RRCP-client VERSION 0.1.0 LANGUAGES CXX) +project(RRCP-client VERSION 0.1.1 LANGUAGES CXX) # ---- add dependencies ---- @@ -21,9 +21,9 @@ FetchContent_Declare( fmt GIT_TAG 11.2.0 GIT_REPOSITORY https://github.com/fmtlib/fmt.git + FIND_PACKAGE_ARGS 11.2.0 NAMES fmt HINTS $ENV{HOME}/.local EXCLUDE_FROM_ALL SYSTEM - FIND_PACKAGE_ARGS 11.2.0 NAMES fmt HINTS $ENV{HOME}/.local ) FetchContent_MakeAvailable(fmt) From fd1d913eeb0d91ba0bb99b1777db9f544e10fec2 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Tue, 13 May 2025 19:36:54 +0200 Subject: [PATCH 087/120] Prevent more clang-tidy warnings --- .clang-tidy | 6 +++--- CMakeLists.txt | 2 +- GNUmakefile | 16 +++++++++------- examples/async_tcp_echo_client.cpp | 17 +++++++++-------- examples/async_tcp_echo_server.cpp | 8 ++++---- examples/blocking_tcp_echo_client.cpp | 13 +++++++------ rrcp_async_tcp_client.cpp | 19 ++++++++++--------- rrcp_client.cpp | 15 ++++++++------- rrcp_helper.cpp | 20 ++++++++++---------- tests/RRCP-test.cpp | 2 +- 10 files changed, 62 insertions(+), 56 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index ecd16a2..8af0133 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,11 +1,11 @@ --- Checks: "-*,\ bugprone-*,\ --bugprone-reserved-identifier,\ +bugprone-reserved-identifier,\ boost-*,\ cert-*,\ clang-analyzer-*,\ --clang-analyzer-unix.BlockInCriticalSection,\ +clang-analyzer-unix.BlockInCriticalSection,\ cppcoreguidelines-*,\ -cppcoreguidelines-avoid-*,\ -cppcoreguidelines-init-variables,\ @@ -19,8 +19,8 @@ misc-*,\ -misc-include-cleaner,\ -misc-no-recursion,\ modernize-*,\ --modernize-avoid-bind,\ -modernize-macro-to-enum,\ +-modernize-use-designated-initializers,\ performance-*,\ -performance-enum-size,\ portability-*,\ diff --git a/CMakeLists.txt b/CMakeLists.txt index 6848a9f..879442c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,9 +21,9 @@ FetchContent_Declare( fmt GIT_TAG 11.2.0 GIT_REPOSITORY https://github.com/fmtlib/fmt.git - FIND_PACKAGE_ARGS 11.2.0 NAMES fmt HINTS $ENV{HOME}/.local EXCLUDE_FROM_ALL SYSTEM + FIND_PACKAGE_ARGS 11.2.0 NAMES fmt HINTS $ENV{HOME}/.local ) FetchContent_MakeAvailable(fmt) diff --git a/GNUmakefile b/GNUmakefile index 7968f8b..f57586d 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -5,14 +5,16 @@ MAKEFLAGS+= --no-builtin-rules MAKEFLAGS+= --warn-undefined-variables +CPPFILES:= $(shell git ls-files ::*.cpp) + .PHONY: all format test check distclean all: build ninja -C build clean: build - - ninja -C $< $@ - - find $< -name '*.gcda' -delete + -ninja -C $< $@ + -find $< -name '*.gcda' -delete distclean: # XXX clean rm -rf build coverage/* *~ ctags @@ -21,11 +23,10 @@ build: CMakeLists.txt cmake -S . -B $@ -G Ninja -D CMAKE_BUILD_TYPE=Debug # --fresh check: all - run-clang-tidy -p build *.cpp + run-clang-tidy -p build *.cpp examples/*.cpp # $(CPPFILES) fix: all - run-clang-tidy -p build -fix \ - -checks='-*,\ + run-clang-tidy -p build -fix -checks='-*,\ hicpp-explicit-conversions,\ hicpp-member-init,\ hicpp-named-parameter,\ @@ -43,13 +44,14 @@ readability-container-data-pointer,\ readability-container-size-empty,\ readability-convert-member-functions-to-static,\ readability-else-after-return,\ +readability-identifier-naming,\ readability-implicit-bool-conversion,\ readability-make-member-function-const,\ readability-redundant-member-init,\ readability-simplify-boolean-expr,\ readability-use-std-min-max,\ ' \ - *.cpp + *.cpp examples/*.cpp # $(CPPFILES) test: all -killall async_tcp_echo_server @@ -71,7 +73,7 @@ test: all format: .clang-format git ls-files ::*.cpp ::*.hpp | xargs clang-format -i - git ls-files ::*CMakeLists.txt | xargs gersemi -i + git ls-files ::*CMakeLists.txt | xargs gersemi -i --no-warn-about-unknown-commands # These rules keep make from trying to use the match-anything rule below # to rebuild the makefiles--ouch! diff --git a/examples/async_tcp_echo_client.cpp b/examples/async_tcp_echo_client.cpp index ecea0a9..3c00233 100644 --- a/examples/async_tcp_echo_client.cpp +++ b/examples/async_tcp_echo_client.cpp @@ -34,13 +34,13 @@ using message_queue = std::deque< std::string >; using namespace rrcp; -constexpr size_t max_length = 65432; -constexpr auto timeout_duration = 1s; +constexpr size_t MAX_LENGTH = 65432; +constexpr auto TIMEOUT_DURATION = 1s; -class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousTCPClient > +class asynchronous_tcp_client : public std::enable_shared_from_this< asynchronous_tcp_client > { public: - AsynchronousTCPClient(boost::asio::io_context& io_context, const std::string& host, const std::string& port) + asynchronous_tcp_client(boost::asio::io_context& io_context, const std::string& host, const std::string& port) : io_context_(io_context), resolver_(io_context), socket_(io_context), timer_(io_context) { resolver_.async_resolve(host, port, @@ -77,7 +77,7 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT return; } fmt::print(stderr, "Client is not connected yet.\n"); - std::this_thread::sleep_for(timeout_duration); // NOLINT(misc-include-cleaner) + std::this_thread::sleep_for(TIMEOUT_DURATION); // NOLINT(misc-include-cleaner) } boost::asio::post(io_context_, @@ -139,7 +139,7 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT if (!connected_) { - timer_.expires_after(timeout_duration); + timer_.expires_after(TIMEOUT_DURATION); timer_.async_wait( [this, self](const boost::system::error_code& ec) { @@ -192,6 +192,7 @@ class AsynchronousTCPClient : public std::enable_shared_from_this< AsynchronousT bool stopped_{false}; }; +// NOLINTNEXTLINE(bugprone-exception-escape) auto main(int argc, char* argv[]) -> int { if (argc != 3) @@ -204,7 +205,7 @@ auto main(int argc, char* argv[]) -> int { boost::asio::io_context io_context; - auto client = std::make_shared< AsynchronousTCPClient >(io_context, argv[1], argv[2]); + auto client = std::make_shared< asynchronous_tcp_client >(io_context, argv[1], argv[2]); std::thread io_thread([&io_context]() { io_context.run(); }); @@ -228,7 +229,7 @@ auto main(int argc, char* argv[]) -> int client->write(command); } - std::this_thread::sleep_for(timeout_duration); + std::this_thread::sleep_for(TIMEOUT_DURATION); client->stop(); io_thread.join(); diff --git a/examples/async_tcp_echo_server.cpp b/examples/async_tcp_echo_server.cpp index 8e4efc1..69b66b6 100644 --- a/examples/async_tcp_echo_server.cpp +++ b/examples/async_tcp_echo_server.cpp @@ -30,7 +30,7 @@ using boost::asio::ip::tcp; class session : public std::enable_shared_from_this< session > { - static constexpr size_t max_length{1024}; + static constexpr size_t MAX_LENGTH{1024}; public: explicit session(tcp::socket socket) : socket_(std::move(socket)) {} @@ -41,7 +41,7 @@ class session : public std::enable_shared_from_this< session > void do_read() { auto self(shared_from_this()); - socket_.async_read_some(boost::asio::buffer(data_.data(), max_length), + socket_.async_read_some(boost::asio::buffer(data_.data(), MAX_LENGTH), [this, self](boost::system::error_code ec, std::size_t length) { if (!ec) @@ -73,7 +73,7 @@ class session : public std::enable_shared_from_this< session > } tcp::socket socket_; - std::array< char, max_length > data_{}; + std::array< char, MAX_LENGTH > data_{}; }; class server @@ -162,7 +162,7 @@ auto main(int argc, char* argv[]) -> int boost::asio::io_context io_context; - server const s(io_context, std::strtol(argv[1], nullptr, 10)); + server const serv(io_context, std::strtol(argv[1], nullptr, 10)); io_context.run(); std::cout << "io_service.run complete, shutdown successful\n"; diff --git a/examples/blocking_tcp_echo_client.cpp b/examples/blocking_tcp_echo_client.cpp index 2f26f6d..fa46bf2 100644 --- a/examples/blocking_tcp_echo_client.cpp +++ b/examples/blocking_tcp_echo_client.cpp @@ -26,7 +26,7 @@ using boost::asio::ip::tcp; -static constexpr int max_length{1024}; +static constexpr int MAX_LENGTH{1024}; auto main(int argc, char* argv[]) -> int { @@ -42,9 +42,9 @@ auto main(int argc, char* argv[]) -> int boost::asio::io_context io_context; - tcp::socket s(io_context); + tcp::socket socket(io_context); tcp::resolver resolver(io_context); - boost::asio::connect(s, resolver.resolve(argv[1], argv[2])); + boost::asio::connect(socket, resolver.resolve(argv[1], argv[2])); for (std::string line; std::getline(std::cin, line); std::cerr << "Enter command: ") { @@ -64,16 +64,17 @@ auto main(int argc, char* argv[]) -> int std::string command = char2esc(line); command.insert(0, 1, START); command += STOP; - boost::asio::write(s, boost::asio::buffer(command.c_str(), command.length())); + boost::asio::write(socket, boost::asio::buffer(command.c_str(), command.length())); // TODO(CK): wait for endchar with timeout! std::string data; boost::asio::dynamic_string_buffer< char, std::string::traits_type, std::string::allocator_type > const sb2 = - boost::asio::dynamic_buffer(data, max_length); + boost::asio::dynamic_buffer(data, MAX_LENGTH); do { - size_t const reply_length = boost::asio::read_until(s, sb2, STOP); // NOLINT(clang-analyzer-deadcode.DeadStores) + // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores) + size_t const reply_length = boost::asio::read_until(socket, sb2, STOP); std::string const response = esc2char(data); if (response.empty()) { diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index aa836fe..0a5b091 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -30,11 +30,12 @@ void print(std::string msg) { fmt::print("{}\n", msg); } } // namespace +// NOLINTNEXTLINE(bugprone-exception-escape) auto main(int argc, char* argv[]) -> int { if (argc != 3) { - fmt::print(stderr, "Usage: {} \n", argv[0]); + fmt::print(stderr, "Usage: {} \n", argv[0]); // NOLINT return EXIT_FAILURE; } @@ -45,14 +46,14 @@ auto main(int argc, char* argv[]) -> int boost::asio::io_context io_context; tcp::resolver resolver(io_context); - auto c = std::make_shared< async_rrcp_client >(io_context); - c->register_trap_handler(&print); - c->start(resolver.resolve(argv[1], argv[2])); + auto client = std::make_shared< async_rrcp_client >(io_context); + client->register_trap_handler(&print); + client->start(resolver.resolve(argv[1], argv[2])); std::thread io_thread([&io_context]() { io_context.run(); }); std::this_thread::sleep_for(TIMEOUT_DURATION); // NOTE: only for gcov results! CK - for (std::string line; c->connected() && std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) + for (std::string line; client->connected() && std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) { const std::string::size_type sz = line.find("//"); if ((sz != std::string::npos)) @@ -71,17 +72,17 @@ auto main(int argc, char* argv[]) -> int continue; } - const auto response = c->write(line); + const auto response = client->write(line); fmt::print("{}\n", response); } std::this_thread::sleep_for(HEARTBEAT_INTERVAL); // NOTE: only for gcov results! CK - c->stop(); + client->stop(); io_thread.join(); } - catch (std::exception& e) + catch (const std::exception& e) { - fmt::print(stderr, "Exception: {}\n", e.what()); + fmt::print(stderr, "Exception: {}\n", e.what()); // NOLINT return EXIT_FAILURE; } diff --git a/rrcp_client.cpp b/rrcp_client.cpp index a78dbea..53a6d02 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -139,6 +139,7 @@ class rrcp_client rrcp_message_queue write_msgs_; }; +// NOLINTNEXTLINE(bugprone-exception-escape) auto main(int argc, char* argv[]) -> int { using namespace std::chrono_literals; @@ -156,9 +157,9 @@ auto main(int argc, char* argv[]) -> int tcp::resolver resolver(io_context); auto endpoints = resolver.resolve(argv[1], argv[2]); - rrcp_client c(io_context, endpoints); + rrcp_client client(io_context, endpoints); - std::thread t([&io_context]() { io_context.run(); }); + std::thread runner([&io_context]() { io_context.run(); }); //================================================================ std::string binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"s}; @@ -172,7 +173,7 @@ auto main(int argc, char* argv[]) -> int assert(quoted.length() == 33); //================================================================ - int i{}; + int count{}; std::this_thread::sleep_for(100ms); // NOLINT(misc-include-cleaner) for (std::string line; std::getline(std::cin, line);) { @@ -188,18 +189,18 @@ auto main(int argc, char* argv[]) -> int continue; } - std::cerr << ++i << '\t' << line << '\n'; + std::cerr << ++count << '\t' << line << '\n'; rrcp_message msg; msg.body_length(line.length()); std::memcpy(msg.body(), line.c_str(), msg.body_length()); msg.encode_body(); msg.encode_header(); - c.write(msg); + client.write(msg); } std::this_thread::sleep_for(100ms); // NOLINT(misc-include-cleaner) - c.close(); - t.join(); + client.close(); + runner.join(); } catch (std::exception& e) { diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 98210e9..37ee1d7 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -20,14 +20,14 @@ auto rrcp::esc2char(std::string_view data) -> std::string auto len = data.size(); for (size_t i = 0; i < len; ++i) { - char c = data[i]; + char ch = data[i]; - if (c == STOP) + if (ch == STOP) { return message; } - if (c == ESC) + if (ch == ESC) { if (i == len - 1) { @@ -38,20 +38,20 @@ auto rrcp::esc2char(std::string_view data) -> std::string switch (next) { case REPLACE_LF: - c = '\n'; + ch = '\n'; break; case REPLACE_CR: - c = '\r'; + ch = '\r'; break; case REPLACE_ESC: - c = ESC; + ch = ESC; break; default: throw std::runtime_error("esc2char: Error - unexpected ESC character!"); } } - message.push_back(c); + message.push_back(ch); } return message; } @@ -59,9 +59,9 @@ auto rrcp::esc2char(std::string_view data) -> std::string auto rrcp::char2esc(std::string_view data) -> std::string { std::string message; - for (char const c : data) + for (char const ch : data) { - switch (c) + switch (ch) { case '\n': message.push_back(ESC); @@ -76,7 +76,7 @@ auto rrcp::char2esc(std::string_view data) -> std::string message.push_back(REPLACE_ESC); break; default: - message.push_back(c); + message.push_back(ch); break; } } diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index 332ea3f..7c74a85 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -251,4 +251,4 @@ ut::suite errors = [] // ============================================================ }; -int main() {} +auto main() -> int {} From 16994fd1b36c140b6b9bad4fa03b487e9ea172ce Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 13 May 2025 22:20:57 +0200 Subject: [PATCH 088/120] Cleanup on OSX --- .clang-tidy | 2 +- CMakeLists.txt | 6 +++--- examples/CMakeLists.txt | 2 +- tests/CMakeLists.txt | 24 +++++++----------------- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 8af0133..c6fa737 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,7 +5,7 @@ bugprone-reserved-identifier,\ boost-*,\ cert-*,\ clang-analyzer-*,\ -clang-analyzer-unix.BlockInCriticalSection,\ +-clang-analyzer-unix.BlockInCriticalSection,\ cppcoreguidelines-*,\ -cppcoreguidelines-avoid-*,\ -cppcoreguidelines-init-variables,\ diff --git a/CMakeLists.txt b/CMakeLists.txt index 879442c..fc1cbcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") COMMAND_ECHO STDOUT ) string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) - set(CMAKE_CXX_STANDARD 23) + set(CMAKE_CXX_STANDARD 20) elseif(LINUX) set(LLVM_PREFIX $ENV{LLVM_ROOT}) endif() @@ -55,7 +55,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # ---- code coverage ---- option(BUILD_TESTING "Build ctest" ${PROJECT_IS_TOP_LEVEL}) -option(BUILD_EXAMPLES "Compile examples too" ${PROJECT_IS_TOP_LEVEL}) +option(BUILD_EXAMPLES "Compile examples too" NO) option( ENABLE_TEST_COVERAGE "Compile with test-coverage flags" @@ -116,7 +116,7 @@ add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) target_link_libraries(rrcp_client PRIVATE rrcp_helper) do_test(rrcp_client --help Usage) -if(APPLE AND BUILD_TESTING) +if(APPLE AND BUILD_EXAMPLES) # TODO(CK): mv to examples too! add_executable(timer timer.cpp) target_link_libraries( diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e7dd4ea..b987ea4 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25...4.0) -project(Base64-examples VERSION 0.1.0 LANGUAGES CXX) +project(Base64-examples VERSION 0.1.1 LANGUAGES CXX) find_package(Poco 1.14 COMPONENTS Foundation REQUIRED) find_package(Threads) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9016d8c..ef8308c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,22 +1,13 @@ include(FetchContent) -FetchContent_Declare( - fmt - GIT_TAG 11.1.4 - GIT_REPOSITORY https://github.com/fmtlib/fmt.git - FIND_PACKAGE_ARGS 11.1.4 NAMES fmt - EXCLUDE_FROM_ALL - SYSTEM -) - # we use boost::ut FetchContent_Declare( ut GIT_TAG v2.3.1 GIT_REPOSITORY https://github.com/boost-ext/ut.git - FIND_PACKAGE_ARGS 2.3.1 NAMES ut EXCLUDE_FROM_ALL SYSTEM + FIND_PACKAGE_ARGS 2.3.1 NAMES ut ) # TODO(CK): We still use googletest too! But will be changed to Boost::ut @@ -24,9 +15,9 @@ FetchContent_Declare( googletest GIT_TAG v1.16.0 GIT_REPOSITORY https://github.com/google/googletest.git - FIND_PACKAGE_ARGS 1.16.0 NAMES GTest COMPONENTS gmain_main EXCLUDE_FROM_ALL SYSTEM + FIND_PACKAGE_ARGS 1.16.0 NAMES GTest COMPONENTS gmain_main ) # For Windows: Prevent overriding the parent project's compiler/linker settings @@ -45,12 +36,11 @@ target_sources( BASE_DIRS ${CMAKE_SOURCE_DIR} FILES ${CMAKE_SOURCE_DIR}/Base64.hpp ) -target_link_libraries( - rrcp_helper - PUBLIC - fmt::fmt-header-only - Boost::headers # Not needed: PUBLIC Boost::beast -) +# target_link_libraries( +# rrcp_helper +# PUBLIC +# Boost::headers # Not needed: PUBLIC Boost::beast +# ) add_executable(RRCP-test RRCP-test.cpp) target_link_libraries(RRCP-test PRIVATE rrcp_helper Boost::ut) From cadb49ff8e85e93e68058c9b4c1f6a05196b0b08 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Wed, 28 May 2025 10:35:53 +0200 Subject: [PATCH 089/120] Deregitster traps again in examples --- rrcp.txt | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/rrcp.txt b/rrcp.txt index 693629c..62fc495 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -28,15 +28,18 @@ M:Audio GAudioVolume M:Audio TAudioVolume1 M:Audio SAudioVolume"Level 0" M:Audio TAudioVolume0 -M:Control SActPreset0 +M:Control GPresetID +M:Control GCurrWF +M:Control TPresetID1 +M:Control TCurrWF1 M:Control GCurrMission M:Control TCurrMission1 +// change preset M:Control SCurrMission"testString" +M:Control SActPreset0 M:Control TCurrMission0 -M:Control GCurrWF -M:Control TCurrWF1 -M:Control GPresetID -M:Control TPresetID1 +M:Control TCurrWF0 +M:Control TPresetID0 M:Control GTxInhibit M:Control TTxInhibit1 M:Control STxInhibit"Disabled" @@ -56,6 +59,8 @@ M:OBIT TGOState1 M:OBIT GTestErrors M:OBIT GTestIDs M:OBIT TErrorEvent1 +M:OBIT TGOState0 +M:OBIT TErrorEvent0 M:RxTx GPowerLevel M:RxTx TPowerLevel1 M:RxTx SPowerLevel"Off" @@ -63,11 +68,14 @@ M:RxTx TPowerLevel0 M:RxTx GVswr M:RxTx TVswr1 M:RxTx GVswrThres +M:RxTx TVswr0 M:RxTx TVswrThres1 M:RxTx GVswrThresLev M:RxTx SVswrThresLev10 -M:Utility GBattStatus +M:RxTx TVswrThres0 M:Utility TBattStatus1 +M:Utility GBattStatus +M:Utility TBattStatus0 M:Utility GErrorText0,"English" M:Utility GInitialInfo"VersionStr","IdString",0 M:Utility GPing"message" From ff763472a6534ae1dc675ed39004fd6f27aee32e Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Mon, 27 Oct 2025 23:18:38 +0100 Subject: [PATCH 090/120] Upgrade most packages used make fix make check make format Prevent or fix clang tidy warnings on OSX with llmv-21 --- .clang-format | 8 +++++++- .clang-tidy | 8 ++++++++ .codespellrc | 6 ++++++ Base64.cpp | 2 +- CMakeLists.txt | 11 ++++++----- GNUmakefile | 8 ++++++-- async_rrcp_client.hpp | 18 ++++++++++-------- examples/CMakeLists.txt | 4 ++-- examples/async_tcp_echo_client.cpp | 14 +++++++------- examples/async_tcp_echo_server.cpp | 12 ++++++------ examples/blocking_tcp_echo_server.cpp | 2 +- rrcp_async_tcp_client.cpp | 2 +- rrcp_client.cpp | 16 ++++++++-------- tests/Base64-test.cpp | 6 +++--- tests/CMakeLists.txt | 4 ++-- 15 files changed, 74 insertions(+), 47 deletions(-) create mode 100644 .codespellrc diff --git a/.clang-format b/.clang-format index e6902fc..50807a7 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,11 @@ +--- BasedOnStyle: Google -Standard: Auto +IndentWidth: 2 +UseTab: Never +--- +Language: Cpp +Standard: c++17 +# Standard: Auto AlignAfterOpenBracket: false AlignEscapedNewlinesLeft: true AlwaysBreakAfterDefinitionReturnType: None diff --git a/.clang-tidy b/.clang-tidy index c6fa737..b16301b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,17 +2,24 @@ Checks: "-*,\ bugprone-*,\ bugprone-reserved-identifier,\ +-bugprone-exception-escape,\ +-bugprone-unused-return-value,\ boost-*,\ +-boost-use-ranges,\ cert-*,\ +-cert-err33-c,\ +-cert-err58-cpp,\ clang-analyzer-*,\ -clang-analyzer-unix.BlockInCriticalSection,\ cppcoreguidelines-*,\ -cppcoreguidelines-avoid-*,\ -cppcoreguidelines-init-variables,\ -cppcoreguidelines-macro-*,\ +-cppcoreguidelines-narrowing-conversions,\ -cppcoreguidelines-owning-memory,\ -cppcoreguidelines-prefer-member-initializer,\ -cppcoreguidelines-pro-bounds-pointer-arithmetic,\ +-cppcoreguidelines-pro-type-reinterpret-cast,\ hicpp-*,\ misc-*,\ -misc-const-correctness,\ @@ -24,6 +31,7 @@ modernize-*,\ performance-*,\ -performance-enum-size,\ portability-*,\ +-portability-avoid-pragma-once,\ readability-*,\ readability-identifier-length,\ readability-identifier-naming,\ diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..01b53b5 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,6 @@ +[codespell] +builtin = clear,rare,en-GB_to_en-US,names,informal,code +check-hidden = +skip = ./.git,./.direnv,./build/*,./prefix/*,./coverage/*,./stagedir/*,*.html,*.xsd,*.xsl,*.pdf,*.log,.*.swp,*~,*.bak,./.cache/* +quiet-level = 2 +ignore-words-list = claus,cancelled,cancelling,stoll,QUE,WS,fo,deque diff --git a/Base64.cpp b/Base64.cpp index 643a568..52ccd2d 100644 --- a/Base64.cpp +++ b/Base64.cpp @@ -40,7 +40,7 @@ class base64 // Function to remove all whitespace characters from a std::string_view (C++20) static auto remove_whitespace(std::string_view input) -> std::string { - auto filtered = input | std::views::filter([](unsigned char c) { return !std::isspace(c); }); + auto filtered = input | std::views::filter([](unsigned char c) -> bool { return !std::isspace(c); }); return {filtered.begin(), filtered.end()}; } #endif diff --git a/CMakeLists.txt b/CMakeLists.txt index fc1cbcd..ac0480b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.25...4.0) +cmake_minimum_required(VERSION 3.25...4.2) project(RRCP-client VERSION 0.1.1 LANGUAGES CXX) @@ -11,7 +11,8 @@ if(NOT TARGET Boost::headers) find_package( Boost 1.71 - COMPONENTS headers signals2 + COMPONENTS + headers # XXX signals2 REQUIRED HINTS $ENV{HOME}/.local ) @@ -19,11 +20,11 @@ endif() FetchContent_Declare( fmt - GIT_TAG 11.2.0 + GIT_TAG 12.0.0 GIT_REPOSITORY https://github.com/fmtlib/fmt.git EXCLUDE_FROM_ALL SYSTEM - FIND_PACKAGE_ARGS 11.2.0 NAMES fmt HINTS $ENV{HOME}/.local + FIND_PACKAGE_ARGS 12.0.0 NAMES fmt HINTS $ENV{HOME}/.local ) FetchContent_MakeAvailable(fmt) @@ -34,7 +35,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if(APPLE) execute_process( OUTPUT_VARIABLE LLVM_PREFIX - COMMAND brew --prefix llvm@20 + COMMAND brew --prefix llvm COMMAND_ECHO STDOUT ) string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) diff --git a/GNUmakefile b/GNUmakefile index f57586d..ba868e7 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -42,13 +42,15 @@ readability-avoid-const-params-in-decls,\ readability-braces-around-statements,\ readability-container-data-pointer,\ readability-container-size-empty,\ -readability-convert-member-functions-to-static,\ +-readability-convert-member-functions-to-static,\ readability-else-after-return,\ readability-identifier-naming,\ readability-implicit-bool-conversion,\ readability-make-member-function-const,\ readability-redundant-member-init,\ readability-simplify-boolean-expr,\ +readability-static-accessed-through-instance,\ +readability-use-concise-preprocessor-directives,\ readability-use-std-min-max,\ ' \ *.cpp examples/*.cpp # $(CPPFILES) @@ -72,8 +74,10 @@ test: all gcovr format: .clang-format - git ls-files ::*.cpp ::*.hpp | xargs clang-format -i + -codespell + git ls-files ::*.cpp ::*.hpp ::*.json | xargs clang-format -i git ls-files ::*CMakeLists.txt | xargs gersemi -i --no-warn-about-unknown-commands + git ls-files ::*.py | xargs black # These rules keep make from trying to use the match-anything rule below # to rebuild the makefiles--ouch! diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index ddd72b6..e1ad320 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -64,7 +64,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client check_deadline(); boost::asio::async_connect(socket_, endpoints, - [self = shared_from_this()](const boost::system::error_code& ec, const tcp::endpoint&) + [self = shared_from_this()](const boost::system::error_code& ec, const tcp::endpoint&) -> void { if (!ec) { @@ -105,7 +105,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client auto command = rrcp::create_command_msg(message, msg_id_str, msg_id_); boost::asio::post(io_context_, - [this, command]() + [this, command]() -> void { bool const write_in_progress{!write_msgs_.empty()}; write_msgs_.push_back(command); @@ -127,7 +127,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client do { boost::asio::post(io_context_, - [this, &response]() + [this, &response]() -> void { if (!read_msgs_.empty()) { @@ -165,7 +165,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client void do_read() { boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), STOP, - [self = shared_from_this()](const boost::system::error_code& ec, std::size_t length) + [self = shared_from_this()](const boost::system::error_code& ec, std::size_t length) -> void { if (!ec) { @@ -204,7 +204,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client void do_write() { boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front()), - [self = shared_from_this()](boost::system::error_code ec, std::size_t /*length*/) + [self = shared_from_this()](boost::system::error_code ec, std::size_t /*length*/) -> void { if (!ec) { @@ -236,12 +236,13 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); // TRACE boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), - [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) + [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) -> void { if (!ec) { self->heartbeat_timer_.expires_after(HEARTBEAT_INTERVAL); - self->heartbeat_timer_.async_wait([self](const boost::system::error_code&) { self->send_heartbeat(); }); + self->heartbeat_timer_.async_wait( + [self](const boost::system::error_code&) -> void { self->send_heartbeat(); }); } else { @@ -265,7 +266,8 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client return; } - deadline_.async_wait([self = shared_from_this()](const boost::system::error_code&) { self->check_deadline(); }); + deadline_.async_wait( + [self = shared_from_this()](const boost::system::error_code&) -> void { self->check_deadline(); }); } boost::asio::io_context& io_context_; diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b987ea4..54bc421 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.25...4.0) +cmake_minimum_required(VERSION 3.25...4.2) project(Base64-examples VERSION 0.1.1 LANGUAGES CXX) @@ -9,7 +9,7 @@ if(NOT TARGET Boost::headers) find_package(Boost 1.71 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) endif() if(NOT TARGET fmt::fmt-header-only) - find_package(fmt 11 REQUIRED HINTS $ENV{HOME}/.local) + find_package(fmt 12 REQUIRED HINTS $ENV{HOME}/.local) endif() enable_testing() diff --git a/examples/async_tcp_echo_client.cpp b/examples/async_tcp_echo_client.cpp index 3c00233..f2c60c0 100644 --- a/examples/async_tcp_echo_client.cpp +++ b/examples/async_tcp_echo_client.cpp @@ -44,12 +44,12 @@ class asynchronous_tcp_client : public std::enable_shared_from_this< asynchronou : io_context_(io_context), resolver_(io_context), socket_(io_context), timer_(io_context) { resolver_.async_resolve(host, port, - [this](boost::system::error_code ec, const tcp::resolver::results_type& results) + [this](boost::system::error_code ec, const tcp::resolver::results_type& results) -> void { if (!ec) { boost::asio::async_connect(socket_, results, - [this](boost::system::error_code ec, const tcp::endpoint&) + [this](boost::system::error_code ec, const tcp::endpoint&) -> void { if (!ec) { @@ -81,7 +81,7 @@ class asynchronous_tcp_client : public std::enable_shared_from_this< asynchronou } boost::asio::post(io_context_, - [this, message]() + [this, message]() -> void { bool const write_in_progress{!write_msgs_.empty()}; write_msgs_.push_back(message); @@ -109,7 +109,7 @@ class asynchronous_tcp_client : public std::enable_shared_from_this< asynchronou { auto self(shared_from_this()); boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front()), - [this, self](boost::system::error_code ec, std::size_t /*length*/) + [this, self](boost::system::error_code ec, std::size_t /*length*/) -> void { if (!ec) { @@ -141,7 +141,7 @@ class asynchronous_tcp_client : public std::enable_shared_from_this< asynchronou { timer_.expires_after(TIMEOUT_DURATION); timer_.async_wait( - [this, self](const boost::system::error_code& ec) + [this, self](const boost::system::error_code& ec) -> void { if (!ec) { @@ -152,7 +152,7 @@ class asynchronous_tcp_client : public std::enable_shared_from_this< asynchronou } boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(data_), STOP, - [this, self](boost::system::error_code ec, std::size_t length) + [this, self](boost::system::error_code ec, std::size_t length) -> void { timer_.cancel(); if (!ec) @@ -207,7 +207,7 @@ auto main(int argc, char* argv[]) -> int auto client = std::make_shared< asynchronous_tcp_client >(io_context, argv[1], argv[2]); - std::thread io_thread([&io_context]() { io_context.run(); }); + std::thread io_thread([&io_context]() -> void { io_context.run(); }); for (std::string line; std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) { diff --git a/examples/async_tcp_echo_server.cpp b/examples/async_tcp_echo_server.cpp index 69b66b6..d7cfbd1 100644 --- a/examples/async_tcp_echo_server.cpp +++ b/examples/async_tcp_echo_server.cpp @@ -42,7 +42,7 @@ class session : public std::enable_shared_from_this< session > { auto self(shared_from_this()); socket_.async_read_some(boost::asio::buffer(data_.data(), MAX_LENGTH), - [this, self](boost::system::error_code ec, std::size_t length) + [this, self](boost::system::error_code ec, std::size_t length) -> void { if (!ec) { @@ -59,7 +59,7 @@ class session : public std::enable_shared_from_this< session > { auto self(shared_from_this()); boost::asio::async_write(socket_, boost::asio::buffer(data_.data(), length), - [this, self](boost::system::error_code ec, std::size_t /*length*/) + [this, self](boost::system::error_code ec, std::size_t /*length*/) -> void { if (!ec) { @@ -88,7 +88,7 @@ class server signals_.add(SIGINT); signals_.add(SIGTERM); -#if defined(SIGQUIT) +#ifdef SIGQUIT signals_.add(SIGQUIT); #endif // defined(SIGQUIT) @@ -101,7 +101,7 @@ class server void do_accept() { acceptor_.async_accept(socket_, - [this](boost::system::error_code ec) + [this](boost::system::error_code ec) -> void { if (!ec) { @@ -120,7 +120,7 @@ class server void do_await_stop() { signals_.async_wait( - [this](std::error_code ec, int signo) + [this](std::error_code ec, int signo) -> void { std::cerr << "Signal handler called for " << signo << "\n"; if (!ec) @@ -162,7 +162,7 @@ auto main(int argc, char* argv[]) -> int boost::asio::io_context io_context; - server const serv(io_context, std::strtol(argv[1], nullptr, 10)); + server const serv(io_context, static_cast< short >(std::strtol(argv[1], nullptr, 10))); io_context.run(); std::cout << "io_service.run complete, shutdown successful\n"; diff --git a/examples/blocking_tcp_echo_server.cpp b/examples/blocking_tcp_echo_server.cpp index c0e2602..06eeada 100644 --- a/examples/blocking_tcp_echo_server.cpp +++ b/examples/blocking_tcp_echo_server.cpp @@ -82,7 +82,7 @@ auto main(int argc, char* argv[]) -> int boost::asio::io_context io_context; - server(io_context, std::strtol(argv[1], nullptr, 10)); + server(io_context, static_cast< short >(std::strtol(argv[1], nullptr, 10))); } catch (std::exception& e) { diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 0a5b091..fbc9b90 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -50,7 +50,7 @@ auto main(int argc, char* argv[]) -> int client->register_trap_handler(&print); client->start(resolver.resolve(argv[1], argv[2])); - std::thread io_thread([&io_context]() { io_context.run(); }); + std::thread io_thread([&io_context]() -> void { io_context.run(); }); std::this_thread::sleep_for(TIMEOUT_DURATION); // NOTE: only for gcov results! CK for (std::string line; client->connected() && std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) diff --git a/rrcp_client.cpp b/rrcp_client.cpp index 53a6d02..4e4c01e 100644 --- a/rrcp_client.cpp +++ b/rrcp_client.cpp @@ -48,7 +48,7 @@ class rrcp_client void write(const rrcp_message& msg) { boost::asio::post(io_context_, - [this, msg]() + [this, msg]() -> void { bool const write_in_progress{!write_msgs_.empty()}; write_msgs_.push_back(msg); @@ -61,15 +61,15 @@ class rrcp_client void close() { - boost::asio::post(io_context_, [this]() { write_msgs_.clear(); }); - boost::asio::post(io_context_, [this]() { socket_.close(); }); + boost::asio::post(io_context_, [this]() -> void { write_msgs_.clear(); }); + boost::asio::post(io_context_, [this]() -> void { socket_.close(); }); } private: void do_connect(const tcp::resolver::results_type& endpoints) { boost::asio::async_connect(socket_, endpoints, - [this](boost::system::error_code ec, const tcp::endpoint&) + [this](boost::system::error_code ec, const tcp::endpoint&) -> void { if (!ec) { @@ -81,7 +81,7 @@ class rrcp_client void do_read_header() { boost::asio::async_read(socket_, boost::asio::buffer(read_msg_.data(), rrcp_message::HEADER_LENGTH), - [this](boost::system::error_code ec, std::size_t /*length*/) + [this](boost::system::error_code ec, std::size_t /*length*/) -> void { if (!ec && read_msg_.decode_header()) { @@ -97,7 +97,7 @@ class rrcp_client void do_read_body() { boost::asio::async_read(socket_, boost::asio::buffer(read_msg_.body(), read_msg_.body_length()), - [this](boost::system::error_code ec, std::size_t /*length*/) + [this](boost::system::error_code ec, std::size_t /*length*/) -> void { if (!ec && read_msg_.decode_body()) { @@ -116,7 +116,7 @@ class rrcp_client void do_write() { boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front().data(), write_msgs_.front().length()), - [this](boost::system::error_code ec, std::size_t /*length*/) + [this](boost::system::error_code ec, std::size_t /*length*/) -> void { if (!ec) { @@ -159,7 +159,7 @@ auto main(int argc, char* argv[]) -> int auto endpoints = resolver.resolve(argv[1], argv[2]); rrcp_client client(io_context, endpoints); - std::thread runner([&io_context]() { io_context.run(); }); + std::thread runner([&io_context]() -> void { io_context.run(); }); //================================================================ std::string binary{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"s}; diff --git a/tests/Base64-test.cpp b/tests/Base64-test.cpp index ebb21bf..695aced 100644 --- a/tests/Base64-test.cpp +++ b/tests/Base64-test.cpp @@ -37,8 +37,8 @@ namespace // see https://datatracker.ietf.org/doc/html/rfc4648#section-10 struct testpattern_t { - const char *bin_; - const char *encoded_; + const char* bin_; + const char* encoded_; } testpattern[] = { // {"", ""}, // {"f", "Zg=="}, // @@ -286,7 +286,7 @@ TEST(Base64Test, RandomBinaryData) } #endif -auto main(int argc, char **argv) -> int +auto main(int argc, char** argv) -> int { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ef8308c..321a6a2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,8 +33,8 @@ target_sources( PRIVATE ${CMAKE_SOURCE_DIR}/Base64.cpp PUBLIC FILE_SET HEADERS - BASE_DIRS ${CMAKE_SOURCE_DIR} - FILES ${CMAKE_SOURCE_DIR}/Base64.hpp + BASE_DIRS ${CMAKE_SOURCE_DIR} + FILES ${CMAKE_SOURCE_DIR}/Base64.hpp ) # target_link_libraries( # rrcp_helper From ff54775dea13d471eea3d0e2cefb8e484aea6942 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 28 Oct 2025 13:18:59 +0100 Subject: [PATCH 091/120] Prepare thread save rrcp client --- CMakeLists.txt | 2 +- GNUmakefile | 69 ++++++++++++++++++++++++++++--------------- async_rrcp_client.hpp | 13 ++++---- rrcp_helper.cpp | 11 +++++-- rrcp_helper.hpp | 2 +- tests/RRCP-test.cpp | 66 ++++++++++++++++++++--------------------- 6 files changed, 98 insertions(+), 65 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ac0480b..7204f48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") COMMAND_ECHO STDOUT ) string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) - set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD 23) elseif(LINUX) set(LLVM_PREFIX $ENV{LLVM_ROOT}) endif() diff --git a/GNUmakefile b/GNUmakefile index ba868e7..ef66f5c 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -5,28 +5,51 @@ MAKEFLAGS+= --no-builtin-rules MAKEFLAGS+= --warn-undefined-variables -CPPFILES:= $(shell git ls-files ::*.cpp) +export hostSystemName=$(shell uname) +export GCOV="llvm-cov gcov" + +ifeq (${hostSystemName},Darwin) + export LLVM_PREFIX:=$(shell brew --prefix llvm) + export LLVM_DIR?=$(shell realpath ${LLVM_PREFIX}) + export PATH:=${LLVM_DIR}/bin:${PATH} + export CXX:=clang++ + + # to test g++-15: + #XXX export CXX:=g++-15 + #XXX export CXXFLAGS:=-stdlib=libstdc++ +else ifeq (${hostSystemName},Linux) + export LLVM_DIR?=/usr/lib/llvm-20 + export PATH:=${LLVM_DIR}/bin:${PATH} + export CXX:=clang++-20 +endif + + +CPPFILES:= $(shell git ls-files ::*.cpp | grep -vw tests) + +CMAKE_BUILD_TYPE?=Debug +BUILD_DIR:=build/$(CMAKE_BUILD_TYPE) .PHONY: all format test check distclean -all: build - ninja -C build +all: $(BUILD_DIR) + ninja -C $(BUILD_DIR) -clean: build +clean: $(BUILD_DIR) -ninja -C $< $@ -find $< -name '*.gcda' -delete distclean: # XXX clean - rm -rf build coverage/* *~ ctags + rm -rf $(BUILD_DIR) build coverage/* *~ ctags -build: CMakeLists.txt - cmake -S . -B $@ -G Ninja -D CMAKE_BUILD_TYPE=Debug # --fresh +$(BUILD_DIR): CMakeLists.txt + -test -d build/appleclang-debug && ln -s build/appleclang-debug $(BUILD_DIR) + cmake -S . -B $@ -G Ninja -D CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) --log-level=VERBOSE # --fresh check: all - run-clang-tidy -p build *.cpp examples/*.cpp # $(CPPFILES) + run-clang-tidy -p $(BUILD_DIR) $(CPPFILES) fix: all - run-clang-tidy -p build -fix -checks='-*,\ + run-clang-tidy -p $(BUILD_DIR) -fix -checks='-*,\ hicpp-explicit-conversions,\ hicpp-member-init,\ hicpp-named-parameter,\ @@ -53,23 +76,23 @@ readability-static-accessed-through-instance,\ readability-use-concise-preprocessor-directives,\ readability-use-std-min-max,\ ' \ - *.cpp examples/*.cpp # $(CPPFILES) + $(CPPFILES) -test: all +test: $(BUILD_DIR) # XXX all -killall async_tcp_echo_server - -echo | build/async_tcp_echo_client localhost 8000 - build/async_tcp_echo_server 8000 & - -(cat rrcp.txt | build/async_tcp_echo_client localhost 8000) & + -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 + $(BUILD_DIR)/async_tcp_echo_server 8000 & + -(cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000) & sleep 1 -killall async_tcp_echo_server - -(echo | build/async_tcp_echo_server 8000) & - cat rrcp.txt | build/rrcp_client localhost 8000 - cat rrcp.txt | build/rrcp_async_tcp_client localhost 8000 - cat rrcp.txt | build/async_tcp_echo_client localhost 8000 - -build/async_tcp_echo_client localhost - -echo | build/async_tcp_echo_client localhost 8001 - cat rrcp.txt | build/blocking_tcp_echo_client localhost 8000 - ctest --test-dir build + -(echo | $(BUILD_DIR)/async_tcp_echo_server 8000) & + cat rrcp.txt | $(BUILD_DIR)/rrcp_client localhost 8000 + cat rrcp.txt | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000 + cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 + -$(BUILD_DIR)/async_tcp_echo_client localhost + -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8001 + cat rrcp.txt | $(BUILD_DIR)/blocking_tcp_echo_client localhost 8000 + ctest --test-dir $(BUILD_DIR) -killall async_tcp_echo_server gcovr @@ -89,5 +112,5 @@ GNUmakefile :: ; # Anything we don't know how to build will use this rule. The command is # a do-nothing command. -% :: build +% :: $(BUILD_DIR) ninja -C $< $@ diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index e1ad320..6a4e96c 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -102,6 +103,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } std::string msg_id_str; + msg_id_ = ++msg_id_ % INVALID_ID; auto command = rrcp::create_command_msg(message, msg_id_str, msg_id_); boost::asio::post(io_context_, @@ -155,8 +157,9 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client fmt::print(stderr, "Stopped, disconnecting ...\n"); stopped_ = true; connected_ = false; - boost::system::error_code ec; - socket_.close(ec); + // boost::system::error_code ec; + // socket_.close(ec); + boost::asio::post(io_context_, [this]() -> void { socket_.close(); }); heartbeat_timer_.cancel(); deadline_.cancel(); } @@ -277,9 +280,9 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::string input_buffer_; message_queue read_msgs_; message_queue write_msgs_; - int msg_id_{10000}; - bool connected_{false}; - bool stopped_{false}; + std::atomic msg_id_{10000}; + std::atomic connected_{false}; + std::atomic stopped_{false}; signal_string_type trap_handler_; }; diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 37ee1d7..5fca4e2 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -141,16 +141,23 @@ auto rrcp::find_response_msg(std::string& response, const std::string& msg_id) - return false; } -extern auto rrcp::create_command_msg(const std::string& message, std::string& msg_id_str, int& msg_id) -> std::string +auto rrcp::create_command_msg(const std::string& message, std::string& msg_id_str, int msg_id) -> std::string { + if(msg_id >= INVALID_ID) + { + msg_id = 1; + } // Insert the next message number for Set/Get request. // But prevent to insert the msg_id for Trap commands! auto trap_cmd = message.find(" T"); if (trap_cmd == std::string::npos) { - msg_id = ++msg_id % INVALID_ID; msg_id_str = fmt::format("{}", (msg_id)); } + else + { + msg_id_str.clear(); + } std::string msg = insertAfterFirstWord(message, msg_id_str); // DEBUG: fmt::print("rrcp MU to send({})\n", msg); diff --git a/rrcp_helper.hpp b/rrcp_helper.hpp index 13fad00..626f51f 100644 --- a/rrcp_helper.hpp +++ b/rrcp_helper.hpp @@ -39,6 +39,6 @@ extern auto insertAfterFirstWord(const std::string& input, const std::string& to extern auto find_response_msg(std::string& response, const std::string& msg_id) -> bool; // helper which returns the command msg with next valid msg_id inserted if needed -extern auto create_command_msg(const std::string& message, std::string& msg_id_str, int& msg_id) -> std::string; +extern auto create_command_msg(const std::string& message, std::string& msg_id_str, int msg_id) -> std::string; } // namespace rrcp diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index 7c74a85..9ce7ff0 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -11,59 +11,59 @@ namespace ut = boost::ut; -ut::suite errors = [] +ut::suite errors = [] -> void { using namespace ut; using namespace std::literals; "find_response_msg"_test = [] - { + -> void { constexpr std::string_view EXPECTED{"gGoState"sv}; const std::string message{"123456 gGoState"}; std::string result{message}; auto found = rrcp::find_response_msg(result, "123456"); expect(found); expect(EXPECTED == result); -#if defined(BOOST_UT_HAS_FORMAT) +#ifdef BOOST_UT_HAS_FORMAT ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotfind_error_response_msg"_test = [] - { + -> void { constexpr std::string_view EXPECTED{"E:1"sv}; const std::string message{"E:1"}; std::string result{message}; auto found = rrcp::find_response_msg(result, "0815"); expect(found); expect(EXPECTED == result); -#if defined(BOOST_UT_HAS_FORMAT) +#ifdef BOOST_UT_HAS_FORMAT ut::log("{} == {}\n", result, EXPECTED); #endif }; "find_error_response_msg"_test = [] - { + -> void { constexpr std::string_view EXPECTED{"E:10"sv}; const std::string message{"E:10 123456"}; std::string result{message}; auto found = rrcp::find_response_msg(result, "123456"); expect(found); expect(EXPECTED == result); -#if defined(BOOST_UT_HAS_FORMAT) +#ifdef BOOST_UT_HAS_FORMAT ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotfind_response_msg"_test = [] - { + -> void { constexpr std::string_view EXPECTED{"d NoGo"sv}; const std::string message{EXPECTED}; std::string result{message}; auto found = rrcp::find_response_msg(result, "123456"); expect(!found); expect(EXPECTED == result); -#if defined(BOOST_UT_HAS_FORMAT) +#ifdef BOOST_UT_HAS_FORMAT ut::log("{} == {}\n", result, EXPECTED); #endif }; @@ -71,45 +71,45 @@ ut::suite errors = [] // ============================================================ "insertAfterFirstWord"_test = [] - { + -> void { constexpr std::string_view EXPECTED{"M:test 123456 GGoState"sv}; const std::string command{"M:test GGoState"}; auto result = rrcp::insertAfterFirstWord(command, "123456"); expect(EXPECTED == result); -#if defined(BOOST_UT_HAS_FORMAT) +#ifdef BOOST_UT_HAS_FORMAT ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotInsertAnEmptyString"_test = [] - { + -> void { constexpr std::string_view EXPECTED{"M:test GGoState"sv}; const std::string command{EXPECTED}; auto result = rrcp::insertAfterFirstWord(command, ""); expect(EXPECTED == result); -#if defined(BOOST_UT_HAS_FORMAT) +#ifdef BOOST_UT_HAS_FORMAT ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotInsertBeforeTrapCmd"_test = [] - { + -> void { constexpr std::string_view EXPECTED{"M:test TGoState1"sv}; const std::string command{EXPECTED}; auto result = rrcp::insertAfterFirstWord(command, ""); expect(EXPECTED == result); -#if defined(BOOST_UT_HAS_FORMAT) +#ifdef BOOST_UT_HAS_FORMAT ut::log("{} == {}\n", result, EXPECTED); #endif }; "doNotInsertAfterSingleWord"_test = [] - { + -> void { constexpr std::string_view EXPECTED{"E:10"sv}; const std::string message{EXPECTED}; auto result = rrcp::insertAfterFirstWord(message, "123456"); expect(EXPECTED == result); -#if defined(BOOST_UT_HAS_FORMAT) +#ifdef BOOST_UT_HAS_FORMAT ut::log("{} == {}\n", result, EXPECTED); #endif }; @@ -117,18 +117,18 @@ ut::suite errors = [] // ============================================================ "create_command_msg"_test = [] - { + -> void { constexpr std::string_view EXPECTED{"\nM:RxTx 1 SPowerLevel\"Off\"\r"sv}; const std::string command{R"(M:RxTx SPowerLevel"Off")"}; std::string msg_id_str; int counter{rrcp::INVALID_ID}; auto result = rrcp::create_command_msg(command, msg_id_str, counter); - expect(1 == counter); + expect("1" == msg_id_str); expect(EXPECTED == result); std::ostringstream quoted; quoted << std::quoted(result.substr(1, result.length() - 1)); // NOTE: w/o START STOP -#if defined(BOOST_UT_HAS_FORMAT) +#ifdef BOOST_UT_HAS_FORMAT ut::log("{} == {}\n", "RRCP MU", quoted.str()); #endif }; @@ -136,33 +136,33 @@ ut::suite errors = [] // ============================================================ "wrong_quoted"_test = [] - { + -> void { expect(throws( [] - { + -> void { constexpr std::string_view WRONG_QUOTED{"\n\x1b\004\r"sv}; auto result = rrcp::esc2char(WRONG_QUOTED); })); }; "empty_str"_test = [] - { + -> void { expect(nothrow( [] - { + -> void { auto result = rrcp::esc2char(""); expect(result.empty()); })); }; - "single_esc_msg"_test = [] { expect(throws([] { auto result = rrcp::esc2char("\x1b\rSINGLE_ESC"); })); }; + "single_esc_msg"_test = [] -> void { expect(throws([] -> void { auto result = rrcp::esc2char("\x1b\rSINGLE_ESC"); })); }; - "esc_as_last_msg"_test = [] { expect(throws([] { auto result = rrcp::esc2char("ESC_AS_LAST\x1b"); })); }; + "esc_as_last_msg"_test = [] -> void { expect(throws([] -> void { auto result = rrcp::esc2char("ESC_AS_LAST\x1b"); })); }; - "to_short_msg"_test = [] { expect(throws([] { auto result = rrcp::esc2char("\x1b\0"s); })); }; + "to_short_msg"_test = [] -> void { expect(throws([] -> void { auto result = rrcp::esc2char("\x1b\0"s); })); }; "basic_quoteing"_test = [] - { + -> void { constexpr std::string_view BINARY{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; auto quoted = rrcp::char2esc(BINARY); @@ -186,7 +186,7 @@ ut::suite errors = [] // ============================================================ "rrcp_message"_test = [] - { + -> void { rrcp_message msg; msg.body_length(MAX_MU_LENGTH); expect(msg.length() == MAX_MU_LENGTH + 4); @@ -206,7 +206,7 @@ ut::suite errors = [] }; "rrcp_message_empty"_test = [] - { + -> void { rrcp_message msg; msg.body_length(0); expect(msg.length() == 4); @@ -219,7 +219,7 @@ ut::suite errors = [] }; "rrcp_message_to_long"_test = [] - { + -> void { const std::string invalid(MAX_MU_LENGTH, '\n'); rrcp_message msg(invalid); expect(!msg.is_valid()); @@ -227,7 +227,7 @@ ut::suite errors = [] }; "rrcp_message_text"_test = [] - { + -> void { constexpr std::string_view COMMAND{"Hallo Server"}; rrcp_message msg; expect(msg.set_msg(COMMAND)); @@ -239,7 +239,7 @@ ut::suite errors = [] }; "rrcp_message_binary"_test = [] - { + -> void { constexpr std::string_view BINARY{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; rrcp_message msg(BINARY); expect(msg.is_valid()); From 4be62f0cc133df69e32283269f5798fca2d64377 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 28 Oct 2025 13:23:48 +0100 Subject: [PATCH 092/120] Add beman infra structur files --- .devcontainer/Dockerfile | 18 + .devcontainer/devcontainer.json | 15 + .devcontainer/postcreate.sh | 4 + .github/CODEOWNERS | 4 + .github/workflows/ci_tests.yml | 117 ++++ .github/workflows/doxygen-gh-pages.yml | 19 + .github/workflows/pre-commit.yml | 13 + .markdownlint.yaml | 9 + .pre-commit-config.yaml | 42 ++ CMakePresets.json | 383 +++++++++++++ cmake/CMakeUserPresets.json | 107 ++++ cmake/presets/CMakeDarwinPresets.json | 48 ++ cmake/presets/CMakeGenericPresets.json | 34 ++ cmake/presets/CMakeLinuxPresets.json | 48 ++ cmake/presets/CMakeWindowsPresets.json | 32 ++ infra/.beman_submodule | 3 + infra/.github/workflows/beman-submodule.yml | 32 ++ infra/.github/workflows/pre-commit.yml | 78 +++ ...reusable-beman-create-issue-when-fault.yml | 28 + infra/.gitignore | 59 ++ infra/.pre-commit-config.yaml | 32 ++ infra/.pre-commit-hooks.yaml | 7 + infra/LICENSE | 219 +++++++ infra/README.md | 55 ++ infra/cmake/appleclang-toolchain.cmake | 44 ++ .../cmake/beman-install-library-config.cmake | 169 ++++++ infra/cmake/gnu-toolchain.cmake | 41 ++ infra/cmake/llvm-libc++-toolchain.cmake | 20 + infra/cmake/llvm-toolchain.cmake | 41 ++ infra/cmake/msvc-toolchain.cmake | 41 ++ infra/cmake/use-fetch-content.cmake | 187 ++++++ infra/tools/beman-submodule/README.md | 63 ++ infra/tools/beman-submodule/beman-submodule | 260 +++++++++ .../test/test_beman_submodule.py | 539 ++++++++++++++++++ lockfile.json | 3 + 35 files changed, 2814 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/postcreate.sh create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/ci_tests.yml create mode 100644 .github/workflows/doxygen-gh-pages.yml create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .markdownlint.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 CMakePresets.json create mode 100644 cmake/CMakeUserPresets.json create mode 100644 cmake/presets/CMakeDarwinPresets.json create mode 100644 cmake/presets/CMakeGenericPresets.json create mode 100644 cmake/presets/CMakeLinuxPresets.json create mode 100644 cmake/presets/CMakeWindowsPresets.json create mode 100644 infra/.beman_submodule create mode 100644 infra/.github/workflows/beman-submodule.yml create mode 100644 infra/.github/workflows/pre-commit.yml create mode 100644 infra/.github/workflows/reusable-beman-create-issue-when-fault.yml create mode 100644 infra/.gitignore create mode 100644 infra/.pre-commit-config.yaml create mode 100644 infra/.pre-commit-hooks.yaml create mode 100644 infra/LICENSE create mode 100644 infra/README.md create mode 100644 infra/cmake/appleclang-toolchain.cmake create mode 100644 infra/cmake/beman-install-library-config.cmake create mode 100644 infra/cmake/gnu-toolchain.cmake create mode 100644 infra/cmake/llvm-libc++-toolchain.cmake create mode 100644 infra/cmake/llvm-toolchain.cmake create mode 100644 infra/cmake/msvc-toolchain.cmake create mode 100644 infra/cmake/use-fetch-content.cmake create mode 100644 infra/tools/beman-submodule/README.md create mode 100755 infra/tools/beman-submodule/beman-submodule create mode 100644 infra/tools/beman-submodule/test/test_beman_submodule.py create mode 100644 lockfile.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..11297a6 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-22.04 + +USER vscode + +# Install latest cmake +RUN wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null +RUN echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null +RUN sudo apt-get update && sudo apt-get install -y cmake + +# Install pre-commit +RUN sudo apt-get install -y python3-pip && pip3 install pre-commit + +# Avoid ASAN Stalling +# Alternative is to update to clang-18 and gcc-13.2 + +RUN sudo sysctl -w vm.mmap_rnd_bits=28 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..e0cbb9c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,15 @@ +{ + "name": "Beman Project Generic Devcontainer", + "build": { + "dockerfile": "Dockerfile" + }, + "postCreateCommand": "bash .devcontainer/postcreate.sh", + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools" + ] + } + } +} diff --git a/.devcontainer/postcreate.sh b/.devcontainer/postcreate.sh new file mode 100644 index 0000000..4293f7e --- /dev/null +++ b/.devcontainer/postcreate.sh @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# Setup pre-commit +pre-commit +pre-commit install diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..49fa500 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# Codeowners for reviews on PRs + +* @dietmarkuehl @camio @neatudarius diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml new file mode 100644 index 0000000..6392abc --- /dev/null +++ b/.github/workflows/ci_tests.yml @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: Continuous Integration Tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + schedule: + - cron: '30 15 * * *' + +jobs: + beman-submodule-check: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.1.0 + + preset-test: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.1.0 + with: + matrix_config: > + [ + {"preset": "gcc-debug", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "gcc-release", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "llvm-debug", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, + {"preset": "llvm-release", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, + {"preset": "msvc-debug", "runner": "windows-latest"}, + {"preset": "msvc-release", "runner": "windows-latest"} + ] + + build-and-test: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.1.0 + with: + matrix_config: > + { + "gcc": [ + { "versions": ["15"], + "tests": [ + { "cxxversions": ["c++26"], + "tests": [ + { "stdlibs": ["libstdc++"], + "tests": [ + "Debug.Default", "Release.Default", "Release.MaxSan", + "Debug.Dynamic", "Debug.Coverage" + ] + } + ] + }, + { "cxxversions": ["c++23"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + }, + { "versions": ["14", "13"], + "tests": [ + { "cxxversions": ["c++26", "c++23"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "clang": [ + { "versions": ["20"], + "tests": [ + {"cxxversions": ["c++26"], + "tests": [ + { "stdlibs": ["libstdc++", "libc++"], + "tests": [ + "Debug.Default", "Release.Default", "Release.MaxSan", + "Debug.Dynamic" + ] + } + ] + }, + { "cxxversions": ["c++23"], + "tests": [ + {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} + ] + } + ] + }, + { "versions": ["19"], + "tests": [ + { "cxxversions": ["c++26", "c++23"], + "tests": [ + {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} + ] + } + ] + }, + { "versions": ["18", "17"], + "tests": [ + { "cxxversions": ["c++26", "c++23"], + "tests": [{"stdlibs": ["libc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "msvc": [ + { "versions": ["latest"], + "tests": [ + { "cxxversions": ["c++23"], + "tests": [ + { "stdlibs": ["stl"], + "tests": ["Debug.Default", "Release.Default"] + } + ] + } + ] + } + ] + } + + create-issue-when-fault: + needs: [preset-test, build-and-test] + if: failure() && github.event_name == 'schedule' + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.1.0 diff --git a/.github/workflows/doxygen-gh-pages.yml b/.github/workflows/doxygen-gh-pages.yml new file mode 100644 index 0000000..7fd2c82 --- /dev/null +++ b/.github/workflows/doxygen-gh-pages.yml @@ -0,0 +1,19 @@ +name: Doxygen GitHub Pages Deploy Action + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: DenverCoder1/doxygen-github-pages-action@v2.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: gh-pages + folder: docs/html + config_file: docs/Doxyfile diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..70895b4 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,13 @@ +name: Lint Check (pre-commit) + +on: + # We have to use pull_request_target here as pull_request does not grant + # enough permission for reviewdog + pull_request_target: + push: + branches: + - main + +jobs: + pre-commit: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-pre-commit.yml@1.1.0 diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..81f5fcd --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,9 @@ +# MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.35.0/doc/md033.md +# Disable inline html linter is needed for
+MD033: false + +# MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.35.0/doc/md013.md +# Conforms to .clang-format ColumnLimit +# Update the comment in .clang-format if we no-longer tie these two column limits. +MD013: + line_length: 119 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6883fce --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-json + - id: check-yaml + exclude: ^\.clang-(format|tidy)$ + - id: check-added-large-files + + # This brings in a portable version of clang-format. + # See also: https://github.com/ssciwr/clang-format-wheel + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v20.1.7 + hooks: + - id: clang-format + types_or: [c++, c, json] + exclude: docs/TODO.json + + # CMake linting and formatting + - repo: https://github.com/BlankSpruce/gersemi + rev: 0.20.1 + hooks: + - id: gersemi + name: CMake linting + + # TODO: Markdown linting + # Config file: .markdownlint.yaml + # - repo: https://github.com/igorshubovych/markdownlint-cli + # rev: v0.43.0 + # hooks: + # - id: markdownlint + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + files: ^.*\.(cmake|cpp|hpp|txt|md|json|in|yaml|yml)$ + args: ["-w", "--ignore-words", ".codespellignore" ] diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..a662c06 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,383 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "_root-config", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_CXX_STANDARD": "23", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "./infra/cmake/use-fetch-content.cmake" + } + }, + { + "name": "_debug-base", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "BEMAN_BUILDSYS_SANITIZER": "TSan" + } + }, + { + "name": "_release-base", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "gcc-debug", + "displayName": "GCC Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/gnu-toolchain.cmake" + } + }, + { + "name": "gcc-release", + "displayName": "GCC Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/gnu-toolchain.cmake" + } + }, + { + "name": "llvm-debug", + "displayName": "Clang Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-toolchain.cmake" + } + }, + { + "name": "llvm-release", + "displayName": "Clang Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-toolchain.cmake" + } + }, + { + "name": "appleclang-debug", + "displayName": "Appleclang Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" + } + }, + { + "name": "appleclang-release", + "displayName": "Appleclang Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" + } + }, + { + "name": "msvc-debug", + "displayName": "MSVC Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "msvc-release", + "displayName": "MSVC Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + } + ], + "buildPresets": [ + { + "name": "_root-build", + "hidden": true, + "jobs": 0 + }, + { + "name": "gcc-debug", + "configurePreset": "gcc-debug", + "inherits": [ + "_root-build" + ] + }, + { + "name": "gcc-release", + "configurePreset": "gcc-release", + "inherits": [ + "_root-build" + ] + }, + { + "name": "llvm-debug", + "configurePreset": "llvm-debug", + "inherits": [ + "_root-build" + ] + }, + { + "name": "llvm-release", + "configurePreset": "llvm-release", + "inherits": [ + "_root-build" + ] + }, + { + "name": "appleclang-debug", + "configurePreset": "appleclang-debug", + "inherits": [ + "_root-build" + ] + }, + { + "name": "appleclang-release", + "configurePreset": "appleclang-release", + "inherits": [ + "_root-build" + ] + }, + { + "name": "msvc-debug", + "configurePreset": "msvc-debug", + "inherits": [ + "_root-build" + ] + }, + { + "name": "msvc-release", + "configurePreset": "msvc-release", + "inherits": [ + "_root-build" + ] + } + ], + "testPresets": [ + { + "name": "_test_base", + "hidden": true, + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error", + "stopOnFailure": true + } + }, + { + "name": "gcc-debug", + "inherits": "_test_base", + "configurePreset": "gcc-debug" + }, + { + "name": "gcc-release", + "inherits": "_test_base", + "configurePreset": "gcc-release" + }, + { + "name": "llvm-debug", + "inherits": "_test_base", + "configurePreset": "llvm-debug" + }, + { + "name": "llvm-release", + "inherits": "_test_base", + "configurePreset": "llvm-release" + }, + { + "name": "appleclang-debug", + "inherits": "_test_base", + "configurePreset": "appleclang-debug" + }, + { + "name": "appleclang-release", + "inherits": "_test_base", + "configurePreset": "appleclang-release" + }, + { + "name": "msvc-debug", + "inherits": "_test_base", + "configurePreset": "msvc-debug" + }, + { + "name": "msvc-release", + "inherits": "_test_base", + "configurePreset": "msvc-release" + } + ], + "workflowPresets": [ + { + "name": "gcc-debug", + "steps": [ + { + "type": "configure", + "name": "gcc-debug" + }, + { + "type": "build", + "name": "gcc-debug" + }, + { + "type": "test", + "name": "gcc-debug" + } + ] + }, + { + "name": "gcc-release", + "steps": [ + { + "type": "configure", + "name": "gcc-release" + }, + { + "type": "build", + "name": "gcc-release" + }, + { + "type": "test", + "name": "gcc-release" + } + ] + }, + { + "name": "llvm-debug", + "steps": [ + { + "type": "configure", + "name": "llvm-debug" + }, + { + "type": "build", + "name": "llvm-debug" + }, + { + "type": "test", + "name": "llvm-debug" + } + ] + }, + { + "name": "llvm-release", + "steps": [ + { + "type": "configure", + "name": "llvm-release" + }, + { + "type": "build", + "name": "llvm-release" + }, + { + "type": "test", + "name": "llvm-release" + } + ] + }, + { + "name": "appleclang-debug", + "steps": [ + { + "type": "configure", + "name": "appleclang-debug" + }, + { + "type": "build", + "name": "appleclang-debug" + }, + { + "type": "test", + "name": "appleclang-debug" + } + ] + }, + { + "name": "appleclang-release", + "steps": [ + { + "type": "configure", + "name": "appleclang-release" + }, + { + "type": "build", + "name": "appleclang-release" + }, + { + "type": "test", + "name": "appleclang-release" + } + ] + }, + { + "name": "msvc-debug", + "steps": [ + { + "type": "configure", + "name": "msvc-debug" + }, + { + "type": "build", + "name": "msvc-debug" + }, + { + "type": "test", + "name": "msvc-debug" + } + ] + }, + { + "name": "msvc-release", + "steps": [ + { + "type": "configure", + "name": "msvc-release" + }, + { + "type": "build", + "name": "msvc-release" + }, + { + "type": "test", + "name": "msvc-release" + } + ] + } + ] +} diff --git a/cmake/CMakeUserPresets.json b/cmake/CMakeUserPresets.json new file mode 100644 index 0000000..331c063 --- /dev/null +++ b/cmake/CMakeUserPresets.json @@ -0,0 +1,107 @@ +{ + "version": 9, + "cmakeMinimumRequired": { + "major": 3, + "minor": 30, + "patch": 0 + }, + "include": [ + "cmake/presets/CMake${hostSystemName}Presets.json" + ], + "buildPresets": [ + { + "name": "debug", + "configurePreset": "debug", + "configuration": "Debug", + "targets": [ + "all" + ] + }, + { + "name": "release", + "configurePreset": "release", + "configuration": "Release", + "targets": [ + "all_verify_interface_header_sets", + "all" + ] + } + ], + "testPresets": [ + { + "name": "test_base", + "hidden": true, + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error", + "stopOnFailure": false + } + }, + { + "name": "debug", + "inherits": "test_base", + "configuration": "Debug", + "configurePreset": "debug" + }, + { + "name": "release", + "inherits": "test_base", + "configuration": "Release", + "configurePreset": "release" + } + ], + "packagePresets": [ + { + "name": "release", + "configurePreset": "release", + "configurations": [ + "Release" + ], + "generators": [ + "TGZ" + ] + } + ], + "workflowPresets": [ + { + "name": "debug", + "steps": [ + { + "type": "configure", + "name": "debug" + }, + { + "type": "build", + "name": "debug" + }, + { + "type": "test", + "name": "debug" + } + ] + }, + { + "name": "release", + "steps": [ + { + "type": "configure", + "name": "release" + }, + { + "type": "build", + "name": "release" + }, + { + "type": "test", + "name": "release" + }, + { + "type": "package", + "name": "release" + } + ] + } + ] +} diff --git a/cmake/presets/CMakeDarwinPresets.json b/cmake/presets/CMakeDarwinPresets.json new file mode 100644 index 0000000..78efdf4 --- /dev/null +++ b/cmake/presets/CMakeDarwinPresets.json @@ -0,0 +1,48 @@ +{ + "version": 6, + "include": [ + "CMakeGenericPresets.json" + ], + "configurePresets": [ + { + "name": "debug-base-Darwin", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "release-base-Darwin", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "debug", + "displayName": "Debug Build", + "inherits": [ + "root-config", + "debug-base-Darwin" + ] + }, + { + "name": "release", + "displayName": "Release Build", + "inherits": [ + "root-config", + "release-base-Darwin" + ] + } + ] +} diff --git a/cmake/presets/CMakeGenericPresets.json b/cmake/presets/CMakeGenericPresets.json new file mode 100644 index 0000000..2f6711b --- /dev/null +++ b/cmake/presets/CMakeGenericPresets.json @@ -0,0 +1,34 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "root-config", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "installDir": "${sourceDir}/stagedir", + "cacheVariables": { + "CMAKE_PREFIX_PATH": { + "type": "path", + "value": "${sourceDir}/stagedir" + }, + "CMAKE_CXX_EXTENSIONS": true, + "CMAKE_CXX_STANDARD": "23", + "CMAKE_CXX_STANDARD_REQUIRED": true, + "CMAKE_EXPORT_COMPILE_COMMANDS": true, + "CMAKE_SKIP_TEST_ALL_DEPENDENCY": false + }, + "warnings": { + "dev": true, + "deprecated": true, + "uninitialized": true, + "unusedCli": true, + "systemVars": false + }, + "errors": { + "dev": false, + "deprecated": true + } + } + ] +} diff --git a/cmake/presets/CMakeLinuxPresets.json b/cmake/presets/CMakeLinuxPresets.json new file mode 100644 index 0000000..7a91735 --- /dev/null +++ b/cmake/presets/CMakeLinuxPresets.json @@ -0,0 +1,48 @@ +{ + "version": 6, + "include": [ + "CMakeGenericPresets.json" + ], + "configurePresets": [ + { + "name": "debug-base-Linux", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "release-base-Linux", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "debug", + "displayName": "Debug Build", + "inherits": [ + "root-config", + "debug-base-Linux" + ] + }, + { + "name": "release", + "displayName": "Release Build", + "inherits": [ + "root-config", + "release-base-Linux" + ] + } + ] +} diff --git a/cmake/presets/CMakeWindowsPresets.json b/cmake/presets/CMakeWindowsPresets.json new file mode 100644 index 0000000..d8834f2 --- /dev/null +++ b/cmake/presets/CMakeWindowsPresets.json @@ -0,0 +1,32 @@ +{ + "version": 6, + "include": [ + "CMakeGenericPresets.json" + ], + "configurePresets": [ + { + "name": "release", + "description": "Windows preset for library developers", + "generator": "Ninja Multi-Config", + "binaryDir": "${sourceDir}/build", + "inherits": [ + "root-config" + ], + "cacheVariables": { + "CMAKE_CXX_COMPILER": "cl" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "debug", + "description": "Windows preset for library developers", + "inherits": [ + "release" + ] + } + ] +} diff --git a/infra/.beman_submodule b/infra/.beman_submodule new file mode 100644 index 0000000..bfed167 --- /dev/null +++ b/infra/.beman_submodule @@ -0,0 +1,3 @@ +[beman_submodule] +remote=https://github.com/bemanproject/infra.git +commit_hash=bb58b2a1cc894d58a55bf745be78f5d27029e245 diff --git a/infra/.github/workflows/beman-submodule.yml b/infra/.github/workflows/beman-submodule.yml new file mode 100644 index 0000000..8435086 --- /dev/null +++ b/infra/.github/workflows/beman-submodule.yml @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: beman-submodule tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + beman-submodule-script-ci: + name: beman_module.py ci + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + - name: Install pytest + run: | + python3 -m pip install pytest + + - name: Run pytest + run: | + cd tools/beman-submodule/ + pytest diff --git a/infra/.github/workflows/pre-commit.yml b/infra/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..9646831 --- /dev/null +++ b/infra/.github/workflows/pre-commit.yml @@ -0,0 +1,78 @@ +name: Lint Check (pre-commit) + +on: + # We have to use pull_request_target here as pull_request does not grant + # enough permission for reviewdog + pull_request_target: + push: + branches: + - main + +jobs: + pre-commit-push: + name: Pre-Commit check on Push + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + # We wish to run pre-commit on all files instead of the changes + # only made in the push commit. + # + # So linting error persists when there's formatting problem. + - uses: pre-commit/action@v3.0.1 + + pre-commit-pr: + name: Pre-Commit check on PR + runs-on: ubuntu-latest + if: ${{ github.event_name == 'pull_request_target' }} + + permissions: + contents: read + checks: write + issues: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # pull_request_target checkout the base of the repo + # We need to checkout the actual pr to lint the changes. + - name: Checkout pr + run: gh pr checkout ${{ github.event.number }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + # we only lint on the changed file in PR. + - name: Get Changed Files + id: changed-files + uses: tj-actions/changed-files@v45 + + # See: + # https://github.com/tj-actions/changed-files?tab=readme-ov-file#using-local-git-directory- + - uses: pre-commit/action@v3.0.1 + id: run-pre-commit + with: + extra_args: --files ${{ steps.changed-files.outputs.all_changed_files }} + + # Review dog posts the suggested change from pre-commit to the pr. + - name: suggester / pre-commit + uses: reviewdog/action-suggester@v1 + if: ${{ failure() && steps.run-pre-commit.conclusion == 'failure' }} + with: + tool_name: pre-commit + level: warning + reviewdog_flags: "-fail-level=error" diff --git a/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml b/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml new file mode 100644 index 0000000..024a51f --- /dev/null +++ b/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: 'Beman issue creation workflow' +on: + workflow_call: + workflow_dispatch: +jobs: + create-issue: + runs-on: ubuntu-latest + steps: + # See https://github.com/cli/cli/issues/5075 + - uses: actions/checkout@v4 + - name: Create issue + run: | + issue_num=$(gh issue list -s open -S "[SCHEDULED-BUILD] infra repo CI job failure" -L 1 --json number | jq 'if length == 0 then -1 else .[0].number end') + body="**CI job failure Report** + - **Time of Failure**: $(date -u '+%B %d, %Y, %H:%M %Z') + - **Commit**: [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) + - **Action Run**: [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + The scheduled job triggered by cron has failed. + Please investigate the logs and recent changes associated with this commit or rerun the workflow if you believe this is an error." + if [[ $issue_num -eq -1 ]]; then + gh issue create --repo ${{ github.repository }} --title "[SCHEDULED-BUILD] infra repo CI job failure" --body "$body" --assignee ${{ github.actor }} + else + gh issue comment --repo ${{ github.repository }} $issue_num --body "$body" + fi + env: + GH_TOKEN: ${{ github.token }} diff --git a/infra/.gitignore b/infra/.gitignore new file mode 100644 index 0000000..b7cdbb5 --- /dev/null +++ b/infra/.gitignore @@ -0,0 +1,59 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Python +__pycache__/ +.pytest_cache/ +*.pyc +*.pyo +*.pyd +*.pyw +*.pyz +*.pywz +*.pyzw +*.pyzwz +*.delete_me + +# MAC OS +*.DS_Store + +# Editor files +.vscode/ +.idea/ + +# Build directories +infra.egg-info/ +beman_tidy.egg-info/ +*.egg-info/ +build/ +dist/ diff --git a/infra/.pre-commit-config.yaml b/infra/.pre-commit-config.yaml new file mode 100644 index 0000000..e806e59 --- /dev/null +++ b/infra/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + + # CMake linting and formatting + - repo: https://github.com/BlankSpruce/gersemi + rev: 0.22.3 + hooks: + - id: gersemi + name: CMake linting + exclude: ^.*/tests/.*/data/ # Exclude test data directories + + # Python linting and formatting + # config file: ruff.toml (not currently present but add if needed) + # https://docs.astral.sh/ruff/configuration/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.13.2 + hooks: + - id: ruff-check + files: ^tools/beman-tidy/ + - id: ruff-format + files: ^tools/beman-tidy/ diff --git a/infra/.pre-commit-hooks.yaml b/infra/.pre-commit-hooks.yaml new file mode 100644 index 0000000..d327587 --- /dev/null +++ b/infra/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: beman-tidy + name: "beman-tidy: bemanification your repo" + entry: ./tools/beman-tidy/beman-tidy + language: script + pass_filenames: false + always_run: true + args: [".", "--verbose"] diff --git a/infra/LICENSE b/infra/LICENSE new file mode 100644 index 0000000..f6db814 --- /dev/null +++ b/infra/LICENSE @@ -0,0 +1,219 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..16b2672 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,55 @@ +# Beman Project Infrastructure Repository + + + +This repository contains the infrastructure for The Beman Project. This is NOT a library repository, +so it does not respect the usual structure of a Beman library repository nor The Beman Standard! + +## Description + +* `cmake/`: CMake modules and toolchain files used by Beman libraries. +* `containers/`: Containers used for CI builds and tests in the Beman org. +* `tools/`: Tools used to manage the infrastructure and the codebase (e.g., linting, formatting, etc.). + +## Usage + +This repository is intended to be used as a beman-submodule in other Beman repositories. See +[the Beman Submodule documentation](./tools/beman-submodule/README.md) for details. + + +### CMake Modules + + +#### `beman_install_library` + +The CMake modules in this repository are intended to be used by Beman libraries. Use the +`beman_add_install_library_config()` function to install your library, along with header +files, any metadata files, and a CMake config file for `find_package()` support. + +```cmake +add_library(beman.something) +add_library(beman::something ALIAS beman.something) + +# ... configure your target as needed ... + +find_package(beman-install-library REQUIRED) +beman_install_library(beman.something) +``` + +Note that the target must be created before calling `beman_install_library()`. The module +also assumes that the target is named using the `beman.something` convention, and it +uses that assumption to derive the names to match other Beman standards and conventions. +If your target does not follow that convention, raise an issue or pull request to add +more configurability to the module. + +The module will configure the target to install: + +* The library target itself +* Any public headers associated with the target +* CMake files for `find_package(beman.something)` support + +Some options for the project and target will also be supported: + +* `BEMAN_INSTALL_CONFIG_FILE_PACKAGES` - a list of package names (e.g., `beman.something`) for which to install the config file + (default: all packages) +* `_INSTALL_CONFIG_FILE_PACKAGE` - a per-project option to enable/disable config file installation (default: `ON` if the project is top-level, `OFF` otherwise). For instance for `beman.something`, the option would be `BEMAN_SOMETHING_INSTALL_CONFIG_FILE_PACKAGE`. diff --git a/infra/cmake/appleclang-toolchain.cmake b/infra/cmake/appleclang-toolchain.cmake new file mode 100644 index 0000000..70ef548 --- /dev/null +++ b/infra/cmake/appleclang-toolchain.cmake @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for apple clang family of compiler. +# Note this is different from LLVM toolchain. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. Note that apple clang does not support leak sanitizer. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include_guard(GLOBAL) + +# Prevent PATH collision with an LLVM clang installation by using the system +# compiler shims +set(CMAKE_C_COMPILER cc) +set(CMAKE_CXX_COMPILER c++) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + set(SANITIZER_FLAGS + "-fsanitize=address -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined" + ) +elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") + set(SANITIZER_FLAGS "-fsanitize=thread") +endif() + +set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/beman-install-library-config.cmake b/infra/cmake/beman-install-library-config.cmake new file mode 100644 index 0000000..e7fd0ad --- /dev/null +++ b/infra/cmake/beman-install-library-config.cmake @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +include_guard(GLOBAL) + +# This file defines the function `beman_install_library` which is used to +# install a library target and its headers, along with optional CMake +# configuration files. +# +# The function is designed to be reusable across different Beman libraries. + +function(beman_install_library name) + # Usage + # ----- + # + # beman_install_library(NAME) + # + # Brief + # ----- + # + # This function installs the specified library target and its headers. + # It also handles the installation of the CMake configuration files if needed. + # + # CMake variables + # --------------- + # + # Note that configuration of the installation is generally controlled by CMake + # cache variables so that they can be controlled by the user or tool running the + # `cmake` command. Neither `CMakeLists.txt` nor `*.cmake` files should set these + # variables directly. + # + # - BEMAN_INSTALL_CONFIG_FILE_PACKAGES: + # List of packages that require config file installation. + # If the package name is in this list, it will install the config file. + # + # - _INSTALL_CONFIG_FILE_PACKAGE: + # Boolean to control config file installation for the specific library. + # The prefix `` is the uppercased name of the library with dots + # replaced by underscores. + # + if(NOT TARGET "${name}") + message(FATAL_ERROR "Target '${name}' does not exist.") + endif() + + if(NOT ARGN STREQUAL "") + message( + FATAL_ERROR + "beman_install_library does not accept extra arguments: ${ARGN}" + ) + endif() + + # Given foo.bar, the component name is bar + string(REPLACE "." ";" name_parts "${name}") + # fail if the name doesn't look like foo.bar + list(LENGTH name_parts name_parts_length) + if(NOT name_parts_length EQUAL 2) + message( + FATAL_ERROR + "beman_install_library expects a name of the form 'beman.', got '${name}'" + ) + endif() + + set(target_name "${name}") + set(install_component_name "${name}") + set(export_name "${name}") + set(package_name "${name}") + list(GET name_parts -1 component_name) + + install( + TARGETS "${target_name}" + COMPONENT "${install_component_name}" + EXPORT "${export_name}" + FILE_SET HEADERS + ) + + set_target_properties( + "${target_name}" + PROPERTIES EXPORT_NAME "${component_name}" + ) + + include(GNUInstallDirs) + + # Determine the prefix for project-specific variables + string(TOUPPER "${name}" project_prefix) + string(REPLACE "." "_" project_prefix "${project_prefix}") + + option( + ${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE + "Enable building examples. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." + ${PROJECT_IS_TOP_LEVEL} + ) + + # By default, install the config package + set(install_config_package ON) + + # Turn OFF installation of config package by default if, + # in order of precedence: + # 1. The specific package variable is set to OFF + # 2. The package name is not in the list of packages to install config files + if(DEFINED BEMAN_INSTALL_CONFIG_FILE_PACKAGES) + if( + NOT "${install_component_name}" + IN_LIST + BEMAN_INSTALL_CONFIG_FILE_PACKAGES + ) + set(install_config_package OFF) + endif() + endif() + if(DEFINED ${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE) + set(install_config_package + ${${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE} + ) + endif() + + if(install_config_package) + message( + DEBUG + "beman-install-library: Installing a config package for '${name}'" + ) + + include(CMakePackageConfigHelpers) + + find_file( + config_file_template + NAMES "${package_name}-config.cmake.in" + PATHS "${CMAKE_CURRENT_SOURCE_DIR}" + NO_DEFAULT_PATH + NO_CACHE + REQUIRED + ) + set(config_package_file + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}-config.cmake" + ) + set(package_install_dir "${CMAKE_INSTALL_LIBDIR}/cmake/${package_name}") + configure_package_config_file( + "${config_file_template}" + "${config_package_file}" + INSTALL_DESTINATION "${package_install_dir}" + PATH_VARS PROJECT_NAME PROJECT_VERSION + ) + + set(config_version_file + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}-config-version.cmake" + ) + write_basic_package_version_file( + "${config_version_file}" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY ExactVersion + ) + + install( + FILES "${config_package_file}" "${config_version_file}" + DESTINATION "${package_install_dir}" + COMPONENT "${install_component_name}" + ) + + set(config_targets_file "${package_name}-targets.cmake") + install( + EXPORT "${export_name}" + DESTINATION "${package_install_dir}" + NAMESPACE beman:: + FILE "${config_targets_file}" + COMPONENT "${install_component_name}" + ) + else() + message( + DEBUG + "beman-install-library: Not installing a config package for '${name}'" + ) + endif() +endfunction() diff --git a/infra/cmake/gnu-toolchain.cmake b/infra/cmake/gnu-toolchain.cmake new file mode 100644 index 0000000..d3b9f92 --- /dev/null +++ b/infra/cmake/gnu-toolchain.cmake @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for GNU family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures gcc and g++ to use all available non-conflicting +# sanitizers. +# - TSan: configures gcc and g++ to enable the use of thread sanitizer + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER gcc) +set(CMAKE_CXX_COMPILER g++) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + set(SANITIZER_FLAGS + "-fsanitize=address -fsanitize=leak -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined -fsanitize-undefined-trap-on-error" + ) +elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") + set(SANITIZER_FLAGS "-fsanitize=thread") +endif() + +set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/llvm-libc++-toolchain.cmake b/infra/cmake/llvm-libc++-toolchain.cmake new file mode 100644 index 0000000..76264c6 --- /dev/null +++ b/infra/cmake/llvm-libc++-toolchain.cmake @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: BSL-1.0 + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for LLVM family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include(${CMAKE_CURRENT_LIST_DIR}/llvm-toolchain.cmake) + +if(NOT CMAKE_CXX_FLAGS MATCHES "-stdlib=libc\\+\\+") + string(APPEND CMAKE_CXX_FLAGS " -stdlib=libc++") +endif() diff --git a/infra/cmake/llvm-toolchain.cmake b/infra/cmake/llvm-toolchain.cmake new file mode 100644 index 0000000..f1623b7 --- /dev/null +++ b/infra/cmake/llvm-toolchain.cmake @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for LLVM family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER clang) +set(CMAKE_CXX_COMPILER clang++) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + set(SANITIZER_FLAGS + "-fsanitize=address -fsanitize=leak -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined -fsanitize-undefined-trap-on-error" + ) +elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") + set(SANITIZER_FLAGS "-fsanitize=thread") +endif() + +set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/msvc-toolchain.cmake b/infra/cmake/msvc-toolchain.cmake new file mode 100644 index 0000000..bdc24de --- /dev/null +++ b/infra/cmake/msvc-toolchain.cmake @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for MSVC family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures cl to use all available non-conflicting sanitizers. +# +# Note that in other toolchain files, TSan is also a possible value for +# BEMAN_BUILDSYS_SANITIZER, however, MSVC does not support thread sanitizer, +# thus this value is omitted. + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER cl) +set(CMAKE_CXX_COMPILER cl) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + # /Zi flag (add debug symbol) is needed when using address sanitizer + # See C5072: https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-c5072 + set(SANITIZER_FLAGS "/fsanitize=address /Zi") +endif() + +set(CMAKE_CXX_FLAGS_DEBUG_INIT "/EHsc /permissive- ${SANITIZER_FLAGS}") +set(CMAKE_C_FLAGS_DEBUG_INIT "/EHsc /permissive- ${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "/EHsc /permissive- /O2 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/use-fetch-content.cmake b/infra/cmake/use-fetch-content.cmake new file mode 100644 index 0000000..4ed4839 --- /dev/null +++ b/infra/cmake/use-fetch-content.cmake @@ -0,0 +1,187 @@ +cmake_minimum_required(VERSION 3.24) + +include(FetchContent) + +if(NOT BEMAN_EXEMPLAR_LOCKFILE) + set(BEMAN_EXEMPLAR_LOCKFILE + "lockfile.json" + CACHE FILEPATH + "Path to the dependency lockfile for the Beman Exemplar." + ) +endif() + +set(BemanExemplar_projectDir "${CMAKE_CURRENT_LIST_DIR}/../..") +message(TRACE "BemanExemplar_projectDir=\"${BemanExemplar_projectDir}\"") + +message(TRACE "BEMAN_EXEMPLAR_LOCKFILE=\"${BEMAN_EXEMPLAR_LOCKFILE}\"") +file( + REAL_PATH + "${BEMAN_EXEMPLAR_LOCKFILE}" + BemanExemplar_lockfile + BASE_DIRECTORY "${BemanExemplar_projectDir}" + EXPAND_TILDE +) +message(DEBUG "Using lockfile: \"${BemanExemplar_lockfile}\"") + +# Force CMake to reconfigure the project if the lockfile changes +set_property( + DIRECTORY "${BemanExemplar_projectDir}" + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS "${BemanExemplar_lockfile}" +) + +# For more on the protocol for this function, see: +# https://cmake.org/cmake/help/latest/command/cmake_language.html#provider-commands +function(BemanExemplar_provideDependency method package_name) + # Read the lockfile + file(READ "${BemanExemplar_lockfile}" BemanExemplar_rootObj) + + # Get the "dependencies" field and store it in BemanExemplar_dependenciesObj + string( + JSON + BemanExemplar_dependenciesObj + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_rootObj}" + "dependencies" + ) + if(BemanExemplar_error) + message(FATAL_ERROR "${BemanExemplar_lockfile}: ${BemanExemplar_error}") + endif() + + # Get the length of the libraries array and store it in BemanExemplar_dependenciesObj + string( + JSON + BemanExemplar_numDependencies + ERROR_VARIABLE BemanExemplar_error + LENGTH "${BemanExemplar_dependenciesObj}" + ) + if(BemanExemplar_error) + message(FATAL_ERROR "${BemanExemplar_lockfile}: ${BemanExemplar_error}") + endif() + + if(BemanExemplar_numDependencies EQUAL 0) + return() + endif() + + # Loop over each dependency object + math(EXPR BemanExemplar_maxIndex "${BemanExemplar_numDependencies} - 1") + foreach(BemanExemplar_index RANGE "${BemanExemplar_maxIndex}") + set(BemanExemplar_errorPrefix + "${BemanExemplar_lockfile}, dependency ${BemanExemplar_index}" + ) + + # Get the dependency object at BemanExemplar_index + # and store it in BemanExemplar_depObj + string( + JSON + BemanExemplar_depObj + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_dependenciesObj}" + "${BemanExemplar_index}" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "name" field and store it in BemanExemplar_name + string( + JSON + BemanExemplar_name + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "name" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "package_name" field and store it in BemanExemplar_pkgName + string( + JSON + BemanExemplar_pkgName + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "package_name" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "git_repository" field and store it in BemanExemplar_repo + string( + JSON + BemanExemplar_repo + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "git_repository" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "git_tag" field and store it in BemanExemplar_tag + string( + JSON + BemanExemplar_tag + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "git_tag" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + if(method STREQUAL "FIND_PACKAGE") + if(package_name STREQUAL BemanExemplar_pkgName) + string( + APPEND + BemanExemplar_debug + "Redirecting find_package calls for ${BemanExemplar_pkgName} " + "to FetchContent logic.\n" + ) + string( + APPEND + BemanExemplar_debug + "Fetching ${BemanExemplar_repo} at " + "${BemanExemplar_tag} according to ${BemanExemplar_lockfile}." + ) + message(DEBUG "${BemanExemplar_debug}") + FetchContent_Declare( + "${BemanExemplar_name}" + GIT_REPOSITORY "${BemanExemplar_repo}" + GIT_TAG "${BemanExemplar_tag}" + EXCLUDE_FROM_ALL + ) + set(INSTALL_GTEST OFF) # Disable GoogleTest installation + FetchContent_MakeAvailable("${BemanExemplar_name}") + + # Important! _FOUND tells CMake that `find_package` is + # not needed for this package anymore + set("${BemanExemplar_pkgName}_FOUND" TRUE PARENT_SCOPE) + endif() + endif() + endforeach() +endfunction() + +cmake_language( + SET_DEPENDENCY_PROVIDER BemanExemplar_provideDependency + SUPPORTED_METHODS FIND_PACKAGE +) + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/tools/beman-submodule/README.md b/infra/tools/beman-submodule/README.md new file mode 100644 index 0000000..36883ad --- /dev/null +++ b/infra/tools/beman-submodule/README.md @@ -0,0 +1,63 @@ +# beman-submodule + + + +## What is this script? + +`beman-submodule` provides some of the features of `git submodule`, adding child git +repositories to a parent git repository, but unlike with `git submodule`, the entire child +repo is directly checked in, so only maintainers, not users, need to run this script. The +command line interface mimics `git submodule`'s. + +## How do I add a beman submodule to my repository? + +The first beman submodule you should add is this repository, `infra/`, which you can +bootstrap by running: + + +```sh +curl -s https://raw.githubusercontent.com/bemanproject/infra/refs/heads/main/tools/beman-submodule/beman-submodule | python3 - add https://github.com/bemanproject/infra.git +``` + +Once that's added, you can run the script from `infra/tools/beman-submodule/beman-submodule`. + +## How do I update a beman submodule to the latest trunk? + +You can run `beman-submodule update --remote` to update all beman submodule to latest +trunk, or e.g. `beman-submodule update --remote infra` to update only a specific one. + +## How does it work under the hood? + +Along with the files from the child repository, it creates a dotfile called +`.beman_submodule`, which looks like this: + +```ini +[beman_submodule] +remote=https://github.com/bemanproject/infra.git +commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77 +``` + +## How do I update a beman submodule to a specific commit or change the remote URL? + +You can edit the corresponding lines in the `.beman_submodule` file and run +`beman-submodule update` to update the state of the beman submodule to the new +`.beman_submodule` settings. + +## How can I make CI ensure that my beman submodules are in a valid state? + +Add this job to your CI workflow: + +```yaml + beman-submodule-test: + runs-on: ubuntu-latest + name: "Check beman submodules for consistency" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: beman submodule consistency check + run: | + (set -o pipefail; ./infra/tools/beman-submodule/beman-submodule status | grep -qvF '+') +``` + +This will fail if the contents of any beman submodule don't match what's specified in the +`.beman_submodule` file. diff --git a/infra/tools/beman-submodule/beman-submodule b/infra/tools/beman-submodule/beman-submodule new file mode 100755 index 0000000..66cb96e --- /dev/null +++ b/infra/tools/beman-submodule/beman-submodule @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import argparse +import configparser +import filecmp +import glob +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def directory_compare( + reference: str | Path, actual: str | Path, ignore, allow_untracked_files: bool): + reference, actual = Path(reference), Path(actual) + + compared = filecmp.dircmp(reference, actual, ignore=ignore) + if (compared.left_only + or (compared.right_only and not allow_untracked_files) + or compared.diff_files): + return False + for common_dir in compared.common_dirs: + path1 = reference / common_dir + path2 = actual / common_dir + if not directory_compare(path1, path2, ignore, allow_untracked_files): + return False + return True + +class BemanSubmodule: + def __init__( + self, dirpath: str | Path, remote: str, commit_hash: str, + allow_untracked_files: bool): + self.dirpath = Path(dirpath) + self.remote = remote + self.commit_hash = commit_hash + self.allow_untracked_files = allow_untracked_files + +def parse_beman_submodule_file(path): + config = configparser.ConfigParser() + read_result = config.read(path) + def fail(): + raise Exception(f'Failed to parse {path} as a .beman_submodule file') + if not read_result: + fail() + if not 'beman_submodule' in config: + fail() + if not 'remote' in config['beman_submodule']: + fail() + if not 'commit_hash' in config['beman_submodule']: + fail() + allow_untracked_files = config.getboolean( + 'beman_submodule', 'allow_untracked_files', fallback=False) + return BemanSubmodule( + Path(path).resolve().parent, + config['beman_submodule']['remote'], + config['beman_submodule']['commit_hash'], + allow_untracked_files) + +def get_beman_submodule(path: str | Path): + beman_submodule_filepath = Path(path) / '.beman_submodule' + + if beman_submodule_filepath.is_file(): + return parse_beman_submodule_file(beman_submodule_filepath) + else: + return None + +def find_beman_submodules_in(path): + path = Path(path) + assert path.is_dir() + + result = [] + for dirpath, _, filenames in path.walk(): + if '.beman_submodule' in filenames: + result.append(parse_beman_submodule_file(dirpath / '.beman_submodule')) + return sorted(result, key=lambda module: module.dirpath) + +def cwd_git_repository_path(): + process = subprocess.run( + ['git', 'rev-parse', '--show-toplevel'], capture_output=True, text=True, + check=False) + if process.returncode == 0: + return process.stdout.strip() + elif "fatal: not a git repository" in process.stderr: + return None + else: + raise Exception("git rev-parse --show-toplevel failed") + +def clone_beman_submodule_into_tmpdir(beman_submodule, remote): + tmpdir = tempfile.TemporaryDirectory() + subprocess.run( + ['git', 'clone', beman_submodule.remote, tmpdir.name], capture_output=True, + check=True) + if not remote: + subprocess.run( + ['git', '-C', tmpdir.name, 'reset', '--hard', beman_submodule.commit_hash], + capture_output=True, check=True) + return tmpdir + +def get_paths(beman_submodule): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) + paths = set(glob.glob('*', root_dir=Path(tmpdir.name), include_hidden=True)) + paths.remove('.git') + return paths + +def beman_submodule_status(beman_submodule): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) + if directory_compare( + tmpdir.name, beman_submodule.dirpath, ['.beman_submodule', '.git'], + beman_submodule.allow_untracked_files): + status_character=' ' + else: + status_character='+' + parent_repo_path = cwd_git_repository_path() + if not parent_repo_path: + raise Exception('this is not a git repository') + relpath = Path(beman_submodule.dirpath).relative_to(Path(parent_repo_path)) + return status_character + ' ' + beman_submodule.commit_hash + ' ' + str(relpath) + +def beman_submodule_update(beman_submodule, remote): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, remote) + tmp_path = Path(tmpdir.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmp_path) + + if beman_submodule.allow_untracked_files: + for path in get_paths(beman_submodule): + path2 = Path(beman_submodule.dirpath) / path + if Path(path2).is_dir(): + shutil.rmtree(path2) + elif Path(path2).is_file(): + os.remove(path2) + else: + shutil.rmtree(beman_submodule.dirpath) + + submodule_path = tmp_path / '.beman_submodule' + with open(submodule_path, 'w') as f: + f.write('[beman_submodule]\n') + f.write(f'remote={beman_submodule.remote}\n') + f.write(f'commit_hash={sha_process.stdout.strip()}\n') + if beman_submodule.allow_untracked_files: + f.write(f'allow_untracked_files=True\n') + shutil.rmtree(tmp_path / '.git') + shutil.copytree(tmp_path, beman_submodule.dirpath, dirs_exist_ok=True) + +def update_command(remote, path): + if not path: + parent_repo_path = cwd_git_repository_path() + if not parent_repo_path: + raise Exception('this is not a git repository') + beman_submodules = find_beman_submodules_in(parent_repo_path) + else: + beman_submodule = get_beman_submodule(path) + if not beman_submodule: + raise Exception(f'{path} is not a beman_submodule') + beman_submodules = [beman_submodule] + for beman_submodule in beman_submodules: + beman_submodule_update(beman_submodule, remote) + +def add_command(repository, path, allow_untracked_files): + tmpdir = tempfile.TemporaryDirectory() + subprocess.run( + ['git', 'clone', repository], capture_output=True, check=True, cwd=tmpdir.name) + repository_name = os.listdir(tmpdir.name)[0] + if not path: + path = Path(repository_name) + else: + path = Path(path) + if not allow_untracked_files and path.exists(): + raise Exception(f'{path} exists') + path.mkdir(exist_ok=allow_untracked_files) + tmpdir_repo = Path(tmpdir.name) / repository_name + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir_repo) + with open(tmpdir_repo / '.beman_submodule', 'w') as f: + f.write('[beman_submodule]\n') + f.write(f'remote={repository}\n') + f.write(f'commit_hash={sha_process.stdout.strip()}\n') + if allow_untracked_files: + f.write(f'allow_untracked_files=True\n') + shutil.rmtree(tmpdir_repo /'.git') + shutil.copytree(tmpdir_repo, path, dirs_exist_ok=True) + +def status_command(paths): + if not paths: + parent_repo_path = cwd_git_repository_path() + if not parent_repo_path: + raise Exception('this is not a git repository') + beman_submodules = find_beman_submodules_in(parent_repo_path) + else: + beman_submodules = [] + for path in paths: + beman_submodule = get_beman_submodule(path) + if not beman_submodule: + raise Exception(f'{path} is not a beman_submodule') + beman_submodules.append(beman_submodule) + for beman_submodule in beman_submodules: + print(beman_submodule_status(beman_submodule)) + +def get_parser(): + parser = argparse.ArgumentParser(description='Beman pseudo-submodule tool') + subparsers = parser.add_subparsers(dest='command', help='available commands') + parser_update = subparsers.add_parser('update', help='update beman_submodules') + parser_update.add_argument( + '--remote', action='store_true', + help='update a beman_submodule to its latest from upstream') + parser_update.add_argument( + 'beman_submodule_path', nargs='?', + help='relative path to the beman_submodule to update') + parser_add = subparsers.add_parser('add', help='add a new beman_submodule') + parser_add.add_argument('repository', help='git repository to add') + parser_add.add_argument( + 'path', nargs='?', help='path where the repository will be added') + parser_add.add_argument( + '--allow-untracked-files', action='store_true', + help='the beman_submodule will not occupy the subdirectory exclusively') + parser_status = subparsers.add_parser( + 'status', help='show the status of beman_submodules') + parser_status.add_argument('paths', nargs='*') + return parser + +def parse_args(args): + return get_parser().parse_args(args); + +def usage(): + return get_parser().format_help() + +def run_command(args): + if args.command == 'update': + update_command(args.remote, args.beman_submodule_path) + elif args.command == 'add': + add_command(args.repository, args.path, args.allow_untracked_files) + elif args.command == 'status': + status_command(args.paths) + else: + raise Exception(usage()) + +def check_for_git(path): + env = os.environ.copy() + if path is not None: + env["PATH"] = path + return shutil.which("git", path=env.get("PATH")) is not None + +def main(): + try: + if not check_for_git(None): + raise Exception('git not found in PATH') + args = parse_args(sys.argv[1:]) + run_command(args) + except Exception as e: + print("Error:", e, file=sys.stderr) + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/infra/tools/beman-submodule/test/test_beman_submodule.py b/infra/tools/beman-submodule/test/test_beman_submodule.py new file mode 100644 index 0000000..600fc07 --- /dev/null +++ b/infra/tools/beman-submodule/test/test_beman_submodule.py @@ -0,0 +1,539 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import glob +import os +import pytest +import shutil +import stat +import subprocess +import tempfile +from pathlib import Path + +# https://stackoverflow.com/a/19011259 +import types +import importlib.machinery +loader = importlib.machinery.SourceFileLoader( + 'beman_submodule', + str(Path(__file__).parent.resolve().parent / 'beman-submodule')) +beman_submodule = types.ModuleType(loader.name) +loader.exec_module(beman_submodule) + +def create_test_git_repository(): + tmpdir = tempfile.TemporaryDirectory() + tmp_path = Path(tmpdir.name) + + subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) + def make_commit(a_txt_contents): + with open(tmp_path / 'a.txt', 'w') as f: + f.write(a_txt_contents) + subprocess.run( + ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + make_commit('A') + make_commit('a') + return tmpdir + +def create_test_git_repository2(): + tmpdir = tempfile.TemporaryDirectory() + tmp_path = Path(tmpdir.name) + + subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) + with open(tmp_path / 'a.txt', 'w') as f: + f.write('a') + subprocess.run( + ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + os.remove(tmp_path / 'a.txt') + subprocess.run( + ['git', 'rm', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + with open(tmp_path / 'b.txt', 'w') as f: + f.write('b') + subprocess.run( + ['git', 'add', 'b.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + return tmpdir + +def test_directory_compare(): + def create_dir_structure(dir_path: Path): + bar_path = dir_path / 'bar' + os.makedirs(bar_path) + + with open(dir_path / 'foo.txt', 'w') as f: + f.write('foo') + with open(bar_path / 'baz.txt', 'w') as f: + f.write('baz') + + with tempfile.TemporaryDirectory() as dir_a, \ + tempfile.TemporaryDirectory() as dir_b: + path_a = Path(dir_a) + path_b = Path(dir_b) + + create_dir_structure(path_a) + create_dir_structure(path_b) + + assert beman_submodule.directory_compare(dir_a, dir_b, [], False) + + with open(path_a / 'bar' / 'quux.txt', 'w') as f: + f.write('quux') + + assert not beman_submodule.directory_compare(path_a, path_b, [], False) + assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], False) + +def test_directory_compare_untracked_files(): + def create_dir_structure(dir_path: Path): + bar_path = dir_path / 'bar' + os.makedirs(bar_path) + + with open(dir_path / 'foo.txt', 'w') as f: + f.write('foo') + with open(bar_path / 'baz.txt', 'w') as f: + f.write('baz') + + with tempfile.TemporaryDirectory() as reference, \ + tempfile.TemporaryDirectory() as actual: + path_a = Path(reference) + path_b = Path(actual) + + create_dir_structure(path_a) + create_dir_structure(path_b) + (path_b / 'c.txt').touch() + + assert beman_submodule.directory_compare(reference, actual, [], True) + + with open(path_a / 'bar' / 'quux.txt', 'w') as f: + f.write('quux') + + assert not beman_submodule.directory_compare(path_a, path_b, [], True) + assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], True) + +def test_parse_beman_submodule_file(): + def valid_file(): + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write( + 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + tmpfile.write( + 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + tmpfile.flush() + module = beman_submodule.parse_beman_submodule_file(tmpfile.name) + assert module.dirpath == Path(tmpfile.name).resolve().parent + assert module.remote == 'git@github.com:bemanproject/infra.git' + assert module.commit_hash == '9b88395a86c4290794e503e94d8213b6c442ae77' + valid_file() + def invalid_file_missing_remote(): + threw = False + try: + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write( + 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + tmpfile.flush() + beman_submodule.parse_beman_submodule_file(tmpfile.name) + except: + threw = True + assert threw + invalid_file_missing_remote() + def invalid_file_missing_commit_hash(): + threw = False + try: + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write( + 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + tmpfile.flush() + beman_submodule.parse_beman_submodule_file(tmpfile.name) + except: + threw = True + assert threw + invalid_file_missing_commit_hash() + def invalid_file_wrong_section(): + threw = False + try: + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[invalid]\n'.encode('utf-8')) + tmpfile.write( + 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + tmpfile.write( + 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + tmpfile.flush() + beman_submodule.parse_beman_submodule_file(tmpfile.name) + except: + threw = True + assert threw + invalid_file_wrong_section() + +def test_get_beman_submodule(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + assert beman_submodule.get_beman_submodule('foo') + os.remove('foo/.beman_submodule') + assert not beman_submodule.get_beman_submodule('foo') + os.chdir(original_cwd) + +def test_find_beman_submodules_in(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + beman_submodules = beman_submodule.find_beman_submodules_in(tmpdir2.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + assert beman_submodules[0].dirpath == Path(tmpdir2.name) / 'bar' + assert beman_submodules[0].remote == tmpdir.name + assert beman_submodules[0].commit_hash == sha + assert beman_submodules[1].dirpath == Path(tmpdir2.name) / 'foo' + assert beman_submodules[1].remote == tmpdir.name + assert beman_submodules[1].commit_hash == sha + os.chdir(original_cwd) + +def test_cwd_git_repository_path(): + original_cwd = Path.cwd() + tmpdir = tempfile.TemporaryDirectory() + os.chdir(tmpdir.name) + assert not beman_submodule.cwd_git_repository_path() + subprocess.run(['git', 'init']) + assert beman_submodule.cwd_git_repository_path() == tmpdir.name + os.chdir(original_cwd) + +def test_clone_beman_submodule_into_tmpdir(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + beman_submodule.add_command(tmpdir.name, 'foo', False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') + module.commit_hash = sha + tmpdir3 = beman_submodule.clone_beman_submodule_into_tmpdir(module, False) + assert not beman_submodule.directory_compare( + tmpdir.name, tmpdir3.name, ['.git'], False) + tmpdir4 = beman_submodule.clone_beman_submodule_into_tmpdir(module, True) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir4.name, ['.git'], False) + subprocess.run( + ['git', 'reset', '--hard', sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir3.name, ['.git'], False) + os.chdir(original_cwd) + +def test_get_paths(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') + assert beman_submodule.get_paths(module) == set(['a.txt']) + os.chdir(original_cwd) + +def test_beman_submodule_status(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + assert ' ' + sha + ' foo' == beman_submodule.beman_submodule_status( + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) + with open(Path(tmpdir2.name) / 'foo' / 'a.txt', 'w') as f: + f.write('b') + assert '+ ' + sha + ' foo' == beman_submodule.beman_submodule_status( + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) + os.chdir(original_cwd) + +def test_update_command_no_paths(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + parent_parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_parent_sha = parent_parent_sha_process.stdout.strip() + subprocess.run( + ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + beman_submodule.update_command(False, None) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + subprocess.run( + ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.update_command(True, None) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + os.chdir(original_cwd) + +def test_update_command_with_path(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + parent_parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_parent_sha = parent_parent_sha_process.stdout.strip() + subprocess.run( + ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + tmpdir_parent_parent_copy = tempfile.TemporaryDirectory() + shutil.copytree(tmpdir.name, tmpdir_parent_parent_copy.name, dirs_exist_ok=True) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + beman_submodule.update_command(False, 'foo') + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + subprocess.run( + ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir_parent_parent_copy.name, + Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.update_command(True, 'foo') + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir_parent_parent_copy.name, + Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + os.chdir(original_cwd) + +def test_update_command_untracked_files(): + tmpdir = create_test_git_repository2() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd(); + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + os.makedirs(Path(tmpdir2.name) / 'foo') + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\nallow_untracked_files=True') + beman_submodule.update_command(False, 'foo') + assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + beman_submodule.update_command(True, 'foo') + assert set(['./foo/b.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + os.chdir(original_cwd) + +def test_add_command(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n' + os.chdir(original_cwd) + +def test_add_command_untracked_files(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + os.makedirs(Path(tmpdir2.name) / 'foo') + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + beman_submodule.add_command(tmpdir.name, 'foo', True) + assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + os.chdir(original_cwd) + +def test_status_command_no_paths(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: + f.write('b') + beman_submodule.status_command([]) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == '+ ' + sha + ' bar\n' + ' ' + sha + ' foo\n' + os.chdir(original_cwd) + +def test_status_command_with_path(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: + f.write('b') + beman_submodule.status_command(['bar']) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == '+ ' + sha + ' bar\n' + os.chdir(original_cwd) + +def test_status_command_untracked_files(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', True) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + beman_submodule.status_command(['foo']) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == ' ' + sha + ' foo\n' + os.chdir(original_cwd) + +def test_check_for_git(): + tmpdir = tempfile.TemporaryDirectory() + assert not beman_submodule.check_for_git(tmpdir.name) + fake_git_path = Path(tmpdir.name) / 'git' + with open(fake_git_path, 'w'): + pass + os.chmod(fake_git_path, stat.S_IRWXU) + assert beman_submodule.check_for_git(tmpdir.name) + +def test_parse_args(): + def plain_update(): + args = beman_submodule.parse_args(['update']) + assert args.command == 'update' + assert not args.remote + assert not args.beman_submodule_path + plain_update() + def update_remote(): + args = beman_submodule.parse_args(['update', '--remote']) + assert args.command == 'update' + assert args.remote + assert not args.beman_submodule_path + update_remote() + def update_path(): + args = beman_submodule.parse_args(['update', 'infra/']) + assert args.command == 'update' + assert not args.remote + assert args.beman_submodule_path == 'infra/' + update_path() + def update_path_remote(): + args = beman_submodule.parse_args(['update', '--remote', 'infra/']) + assert args.command == 'update' + assert args.remote + assert args.beman_submodule_path == 'infra/' + update_path_remote() + def plain_add(): + args = beman_submodule.parse_args(['add', 'git@github.com:bemanproject/infra.git']) + assert args.command == 'add' + assert args.repository == 'git@github.com:bemanproject/infra.git' + assert not args.path + plain_add() + def add_path(): + args = beman_submodule.parse_args( + ['add', 'git@github.com:bemanproject/infra.git', 'infra/']) + assert args.command == 'add' + assert args.repository == 'git@github.com:bemanproject/infra.git' + assert args.path == 'infra/' + add_path() + def plain_status(): + args = beman_submodule.parse_args(['status']) + assert args.command == 'status' + assert args.paths == [] + plain_status() + def status_one_module(): + args = beman_submodule.parse_args(['status', 'infra/']) + assert args.command == 'status' + assert args.paths == ['infra/'] + status_one_module() + def status_multiple_modules(): + args = beman_submodule.parse_args(['status', 'infra/', 'foobar/']) + assert args.command == 'status' + assert args.paths == ['infra/', 'foobar/'] + status_multiple_modules() diff --git a/lockfile.json b/lockfile.json new file mode 100644 index 0000000..4208a98 --- /dev/null +++ b/lockfile.json @@ -0,0 +1,3 @@ +{ + "dependencies": [] +} From 190f68fa8e98059fa1464c0098e4ca923d617728 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 28 Oct 2025 14:50:49 +0100 Subject: [PATCH 093/120] Add async_rrcp_client_threadsafe.hpp --- GNUmakefile | 2 +- async_rrcp_client.hpp | 28 +- async_rrcp_client_threadsafe.hpp | 468 ++++++++++++ .../test/test_beman_submodule.py | 680 ++++++++++++------ rrcp_helper.cpp | 2 +- tests/RRCP-test.cpp | 76 +- 6 files changed, 976 insertions(+), 280 deletions(-) create mode 100644 async_rrcp_client_threadsafe.hpp diff --git a/GNUmakefile b/GNUmakefile index ef66f5c..8c43f15 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -42,7 +42,7 @@ distclean: # XXX clean rm -rf $(BUILD_DIR) build coverage/* *~ ctags $(BUILD_DIR): CMakeLists.txt - -test -d build/appleclang-debug && ln -s build/appleclang-debug $(BUILD_DIR) + -test -d build/appleclang-debug && ln -f -s $(CURDIR)/build/appleclang-debug $(CURDIR)/$(BUILD_DIR) cmake -S . -B $@ -G Ninja -D CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) --log-level=VERBOSE # --fresh check: all diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 6a4e96c..d424208 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -14,6 +14,7 @@ #include +#include #include // for starts_with #include // for trim_left, trim_right #include @@ -27,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -154,14 +154,18 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client void stop() { - fmt::print(stderr, "Stopped, disconnecting ...\n"); - stopped_ = true; - connected_ = false; - // boost::system::error_code ec; - // socket_.close(ec); - boost::asio::post(io_context_, [this]() -> void { socket_.close(); }); - heartbeat_timer_.cancel(); - deadline_.cancel(); + boost::asio::post(io_context_, + [this, self = shared_from_this()]() -> void + { + fmt::print(stderr, "Stopped, disconnecting ...\n"); + stopped_ = true; + connected_ = false; + + boost::system::error_code ec; + socket_.close(ec); + heartbeat_timer_.cancel(); + deadline_.cancel(); + }); } private: @@ -280,9 +284,9 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client std::string input_buffer_; message_queue read_msgs_; message_queue write_msgs_; - std::atomic msg_id_{10000}; - std::atomic connected_{false}; - std::atomic stopped_{false}; + std::atomic< int > msg_id_{10000}; + std::atomic< bool > connected_{false}; + std::atomic< bool > stopped_{false}; signal_string_type trap_handler_; }; diff --git a/async_rrcp_client_threadsafe.hpp b/async_rrcp_client_threadsafe.hpp new file mode 100644 index 0000000..3fb7b7f --- /dev/null +++ b/async_rrcp_client_threadsafe.hpp @@ -0,0 +1,468 @@ +#pragma once + +/*** + * async_rrcp_client_threadsafe.hpp + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com) + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * + * Moderniced from Claus Klein and ChatGPT + * Thread-safe implementation with preserved interface + ***/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rrcp_helper.hpp" + +namespace rrcp +{ + +using boost::asio::ip::tcp; +using namespace std::chrono_literals; + +constexpr size_t MAX_LENGTH = 65432; +constexpr auto TIMEOUT_DURATION = 3s; +constexpr auto HEARTBEAT_INTERVAL = 10s; + +class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client > +{ + using message_queue = std::deque< std::string >; + using signal_string_type = boost::signals2::signal< void(std::string) >; + using response_promise_type = std::promise< std::string >; + + public: + explicit async_rrcp_client(boost::asio::io_context& io_context) + : io_context_(io_context), + strand_(boost::asio::make_strand(io_context)), + socket_(strand_), + deadline_(strand_), + heartbeat_timer_(strand_) + { + deadline_.expires_at(boost::asio::steady_timer::time_point::max()); + } + + void start(const tcp::resolver::results_type& endpoints) + { + boost::asio::post(strand_, + [this, endpoints, self = shared_from_this()]() -> void + { + deadline_.expires_after(TIMEOUT_DURATION); + check_deadline(); + + boost::asio::async_connect(socket_, endpoints, + [self](const boost::system::error_code& ec, const tcp::endpoint&) -> void + { + if (!ec) + { + fmt::print(stderr, "Connected to server.\n"); + self->connected_.store(true); + self->notify_connection_waiters(); + self->do_read(); + self->send_heartbeat(); + } + else + { + fmt::print(stderr, "Failed to connect: {}\n", ec.message()); + self->notify_connection_waiters(); + } + }); + }); + } + + void register_trap_handler(const std::function< void(std::string) >& handler) + { + boost::asio::post(strand_, [this, handler, self = shared_from_this()]() -> void { trap_handler_.connect(handler); }); + } + + [[nodiscard]] auto connected() const -> bool { return connected_.load(); } + + // Thread-safe synchronous write with preserved interface + [[nodiscard]] auto write(const std::string& message) -> std::string + { + auto promise = std::make_shared< response_promise_type >(); + auto future = promise->get_future(); + + boost::asio::post(strand_, + [this, message, promise, self = shared_from_this()]() -> void + { + if (!connected_.load()) + { + // Queue request until connected + pending_writes_.emplace_back(message, promise); + return; + } + + execute_write_request(message, promise); + }); + + // Wait for response with timeout + if (future.wait_for(TIMEOUT_DURATION) == std::future_status::timeout) + { + return {}; // Timeout - return empty string + } + + try + { + const auto response = future.get(); + fmt::print(stderr, "Returning {}\n", response); + return response; + } + catch (const std::exception&) + { + return {}; // Error - return empty string + } + } + + void stop() + { + boost::asio::post(strand_, + [this, self = shared_from_this()]() -> void + { + fmt::print(stderr, "Stopped, disconnecting ...\n"); + stopped_.store(true); + connected_.store(false); + + boost::system::error_code ec; + socket_.close(ec); + heartbeat_timer_.cancel(); + deadline_.cancel(); + + // Clear pending operations and notify waiters + clear_pending_operations(); + notify_connection_waiters(); + }); + } + + private: + // Helper method to safely parse RRCP message with bounds checking + static auto parse_rrcp_message(const std::string& buffer, std::size_t length, std::string& parsed_line) -> bool + { + // Minimum RRCP message: START + at least 1 char + STOP = 3 bytes + if (length < 3) + { + fmt::print(stderr, "Warning: Message too short (length={}) - expected minimum 3 bytes\n", length); + return false; + } + + // Validate buffer size + if (buffer.size() < length) + { + fmt::print(stderr, "Error: Buffer size ({}) smaller than expected length ({})\n", buffer.size(), length); + return false; + } + + // Check for START delimiter at beginning + if (buffer[0] != START) + { + fmt::print(stderr, "Warning: Missing START delimiter (found 0x{:02X})\n", static_cast< unsigned char >(buffer[0])); + return false; + } + + // Check for STOP delimiter at expected position + if (buffer[length - 1] != STOP) + { + fmt::print(stderr, "Warning: Missing STOP delimiter at position {} (found 0x{:02X})\n", length - 1, + static_cast< unsigned char >(buffer[length - 1])); + return false; + } + + // Extract message content (without START and STOP) + if (length >= 3) + { + parsed_line = esc2char(buffer.substr(1, length - 2)); + return true; + } + + return false; + } + + void execute_write_request(const std::string& message, std::shared_ptr< response_promise_type > promise) + { + std::string msg_id_str; + int current_id = next_message_id_.fetch_add(1); + auto command = rrcp::create_command_msg(message, msg_id_str, current_id); + + // Store promise for response correlation + pending_responses_[msg_id_str] = std::move(promise); + + bool const write_in_progress{!write_msgs_.empty()}; + write_msgs_.push_back(command); + + if (!write_in_progress) + { + deadline_.expires_after(TIMEOUT_DURATION); + do_write(); + } + } + + void notify_connection_waiters() + { + // Process pending writes that were waiting for connection + for (auto& [message, promise] : pending_writes_) + { + if (connected_.load()) + { + execute_write_request(message, promise); + } + else + { + // Connection failed - fulfill promise with empty response + try + { + promise->set_value(""); + } + // NOLINTNEXTLINE(bugprone-empty-catch) + catch (const std::exception&) + { + // Promise already fulfilled - ignore + } + } + } + pending_writes_.clear(); + } + + void clear_pending_operations() + { + // Fulfill all pending promises with empty responses + for (auto& [msg_id, promise] : pending_responses_) + { + try + { + promise->set_value(""); + } + // NOLINTNEXTLINE(bugprone-empty-catch) + catch (const std::exception&) + { + // Promise already fulfilled - ignore + } + } + pending_responses_.clear(); + + for (auto& [message, promise] : pending_writes_) + { + try + { + promise->set_value(""); + } + // NOLINTNEXTLINE(bugprone-empty-catch) + catch (const std::exception&) + { + // Promise already fulfilled - ignore + } + } + pending_writes_.clear(); + + write_msgs_.clear(); + } + + void do_read() + { + boost::asio::async_read_until(socket_, boost::asio::dynamic_buffer(input_buffer_), STOP, + [self = shared_from_this()](const boost::system::error_code& ec, std::size_t length) -> void + { + if (!ec) + { + //========================== RRCP ============================ + std::string parsed_line; + + // Use safe parsing helper with comprehensive bounds checking + if (!rrcp::async_rrcp_client::parse_rrcp_message(self->input_buffer_, length, parsed_line)) + { + // Parsing failed - message was malformed, skip it + self->input_buffer_.erase(0, length); + self->deadline_.expires_after(HEARTBEAT_INTERVAL + TIMEOUT_DURATION); + self->do_read(); + return; + } + + // Successfully parsed, remove processed data from buffer + self->input_buffer_.erase(0, length); + //========================== END ============================ + + // TODO(CK): maby refactored to helper class? + // Validate parsed content is not empty + if (parsed_line.empty()) + { + fmt::print(stderr, "Warning: Parsed empty message content - skipping\n"); + self->deadline_.expires_after(HEARTBEAT_INTERVAL + TIMEOUT_DURATION); + self->do_read(); + return; + } + + //========================== RRCP ============================ + // Process different message types + if (boost::algorithm::starts_with(parsed_line, "d")) // Trap data message + { + // Handle trap data messages + fmt::print(stderr, "trap data: {}\n", parsed_line); + self->trap_handler_(parsed_line); + } + else if (!boost::algorithm::starts_with(parsed_line, "gPing")) + { + // Handle response messages (but ignore heartbeat responses) + fmt::print(stderr, "{}\n", parsed_line); + self->handle_response(parsed_line); + } + // Note: gPing messages are silently ignored (heartbeat responses) + //========================== END ============================ + + self->deadline_.expires_after(HEARTBEAT_INTERVAL + TIMEOUT_DURATION); + self->do_read(); + } + else + { + fmt::print(stderr, "Error reading message: {}\n", ec.message()); + self->stop(); + } + }); + } + + void handle_response(const std::string& response) + { + // Find matching pending response + for (auto it = pending_responses_.begin(); it != pending_responses_.end(); ++it) + { + std::string clean_response = response; + if (rrcp::find_response_msg(clean_response, it->first)) + { + auto promise = it->second; + pending_responses_.erase(it); + + // Fulfill promise with response + try + { + promise->set_value(clean_response); + } + // NOLINTNEXTLINE(bugprone-empty-catch) + catch (const std::exception&) + { + // Promise already fulfilled - ignore + } + return; + } + } + } + + void do_write() + { + boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front()), + [self = shared_from_this()](boost::system::error_code ec, std::size_t /*length*/) -> void + { + if (!ec) + { + self->write_msgs_.pop_front(); + if (!self->write_msgs_.empty()) + { + self->do_write(); + } + } + else + { + // There are no more endpoints to try. Shut down the client. + fmt::print(stderr, "Error writing message: {}\n", ec.message()); + self->stop(); + } + }); + } + + void send_heartbeat() + { + if (stopped_.load()) + { + return; + } + + //========================== RRCP ============================ + std::string heartbeat_message{START + char2esc("M:Utility GPing\"async client\"") + STOP}; + //========================== END ============================ + + fmt::print(stderr, "Send heartbeat: {}\n", heartbeat_message); // TRACE + boost::asio::async_write(socket_, boost::asio::buffer(heartbeat_message), + [self = shared_from_this()](const boost::system::error_code& ec, std::size_t) -> void + { + if (!ec) + { + self->heartbeat_timer_.expires_after(HEARTBEAT_INTERVAL); + self->heartbeat_timer_.async_wait( + [self](const boost::system::error_code&) -> void { self->send_heartbeat(); }); + } + else + { + fmt::print(stderr, "Error sending heartbeat: {}\n", ec.message()); + self->stop(); + } + }); + } + + void check_deadline() + { + if (stopped_.load()) + { + return; + } + + if (deadline_.expiry() <= boost::asio::steady_timer::clock_type::now()) + { + fmt::print(stderr, "No response from server, stopping ...\n"); + stop(); + return; + } + + deadline_.async_wait( + [self = shared_from_this()](const boost::system::error_code&) -> void { self->check_deadline(); }); + } + + // Core networking components + boost::asio::io_context& io_context_; + boost::asio::strand< boost::asio::io_context::executor_type > strand_; + tcp::socket socket_; + boost::asio::steady_timer deadline_; + boost::asio::steady_timer heartbeat_timer_; + + // I/O buffers (protected by strand) + std::string input_buffer_; + message_queue write_msgs_; + + // Thread-safe state + std::atomic< bool > connected_{false}; + std::atomic< bool > stopped_{false}; + std::atomic< int > next_message_id_{10000}; + + // Response correlation system (protected by strand) + std::unordered_map< std::string, std::shared_ptr< response_promise_type > > pending_responses_; + std::vector< std::pair< std::string, std::shared_ptr< response_promise_type > > > pending_writes_; + + // Signal handling + signal_string_type trap_handler_; +}; + +} // namespace rrcp diff --git a/infra/tools/beman-submodule/test/test_beman_submodule.py b/infra/tools/beman-submodule/test/test_beman_submodule.py index 600fc07..b3dcbd5 100644 --- a/infra/tools/beman-submodule/test/test_beman_submodule.py +++ b/infra/tools/beman-submodule/test/test_beman_submodule.py @@ -12,68 +12,113 @@ # https://stackoverflow.com/a/19011259 import types import importlib.machinery + loader = importlib.machinery.SourceFileLoader( - 'beman_submodule', - str(Path(__file__).parent.resolve().parent / 'beman-submodule')) + "beman_submodule", str(Path(__file__).parent.resolve().parent / "beman-submodule") +) beman_submodule = types.ModuleType(loader.name) loader.exec_module(beman_submodule) + def create_test_git_repository(): tmpdir = tempfile.TemporaryDirectory() tmp_path = Path(tmpdir.name) - subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run(["git", "init"], check=True, cwd=tmpdir.name, capture_output=True) + def make_commit(a_txt_contents): - with open(tmp_path / 'a.txt', 'w') as f: + with open(tmp_path / "a.txt", "w") as f: f.write(a_txt_contents) subprocess.run( - ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + ["git", "add", "a.txt"], check=True, cwd=tmpdir.name, capture_output=True + ) subprocess.run( - ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', - '--author="test "', '-m', 'test'], - check=True, cwd=tmpdir.name, capture_output=True) - make_commit('A') - make_commit('a') + [ + "git", + "-c", + "user.name=test", + "-c", + "user.email=test@example.com", + "commit", + '--author="test "', + "-m", + "test", + ], + check=True, + cwd=tmpdir.name, + capture_output=True, + ) + + make_commit("A") + make_commit("a") return tmpdir + def create_test_git_repository2(): tmpdir = tempfile.TemporaryDirectory() tmp_path = Path(tmpdir.name) - subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) - with open(tmp_path / 'a.txt', 'w') as f: - f.write('a') + subprocess.run(["git", "init"], check=True, cwd=tmpdir.name, capture_output=True) + with open(tmp_path / "a.txt", "w") as f: + f.write("a") subprocess.run( - ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + ["git", "add", "a.txt"], check=True, cwd=tmpdir.name, capture_output=True + ) subprocess.run( - ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', - '--author="test "', '-m', 'test'], - check=True, cwd=tmpdir.name, capture_output=True) - os.remove(tmp_path / 'a.txt') + [ + "git", + "-c", + "user.name=test", + "-c", + "user.email=test@example.com", + "commit", + '--author="test "', + "-m", + "test", + ], + check=True, + cwd=tmpdir.name, + capture_output=True, + ) + os.remove(tmp_path / "a.txt") subprocess.run( - ['git', 'rm', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) - with open(tmp_path / 'b.txt', 'w') as f: - f.write('b') + ["git", "rm", "a.txt"], check=True, cwd=tmpdir.name, capture_output=True + ) + with open(tmp_path / "b.txt", "w") as f: + f.write("b") subprocess.run( - ['git', 'add', 'b.txt'], check=True, cwd=tmpdir.name, capture_output=True) + ["git", "add", "b.txt"], check=True, cwd=tmpdir.name, capture_output=True + ) subprocess.run( - ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', - '--author="test "', '-m', 'test'], - check=True, cwd=tmpdir.name, capture_output=True) + [ + "git", + "-c", + "user.name=test", + "-c", + "user.email=test@example.com", + "commit", + '--author="test "', + "-m", + "test", + ], + check=True, + cwd=tmpdir.name, + capture_output=True, + ) return tmpdir + def test_directory_compare(): def create_dir_structure(dir_path: Path): - bar_path = dir_path / 'bar' + bar_path = dir_path / "bar" os.makedirs(bar_path) - with open(dir_path / 'foo.txt', 'w') as f: - f.write('foo') - with open(bar_path / 'baz.txt', 'w') as f: - f.write('baz') + with open(dir_path / "foo.txt", "w") as f: + f.write("foo") + with open(bar_path / "baz.txt", "w") as f: + f.write("baz") - with tempfile.TemporaryDirectory() as dir_a, \ - tempfile.TemporaryDirectory() as dir_b: + with tempfile.TemporaryDirectory() as dir_a, tempfile.TemporaryDirectory() as dir_b: path_a = Path(dir_a) path_b = Path(dir_b) @@ -82,458 +127,637 @@ def create_dir_structure(dir_path: Path): assert beman_submodule.directory_compare(dir_a, dir_b, [], False) - with open(path_a / 'bar' / 'quux.txt', 'w') as f: - f.write('quux') + with open(path_a / "bar" / "quux.txt", "w") as f: + f.write("quux") assert not beman_submodule.directory_compare(path_a, path_b, [], False) - assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], False) + assert beman_submodule.directory_compare(path_a, path_b, ["quux.txt"], False) + def test_directory_compare_untracked_files(): def create_dir_structure(dir_path: Path): - bar_path = dir_path / 'bar' + bar_path = dir_path / "bar" os.makedirs(bar_path) - with open(dir_path / 'foo.txt', 'w') as f: - f.write('foo') - with open(bar_path / 'baz.txt', 'w') as f: - f.write('baz') + with open(dir_path / "foo.txt", "w") as f: + f.write("foo") + with open(bar_path / "baz.txt", "w") as f: + f.write("baz") - with tempfile.TemporaryDirectory() as reference, \ - tempfile.TemporaryDirectory() as actual: + with tempfile.TemporaryDirectory() as reference, tempfile.TemporaryDirectory() as actual: path_a = Path(reference) path_b = Path(actual) create_dir_structure(path_a) create_dir_structure(path_b) - (path_b / 'c.txt').touch() + (path_b / "c.txt").touch() assert beman_submodule.directory_compare(reference, actual, [], True) - with open(path_a / 'bar' / 'quux.txt', 'w') as f: - f.write('quux') + with open(path_a / "bar" / "quux.txt", "w") as f: + f.write("quux") assert not beman_submodule.directory_compare(path_a, path_b, [], True) - assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], True) + assert beman_submodule.directory_compare(path_a, path_b, ["quux.txt"], True) + def test_parse_beman_submodule_file(): def valid_file(): tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write("[beman_submodule]\n".encode("utf-8")) + tmpfile.write("remote=git@github.com:bemanproject/infra.git\n".encode("utf-8")) tmpfile.write( - 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) - tmpfile.write( - 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + "commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n".encode("utf-8") + ) tmpfile.flush() module = beman_submodule.parse_beman_submodule_file(tmpfile.name) assert module.dirpath == Path(tmpfile.name).resolve().parent - assert module.remote == 'git@github.com:bemanproject/infra.git' - assert module.commit_hash == '9b88395a86c4290794e503e94d8213b6c442ae77' + assert module.remote == "git@github.com:bemanproject/infra.git" + assert module.commit_hash == "9b88395a86c4290794e503e94d8213b6c442ae77" + valid_file() + def invalid_file_missing_remote(): threw = False try: tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write("[beman_submodule]\n".encode("utf-8")) tmpfile.write( - 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + "commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n".encode("utf-8") + ) tmpfile.flush() beman_submodule.parse_beman_submodule_file(tmpfile.name) except: threw = True assert threw + invalid_file_missing_remote() + def invalid_file_missing_commit_hash(): threw = False try: tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write("[beman_submodule]\n".encode("utf-8")) tmpfile.write( - 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + "remote=git@github.com:bemanproject/infra.git\n".encode("utf-8") + ) tmpfile.flush() beman_submodule.parse_beman_submodule_file(tmpfile.name) except: threw = True assert threw + invalid_file_missing_commit_hash() + def invalid_file_wrong_section(): threw = False try: tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write('[invalid]\n'.encode('utf-8')) + tmpfile.write("[invalid]\n".encode("utf-8")) tmpfile.write( - 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + "remote=git@github.com:bemanproject/infra.git\n".encode("utf-8") + ) tmpfile.write( - 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + "commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n".encode("utf-8") + ) tmpfile.flush() beman_submodule.parse_beman_submodule_file(tmpfile.name) except: threw = True assert threw + invalid_file_wrong_section() + def test_get_beman_submodule(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo', False) - assert beman_submodule.get_beman_submodule('foo') - os.remove('foo/.beman_submodule') - assert not beman_submodule.get_beman_submodule('foo') + beman_submodule.add_command(tmpdir.name, "foo", False) + assert beman_submodule.get_beman_submodule("foo") + os.remove("foo/.beman_submodule") + assert not beman_submodule.get_beman_submodule("foo") os.chdir(original_cwd) + def test_find_beman_submodules_in(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo', False) - beman_submodule.add_command(tmpdir.name, 'bar', False) + beman_submodule.add_command(tmpdir.name, "foo", False) + beman_submodule.add_command(tmpdir.name, "bar", False) beman_submodules = beman_submodule.find_beman_submodules_in(tmpdir2.name) sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) sha = sha_process.stdout.strip() - assert beman_submodules[0].dirpath == Path(tmpdir2.name) / 'bar' + assert beman_submodules[0].dirpath == Path(tmpdir2.name) / "bar" assert beman_submodules[0].remote == tmpdir.name assert beman_submodules[0].commit_hash == sha - assert beman_submodules[1].dirpath == Path(tmpdir2.name) / 'foo' + assert beman_submodules[1].dirpath == Path(tmpdir2.name) / "foo" assert beman_submodules[1].remote == tmpdir.name assert beman_submodules[1].commit_hash == sha os.chdir(original_cwd) + def test_cwd_git_repository_path(): original_cwd = Path.cwd() tmpdir = tempfile.TemporaryDirectory() os.chdir(tmpdir.name) assert not beman_submodule.cwd_git_repository_path() - subprocess.run(['git', 'init']) + subprocess.run(["git", "init"]) assert beman_submodule.cwd_git_repository_path() == tmpdir.name os.chdir(original_cwd) + def test_clone_beman_submodule_into_tmpdir(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD^"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) sha = sha_process.stdout.strip() - beman_submodule.add_command(tmpdir.name, 'foo', False) - module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') + beman_submodule.add_command(tmpdir.name, "foo", False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / "foo") module.commit_hash = sha tmpdir3 = beman_submodule.clone_beman_submodule_into_tmpdir(module, False) assert not beman_submodule.directory_compare( - tmpdir.name, tmpdir3.name, ['.git'], False) + tmpdir.name, tmpdir3.name, [".git"], False + ) tmpdir4 = beman_submodule.clone_beman_submodule_into_tmpdir(module, True) - assert beman_submodule.directory_compare(tmpdir.name, tmpdir4.name, ['.git'], False) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir4.name, [".git"], False) subprocess.run( - ['git', 'reset', '--hard', sha], capture_output=True, check=True, - cwd=tmpdir.name) - assert beman_submodule.directory_compare(tmpdir.name, tmpdir3.name, ['.git'], False) + ["git", "reset", "--hard", sha], + capture_output=True, + check=True, + cwd=tmpdir.name, + ) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir3.name, [".git"], False) os.chdir(original_cwd) + def test_get_paths(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo', False) - module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') - assert beman_submodule.get_paths(module) == set(['a.txt']) + beman_submodule.add_command(tmpdir.name, "foo", False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / "foo") + assert beman_submodule.get_paths(module) == set(["a.txt"]) os.chdir(original_cwd) + def test_beman_submodule_status(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, "foo", False) sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) sha = sha_process.stdout.strip() - assert ' ' + sha + ' foo' == beman_submodule.beman_submodule_status( - beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) - with open(Path(tmpdir2.name) / 'foo' / 'a.txt', 'w') as f: - f.write('b') - assert '+ ' + sha + ' foo' == beman_submodule.beman_submodule_status( - beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) + assert " " + sha + " foo" == beman_submodule.beman_submodule_status( + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / "foo") + ) + with open(Path(tmpdir2.name) / "foo" / "a.txt", "w") as f: + f.write("b") + assert "+ " + sha + " foo" == beman_submodule.beman_submodule_status( + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / "foo") + ) os.chdir(original_cwd) + def test_update_command_no_paths(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) orig_sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) orig_sha = orig_sha_process.stdout.strip() parent_sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD^"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) parent_sha = parent_sha_process.stdout.strip() parent_parent_sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD^"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) parent_parent_sha = parent_parent_sha_process.stdout.strip() subprocess.run( - ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, - cwd=tmpdir.name) - beman_submodule.add_command(tmpdir.name, 'foo', False) - beman_submodule.add_command(tmpdir.name, 'bar', False) + ["git", "reset", "--hard", parent_parent_sha], + capture_output=True, + check=True, + cwd=tmpdir.name, + ) + beman_submodule.add_command(tmpdir.name, "foo", False) + beman_submodule.add_command(tmpdir.name, "bar", False) subprocess.run( - ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, - cwd=tmpdir.name) - with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: - f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') - with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: - f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + ["git", "reset", "--hard", orig_sha], + capture_output=True, + check=True, + cwd=tmpdir.name, + ) + with open(Path(tmpdir2.name) / "foo" / ".beman_submodule", "w") as f: + f.write(f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n") + with open(Path(tmpdir2.name) / "bar" / ".beman_submodule", "w") as f: + f.write(f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n") beman_submodule.update_command(False, None) - with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: - assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' - with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: - assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + with open(Path(tmpdir2.name) / "foo" / ".beman_submodule", "r") as f: + assert ( + f.read() + == f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n" + ) + with open(Path(tmpdir2.name) / "bar" / ".beman_submodule", "r") as f: + assert ( + f.read() + == f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n" + ) subprocess.run( - ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, - cwd=tmpdir.name) + ["git", "reset", "--hard", parent_sha], + capture_output=True, + check=True, + cwd=tmpdir.name, + ) assert beman_submodule.directory_compare( - tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + tmpdir.name, Path(tmpdir2.name) / "foo", [".git", ".beman_submodule"], False + ) assert beman_submodule.directory_compare( - tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + tmpdir.name, Path(tmpdir2.name) / "bar", [".git", ".beman_submodule"], False + ) subprocess.run( - ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, - cwd=tmpdir.name) + ["git", "reset", "--hard", orig_sha], + capture_output=True, + check=True, + cwd=tmpdir.name, + ) beman_submodule.update_command(True, None) - with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: - assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' - with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: - assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + with open(Path(tmpdir2.name) / "foo" / ".beman_submodule", "r") as f: + assert ( + f.read() + == f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n" + ) + with open(Path(tmpdir2.name) / "bar" / ".beman_submodule", "r") as f: + assert ( + f.read() + == f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n" + ) assert beman_submodule.directory_compare( - tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + tmpdir.name, Path(tmpdir2.name) / "foo", [".git", ".beman_submodule"], False + ) assert beman_submodule.directory_compare( - tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + tmpdir.name, Path(tmpdir2.name) / "bar", [".git", ".beman_submodule"], False + ) os.chdir(original_cwd) + def test_update_command_with_path(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) orig_sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) orig_sha = orig_sha_process.stdout.strip() parent_sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD^"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) parent_sha = parent_sha_process.stdout.strip() parent_parent_sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD^"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) parent_parent_sha = parent_parent_sha_process.stdout.strip() subprocess.run( - ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, - cwd=tmpdir.name) + ["git", "reset", "--hard", parent_parent_sha], + capture_output=True, + check=True, + cwd=tmpdir.name, + ) tmpdir_parent_parent_copy = tempfile.TemporaryDirectory() shutil.copytree(tmpdir.name, tmpdir_parent_parent_copy.name, dirs_exist_ok=True) - beman_submodule.add_command(tmpdir.name, 'foo', False) - beman_submodule.add_command(tmpdir.name, 'bar', False) + beman_submodule.add_command(tmpdir.name, "foo", False) + beman_submodule.add_command(tmpdir.name, "bar", False) subprocess.run( - ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, - cwd=tmpdir.name) - with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: - f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') - with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: - f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') - beman_submodule.update_command(False, 'foo') - with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: - assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' - with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: - assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + ["git", "reset", "--hard", orig_sha], + capture_output=True, + check=True, + cwd=tmpdir.name, + ) + with open(Path(tmpdir2.name) / "foo" / ".beman_submodule", "w") as f: + f.write(f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n") + with open(Path(tmpdir2.name) / "bar" / ".beman_submodule", "w") as f: + f.write(f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n") + beman_submodule.update_command(False, "foo") + with open(Path(tmpdir2.name) / "foo" / ".beman_submodule", "r") as f: + assert ( + f.read() + == f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n" + ) + with open(Path(tmpdir2.name) / "bar" / ".beman_submodule", "r") as f: + assert ( + f.read() + == f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n" + ) subprocess.run( - ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, - cwd=tmpdir.name) + ["git", "reset", "--hard", parent_sha], + capture_output=True, + check=True, + cwd=tmpdir.name, + ) assert beman_submodule.directory_compare( - tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + tmpdir.name, Path(tmpdir2.name) / "foo", [".git", ".beman_submodule"], False + ) assert beman_submodule.directory_compare( tmpdir_parent_parent_copy.name, - Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + Path(tmpdir2.name) / "bar", + [".git", ".beman_submodule"], + False, + ) subprocess.run( - ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, - cwd=tmpdir.name) - beman_submodule.update_command(True, 'foo') - with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: - assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' - with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: - assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + ["git", "reset", "--hard", orig_sha], + capture_output=True, + check=True, + cwd=tmpdir.name, + ) + beman_submodule.update_command(True, "foo") + with open(Path(tmpdir2.name) / "foo" / ".beman_submodule", "r") as f: + assert ( + f.read() + == f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n" + ) + with open(Path(tmpdir2.name) / "bar" / ".beman_submodule", "r") as f: + assert ( + f.read() + == f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n" + ) assert beman_submodule.directory_compare( - tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + tmpdir.name, Path(tmpdir2.name) / "foo", [".git", ".beman_submodule"], False + ) assert beman_submodule.directory_compare( tmpdir_parent_parent_copy.name, - Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + Path(tmpdir2.name) / "bar", + [".git", ".beman_submodule"], + False, + ) os.chdir(original_cwd) + def test_update_command_untracked_files(): tmpdir = create_test_git_repository2() tmpdir2 = create_test_git_repository() - original_cwd = Path.cwd(); + original_cwd = Path.cwd() os.chdir(tmpdir2.name) orig_sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) orig_sha = orig_sha_process.stdout.strip() parent_sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD^"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) parent_sha = parent_sha_process.stdout.strip() - os.makedirs(Path(tmpdir2.name) / 'foo') - (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() - with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: - f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\nallow_untracked_files=True') - beman_submodule.update_command(False, 'foo') - assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) - beman_submodule.update_command(True, 'foo') - assert set(['./foo/b.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + os.makedirs(Path(tmpdir2.name) / "foo") + (Path(tmpdir2.name) / "foo" / "c.txt").touch() + with open(Path(tmpdir2.name) / "foo" / ".beman_submodule", "w") as f: + f.write( + f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\nallow_untracked_files=True" + ) + beman_submodule.update_command(False, "foo") + assert set(["./foo/a.txt", "./foo/c.txt"]) == set(glob.glob("./foo/*.txt")) + beman_submodule.update_command(True, "foo") + assert set(["./foo/b.txt", "./foo/c.txt"]) == set(glob.glob("./foo/*.txt")) os.chdir(original_cwd) + def test_add_command(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, "foo", False) sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) sha = sha_process.stdout.strip() assert beman_submodule.directory_compare( - tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) - with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: - assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n' + tmpdir.name, Path(tmpdir2.name) / "foo", [".git", ".beman_submodule"], False + ) + with open(Path(tmpdir2.name) / "foo" / ".beman_submodule", "r") as f: + assert ( + f.read() == f"[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n" + ) os.chdir(original_cwd) + def test_add_command_untracked_files(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) - os.makedirs(Path(tmpdir2.name) / 'foo') - (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() - beman_submodule.add_command(tmpdir.name, 'foo', True) - assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + os.makedirs(Path(tmpdir2.name) / "foo") + (Path(tmpdir2.name) / "foo" / "c.txt").touch() + beman_submodule.add_command(tmpdir.name, "foo", True) + assert set(["./foo/a.txt", "./foo/c.txt"]) == set(glob.glob("./foo/*.txt")) os.chdir(original_cwd) + def test_status_command_no_paths(capsys): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo', False) - beman_submodule.add_command(tmpdir.name, 'bar', False) + beman_submodule.add_command(tmpdir.name, "foo", False) + beman_submodule.add_command(tmpdir.name, "bar", False) sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) - with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: - f.write('b') + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) + with open(Path(tmpdir2.name) / "bar" / "a.txt", "w") as f: + f.write("b") beman_submodule.status_command([]) sha = sha_process.stdout.strip() - assert capsys.readouterr().out == '+ ' + sha + ' bar\n' + ' ' + sha + ' foo\n' + assert capsys.readouterr().out == "+ " + sha + " bar\n" + " " + sha + " foo\n" os.chdir(original_cwd) + def test_status_command_with_path(capsys): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo', False) - beman_submodule.add_command(tmpdir.name, 'bar', False) + beman_submodule.add_command(tmpdir.name, "foo", False) + beman_submodule.add_command(tmpdir.name, "bar", False) sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) - with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: - f.write('b') - beman_submodule.status_command(['bar']) + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) + with open(Path(tmpdir2.name) / "bar" / "a.txt", "w") as f: + f.write("b") + beman_submodule.status_command(["bar"]) sha = sha_process.stdout.strip() - assert capsys.readouterr().out == '+ ' + sha + ' bar\n' + assert capsys.readouterr().out == "+ " + sha + " bar\n" os.chdir(original_cwd) + def test_status_command_untracked_files(capsys): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo', True) + beman_submodule.add_command(tmpdir.name, "foo", True) sha_process = subprocess.run( - ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, - cwd=tmpdir.name) - (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() - beman_submodule.status_command(['foo']) + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + text=True, + cwd=tmpdir.name, + ) + (Path(tmpdir2.name) / "foo" / "c.txt").touch() + beman_submodule.status_command(["foo"]) sha = sha_process.stdout.strip() - assert capsys.readouterr().out == ' ' + sha + ' foo\n' + assert capsys.readouterr().out == " " + sha + " foo\n" os.chdir(original_cwd) + def test_check_for_git(): tmpdir = tempfile.TemporaryDirectory() assert not beman_submodule.check_for_git(tmpdir.name) - fake_git_path = Path(tmpdir.name) / 'git' - with open(fake_git_path, 'w'): + fake_git_path = Path(tmpdir.name) / "git" + with open(fake_git_path, "w"): pass os.chmod(fake_git_path, stat.S_IRWXU) assert beman_submodule.check_for_git(tmpdir.name) + def test_parse_args(): def plain_update(): - args = beman_submodule.parse_args(['update']) - assert args.command == 'update' + args = beman_submodule.parse_args(["update"]) + assert args.command == "update" assert not args.remote assert not args.beman_submodule_path + plain_update() + def update_remote(): - args = beman_submodule.parse_args(['update', '--remote']) - assert args.command == 'update' + args = beman_submodule.parse_args(["update", "--remote"]) + assert args.command == "update" assert args.remote assert not args.beman_submodule_path + update_remote() + def update_path(): - args = beman_submodule.parse_args(['update', 'infra/']) - assert args.command == 'update' + args = beman_submodule.parse_args(["update", "infra/"]) + assert args.command == "update" assert not args.remote - assert args.beman_submodule_path == 'infra/' + assert args.beman_submodule_path == "infra/" + update_path() + def update_path_remote(): - args = beman_submodule.parse_args(['update', '--remote', 'infra/']) - assert args.command == 'update' + args = beman_submodule.parse_args(["update", "--remote", "infra/"]) + assert args.command == "update" assert args.remote - assert args.beman_submodule_path == 'infra/' + assert args.beman_submodule_path == "infra/" + update_path_remote() + def plain_add(): - args = beman_submodule.parse_args(['add', 'git@github.com:bemanproject/infra.git']) - assert args.command == 'add' - assert args.repository == 'git@github.com:bemanproject/infra.git' + args = beman_submodule.parse_args( + ["add", "git@github.com:bemanproject/infra.git"] + ) + assert args.command == "add" + assert args.repository == "git@github.com:bemanproject/infra.git" assert not args.path + plain_add() + def add_path(): args = beman_submodule.parse_args( - ['add', 'git@github.com:bemanproject/infra.git', 'infra/']) - assert args.command == 'add' - assert args.repository == 'git@github.com:bemanproject/infra.git' - assert args.path == 'infra/' + ["add", "git@github.com:bemanproject/infra.git", "infra/"] + ) + assert args.command == "add" + assert args.repository == "git@github.com:bemanproject/infra.git" + assert args.path == "infra/" + add_path() + def plain_status(): - args = beman_submodule.parse_args(['status']) - assert args.command == 'status' + args = beman_submodule.parse_args(["status"]) + assert args.command == "status" assert args.paths == [] + plain_status() + def status_one_module(): - args = beman_submodule.parse_args(['status', 'infra/']) - assert args.command == 'status' - assert args.paths == ['infra/'] + args = beman_submodule.parse_args(["status", "infra/"]) + assert args.command == "status" + assert args.paths == ["infra/"] + status_one_module() + def status_multiple_modules(): - args = beman_submodule.parse_args(['status', 'infra/', 'foobar/']) - assert args.command == 'status' - assert args.paths == ['infra/', 'foobar/'] + args = beman_submodule.parse_args(["status", "infra/", "foobar/"]) + assert args.command == "status" + assert args.paths == ["infra/", "foobar/"] + status_multiple_modules() diff --git a/rrcp_helper.cpp b/rrcp_helper.cpp index 5fca4e2..1248eb5 100644 --- a/rrcp_helper.cpp +++ b/rrcp_helper.cpp @@ -143,7 +143,7 @@ auto rrcp::find_response_msg(std::string& response, const std::string& msg_id) - auto rrcp::create_command_msg(const std::string& message, std::string& msg_id_str, int msg_id) -> std::string { - if(msg_id >= INVALID_ID) + if (msg_id >= INVALID_ID) { msg_id = 1; } diff --git a/tests/RRCP-test.cpp b/tests/RRCP-test.cpp index 9ce7ff0..4adc431 100644 --- a/tests/RRCP-test.cpp +++ b/tests/RRCP-test.cpp @@ -16,8 +16,8 @@ ut::suite errors = [] -> void using namespace ut; using namespace std::literals; - "find_response_msg"_test = [] - -> void { + "find_response_msg"_test = [] -> void + { constexpr std::string_view EXPECTED{"gGoState"sv}; const std::string message{"123456 gGoState"}; std::string result{message}; @@ -29,8 +29,8 @@ ut::suite errors = [] -> void #endif }; - "doNotfind_error_response_msg"_test = [] - -> void { + "doNotfind_error_response_msg"_test = [] -> void + { constexpr std::string_view EXPECTED{"E:1"sv}; const std::string message{"E:1"}; std::string result{message}; @@ -42,8 +42,8 @@ ut::suite errors = [] -> void #endif }; - "find_error_response_msg"_test = [] - -> void { + "find_error_response_msg"_test = [] -> void + { constexpr std::string_view EXPECTED{"E:10"sv}; const std::string message{"E:10 123456"}; std::string result{message}; @@ -55,8 +55,8 @@ ut::suite errors = [] -> void #endif }; - "doNotfind_response_msg"_test = [] - -> void { + "doNotfind_response_msg"_test = [] -> void + { constexpr std::string_view EXPECTED{"d NoGo"sv}; const std::string message{EXPECTED}; std::string result{message}; @@ -70,8 +70,8 @@ ut::suite errors = [] -> void // ============================================================ - "insertAfterFirstWord"_test = [] - -> void { + "insertAfterFirstWord"_test = [] -> void + { constexpr std::string_view EXPECTED{"M:test 123456 GGoState"sv}; const std::string command{"M:test GGoState"}; auto result = rrcp::insertAfterFirstWord(command, "123456"); @@ -81,8 +81,8 @@ ut::suite errors = [] -> void #endif }; - "doNotInsertAnEmptyString"_test = [] - -> void { + "doNotInsertAnEmptyString"_test = [] -> void + { constexpr std::string_view EXPECTED{"M:test GGoState"sv}; const std::string command{EXPECTED}; auto result = rrcp::insertAfterFirstWord(command, ""); @@ -92,8 +92,8 @@ ut::suite errors = [] -> void #endif }; - "doNotInsertBeforeTrapCmd"_test = [] - -> void { + "doNotInsertBeforeTrapCmd"_test = [] -> void + { constexpr std::string_view EXPECTED{"M:test TGoState1"sv}; const std::string command{EXPECTED}; auto result = rrcp::insertAfterFirstWord(command, ""); @@ -103,8 +103,8 @@ ut::suite errors = [] -> void #endif }; - "doNotInsertAfterSingleWord"_test = [] - -> void { + "doNotInsertAfterSingleWord"_test = [] -> void + { constexpr std::string_view EXPECTED{"E:10"sv}; const std::string message{EXPECTED}; auto result = rrcp::insertAfterFirstWord(message, "123456"); @@ -116,8 +116,8 @@ ut::suite errors = [] -> void // ============================================================ - "create_command_msg"_test = [] - -> void { + "create_command_msg"_test = [] -> void + { constexpr std::string_view EXPECTED{"\nM:RxTx 1 SPowerLevel\"Off\"\r"sv}; const std::string command{R"(M:RxTx SPowerLevel"Off")"}; std::string msg_id_str; @@ -135,21 +135,21 @@ ut::suite errors = [] -> void // ============================================================ - "wrong_quoted"_test = [] - -> void { + "wrong_quoted"_test = [] -> void + { expect(throws( - [] - -> void { + [] -> void + { constexpr std::string_view WRONG_QUOTED{"\n\x1b\004\r"sv}; auto result = rrcp::esc2char(WRONG_QUOTED); })); }; - "empty_str"_test = [] - -> void { + "empty_str"_test = [] -> void + { expect(nothrow( - [] - -> void { + [] -> void + { auto result = rrcp::esc2char(""); expect(result.empty()); })); @@ -161,8 +161,8 @@ ut::suite errors = [] -> void "to_short_msg"_test = [] -> void { expect(throws([] -> void { auto result = rrcp::esc2char("\x1b\0"s); })); }; - "basic_quoteing"_test = [] - -> void { + "basic_quoteing"_test = [] -> void + { constexpr std::string_view BINARY{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; auto quoted = rrcp::char2esc(BINARY); @@ -185,8 +185,8 @@ ut::suite errors = [] -> void // ============================================================ - "rrcp_message"_test = [] - -> void { + "rrcp_message"_test = [] -> void + { rrcp_message msg; msg.body_length(MAX_MU_LENGTH); expect(msg.length() == MAX_MU_LENGTH + 4); @@ -205,8 +205,8 @@ ut::suite errors = [] -> void expect(msg.get_data().length() == 4); }; - "rrcp_message_empty"_test = [] - -> void { + "rrcp_message_empty"_test = [] -> void + { rrcp_message msg; msg.body_length(0); expect(msg.length() == 4); @@ -218,16 +218,16 @@ ut::suite errors = [] -> void // FIXME: expect(nothrow([&] {msg.decode_header();} )); }; - "rrcp_message_to_long"_test = [] - -> void { + "rrcp_message_to_long"_test = [] -> void + { const std::string invalid(MAX_MU_LENGTH, '\n'); rrcp_message msg(invalid); expect(!msg.is_valid()); expect(!msg.set_msg(invalid)); }; - "rrcp_message_text"_test = [] - -> void { + "rrcp_message_text"_test = [] -> void + { constexpr std::string_view COMMAND{"Hallo Server"}; rrcp_message msg; expect(msg.set_msg(COMMAND)); @@ -238,8 +238,8 @@ ut::suite errors = [] -> void expect(COMMAND == result); }; - "rrcp_message_binary"_test = [] - -> void { + "rrcp_message_binary"_test = [] -> void + { constexpr std::string_view BINARY{"\nAB_(\0\001\002\003\004\005\006\a\b\n\r\t\v\x1b\20\'\"\?)-CD\r"sv}; rrcp_message msg(BINARY); expect(msg.is_valid()); From e9f4ab9fe0d670ce67c86049c55904866b2f838f Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 28 Oct 2025 15:01:34 +0100 Subject: [PATCH 094/120] Update pre-commit config --- .clang-tidy | 1 - .codespellignore | 10 + .codespellrc | 2 +- .pre-commit-config.yaml | 6 +- examples/README.md | 1 - tests/base64.c | 427 ++++++++++++++++------------------------ tests/base64.h | 21 +- 7 files changed, 191 insertions(+), 277 deletions(-) create mode 100644 .codespellignore diff --git a/.clang-tidy b/.clang-tidy index b16301b..0392d1d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -54,4 +54,3 @@ CheckOptions: - { key: readability-identifier-length.MinimumParameterNameLength, value: 1 } - { key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals, value: true } ... - diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 0000000..67ad481 --- /dev/null +++ b/.codespellignore @@ -0,0 +1,10 @@ +QUE +WS +cancelled +cancelling +claus +copyable +deque +fo +pullrequest +statics diff --git a/.codespellrc b/.codespellrc index 01b53b5..0486ff6 100644 --- a/.codespellrc +++ b/.codespellrc @@ -3,4 +3,4 @@ builtin = clear,rare,en-GB_to_en-US,names,informal,code check-hidden = skip = ./.git,./.direnv,./build/*,./prefix/*,./coverage/*,./stagedir/*,*.html,*.xsd,*.xsl,*.pdf,*.log,.*.swp,*~,*.bak,./.cache/* quiet-level = 2 -ignore-words-list = claus,cancelled,cancelling,stoll,QUE,WS,fo,deque +ignore-words = .codespellignore diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6883fce..b83a594 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -14,7 +14,7 @@ repos: # This brings in a portable version of clang-format. # See also: https://github.com/ssciwr/clang-format-wheel - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.7 + rev: v21.1.2 hooks: - id: clang-format types_or: [c++, c, json] @@ -22,7 +22,7 @@ repos: # CMake linting and formatting - repo: https://github.com/BlankSpruce/gersemi - rev: 0.20.1 + rev: 0.22.3 hooks: - id: gersemi name: CMake linting diff --git a/examples/README.md b/examples/README.md index 043b5cb..65c3b31 100644 --- a/examples/README.md +++ b/examples/README.md @@ -22,4 +22,3 @@ hexdump -C base64.dat cat base64.txt IUAjJCVeJiooKV9+PD4K - diff --git a/tests/base64.c b/tests/base64.c index 89efbd6..ce6f17e 100644 --- a/tests/base64.c +++ b/tests/base64.c @@ -41,7 +41,7 @@ * */ -//XXX #include +// XXX #include /* Get prototype. */ #include "base64.h" @@ -53,52 +53,31 @@ #include /* C89 compliant way to cast 'char' to 'unsigned char'. */ -static inline unsigned char -to_uchar (char ch) -{ - return ch; -} +static inline unsigned char to_uchar(char ch) { return ch; } /* Base64 encode IN array of size INLEN into OUT array of size OUTLEN. If OUTLEN is less than BASE64_LENGTH(INLEN), write as many bytes as possible. If OUTLEN is larger than BASE64_LENGTH(INLEN), also zero terminate the output buffer. */ -void -base64_encode (const char *restrict in, size_t inlen, - char *restrict out, size_t outlen) +void base64_encode(const char* restrict in, size_t inlen, char* restrict out, size_t outlen) { - static const char b64str[64] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + static const char b64str[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; while (inlen && outlen) - { - *out++ = b64str[(to_uchar (in[0]) >> 2) & 0x3f]; - if (!--outlen) - break; - *out++ = b64str[((to_uchar (in[0]) << 4) - + (--inlen ? to_uchar (in[1]) >> 4 : 0)) - & 0x3f]; - if (!--outlen) - break; - *out++ = - (inlen - ? b64str[((to_uchar (in[1]) << 2) - + (--inlen ? to_uchar (in[2]) >> 6 : 0)) - & 0x3f] - : '='); - if (!--outlen) - break; - *out++ = inlen ? b64str[to_uchar (in[2]) & 0x3f] : '='; - if (!--outlen) - break; - if (inlen) - inlen--; - if (inlen) - in += 3; - } - - if (outlen) - *out = '\0'; + { + *out++ = b64str[(to_uchar(in[0]) >> 2) & 0x3f]; + if (!--outlen) break; + *out++ = b64str[((to_uchar(in[0]) << 4) + (--inlen ? to_uchar(in[1]) >> 4 : 0)) & 0x3f]; + if (!--outlen) break; + *out++ = (inlen ? b64str[((to_uchar(in[1]) << 2) + (--inlen ? to_uchar(in[2]) >> 6 : 0)) & 0x3f] : '='); + if (!--outlen) break; + *out++ = inlen ? b64str[to_uchar(in[2]) & 0x3f] : '='; + if (!--outlen) break; + if (inlen) inlen--; + if (inlen) in += 3; + } + + if (outlen) *out = '\0'; } /* Allocate a buffer and store zero terminated base64 encoded data @@ -110,10 +89,9 @@ base64_encode (const char *restrict in, size_t inlen, memory allocation failed, OUT is set to NULL, and the return value indicates length of the requested memory block, i.e., BASE64_LENGTH(inlen) + 1. */ -size_t -base64_encode_alloc (const char *in, size_t inlen, char **out) +size_t base64_encode_alloc(const char* in, size_t inlen, char** out) { - size_t outlen = 1 + BASE64_LENGTH (inlen); + size_t outlen = 1 + BASE64_LENGTH(inlen); /* Check for overflow in outlen computation. * @@ -128,16 +106,15 @@ base64_encode_alloc (const char *in, size_t inlen, char **out) * (inlen > 4). */ if (inlen > outlen) - { - *out = NULL; - return 0; - } + { + *out = NULL; + return 0; + } - *out = malloc (outlen); - if (!*out) - return outlen; + *out = malloc(outlen); + if (!*out) return outlen; - base64_encode (in, inlen, *out, outlen); + base64_encode(in, inlen, *out, outlen); return outlen - 1; } @@ -151,154 +128,105 @@ base64_encode_alloc (const char *in, size_t inlen, char **out) IBM C V6 for AIX mishandles "#define B64(x) ...'x'...", so use "_" as the formal parameter rather than "x". */ -#define B64(_) \ - ((_) == 'A' ? 0 \ - : (_) == 'B' ? 1 \ - : (_) == 'C' ? 2 \ - : (_) == 'D' ? 3 \ - : (_) == 'E' ? 4 \ - : (_) == 'F' ? 5 \ - : (_) == 'G' ? 6 \ - : (_) == 'H' ? 7 \ - : (_) == 'I' ? 8 \ - : (_) == 'J' ? 9 \ - : (_) == 'K' ? 10 \ - : (_) == 'L' ? 11 \ - : (_) == 'M' ? 12 \ - : (_) == 'N' ? 13 \ - : (_) == 'O' ? 14 \ - : (_) == 'P' ? 15 \ - : (_) == 'Q' ? 16 \ - : (_) == 'R' ? 17 \ - : (_) == 'S' ? 18 \ - : (_) == 'T' ? 19 \ - : (_) == 'U' ? 20 \ - : (_) == 'V' ? 21 \ - : (_) == 'W' ? 22 \ - : (_) == 'X' ? 23 \ - : (_) == 'Y' ? 24 \ - : (_) == 'Z' ? 25 \ - : (_) == 'a' ? 26 \ - : (_) == 'b' ? 27 \ - : (_) == 'c' ? 28 \ - : (_) == 'd' ? 29 \ - : (_) == 'e' ? 30 \ - : (_) == 'f' ? 31 \ - : (_) == 'g' ? 32 \ - : (_) == 'h' ? 33 \ - : (_) == 'i' ? 34 \ - : (_) == 'j' ? 35 \ - : (_) == 'k' ? 36 \ - : (_) == 'l' ? 37 \ - : (_) == 'm' ? 38 \ - : (_) == 'n' ? 39 \ - : (_) == 'o' ? 40 \ - : (_) == 'p' ? 41 \ - : (_) == 'q' ? 42 \ - : (_) == 'r' ? 43 \ - : (_) == 's' ? 44 \ - : (_) == 't' ? 45 \ - : (_) == 'u' ? 46 \ - : (_) == 'v' ? 47 \ - : (_) == 'w' ? 48 \ - : (_) == 'x' ? 49 \ - : (_) == 'y' ? 50 \ - : (_) == 'z' ? 51 \ - : (_) == '0' ? 52 \ - : (_) == '1' ? 53 \ - : (_) == '2' ? 54 \ - : (_) == '3' ? 55 \ - : (_) == '4' ? 56 \ - : (_) == '5' ? 57 \ - : (_) == '6' ? 58 \ - : (_) == '7' ? 59 \ - : (_) == '8' ? 60 \ - : (_) == '9' ? 61 \ - : (_) == '+' ? 62 \ - : (_) == '/' ? 63 \ - : -1) - -static const signed char b64[0x100] = { - B64 (0), B64 (1), B64 (2), B64 (3), - B64 (4), B64 (5), B64 (6), B64 (7), - B64 (8), B64 (9), B64 (10), B64 (11), - B64 (12), B64 (13), B64 (14), B64 (15), - B64 (16), B64 (17), B64 (18), B64 (19), - B64 (20), B64 (21), B64 (22), B64 (23), - B64 (24), B64 (25), B64 (26), B64 (27), - B64 (28), B64 (29), B64 (30), B64 (31), - B64 (32), B64 (33), B64 (34), B64 (35), - B64 (36), B64 (37), B64 (38), B64 (39), - B64 (40), B64 (41), B64 (42), B64 (43), - B64 (44), B64 (45), B64 (46), B64 (47), - B64 (48), B64 (49), B64 (50), B64 (51), - B64 (52), B64 (53), B64 (54), B64 (55), - B64 (56), B64 (57), B64 (58), B64 (59), - B64 (60), B64 (61), B64 (62), B64 (63), - B64 (64), B64 (65), B64 (66), B64 (67), - B64 (68), B64 (69), B64 (70), B64 (71), - B64 (72), B64 (73), B64 (74), B64 (75), - B64 (76), B64 (77), B64 (78), B64 (79), - B64 (80), B64 (81), B64 (82), B64 (83), - B64 (84), B64 (85), B64 (86), B64 (87), - B64 (88), B64 (89), B64 (90), B64 (91), - B64 (92), B64 (93), B64 (94), B64 (95), - B64 (96), B64 (97), B64 (98), B64 (99), - B64 (100), B64 (101), B64 (102), B64 (103), - B64 (104), B64 (105), B64 (106), B64 (107), - B64 (108), B64 (109), B64 (110), B64 (111), - B64 (112), B64 (113), B64 (114), B64 (115), - B64 (116), B64 (117), B64 (118), B64 (119), - B64 (120), B64 (121), B64 (122), B64 (123), - B64 (124), B64 (125), B64 (126), B64 (127), - B64 (128), B64 (129), B64 (130), B64 (131), - B64 (132), B64 (133), B64 (134), B64 (135), - B64 (136), B64 (137), B64 (138), B64 (139), - B64 (140), B64 (141), B64 (142), B64 (143), - B64 (144), B64 (145), B64 (146), B64 (147), - B64 (148), B64 (149), B64 (150), B64 (151), - B64 (152), B64 (153), B64 (154), B64 (155), - B64 (156), B64 (157), B64 (158), B64 (159), - B64 (160), B64 (161), B64 (162), B64 (163), - B64 (164), B64 (165), B64 (166), B64 (167), - B64 (168), B64 (169), B64 (170), B64 (171), - B64 (172), B64 (173), B64 (174), B64 (175), - B64 (176), B64 (177), B64 (178), B64 (179), - B64 (180), B64 (181), B64 (182), B64 (183), - B64 (184), B64 (185), B64 (186), B64 (187), - B64 (188), B64 (189), B64 (190), B64 (191), - B64 (192), B64 (193), B64 (194), B64 (195), - B64 (196), B64 (197), B64 (198), B64 (199), - B64 (200), B64 (201), B64 (202), B64 (203), - B64 (204), B64 (205), B64 (206), B64 (207), - B64 (208), B64 (209), B64 (210), B64 (211), - B64 (212), B64 (213), B64 (214), B64 (215), - B64 (216), B64 (217), B64 (218), B64 (219), - B64 (220), B64 (221), B64 (222), B64 (223), - B64 (224), B64 (225), B64 (226), B64 (227), - B64 (228), B64 (229), B64 (230), B64 (231), - B64 (232), B64 (233), B64 (234), B64 (235), - B64 (236), B64 (237), B64 (238), B64 (239), - B64 (240), B64 (241), B64 (242), B64 (243), - B64 (244), B64 (245), B64 (246), B64 (247), - B64 (248), B64 (249), B64 (250), B64 (251), - B64 (252), B64 (253), B64 (254), B64 (255) -}; +#define B64(_) \ + ((_) == 'A' ? 0 \ + : (_) == 'B' ? 1 \ + : (_) == 'C' ? 2 \ + : (_) == 'D' ? 3 \ + : (_) == 'E' ? 4 \ + : (_) == 'F' ? 5 \ + : (_) == 'G' ? 6 \ + : (_) == 'H' ? 7 \ + : (_) == 'I' ? 8 \ + : (_) == 'J' ? 9 \ + : (_) == 'K' ? 10 \ + : (_) == 'L' ? 11 \ + : (_) == 'M' ? 12 \ + : (_) == 'N' ? 13 \ + : (_) == 'O' ? 14 \ + : (_) == 'P' ? 15 \ + : (_) == 'Q' ? 16 \ + : (_) == 'R' ? 17 \ + : (_) == 'S' ? 18 \ + : (_) == 'T' ? 19 \ + : (_) == 'U' ? 20 \ + : (_) == 'V' ? 21 \ + : (_) == 'W' ? 22 \ + : (_) == 'X' ? 23 \ + : (_) == 'Y' ? 24 \ + : (_) == 'Z' ? 25 \ + : (_) == 'a' ? 26 \ + : (_) == 'b' ? 27 \ + : (_) == 'c' ? 28 \ + : (_) == 'd' ? 29 \ + : (_) == 'e' ? 30 \ + : (_) == 'f' ? 31 \ + : (_) == 'g' ? 32 \ + : (_) == 'h' ? 33 \ + : (_) == 'i' ? 34 \ + : (_) == 'j' ? 35 \ + : (_) == 'k' ? 36 \ + : (_) == 'l' ? 37 \ + : (_) == 'm' ? 38 \ + : (_) == 'n' ? 39 \ + : (_) == 'o' ? 40 \ + : (_) == 'p' ? 41 \ + : (_) == 'q' ? 42 \ + : (_) == 'r' ? 43 \ + : (_) == 's' ? 44 \ + : (_) == 't' ? 45 \ + : (_) == 'u' ? 46 \ + : (_) == 'v' ? 47 \ + : (_) == 'w' ? 48 \ + : (_) == 'x' ? 49 \ + : (_) == 'y' ? 50 \ + : (_) == 'z' ? 51 \ + : (_) == '0' ? 52 \ + : (_) == '1' ? 53 \ + : (_) == '2' ? 54 \ + : (_) == '3' ? 55 \ + : (_) == '4' ? 56 \ + : (_) == '5' ? 57 \ + : (_) == '6' ? 58 \ + : (_) == '7' ? 59 \ + : (_) == '8' ? 60 \ + : (_) == '9' ? 61 \ + : (_) == '+' ? 62 \ + : (_) == '/' ? 63 \ + : -1) + +static const signed char b64[0x100] = {B64(0), B64(1), B64(2), B64(3), B64(4), B64(5), B64(6), B64(7), B64(8), B64(9), + B64(10), B64(11), B64(12), B64(13), B64(14), B64(15), B64(16), B64(17), B64(18), B64(19), B64(20), B64(21), B64(22), + B64(23), B64(24), B64(25), B64(26), B64(27), B64(28), B64(29), B64(30), B64(31), B64(32), B64(33), B64(34), B64(35), + B64(36), B64(37), B64(38), B64(39), B64(40), B64(41), B64(42), B64(43), B64(44), B64(45), B64(46), B64(47), B64(48), + B64(49), B64(50), B64(51), B64(52), B64(53), B64(54), B64(55), B64(56), B64(57), B64(58), B64(59), B64(60), B64(61), + B64(62), B64(63), B64(64), B64(65), B64(66), B64(67), B64(68), B64(69), B64(70), B64(71), B64(72), B64(73), B64(74), + B64(75), B64(76), B64(77), B64(78), B64(79), B64(80), B64(81), B64(82), B64(83), B64(84), B64(85), B64(86), B64(87), + B64(88), B64(89), B64(90), B64(91), B64(92), B64(93), B64(94), B64(95), B64(96), B64(97), B64(98), B64(99), B64(100), + B64(101), B64(102), B64(103), B64(104), B64(105), B64(106), B64(107), B64(108), B64(109), B64(110), B64(111), B64(112), + B64(113), B64(114), B64(115), B64(116), B64(117), B64(118), B64(119), B64(120), B64(121), B64(122), B64(123), B64(124), + B64(125), B64(126), B64(127), B64(128), B64(129), B64(130), B64(131), B64(132), B64(133), B64(134), B64(135), B64(136), + B64(137), B64(138), B64(139), B64(140), B64(141), B64(142), B64(143), B64(144), B64(145), B64(146), B64(147), B64(148), + B64(149), B64(150), B64(151), B64(152), B64(153), B64(154), B64(155), B64(156), B64(157), B64(158), B64(159), B64(160), + B64(161), B64(162), B64(163), B64(164), B64(165), B64(166), B64(167), B64(168), B64(169), B64(170), B64(171), B64(172), + B64(173), B64(174), B64(175), B64(176), B64(177), B64(178), B64(179), B64(180), B64(181), B64(182), B64(183), B64(184), + B64(185), B64(186), B64(187), B64(188), B64(189), B64(190), B64(191), B64(192), B64(193), B64(194), B64(195), B64(196), + B64(197), B64(198), B64(199), B64(200), B64(201), B64(202), B64(203), B64(204), B64(205), B64(206), B64(207), B64(208), + B64(209), B64(210), B64(211), B64(212), B64(213), B64(214), B64(215), B64(216), B64(217), B64(218), B64(219), B64(220), + B64(221), B64(222), B64(223), B64(224), B64(225), B64(226), B64(227), B64(228), B64(229), B64(230), B64(231), B64(232), + B64(233), B64(234), B64(235), B64(236), B64(237), B64(238), B64(239), B64(240), B64(241), B64(242), B64(243), B64(244), + B64(245), B64(246), B64(247), B64(248), B64(249), B64(250), B64(251), B64(252), B64(253), B64(254), B64(255)}; #if UCHAR_MAX == 255 -# define uchar_in_range(c) true +#define uchar_in_range(c) true #else -# define uchar_in_range(c) ((c) <= 255) +#define uchar_in_range(c) ((c) <= 255) #endif /* Return true if CH is a character from the Base64 alphabet, and false otherwise. Note that '=' is padding and not considered to be part of the alphabet. */ -bool -isbase64 (char ch) -{ - return uchar_in_range (to_uchar (ch)) && 0 <= b64[to_uchar (ch)]; -} +bool isbase64(char ch) { return uchar_in_range(to_uchar(ch)) && 0 <= b64[to_uchar(ch)]; } /* Decode base64 encoded input array IN of length INLEN to output array OUT that can hold *OUTLEN bytes. Return true if decoding was @@ -309,78 +237,63 @@ isbase64 (char ch) encountered, decoding is stopped and false is returned. This means that, when applicable, you must remove any line terminators that is part of the data stream before calling this function. */ -bool -base64_decode (const char *restrict in, size_t inlen, - char *restrict out, size_t *outlen) +bool base64_decode(const char* restrict in, size_t inlen, char* restrict out, size_t* outlen) { size_t outleft = *outlen; while (inlen >= 2) + { + if (!isbase64(in[0]) || !isbase64(in[1])) break; + + if (outleft) { - if (!isbase64 (in[0]) || !isbase64 (in[1])) - break; + *out++ = ((b64[to_uchar(in[0])] << 2) | (b64[to_uchar(in[1])] >> 4)); + outleft--; + } - if (outleft) - { - *out++ = ((b64[to_uchar (in[0])] << 2) - | (b64[to_uchar (in[1])] >> 4)); - outleft--; - } + if (inlen == 2) break; - if (inlen == 2) - break; + if (in[2] == '=') + { + if (inlen != 4) break; - if (in[2] == '=') - { - if (inlen != 4) - break; + if (in[3] != '=') break; + } + else + { + if (!isbase64(in[2])) break; - if (in[3] != '=') - break; + if (outleft) + { + *out++ = (((b64[to_uchar(in[1])] << 4) & 0xf0) | (b64[to_uchar(in[2])] >> 2)); + outleft--; + } - } + if (inlen == 3) break; + + if (in[3] == '=') + { + if (inlen != 4) break; + } else + { + if (!isbase64(in[3])) break; + + if (outleft) { - if (!isbase64 (in[2])) - break; - - if (outleft) - { - *out++ = (((b64[to_uchar (in[1])] << 4) & 0xf0) - | (b64[to_uchar (in[2])] >> 2)); - outleft--; - } - - if (inlen == 3) - break; - - if (in[3] == '=') - { - if (inlen != 4) - break; - } - else - { - if (!isbase64 (in[3])) - break; - - if (outleft) - { - *out++ = (((b64[to_uchar (in[2])] << 6) & 0xc0) - | b64[to_uchar (in[3])]); - outleft--; - } - } + *out++ = (((b64[to_uchar(in[2])] << 6) & 0xc0) | b64[to_uchar(in[3])]); + outleft--; } - - in += 4; - inlen -= 4; + } } + in += 4; + inlen -= 4; + } + *outlen -= outleft; - if (inlen != 0) - return false; + if (inlen != 0) return false; return true; } @@ -396,9 +309,7 @@ base64_decode (const char *restrict in, size_t inlen, decoding and memory error.) The function returns false if the input was invalid, in which case *OUT is NULL and *OUTLEN is undefined. */ -bool -base64_decode_alloc (const char *in, size_t inlen, char **out, - size_t *outlen) +bool base64_decode_alloc(const char* in, size_t inlen, char** out, size_t* outlen) { /* This may allocate a few bytes too much, depending on input, but it's not worth the extra CPU time to compute the exact amount. @@ -407,19 +318,17 @@ base64_decode_alloc (const char *in, size_t inlen, char **out, Dividing before multiplying avoids the possibility of overflow. */ size_t needlen = 3 * (inlen / 4) + 2; - *out = malloc (needlen); - if (!*out) - return true; + *out = malloc(needlen); + if (!*out) return true; - if (!base64_decode (in, inlen, *out, &needlen)) - { - free (*out); - *out = NULL; - return false; - } + if (!base64_decode(in, inlen, *out, &needlen)) + { + free(*out); + *out = NULL; + return false; + } - if (outlen) - *outlen = needlen; + if (outlen) *outlen = needlen; return true; } diff --git a/tests/base64.h b/tests/base64.h index 0e82ece..1ec85aa 100644 --- a/tests/base64.h +++ b/tests/base64.h @@ -17,29 +17,26 @@ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef BASE64_H -# define BASE64_H +#define BASE64_H /* Get size_t. */ -# include +#include /* Get bool. */ -# include +#include /* This uses that the expression (n+(k-1))/k means the smallest integer >= n/k, i.e., the ceiling of n/k. */ -# define BASE64_LENGTH(inlen) ((((inlen) + 2) / 3) * 4) +#define BASE64_LENGTH(inlen) ((((inlen) + 2) / 3) * 4) -extern bool isbase64 (char ch); +extern bool isbase64(char ch); -extern void base64_encode (const char *in, size_t inlen, - char *out, size_t outlen); +extern void base64_encode(const char* in, size_t inlen, char* out, size_t outlen); -extern size_t base64_encode_alloc (const char *in, size_t inlen, char **out); +extern size_t base64_encode_alloc(const char* in, size_t inlen, char** out); -extern bool base64_decode (const char *in, size_t inlen, - char *out, size_t *outlen); +extern bool base64_decode(const char* in, size_t inlen, char* out, size_t* outlen); -extern bool base64_decode_alloc (const char *in, size_t inlen, - char **out, size_t *outlen); +extern bool base64_decode_alloc(const char* in, size_t inlen, char** out, size_t* outlen); #endif /* BASE64_H */ From f92413784964c44d847a62f872c2adbdd7df29df Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 28 Oct 2025 15:25:06 +0100 Subject: [PATCH 095/120] Install libboost-dev in docker too --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 11297a6..5af7eff 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -7,7 +7,7 @@ USER vscode # Install latest cmake RUN wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null RUN echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null -RUN sudo apt-get update && sudo apt-get install -y cmake +RUN sudo apt-get update && sudo apt-get install -y cmake libboost-dev # Install pre-commit RUN sudo apt-get install -y python3-pip && pip3 install pre-commit From 0bf2cc7102abdfe54e8433ba2f5460c622b305c4 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 28 Oct 2025 19:17:17 +0100 Subject: [PATCH 096/120] Use boost-cmake and CPM.cmake --- .devcontainer/Dockerfile | 2 +- CMakeLists.txt | 30 +++++++++++++++++++----------- GNUmakefile | 1 + async_rrcp_client.hpp | 8 +++++++- async_rrcp_client_threadsafe.hpp | 10 +++++----- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 5af7eff..93533e2 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -10,7 +10,7 @@ RUN echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https: RUN sudo apt-get update && sudo apt-get install -y cmake libboost-dev # Install pre-commit -RUN sudo apt-get install -y python3-pip && pip3 install pre-commit +RUN sudo apt-get install -y python3-pip && pip3 install pre-commit gcovr ninja cmake # Avoid ASAN Stalling # Alternative is to update to clang-18 and gcc-13.2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7204f48..0a1c30b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,20 +4,28 @@ project(RRCP-client VERSION 0.1.1 LANGUAGES CXX) # ---- add dependencies ---- -include(FetchContent) +include(cmake/CPM.cmake) find_package(Threads) +find_package(Boost 1.87 COMPONENTS headers signals2 HINTS $ENV{HOME}/.local) + if(NOT TARGET Boost::headers) - find_package( - Boost - 1.71 - COMPONENTS - headers # XXX signals2 - REQUIRED - HINTS $ENV{HOME}/.local + # + # install Boost headers only interfaces lib as Boost::boost + # + set(BOOST_INCLUDE_LIBRARIES algorithm asio beast signals2) + cpmaddpackage( + NAME boost-cmake + VERSION 1.87.0.5 + GIT_TAG v1.87.0-rc6 + GITHUB_REPOSITORY ClausKlein/boost-cmake + EXCLUDE_FROM_ALL YES + SYSTEM YES ) endif() +include(FetchContent) + FetchContent_Declare( fmt GIT_TAG 12.0.0 @@ -88,7 +96,7 @@ endfunction() add_executable(async_tcp_echo_server examples/async_tcp_echo_server.cpp) target_link_libraries( async_tcp_echo_server - PUBLIC Threads::Threads Boost::headers + PUBLIC Threads::Threads Boost::boost ) do_test(async_tcp_echo_server "" port) @@ -102,7 +110,7 @@ target_sources( ) target_link_libraries( rrcp_helper - PUBLIC Threads::Threads Boost::headers fmt::fmt-header-only + PUBLIC Threads::Threads Boost::boost fmt::fmt-header-only ) add_executable(async_tcp_echo_client examples/async_tcp_echo_client.cpp) @@ -122,7 +130,7 @@ if(APPLE AND BUILD_EXAMPLES) add_executable(timer timer.cpp) target_link_libraries( timer - PUBLIC Threads::Threads Boost::headers fmt::fmt-header-only + PUBLIC Threads::Threads Boost::boost fmt::fmt-header-only ) add_test(NAME timer COMMAND timer) endif() diff --git a/GNUmakefile b/GNUmakefile index 8c43f15..b944cd4 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -7,6 +7,7 @@ MAKEFLAGS+= --warn-undefined-variables export hostSystemName=$(shell uname) export GCOV="llvm-cov gcov" +export CPM_USE_LOCAL_PACKAGES=YES ifeq (${hostSystemName},Darwin) export LLVM_PREFIX:=$(shell brew --prefix llvm) diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index d424208..f18e09a 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -281,12 +281,18 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client tcp::socket socket_; boost::asio::steady_timer deadline_; boost::asio::steady_timer heartbeat_timer_; + + // I/O buffers (protected by io_context) std::string input_buffer_; message_queue read_msgs_; message_queue write_msgs_; - std::atomic< int > msg_id_{10000}; + + // Thread-safe state std::atomic< bool > connected_{false}; std::atomic< bool > stopped_{false}; + std::atomic< int > msg_id_{10000}; + + // Signal handling signal_string_type trap_handler_; }; diff --git a/async_rrcp_client_threadsafe.hpp b/async_rrcp_client_threadsafe.hpp index 3fb7b7f..22699f9 100644 --- a/async_rrcp_client_threadsafe.hpp +++ b/async_rrcp_client_threadsafe.hpp @@ -83,7 +83,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { if (!ec) { - fmt::print(stderr, "Connected to server.\n"); + fmt::print(stderr, "Connected to server.\n"); // TRACE self->connected_.store(true); self->notify_connection_waiters(); self->do_read(); @@ -133,7 +133,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client try { const auto response = future.get(); - fmt::print(stderr, "Returning {}\n", response); + fmt::print(stderr, "Returning {}\n", response); // TRACE return response; } catch (const std::exception&) @@ -147,7 +147,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client boost::asio::post(strand_, [this, self = shared_from_this()]() -> void { - fmt::print(stderr, "Stopped, disconnecting ...\n"); + fmt::print(stderr, "Stopped, disconnecting ...\n"); // TRACE stopped_.store(true); connected_.store(false); @@ -323,13 +323,13 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client if (boost::algorithm::starts_with(parsed_line, "d")) // Trap data message { // Handle trap data messages - fmt::print(stderr, "trap data: {}\n", parsed_line); + fmt::print(stderr, "trap data: {}\n", parsed_line); // TRACE self->trap_handler_(parsed_line); } else if (!boost::algorithm::starts_with(parsed_line, "gPing")) { // Handle response messages (but ignore heartbeat responses) - fmt::print(stderr, "{}\n", parsed_line); + fmt::print(stderr, "{}\n", parsed_line); // TRACE self->handle_response(parsed_line); } // Note: gPing messages are silently ignored (heartbeat responses) From 1562381e077d51d7e2162fd02232986273b78b63 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 28 Oct 2025 21:18:40 +0100 Subject: [PATCH 097/120] Disable not threadsave tests for now --- GNUmakefile | 16 +++++++-------- async_rrcp_client.hpp | 8 ++++++++ async_rrcp_client_threadsafe.hpp | 5 +++++ cmake/CPM.cmake | 34 ++++++++++++++++++++++++++++++++ rrcp_async_tcp_client.cpp | 5 +++++ 5 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 cmake/CPM.cmake diff --git a/GNUmakefile b/GNUmakefile index b944cd4..534a939 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -6,7 +6,7 @@ MAKEFLAGS+= --no-builtin-rules MAKEFLAGS+= --warn-undefined-variables export hostSystemName=$(shell uname) -export GCOV="llvm-cov gcov" +export GCOV=llvm-cov gcov export CPM_USE_LOCAL_PACKAGES=YES ifeq (${hostSystemName},Darwin) @@ -81,18 +81,18 @@ readability-use-std-min-max,\ test: $(BUILD_DIR) # XXX all -killall async_tcp_echo_server - -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 + # -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 $(BUILD_DIR)/async_tcp_echo_server 8000 & - -(cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000) & + # -(cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000) & sleep 1 -killall async_tcp_echo_server -(echo | $(BUILD_DIR)/async_tcp_echo_server 8000) & - cat rrcp.txt | $(BUILD_DIR)/rrcp_client localhost 8000 + # cat rrcp.txt | $(BUILD_DIR)/rrcp_client localhost 8000 cat rrcp.txt | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000 - cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 - -$(BUILD_DIR)/async_tcp_echo_client localhost - -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8001 - cat rrcp.txt | $(BUILD_DIR)/blocking_tcp_echo_client localhost 8000 + # cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 + # -$(BUILD_DIR)/async_tcp_echo_client localhost + # -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8001 + # cat rrcp.txt | $(BUILD_DIR)/blocking_tcp_echo_client localhost 8000 ctest --test-dir $(BUILD_DIR) -killall async_tcp_echo_server gcovr diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index f18e09a..307b260 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -98,6 +98,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { return {}; } + fmt::print(stderr, "Client is not connected yet.\n"); // TRACE std::this_thread::sleep_for(TIMEOUT_DURATION); } @@ -111,6 +112,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { bool const write_in_progress{!write_msgs_.empty()}; write_msgs_.push_back(command); + if (!write_in_progress) { deadline_.expires_after(TIMEOUT_DURATION); @@ -154,6 +156,11 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client void stop() { + if (stopped_) + { + return; + } + boost::asio::post(io_context_, [this, self = shared_from_this()]() -> void { @@ -183,6 +190,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client // TODO(CK): maby refactored to helper class? //========================== RRCP ============================ + // Process different message types if (boost::algorithm::starts_with(line, "d")) // Trap data message { // Handle trap data messages diff --git a/async_rrcp_client_threadsafe.hpp b/async_rrcp_client_threadsafe.hpp index 22699f9..a22057d 100644 --- a/async_rrcp_client_threadsafe.hpp +++ b/async_rrcp_client_threadsafe.hpp @@ -144,6 +144,11 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client void stop() { + if (stopped_.load()) + { + return; + } + boost::asio::post(strand_, [this, self = shared_from_this()]() -> void { diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..bc61a2e --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.42.0) +set(CPM_HASH_SUM + "2020b4fc42dba44817983e06342e682ecfc3d2f484a581f11cc5731fbe4dce8a" +) + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION + "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake" + ) +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION + "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake" + ) +else() + set(CPM_DOWNLOAD_LOCATION + "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake" + ) +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file( + DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} + EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index fbc9b90..b603f02 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -21,7 +21,12 @@ #include #include +#define USE_SIMPLE_RRCP_CLINT +#ifdef USE_SIMPLE_RRCP_CLINT #include "async_rrcp_client.hpp" +#else +#include "async_rrcp_client_threadsafe.hpp" +#endif namespace { From 068d851ec503c6f16a8bf130b3686c2fbf61da60 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 28 Oct 2025 21:42:08 +0100 Subject: [PATCH 098/120] Use Boost::headers only --- CMakeLists.txt | 14 +++++++------- examples/CMakeLists.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a1c30b..ee95d25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,10 +4,10 @@ project(RRCP-client VERSION 0.1.1 LANGUAGES CXX) # ---- add dependencies ---- -include(cmake/CPM.cmake) - find_package(Threads) -find_package(Boost 1.87 COMPONENTS headers signals2 HINTS $ENV{HOME}/.local) +#XXX find_package(Boost 1.87 COMPONENTS headers signals2 HINTS $ENV{HOME}/.local) + +include(cmake/CPM.cmake) if(NOT TARGET Boost::headers) # @@ -16,7 +16,7 @@ if(NOT TARGET Boost::headers) set(BOOST_INCLUDE_LIBRARIES algorithm asio beast signals2) cpmaddpackage( NAME boost-cmake - VERSION 1.87.0.5 + VERSION 1.87.0.6 GIT_TAG v1.87.0-rc6 GITHUB_REPOSITORY ClausKlein/boost-cmake EXCLUDE_FROM_ALL YES @@ -96,7 +96,7 @@ endfunction() add_executable(async_tcp_echo_server examples/async_tcp_echo_server.cpp) target_link_libraries( async_tcp_echo_server - PUBLIC Threads::Threads Boost::boost + PUBLIC Threads::Threads Boost::headers ) do_test(async_tcp_echo_server "" port) @@ -110,7 +110,7 @@ target_sources( ) target_link_libraries( rrcp_helper - PUBLIC Threads::Threads Boost::boost fmt::fmt-header-only + PUBLIC Threads::Threads Boost::headers fmt::fmt-header-only ) add_executable(async_tcp_echo_client examples/async_tcp_echo_client.cpp) @@ -130,7 +130,7 @@ if(APPLE AND BUILD_EXAMPLES) add_executable(timer timer.cpp) target_link_libraries( timer - PUBLIC Threads::Threads Boost::boost fmt::fmt-header-only + PUBLIC Threads::Threads Boost::headers fmt::fmt-header-only ) add_test(NAME timer COMMAND timer) endif() diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 54bc421..fa2f858 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -6,7 +6,7 @@ find_package(Poco 1.14 COMPONENTS Foundation REQUIRED) find_package(Threads) if(NOT TARGET Boost::headers) - find_package(Boost 1.71 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) + find_package(Boost 1.87 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) endif() if(NOT TARGET fmt::fmt-header-only) find_package(fmt 12 REQUIRED HINTS $ENV{HOME}/.local) From 3cdfa37e4f5f242a9b520bfaa8e435ad74482e41 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 28 Oct 2025 22:05:18 +0100 Subject: [PATCH 099/120] Ignore CODEOWNERS --- .github/CODEOWNERS | 4 ---- .gitignore | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 49fa500..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -# Codeowners for reviews on PRs - -* @dietmarkuehl @camio @neatudarius diff --git a/.gitignore b/.gitignore index 5e8d57b..f234df3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ build/ coverage/* +CODEOWNERS tags .*swp *.log From 1cbeeef7dd781377987c6ae4c1f233c7e90eeb2d Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 29 Oct 2025 08:50:05 +0100 Subject: [PATCH 100/120] Setup env in CI workflow file This should fix the cmake error while fetch an archive file: "Pathname cannot be converted from UTF-16LE to current locale" --- .github/workflows/ci_tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 6392abc..612a46d 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -11,6 +11,10 @@ on: schedule: - cron: '30 15 * * *' +env: + LANG: en_US.UTF-8 + LC_ALL: en_US.UTF-8 + jobs: beman-submodule-check: uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.1.0 From 27c8a80b06a70ffd22bdb52e6e6d06d6a863c795 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 29 Oct 2025 10:44:37 +0100 Subject: [PATCH 101/120] Add more Boost find hacks --- CMakeLists.txt | 69 ++++++++++++++++++++++++----------------- examples/CMakeLists.txt | 13 +++++--- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ee95d25..e6d3997 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,23 +5,33 @@ project(RRCP-client VERSION 0.1.1 LANGUAGES CXX) # ---- add dependencies ---- find_package(Threads) -#XXX find_package(Boost 1.87 COMPONENTS headers signals2 HINTS $ENV{HOME}/.local) - -include(cmake/CPM.cmake) - -if(NOT TARGET Boost::headers) - # - # install Boost headers only interfaces lib as Boost::boost - # - set(BOOST_INCLUDE_LIBRARIES algorithm asio beast signals2) - cpmaddpackage( - NAME boost-cmake - VERSION 1.87.0.6 - GIT_TAG v1.87.0-rc6 - GITHUB_REPOSITORY ClausKlein/boost-cmake - EXCLUDE_FROM_ALL YES - SYSTEM YES - ) + +set(BOOST_INCLUDE_LIBRARIES algorithm asio beast signals2) + +# see too: https://cmake.org/cmake/help/latest/module/FindBoost.html +set(Boost_DEBUG ON) +find_package(Boost 1.87) # XXX COMPONENTS ${BOOST_INCLUDE_LIBRARIES} HINTS $ENV{HOME}/.local) +if(Boost_FOUND) + set(BOOST_LIBRARIES Boost::headers) +else() + include(cmake/CPM.cmake) + + set(BOOST_LIBRARIES ${BOOST_INCLUDE_LIBRARIES}) + list(TRANSFORM BOOST_LIBRARIES PREPEND Boost::) + + if(NOT TARGET Boost::headers) + # + # build only the requested Boost components + # + cpmaddpackage( + NAME boost-cmake + VERSION 1.87.0.6 + GIT_TAG v1.87.0-rc6 + GITHUB_REPOSITORY ClausKlein/boost-cmake + EXCLUDE_FROM_ALL NO + SYSTEM YES + ) + endif() endif() include(FetchContent) @@ -70,7 +80,7 @@ option( "Compile with test-coverage flags" ${PROJECT_IS_TOP_LEVEL} ) -if(ENABLE_TEST_COVERAGE AND CMAKE_BUILD_TYPE STREQUAL Debug) +if(UNIX AND ENABLE_TEST_COVERAGE AND CMAKE_BUILD_TYPE STREQUAL Debug) message(WARNING "ENABLE_TEST_COVERAGE is set!") add_compile_options(-O0 -g -fprofile-arcs -ftest-coverage) add_link_options(-fprofile-arcs -ftest-coverage) @@ -96,7 +106,7 @@ endfunction() add_executable(async_tcp_echo_server examples/async_tcp_echo_server.cpp) target_link_libraries( async_tcp_echo_server - PUBLIC Threads::Threads Boost::headers + PUBLIC Threads::Threads ${BOOST_LIBRARIES} ) do_test(async_tcp_echo_server "" port) @@ -110,27 +120,30 @@ target_sources( ) target_link_libraries( rrcp_helper - PUBLIC Threads::Threads Boost::headers fmt::fmt-header-only + PUBLIC Threads::Threads ${BOOST_LIBRARIES} fmt::fmt-header-only ) add_executable(async_tcp_echo_client examples/async_tcp_echo_client.cpp) target_link_libraries(async_tcp_echo_client PRIVATE rrcp_helper) do_test(async_tcp_echo_client --help Usage) -add_executable(blocking_tcp_echo_client examples/blocking_tcp_echo_client.cpp) -target_link_libraries(blocking_tcp_echo_client PRIVATE rrcp_helper) -do_test(blocking_tcp_echo_client --help Usage) +if(BUILD_EXAMPLES AND NOT ENABLE_TEST_COVERAGE) + add_executable( + blocking_tcp_echo_client + examples/blocking_tcp_echo_client.cpp + ) + target_link_libraries(blocking_tcp_echo_client PRIVATE rrcp_helper) + do_test(blocking_tcp_echo_client --help Usage) -add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) -target_link_libraries(rrcp_client PRIVATE rrcp_helper) -do_test(rrcp_client --help Usage) + add_executable(rrcp_client rrcp_client.cpp rrcp_message.hpp) + target_link_libraries(rrcp_client PRIVATE rrcp_helper) + do_test(rrcp_client --help Usage) -if(APPLE AND BUILD_EXAMPLES) # TODO(CK): mv to examples too! add_executable(timer timer.cpp) target_link_libraries( timer - PUBLIC Threads::Threads Boost::headers fmt::fmt-header-only + PUBLIC Threads::Threads ${BOOST_LIBRARIES} fmt::fmt-header-only ) add_test(NAME timer COMMAND timer) endif() diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index fa2f858..8ee6a28 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,12 +2,14 @@ cmake_minimum_required(VERSION 3.25...4.2) project(Base64-examples VERSION 0.1.1 LANGUAGES CXX) -find_package(Poco 1.14 COMPONENTS Foundation REQUIRED) find_package(Threads) +find_package(Poco 1.14 COMPONENTS Foundation REQUIRED) + if(NOT TARGET Boost::headers) - find_package(Boost 1.87 COMPONENTS headers REQUIRED HINTS $ENV{HOME}/.local) + find_package(Boost) endif() + if(NOT TARGET fmt::fmt-header-only) find_package(fmt 12 REQUIRED HINTS $ENV{HOME}/.local) endif() @@ -51,6 +53,9 @@ if(APPLE AND NOT ENABLE_TEST_COVERAGE) do_test(async_tcp_client --help Usage) add_executable(blocking_tcp_echo_server blocking_tcp_echo_server.cpp) - target_link_libraries(blocking_tcp_echo_server PUBLIC Boost::headers) - # FIXME: do_test(blocking_tcp_echo_server port Usage) + target_link_libraries( + blocking_tcp_echo_server + PUBLIC Threads::Threads Boost::headers + ) + # TODO(CK): do_test(blocking_tcp_echo_server port Usage) endif() From 9ffa186d645fd345fba90c7af3308db992884b15 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Wed, 29 Oct 2025 18:32:22 +0100 Subject: [PATCH 102/120] Use threadsave version --- CMakeLists.txt | 3 --- GNUmakefile | 18 ++++++++++-------- rrcp_async_tcp_client.cpp | 4 ++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e6d3997..813cb06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,15 +57,12 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") COMMAND_ECHO STDOUT ) string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) - set(CMAKE_CXX_STANDARD 23) elseif(LINUX) set(LLVM_PREFIX $ENV{LLVM_ROOT}) endif() add_compile_options(-fexperimental-library) add_link_options(-L${LLVM_PREFIX}/lib/c++ -lc++experimental) -else() - set(CMAKE_CXX_STANDARD 17) endif() set(CMAKE_CXX_EXTENSIONS OFF) diff --git a/GNUmakefile b/GNUmakefile index 534a939..4a45bc4 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -19,21 +19,21 @@ ifeq (${hostSystemName},Darwin) #XXX export CXX:=g++-15 #XXX export CXXFLAGS:=-stdlib=libstdc++ else ifeq (${hostSystemName},Linux) - export LLVM_DIR?=/usr/lib/llvm-20 + export LLVM_DIR?=/usr/lib/llvm-19 export PATH:=${LLVM_DIR}/bin:${PATH} - export CXX:=clang++-20 + export CXX:=clang++-19 endif - CPPFILES:= $(shell git ls-files ::*.cpp | grep -vw tests) -CMAKE_BUILD_TYPE?=Debug -BUILD_DIR:=build/$(CMAKE_BUILD_TYPE) +PRESET_NAME?=debug +BUILD_DIR:=build/$(PRESET_NAME) .PHONY: all format test check distclean all: $(BUILD_DIR) - ninja -C $(BUILD_DIR) + cmake --workflow --preset $(PRESET_NAME) + # XXX ninja -C $(BUILD_DIR) clean: $(BUILD_DIR) -ninja -C $< $@ @@ -43,8 +43,10 @@ distclean: # XXX clean rm -rf $(BUILD_DIR) build coverage/* *~ ctags $(BUILD_DIR): CMakeLists.txt - -test -d build/appleclang-debug && ln -f -s $(CURDIR)/build/appleclang-debug $(CURDIR)/$(BUILD_DIR) - cmake -S . -B $@ -G Ninja -D CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) --log-level=VERBOSE # --fresh + -test -f CMakeUserPresets.json || ln -f -s cmake/CMakeUserPresets.json . + cmake --preset $(PRESET_NAME) --log-level=VERBOSE # --fresh + # -test -d build/Debug && ln -f -s $(CURDIR)/build/Debug $(CURDIR)/$(BUILD_DIR) + # XXX cmake -S . -B $@ -G Ninja -D CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) check: all run-clang-tidy -p $(BUILD_DIR) $(CPPFILES) diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index b603f02..061001c 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -21,8 +21,8 @@ #include #include -#define USE_SIMPLE_RRCP_CLINT -#ifdef USE_SIMPLE_RRCP_CLINT +// #define USE_SIMPLE_RRCP_CLIENT +#ifdef USE_SIMPLE_RRCP_CLIENT #include "async_rrcp_client.hpp" #else #include "async_rrcp_client_threadsafe.hpp" From 228c7dcdb418ce8b8bdb0238525cffb22dafe230 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Wed, 29 Oct 2025 20:56:16 +0100 Subject: [PATCH 103/120] Add python test script --- .codespellignore | 2 ++ .codespellrc | 2 +- .gitignore | 7 +++--- CMakeLists.txt | 15 ++++++++++++ GNUmakefile | 10 ++------ LICENSE_1_0.txt | 23 ++++++++++++++++++ run_test.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 LICENSE_1_0.txt create mode 100755 run_test.py diff --git a/.codespellignore b/.codespellignore index 67ad481..918696e 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,4 +1,6 @@ QUE +UInt +WRONLY WS cancelled cancelling diff --git a/.codespellrc b/.codespellrc index 0486ff6..537dabf 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,6 +1,6 @@ [codespell] builtin = clear,rare,en-GB_to_en-US,names,informal,code check-hidden = -skip = ./.git,./.direnv,./build/*,./prefix/*,./coverage/*,./stagedir/*,*.html,*.xsd,*.xsl,*.pdf,*.log,.*.swp,*~,*.bak,./.cache/* +skip = ./.git,./.direnv,./build/*,./prefix/*,./coverage/*,./stagedir/*,*.html,*.xsd,*.xsl,*.pdf,*.log,.*.swp,*~,*.bak,./tags quiet-level = 2 ignore-words = .codespellignore diff --git a/.gitignore b/.gitignore index f234df3..78955a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ +*.log +.*swp +/CMakeUserPresets.json +CODEOWNERS build/ coverage/* -CODEOWNERS tags -.*swp -*.log diff --git a/CMakeLists.txt b/CMakeLists.txt index 813cb06..57da5b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,13 @@ do_test(async_tcp_echo_server "" port) # ---- rrcp class sources and helpers as a library ---- +find_program( + PYTHON_EXECUTABLE + NAMES python3 python + REQUIRED + HINTS $ENV{VIRTUAL_ENV}/bin +) + add_library(rrcp_helper STATIC) target_sources( rrcp_helper @@ -123,6 +130,14 @@ target_link_libraries( add_executable(async_tcp_echo_client examples/async_tcp_echo_client.cpp) target_link_libraries(async_tcp_echo_client PRIVATE rrcp_helper) do_test(async_tcp_echo_client --help Usage) +add_test( + NAME async_tcp_echo_client_test + COMMAND + ${PYTHON_EXECUTABLE} # + ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # + --client $ --server + $ +) if(BUILD_EXAMPLES AND NOT ENABLE_TEST_COVERAGE) add_executable( diff --git a/GNUmakefile b/GNUmakefile index 4a45bc4..6f11f9e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -83,20 +83,14 @@ readability-use-std-min-max,\ test: $(BUILD_DIR) # XXX all -killall async_tcp_echo_server - # -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 $(BUILD_DIR)/async_tcp_echo_server 8000 & - # -(cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000) & - sleep 1 - -killall async_tcp_echo_server - -(echo | $(BUILD_DIR)/async_tcp_echo_server 8000) & - # cat rrcp.txt | $(BUILD_DIR)/rrcp_client localhost 8000 + -(echo | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000) & cat rrcp.txt | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000 # cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 # -$(BUILD_DIR)/async_tcp_echo_client localhost # -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8001 - # cat rrcp.txt | $(BUILD_DIR)/blocking_tcp_echo_client localhost 8000 - ctest --test-dir $(BUILD_DIR) -killall async_tcp_echo_server + ctest --test-dir $(BUILD_DIR) --rerun-failed --output-on-failure gcovr format: .clang-format diff --git a/LICENSE_1_0.txt b/LICENSE_1_0.txt new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/LICENSE_1_0.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/run_test.py b/run_test.py new file mode 100755 index 0000000..0e52486 --- /dev/null +++ b/run_test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import argparse +import os +import signal +import subprocess +import sys +import tempfile +import time +from pathlib import Path +from typing import List + +HERE = Path(__file__).resolve().parent +PROJECT_DIR = HERE.parent.parent.parent + + +def main(args: List[str]): + parser = argparse.ArgumentParser() + parser.add_argument( + "--client", + help="The client to test", + ) + parser.add_argument( + "--server", + help="The server to run", + ) + parser.add_argument( + "--timeout", + "-t", + type=int, + default=3, + help="Number of seconds to run; defaults to %(default)s", + ) + args = parser.parse_args(args) + + client = subprocess.Popen([args.client, "localhost", "8000"]) + + try: + subprocess.run( + [args.server, "8000"], + timeout=args.timeout, + check=True, + ) + except subprocess.TimeoutExpired: + print("Test expectedly timed out; sending SIGINT") + client.send_signal(signal.SIGINT) + time.sleep(1) + try: + client.wait(timeout=1) + except subprocess.TimeoutExpired: + print("client does not respond to SIGINT in time, sending SIGKILL") + client.kill() + client.wait() + except Exception as ex: + print("ERROR:", ex) + client.kill() + client.wait() + raise + + +if __name__ == "__main__": + main(sys.argv[1:]) From 0fd075c6c25edd78ec33cb25a263c33247aeca45 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 30 Oct 2025 10:57:08 +0100 Subject: [PATCH 104/120] Test with corrupted respoce messages too --- CMakeLists.txt | 59 ++++++++++++++++++++++-------- GNUmakefile | 6 +-- async_rrcp_client_threadsafe.hpp | 6 +++ examples/async_tcp_echo_server.cpp | 25 ++++++++++++- rrcp.txt | 6 +-- 5 files changed, 80 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 57da5b1..63d42df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,7 +81,7 @@ if(UNIX AND ENABLE_TEST_COVERAGE AND CMAKE_BUILD_TYPE STREQUAL Debug) message(WARNING "ENABLE_TEST_COVERAGE is set!") add_compile_options(-O0 -g -fprofile-arcs -ftest-coverage) add_link_options(-fprofile-arcs -ftest-coverage) - # FIXME: add_compile_definitions(TARGET_CODE_COVERAGE) + # FIXME! add_compile_definitions(TARGET_CODE_COVERAGE) endif() # ---- ctest ---- @@ -98,7 +98,7 @@ function(do_test target arg result) endif() endfunction() -# ---- server needed for tests ---- +# ---- echo server needed for tests ---- add_executable(async_tcp_echo_server examples/async_tcp_echo_server.cpp) target_link_libraries( @@ -120,26 +120,34 @@ add_library(rrcp_helper STATIC) target_sources( rrcp_helper PRIVATE rrcp_helper.cpp - PUBLIC FILE_SET HEADERS FILES async_rrcp_client.hpp rrcp_helper.hpp + PUBLIC + FILE_SET + HEADERS # + FILES + async_rrcp_client.hpp + async_rrcp_client_threadsafe.hpp + rrcp_helper.hpp ) target_link_libraries( rrcp_helper PUBLIC Threads::Threads ${BOOST_LIBRARIES} fmt::fmt-header-only ) -add_executable(async_tcp_echo_client examples/async_tcp_echo_client.cpp) -target_link_libraries(async_tcp_echo_client PRIVATE rrcp_helper) -do_test(async_tcp_echo_client --help Usage) -add_test( - NAME async_tcp_echo_client_test - COMMAND - ${PYTHON_EXECUTABLE} # - ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # - --client $ --server - $ -) +# ---- simple rrcp client class usage examples ---- if(BUILD_EXAMPLES AND NOT ENABLE_TEST_COVERAGE) + add_executable(async_tcp_echo_client examples/async_tcp_echo_client.cpp) + target_link_libraries(async_tcp_echo_client PRIVATE rrcp_helper) + do_test(async_tcp_echo_client --help Usage) + add_test( + NAME async_tcp_echo_client_test + COMMAND + ${PYTHON_EXECUTABLE} # + ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # + --client $ # + --server $ + ) + add_executable( blocking_tcp_echo_client examples/blocking_tcp_echo_client.cpp @@ -160,11 +168,32 @@ if(BUILD_EXAMPLES AND NOT ENABLE_TEST_COVERAGE) add_test(NAME timer COMMAND timer) endif() -# ---- rrcp client class usage examples main ---- +# ---- theadsafe rrcp client class usage examples main ---- + +add_executable(rrcp_async_tcp_client_threadsafe rrcp_async_tcp_client.cpp) +target_link_libraries(rrcp_async_tcp_client_threadsafe PRIVATE rrcp_helper) +do_test(rrcp_async_tcp_client_threadsafe --help Usage) +add_test( + NAME async_tcp_echo_client_threadsafe_test + COMMAND + ${PYTHON_EXECUTABLE} # + ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # + --client $ # + --server $ +) add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) target_link_libraries(rrcp_async_tcp_client PRIVATE rrcp_helper) +target_compile_definitions(rrcp_async_tcp_client PRIVATE USE_SIMPLE_RRCP_CLIENT) do_test(rrcp_async_tcp_client --help Usage) +add_test( + NAME async_tcp_echo_client_test + COMMAND + ${PYTHON_EXECUTABLE} # + ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # + --client $ # + --server $ +) if(BUILD_TESTING) add_subdirectory(tests) diff --git a/GNUmakefile b/GNUmakefile index 6f11f9e..ad0c2a3 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -33,7 +33,6 @@ BUILD_DIR:=build/$(PRESET_NAME) all: $(BUILD_DIR) cmake --workflow --preset $(PRESET_NAME) - # XXX ninja -C $(BUILD_DIR) clean: $(BUILD_DIR) -ninja -C $< $@ @@ -46,7 +45,6 @@ $(BUILD_DIR): CMakeLists.txt -test -f CMakeUserPresets.json || ln -f -s cmake/CMakeUserPresets.json . cmake --preset $(PRESET_NAME) --log-level=VERBOSE # --fresh # -test -d build/Debug && ln -f -s $(CURDIR)/build/Debug $(CURDIR)/$(BUILD_DIR) - # XXX cmake -S . -B $@ -G Ninja -D CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) check: all run-clang-tidy -p $(BUILD_DIR) $(CPPFILES) @@ -81,11 +79,13 @@ readability-use-std-min-max,\ ' \ $(CPPFILES) -test: $(BUILD_DIR) # XXX all +test: all -killall async_tcp_echo_server $(BUILD_DIR)/async_tcp_echo_server 8000 & -(echo | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000) & + cat rrcp.txt | $(BUILD_DIR)/rrcp_async_tcp_client_threadsafe localhost 8000 cat rrcp.txt | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000 + # NOTE: simple example only! # cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 # -$(BUILD_DIR)/async_tcp_echo_client localhost # -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8001 diff --git a/async_rrcp_client_threadsafe.hpp b/async_rrcp_client_threadsafe.hpp index a22057d..4dbe5a7 100644 --- a/async_rrcp_client_threadsafe.hpp +++ b/async_rrcp_client_threadsafe.hpp @@ -127,6 +127,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client // Wait for response with timeout if (future.wait_for(TIMEOUT_DURATION) == std::future_status::timeout) { + fmt::print(stderr, "Error: Timeout {}!\n", __func__); return {}; // Timeout - return empty string } @@ -138,6 +139,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } catch (const std::exception&) { + fmt::print(stderr, "Exception: {}!\n", __func__); return {}; // Error - return empty string } } @@ -249,6 +251,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client catch (const std::exception&) { // Promise already fulfilled - ignore + fmt::print(stderr, "Exception: {}!\n", __func__); } } } @@ -268,6 +271,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client catch (const std::exception&) { // Promise already fulfilled - ignore + fmt::print(stderr, "Exception: {}!\n", __func__); } } pending_responses_.clear(); @@ -282,6 +286,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client catch (const std::exception&) { // Promise already fulfilled - ignore + fmt::print(stderr, "Exception: {}!\n", __func__); } } pending_writes_.clear(); @@ -371,6 +376,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client catch (const std::exception&) { // Promise already fulfilled - ignore + fmt::print(stderr, "Exception: {}!\n", __func__); } return; } diff --git a/examples/async_tcp_echo_server.cpp b/examples/async_tcp_echo_server.cpp index d7cfbd1..cfd2a3e 100644 --- a/examples/async_tcp_echo_server.cpp +++ b/examples/async_tcp_echo_server.cpp @@ -9,6 +9,7 @@ // // Moderniced from Claus Klein and ChatGPT +#include #include #include #include @@ -17,6 +18,8 @@ #include #include #include +#include +#include #include #ifdef TARGET_CODE_COVERAGE @@ -46,7 +49,16 @@ class session : public std::enable_shared_from_this< session > { if (!ec) { - do_write(length); + if ((std::string_view(data_.data(), length).contains("M:Utility")) || + (std::string_view(data_.data(), length).contains("M:A")) || + (std::string_view(data_.data(), length).contains("M:C"))) + { + do_write(length); + } + else + { + do_write(gen_random(length)); + } } else { @@ -72,6 +84,17 @@ class session : public std::enable_shared_from_this< session > }); } + static size_t gen_random(size_t input) + { + static std::random_device rd; // a seed source for the random number engine + static std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd() + static std::uniform_int_distribution<> distrib(13, MAX_LENGTH); + + // Use distrib to transform the random unsigned int + // generated by gen into an int in [3, input] + return std::max((distrib(gen) % input), static_cast< size_t >(3)); + } + tcp::socket socket_; std::array< char, MAX_LENGTH > data_{}; }; diff --git a/rrcp.txt b/rrcp.txt index 62fc495..7d24ae4 100644 --- a/rrcp.txt +++ b/rrcp.txt @@ -1,20 +1,20 @@ M:Utility GInitialInfo"v0.8.15","async client",10 // with optional Message Number -M:Radio 10002 SString"\rHallo\tWorld\n" +M:Utility 10002 SString"\rHallo\tWorld\n" // NOTE: w/o MibName! E:2 10002 // MU error // NOTE: M:WF.FF.Main 123456 T Octet 1 // NOTE: M:WF.FF.Main 123456 t // NOTE: M:OBit L:1 123456 GGoState // with optional Logical Address -M:Audio GAudioVolume // without optionl parts +// M:Audio GAudioVolume // without optionl parts // NOTE: M:Log SStruct 1,-1,3.14 // multiple parameters // NOTE: M:RADIO T FREQUENCY 1 // register trap // NOTE: M:RADIO t // trap response OK // NOTE: M:RADIO d FREQUENCY 123456789 // trap data message -// NOTE: M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data +M:Utility S Binary #36:AAECAwQFBgcICQoLDA0OD8KAwoHDvsO/Cg== // bas64 encoded binary data // // The more real samples: From d0299d85ed834f923e4f3bdfe9425f1094e595dc Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Thu, 30 Oct 2025 16:29:05 +0100 Subject: [PATCH 105/120] Make ctest more usable --- CMakeLists.txt | 12 +-- rrcp_async_tcp_client.cpp | 23 ++++- run_test.py | 186 +++++++++++++++++++++++++++++++++----- 3 files changed, 188 insertions(+), 33 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 63d42df..9726c0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,10 +176,10 @@ do_test(rrcp_async_tcp_client_threadsafe --help Usage) add_test( NAME async_tcp_echo_client_threadsafe_test COMMAND - ${PYTHON_EXECUTABLE} # - ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # + ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # --client $ # - --server $ + --server $ # + --input ${CMAKE_CURRENT_SOURCE_DIR}/rrcp.txt # ) add_executable(rrcp_async_tcp_client rrcp_async_tcp_client.cpp) @@ -189,10 +189,10 @@ do_test(rrcp_async_tcp_client --help Usage) add_test( NAME async_tcp_echo_client_test COMMAND - ${PYTHON_EXECUTABLE} # - ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # + ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # --client $ # - --server $ + --server $ # + --input ${CMAKE_CURRENT_SOURCE_DIR}/rrcp.txt # ) if(BUILD_TESTING) diff --git a/rrcp_async_tcp_client.cpp b/rrcp_async_tcp_client.cpp index 061001c..b3dd392 100644 --- a/rrcp_async_tcp_client.cpp +++ b/rrcp_async_tcp_client.cpp @@ -16,12 +16,13 @@ #include #include #include +#include #include #include #include #include -// #define USE_SIMPLE_RRCP_CLIENT +// optional #define USE_SIMPLE_RRCP_CLIENT #ifdef USE_SIMPLE_RRCP_CLIENT #include "async_rrcp_client.hpp" #else @@ -38,12 +39,26 @@ void print(std::string msg) { fmt::print("{}\n", msg); } // NOLINTNEXTLINE(bugprone-exception-escape) auto main(int argc, char* argv[]) -> int { - if (argc != 3) + if (argc < 3) { - fmt::print(stderr, "Usage: {} \n", argv[0]); // NOLINT + fmt::print(stderr, "Usage: {} [input_file]\n", argv[0]); // NOLINT return EXIT_FAILURE; } + std::ifstream file; // persistent file object (if used) + std::istream* input_str = &std::cin; // pointer to chosen input stream + + if (argc == 4) + { + file.open(argv[3]); // NOLINT + if (!file) + { + fmt::print(stderr, "cannot open input file: {}\n", argv[3]); // NOLINT + return 2; + } + input_str = &file; + } + try { using namespace rrcp; @@ -58,7 +73,7 @@ auto main(int argc, char* argv[]) -> int std::thread io_thread([&io_context]() -> void { io_context.run(); }); std::this_thread::sleep_for(TIMEOUT_DURATION); // NOTE: only for gcov results! CK - for (std::string line; client->connected() && std::getline(std::cin, line); fmt::print(stderr, "Enter command: ")) + for (std::string line; client->connected() && std::getline(*input_str, line); fmt::print(stderr, "Enter command: ")) { const std::string::size_type sz = line.find("//"); if ((sz != std::string::npos)) diff --git a/run_test.py b/run_test.py index 0e52486..17070ee 100755 --- a/run_test.py +++ b/run_test.py @@ -1,15 +1,66 @@ #!/usr/bin/env python3 +""" +Simple test harness that runs a server and a client. + +Behavior: + +Start the server first. If the server cannot be started (or exits immediately), +terminate and exit non-zero. +Then start the client. +Let the server run for --timeout seconds. +If the server times out: try to shut it down gracefully (SIGINT), +wait a short grace period, then SIGKILL if necessary. +After the server has stopped, ask the client to exit (SIGINT) and wait a little. +If the client does not exit, kill it hard. +On any failure to start a subprocess, make sure the other process is terminated. +""" import argparse -import os import signal import subprocess import sys -import tempfile import time + from pathlib import Path from typing import List + +def send_and_wait(proc: subprocess.Popen, sig: int, wait: float) -> bool: + """Send signal to proc and wait up to timeout seconds. Return True if exited.""" + if proc is None: + return True + if proc.poll() is not None: + return True + try: + proc.send_signal(sig) + except Exception: + # fallback to terminate if send_signal fails + try: + proc.terminate() + except Exception: + pass + try: + proc.wait(timeout=wait) + return True + except subprocess.TimeoutExpired: + return False + + +def force_kill(proc: subprocess.Popen) -> None: + """Kill proc and wait (best-effort).""" + if proc is None: + return + try: + proc.kill() + except Exception: + pass + try: + proc.wait(timeout=1.0) + except Exception: + # give up + pass + + HERE = Path(__file__).resolve().parent PROJECT_DIR = HERE.parent.parent.parent @@ -24,39 +75,128 @@ def main(args: List[str]): "--server", help="The server to run", ) + parser.add_argument( + "--input", + help="The input file to read", + ) parser.add_argument( "--timeout", "-t", type=int, - default=3, + default=13, help="Number of seconds to run; defaults to %(default)s", ) args = parser.parse_args(args) + port = "8000" + + # client = subprocess.Popen([args.client, "localhost", port, args.input]) + # try: + # subprocess.run( + # [args.server, port], + # timeout=args.timeout, + # check=True, + # ) + # except subprocess.TimeoutExpired: + # print("Test expectedly timed out; sending SIGINT") + # client.send_signal(signal.SIGINT) + # time.sleep(1) + # try: + # client.wait(timeout=1) + # except subprocess.TimeoutExpired: + # print("client does not respond to SIGINT in time, sending SIGKILL") + # client.kill() + # client.wait() + # except Exception as ex: + # print("ERROR:", ex) + # client.kill() + # client.wait() + # raise - client = subprocess.Popen([args.client, "localhost", "8000"]) + server = None + client = None try: - subprocess.run( - [args.server, "8000"], - timeout=args.timeout, - check=True, - ) - except subprocess.TimeoutExpired: - print("Test expectedly timed out; sending SIGINT") - client.send_signal(signal.SIGINT) - time.sleep(1) + # Start server + server_cmd = [args.server, port] + print("Starting server:", " ".join(server_cmd)) + try: + server = subprocess.Popen(server_cmd) + except Exception as e: + print("Failed to start server:", e, file=sys.stderr) + return 2 + + # Give the server a short moment to fail fast (binary not found or immediate error) + time.sleep(0.1) + if server.poll() is not None: + print( + f"Server exited immediately with code {server.returncode}", + file=sys.stderr, + ) + return 3 + + # Start client + client_cmd = [args.client, "localhost", port] + if args.input: + client_cmd.append(args.input) + print("Starting client:", " ".join(client_cmd)) try: - client.wait(timeout=1) + client = subprocess.Popen(client_cmd) + except Exception as e: + print("Failed to start client:", e, file=sys.stderr) + # Make sure server is torn down + if server.poll() is None: + force_kill(server) + return 4 + + # Wait for server to finish or timeout + try: + print(f"Waiting up to {args.timeout} seconds for server to finish...") + server.wait(timeout=args.timeout) + print(f"Server exited (code {server.returncode}).") except subprocess.TimeoutExpired: - print("client does not respond to SIGINT in time, sending SIGKILL") - client.kill() - client.wait() - except Exception as ex: - print("ERROR:", ex) - client.kill() - client.wait() - raise + print("Timeout expired. Attempting graceful server shutdown (SIGINT).") + # Try graceful shutdown via SIGINT + graceful = send_and_wait(server, signal.SIGINT, wait=3.0) + if not graceful: + print("Server did not exit after SIGINT; killing it.") + force_kill(server) + else: + print("Server exited gracefully after SIGINT.") + + # Ensure server is not running + if server.poll() is None: + # As a last resort + print("Server still alive after attempts; killing.") + force_kill(server) + + # Now ask client to exit gracefully + if client and client.poll() is None: + print("Requesting client to exit (SIGINT).") + client_graceful = send_and_wait(client, signal.SIGINT, wait=1.0) + if not client_graceful: + print("Client did not exit after SIGINT; killing client.") + force_kill(client) + else: + print("Client exited gracefully.") + else: + if client: + print(f"Client already exited (code {client.returncode}).") + + # Return server exit code if non-zero, else client's exit code (or 0) + # XXX if server.returncode not in (None, 0): return server.returncode + if client: + return client.returncode if client.returncode is not None else 0 + return 0 + + finally: + # Cleanup any lingering processes + if client and client.poll() is None: + print("Final cleanup: killing client.") + force_kill(client) + if server and server.poll() is None: + print("Final cleanup: killing server.") + force_kill(server) if __name__ == "__main__": - main(sys.argv[1:]) + sys.exit(main(sys.argv[1:])) From 0e90027421efb5ac38a1bb67e41dac58ec703c56 Mon Sep 17 00:00:00 2001 From: Claus Klein Date: Thu, 30 Oct 2025 17:15:19 +0100 Subject: [PATCH 106/120] Cleanup test script --- CMakeLists.txt | 2 +- async_rrcp_client.hpp | 2 +- run_test.py | 70 ++++++++++++++----------------------------- 3 files changed, 25 insertions(+), 49 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9726c0f..532ea7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,7 +81,7 @@ if(UNIX AND ENABLE_TEST_COVERAGE AND CMAKE_BUILD_TYPE STREQUAL Debug) message(WARNING "ENABLE_TEST_COVERAGE is set!") add_compile_options(-O0 -g -fprofile-arcs -ftest-coverage) add_link_options(-fprofile-arcs -ftest-coverage) - # FIXME! add_compile_definitions(TARGET_CODE_COVERAGE) + # FIXME: add_compile_definitions(TARGET_CODE_COVERAGE) endif() # ---- ctest ---- diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index 307b260..b1a9b25 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -44,7 +44,7 @@ using boost::asio::ip::tcp; using namespace std::chrono_literals; constexpr size_t MAX_LENGTH = 65432; -constexpr auto TIMEOUT_DURATION = 3s; +constexpr auto TIMEOUT_DURATION = 1s; constexpr auto HEARTBEAT_INTERVAL = 10s; class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client > diff --git a/run_test.py b/run_test.py index 17070ee..7fa9ccb 100755 --- a/run_test.py +++ b/run_test.py @@ -83,35 +83,11 @@ def main(args: List[str]): "--timeout", "-t", type=int, - default=13, + default=29, help="Number of seconds to run; defaults to %(default)s", ) args = parser.parse_args(args) port = "8000" - - # client = subprocess.Popen([args.client, "localhost", port, args.input]) - # try: - # subprocess.run( - # [args.server, port], - # timeout=args.timeout, - # check=True, - # ) - # except subprocess.TimeoutExpired: - # print("Test expectedly timed out; sending SIGINT") - # client.send_signal(signal.SIGINT) - # time.sleep(1) - # try: - # client.wait(timeout=1) - # except subprocess.TimeoutExpired: - # print("client does not respond to SIGINT in time, sending SIGKILL") - # client.kill() - # client.wait() - # except Exception as ex: - # print("ERROR:", ex) - # client.kill() - # client.wait() - # raise - server = None client = None @@ -148,20 +124,33 @@ def main(args: List[str]): force_kill(server) return 4 - # Wait for server to finish or timeout + # Wait for client to finish or timeout try: - print(f"Waiting up to {args.timeout} seconds for server to finish...") - server.wait(timeout=args.timeout) - print(f"Server exited (code {server.returncode}).") + print(f"Waiting up to {args.timeout} seconds for client to finish...") + client.wait(timeout=args.timeout) + print(f"Client exited (code {client.returncode}).") except subprocess.TimeoutExpired: - print("Timeout expired. Attempting graceful server shutdown (SIGINT).") + print("Timeout expired. Attempting graceful client shutdown (SIGINT).") # Try graceful shutdown via SIGINT - graceful = send_and_wait(server, signal.SIGINT, wait=3.0) + graceful = send_and_wait(client, signal.SIGINT, wait=3.0) if not graceful: - print("Server did not exit after SIGINT; killing it.") + print("Client did not exit after SIGINT; killing it.") + force_kill(client) + else: + print("Client exited gracefully after SIGINT.") + + # Now ask server to exit gracefully + if server and server.poll() is None: + print("Requesting server to exit (SIGINT).") + client_graceful = send_and_wait(server, signal.SIGINT, wait=1.0) + if not client_graceful: + print("Server did not exit after SIGINT; killing server.") force_kill(server) else: - print("Server exited gracefully after SIGINT.") + print("Server exited gracefully.") + else: + if server: + print(f"Server already exited (code {server.returncode}).") # Ensure server is not running if server.poll() is None: @@ -169,21 +158,8 @@ def main(args: List[str]): print("Server still alive after attempts; killing.") force_kill(server) - # Now ask client to exit gracefully - if client and client.poll() is None: - print("Requesting client to exit (SIGINT).") - client_graceful = send_and_wait(client, signal.SIGINT, wait=1.0) - if not client_graceful: - print("Client did not exit after SIGINT; killing client.") - force_kill(client) - else: - print("Client exited gracefully.") - else: - if client: - print(f"Client already exited (code {client.returncode}).") - # Return server exit code if non-zero, else client's exit code (or 0) - # XXX if server.returncode not in (None, 0): return server.returncode + # NO! if server.returncode not in (None, 0): return server.returncode if client: return client.returncode if client.returncode is not None else 0 return 0 From de5c4bec7af9a44630b7cf18867ff299363c6326 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 30 Oct 2025 22:30:54 +0100 Subject: [PATCH 107/120] Add timout handling to simple rrcp client read --- GNUmakefile | 5 +++- async_rrcp_client.hpp | 15 +++++++---- async_rrcp_client_threadsafe.hpp | 23 +++++++++++------ examples/async_tcp_echo_server.cpp | 41 +++++++++++++++++++++--------- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index ad0c2a3..bdd5ff4 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -81,9 +81,12 @@ readability-use-std-min-max,\ test: all -killall async_tcp_echo_server + -(echo Ping | $(BUILD_DIR)/rrcp_async_tcp_client_threadsafe localhost 8000) & + -(echo Ping | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000) & $(BUILD_DIR)/async_tcp_echo_server 8000 & - -(echo | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000) & + -(echo | $(BUILD_DIR)/rrcp_async_tcp_client_threadsafe localhost 8000) & cat rrcp.txt | $(BUILD_DIR)/rrcp_async_tcp_client_threadsafe localhost 8000 + -(echo | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000) & cat rrcp.txt | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000 # NOTE: simple example only! # cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 diff --git a/async_rrcp_client.hpp b/async_rrcp_client.hpp index b1a9b25..447e639 100644 --- a/async_rrcp_client.hpp +++ b/async_rrcp_client.hpp @@ -44,7 +44,7 @@ using boost::asio::ip::tcp; using namespace std::chrono_literals; constexpr size_t MAX_LENGTH = 65432; -constexpr auto TIMEOUT_DURATION = 1s; +constexpr auto TIMEOUT_DURATION = 3s; constexpr auto HEARTBEAT_INTERVAL = 10s; class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client > @@ -127,6 +127,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client auto read(const std::string& msg_id) -> std::string { std::string response; + auto count = TIMEOUT_DURATION / 125ms; do { @@ -149,7 +150,11 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } } std::this_thread::sleep_for(125ms); - } while (!stopped_); + } while (!stopped_ && --count); + if (!count) + { + fmt::print(stderr, "Error: Timeout read!\n"); + } return response; } @@ -210,7 +215,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } else { - fmt::print(stderr, "Error reading message: {}\n", ec.message()); + fmt::print(stderr, "Error: reading message: {}\n", ec.message()); self->stop(); } }); @@ -232,7 +237,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client else { // There are no more endpoints to try. Shut down the client. - fmt::print(stderr, "Error writing message: {}\n", ec.message()); + fmt::print(stderr, "Error: writing message: {}\n", ec.message()); self->stop(); } }); @@ -261,7 +266,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } else { - fmt::print(stderr, "Error sending heartbeat: {}\n", ec.message()); + fmt::print(stderr, "Error: sending heartbeat: {}\n", ec.message()); self->stop(); } }); diff --git a/async_rrcp_client_threadsafe.hpp b/async_rrcp_client_threadsafe.hpp index 4dbe5a7..3336bd5 100644 --- a/async_rrcp_client_threadsafe.hpp +++ b/async_rrcp_client_threadsafe.hpp @@ -170,6 +170,8 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } private: +#define USE_PEDANTIC_CHECKES +#ifdef USE_PEDANTIC_CHECKES // Helper method to safely parse RRCP message with bounds checking static auto parse_rrcp_message(const std::string& buffer, std::size_t length, std::string& parsed_line) -> bool { @@ -211,6 +213,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client return false; } +#endif void execute_write_request(const std::string& message, std::shared_ptr< response_promise_type > promise) { @@ -301,9 +304,9 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client { if (!ec) { - //========================== RRCP ============================ + //========================== RRCP ============================ +#ifdef USE_PEDANTIC_CHECKES std::string parsed_line; - // Use safe parsing helper with comprehensive bounds checking if (!rrcp::async_rrcp_client::parse_rrcp_message(self->input_buffer_, length, parsed_line)) { @@ -313,12 +316,14 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client self->do_read(); return; } - +#else + std::string parsed_line = esc2char(self->input_buffer_.substr(1, length - 1)); // TODO(CK): check START, STOP? +#endif // Successfully parsed, remove processed data from buffer self->input_buffer_.erase(0, length); - //========================== END ============================ + //========================== END ============================ - // TODO(CK): maby refactored to helper class? +#ifdef USE_PEDANTIC_CHECKES // Validate parsed content is not empty if (parsed_line.empty()) { @@ -327,7 +332,9 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client self->do_read(); return; } +#endif + // TODO(CK): maby refactored to helper class? //========================== RRCP ============================ // Process different message types if (boost::algorithm::starts_with(parsed_line, "d")) // Trap data message @@ -350,7 +357,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } else { - fmt::print(stderr, "Error reading message: {}\n", ec.message()); + fmt::print(stderr, "Error: reading message: {}\n", ec.message()); self->stop(); } }); @@ -399,7 +406,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client else { // There are no more endpoints to try. Shut down the client. - fmt::print(stderr, "Error writing message: {}\n", ec.message()); + fmt::print(stderr, "Error: writing message: {}\n", ec.message()); self->stop(); } }); @@ -428,7 +435,7 @@ class async_rrcp_client : public std::enable_shared_from_this< async_rrcp_client } else { - fmt::print(stderr, "Error sending heartbeat: {}\n", ec.message()); + fmt::print(stderr, "Error: sending heartbeat: {}\n", ec.message()); self->stop(); } }); diff --git a/examples/async_tcp_echo_server.cpp b/examples/async_tcp_echo_server.cpp index cfd2a3e..934daf9 100644 --- a/examples/async_tcp_echo_server.cpp +++ b/examples/async_tcp_echo_server.cpp @@ -33,10 +33,10 @@ using boost::asio::ip::tcp; class session : public std::enable_shared_from_this< session > { - static constexpr size_t MAX_LENGTH{1024}; + static constexpr std::size_t MAX_LENGTH{1024}; public: - explicit session(tcp::socket socket) : socket_(std::move(socket)) {} + explicit session(tcp::socket socket) : my_socket_(std::move(socket)) {} void start() { do_read(); } @@ -44,7 +44,7 @@ class session : public std::enable_shared_from_this< session > void do_read() { auto self(shared_from_this()); - socket_.async_read_some(boost::asio::buffer(data_.data(), MAX_LENGTH), + my_socket_.async_read_some(boost::asio::buffer(data_.data(), MAX_LENGTH), [this, self](boost::system::error_code ec, std::size_t length) -> void { if (!ec) @@ -57,12 +57,28 @@ class session : public std::enable_shared_from_this< session > } else { - do_write(gen_random(length)); + std::size_t new_len = gen_random(length); + +#define CHANGE_ECHO_MSG +#ifndef CHANGE_ECHO_MSG + if ((new_len % 2) == 0) + { + data_, data()[0] = 0x20; // change content + new_len = length; // but not size! + } + if ((new_len % 2) == 1) + { + data_, data()[1] = 0x21; // change content + new_len = length; // but not size! + } +#endif + + do_write(new_len); } } else { - socket_.close(); + my_socket_.close(); } }); } @@ -70,7 +86,7 @@ class session : public std::enable_shared_from_this< session > void do_write(std::size_t length) { auto self(shared_from_this()); - boost::asio::async_write(socket_, boost::asio::buffer(data_.data(), length), + boost::asio::async_write(my_socket_, boost::asio::buffer(data_.data(), length), [this, self](boost::system::error_code ec, std::size_t /*length*/) -> void { if (!ec) @@ -79,23 +95,24 @@ class session : public std::enable_shared_from_this< session > } else { - socket_.close(); + my_socket_.close(); } }); } - static size_t gen_random(size_t input) + static std::size_t gen_random(std::size_t input) { static std::random_device rd; // a seed source for the random number engine static std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd() - static std::uniform_int_distribution<> distrib(13, MAX_LENGTH); + static std::uniform_int_distribution<> distrib(27, MAX_LENGTH); + ++input; // Use distrib to transform the random unsigned int - // generated by gen into an int in [3, input] - return std::max((distrib(gen) % input), static_cast< size_t >(3)); + // generated by gen into an value in [3, input] + return std::max((distrib(gen) % input), static_cast< std::size_t >(3)); } - tcp::socket socket_; + tcp::socket my_socket_; std::array< char, MAX_LENGTH > data_{}; }; From 51fb658b7e5daa095887b1b30169d58ce84790eb Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Thu, 30 Oct 2025 23:07:10 +0100 Subject: [PATCH 108/120] Refactory py test script --- run_test.py | 82 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/run_test.py b/run_test.py index 7fa9ccb..eab7d44 100755 --- a/run_test.py +++ b/run_test.py @@ -7,12 +7,15 @@ Start the server first. If the server cannot be started (or exits immediately), terminate and exit non-zero. Then start the client. -Let the server run for --timeout seconds. -If the server times out: try to shut it down gracefully (SIGINT), +Let the client run for --timeout seconds. +If the client times out: try to shut it down gracefully (SIGINT), wait a short grace period, then SIGKILL if necessary. -After the server has stopped, ask the client to exit (SIGINT) and wait a little. -If the client does not exit, kill it hard. +After the client has stopped, ask the server to exit (SIGINT) and wait a little. +If the server does not exit, kill it hard. On any failure to start a subprocess, make sure the other process is terminated. +return the exit result of client only (that is our SW to test)! + +Created from Claus Klein and ChatGPT as reviewer """ import argparse @@ -44,6 +47,7 @@ def send_and_wait(proc: subprocess.Popen, sig: int, wait: float) -> bool: return True except subprocess.TimeoutExpired: return False + return True def force_kill(proc: subprocess.Popen) -> None: @@ -61,6 +65,39 @@ def force_kill(proc: subprocess.Popen) -> None: pass +def start_process( + cmd: list[str], role: str, check_delay: float = 0.1 +) -> subprocess.Popen: + """ + Start a subprocess (client or server) and ensure it doesn't fail immediately. + + Args: + cmd: Command line list to run (e.g. ['python3', 'server.py', '8000']). + role: A label used for logging ('server', 'client', etc.). + check_delay: Seconds to wait before checking if the process has exited. + + Returns: + subprocess.Popen instance of the started process. + + Raises: + RuntimeError if the process cannot start or exits immediately. + """ + print(f"Starting {role}:", " ".join(cmd)) + try: + proc = subprocess.Popen(cmd) + except Exception as e: + raise RuntimeError(f"Failed to start {role}: {e}") from e + + # Allow short delay to detect immediate failure (e.g., missing binary) + time.sleep(check_delay) + if proc.poll() is not None: + raise RuntimeError( + f"{role.capitalize()} exited immediately with code {proc.returncode}" + ) + + return proc + + HERE = Path(__file__).resolve().parent PROJECT_DIR = HERE.parent.parent.parent @@ -93,34 +130,21 @@ def main(args: List[str]): try: # Start server - server_cmd = [args.server, port] - print("Starting server:", " ".join(server_cmd)) try: - server = subprocess.Popen(server_cmd) + server = start_process([args.server, port], role="server") except Exception as e: - print("Failed to start server:", e, file=sys.stderr) + print(e, file=sys.stderr) return 2 - # Give the server a short moment to fail fast (binary not found or immediate error) - time.sleep(0.1) - if server.poll() is not None: - print( - f"Server exited immediately with code {server.returncode}", - file=sys.stderr, - ) - return 3 - # Start client - client_cmd = [args.client, "localhost", port] - if args.input: - client_cmd.append(args.input) - print("Starting client:", " ".join(client_cmd)) try: - client = subprocess.Popen(client_cmd) + client_cmd = [args.client, "localhost", port] + if args.input: + client_cmd.append(args.input) + client = start_process(client_cmd, role="client") except Exception as e: - print("Failed to start client:", e, file=sys.stderr) - # Make sure server is torn down - if server.poll() is None: + print(e, file=sys.stderr) + if server and server.poll() is None: force_kill(server) return 4 @@ -152,13 +176,7 @@ def main(args: List[str]): if server: print(f"Server already exited (code {server.returncode}).") - # Ensure server is not running - if server.poll() is None: - # As a last resort - print("Server still alive after attempts; killing.") - force_kill(server) - - # Return server exit code if non-zero, else client's exit code (or 0) + # NOTE: We ignore server exit code; We return only client's exit code (or 0) # NO! if server.returncode not in (None, 0): return server.returncode if client: return client.returncode if client.returncode is not None else 0 From f70ecba15e28ac816b49823ced0cbd6c7a467c73 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 09:28:15 +0100 Subject: [PATCH 109/120] =?UTF-8?q?Bump=20version:=200.2.0-rc1=20=E2=86=92?= =?UTF-8?q?=200.2.0-rc2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 30 ++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 1 + CMakeLists.txt | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..125e859 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,30 @@ +[bumpversion] +current_version = 0.2.0-rc2 +commit = False +message = Bump version: {current_version} → {new_version} +tag_message = Release v{new_version} +tag_name = v{new_version} +tag = True +parse = (?P\d+)\.(?P\d+)\.(?P\d+)([-](?P(dev|rc))(?P\d+))? +serialize = + {major}.{minor}.{patch}-{release}{build} + {major}.{minor}.{patch} + +[bumpversion:part:release] +first_value = dev +optional_value = ga +values = + dev + rc + ga + +[bumpversion:part:build] +first_value = 0 + +[bumpversion:file:CMakeLists.txt] +search = {current_version} +replace = {new_version} +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P\d+))? +serialize = + {major}.{minor}.{patch}.{build} + {major}.{minor}.{patch} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b83a594..21cf883 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ repos: rev: v6.0.0 hooks: - id: trailing-whitespace + exclude: ^\.bumpversion.cfg$ - id: end-of-file-fixer - id: check-json - id: check-yaml diff --git a/CMakeLists.txt b/CMakeLists.txt index 532ea7d..b886b97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25...4.2) -project(RRCP-client VERSION 0.1.1 LANGUAGES CXX) +project(RRCP-client VERSION 0.2.0.2 LANGUAGES CXX) # ---- add dependencies ---- From 2c4ae847c433fcff3a1fcaa9c4ebc582e2040459 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 10:39:21 +0100 Subject: [PATCH 110/120] Change CI workflow files We check only on Linux yet with modern gcc and clang! --- .github/workflows/ci_tests.yml | 121 ------------------------- .github/workflows/clang.yml | 43 +++++++++ .github/workflows/doxygen-gh-pages.yml | 19 ---- .github/workflows/gcc.yml | 42 +++++++++ .github/workflows/pre-commit.yml | 1 + 5 files changed, 86 insertions(+), 140 deletions(-) delete mode 100644 .github/workflows/ci_tests.yml create mode 100644 .github/workflows/clang.yml delete mode 100644 .github/workflows/doxygen-gh-pages.yml create mode 100644 .github/workflows/gcc.yml diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml deleted file mode 100644 index 612a46d..0000000 --- a/.github/workflows/ci_tests.yml +++ /dev/null @@ -1,121 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -name: Continuous Integration Tests - -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - schedule: - - cron: '30 15 * * *' - -env: - LANG: en_US.UTF-8 - LC_ALL: en_US.UTF-8 - -jobs: - beman-submodule-check: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.1.0 - - preset-test: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.1.0 - with: - matrix_config: > - [ - {"preset": "gcc-debug", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, - {"preset": "gcc-release", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, - {"preset": "llvm-debug", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, - {"preset": "llvm-release", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, - {"preset": "msvc-debug", "runner": "windows-latest"}, - {"preset": "msvc-release", "runner": "windows-latest"} - ] - - build-and-test: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.1.0 - with: - matrix_config: > - { - "gcc": [ - { "versions": ["15"], - "tests": [ - { "cxxversions": ["c++26"], - "tests": [ - { "stdlibs": ["libstdc++"], - "tests": [ - "Debug.Default", "Release.Default", "Release.MaxSan", - "Debug.Dynamic", "Debug.Coverage" - ] - } - ] - }, - { "cxxversions": ["c++23"], - "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] - } - ] - }, - { "versions": ["14", "13"], - "tests": [ - { "cxxversions": ["c++26", "c++23"], - "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] - } - ] - } - ], - "clang": [ - { "versions": ["20"], - "tests": [ - {"cxxversions": ["c++26"], - "tests": [ - { "stdlibs": ["libstdc++", "libc++"], - "tests": [ - "Debug.Default", "Release.Default", "Release.MaxSan", - "Debug.Dynamic" - ] - } - ] - }, - { "cxxversions": ["c++23"], - "tests": [ - {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} - ] - } - ] - }, - { "versions": ["19"], - "tests": [ - { "cxxversions": ["c++26", "c++23"], - "tests": [ - {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} - ] - } - ] - }, - { "versions": ["18", "17"], - "tests": [ - { "cxxversions": ["c++26", "c++23"], - "tests": [{"stdlibs": ["libc++"], "tests": ["Release.Default"]}] - } - ] - } - ], - "msvc": [ - { "versions": ["latest"], - "tests": [ - { "cxxversions": ["c++23"], - "tests": [ - { "stdlibs": ["stl"], - "tests": ["Debug.Default", "Release.Default"] - } - ] - } - ] - } - ] - } - - create-issue-when-fault: - needs: [preset-test, build-and-test] - if: failure() && github.event_name == 'schedule' - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.1.0 diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml new file mode 100644 index 0000000..1633095 --- /dev/null +++ b/.github/workflows/clang.yml @@ -0,0 +1,43 @@ +name: Clang + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ develop ] + workflow_dispatch: + schedule: + - cron: '30 15 * * 6' + +jobs: + clang: + strategy: + fail-fast: false + matrix: + version: [20, 21] + + runs-on: ubuntu-latest + + container: + image: ghcr.io/mattkretz/cplusplus-ci/clang${{ matrix.version }} + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: { python-version: "3.13" } + + - name: Setup Cpp + uses: aminya/setup-cpp@v1 + with: + # compiler: llvm-${{ matrix.version }} + cmake: 4.0.4 + ninja: 1.13.0 + gcovr: true + + - name: Run test suite + env: + CXX: clang++-${{ matrix.version }} + run: | + export PATH=$HOME/.local/bin:$PATH + PRESET_NAME=llvm-release make test diff --git a/.github/workflows/doxygen-gh-pages.yml b/.github/workflows/doxygen-gh-pages.yml deleted file mode 100644 index 7fd2c82..0000000 --- a/.github/workflows/doxygen-gh-pages.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Doxygen GitHub Pages Deploy Action - -on: - push: - branches: - - main - -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: DenverCoder1/doxygen-github-pages-action@v2.0.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: gh-pages - folder: docs/html - config_file: docs/Doxyfile diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml new file mode 100644 index 0000000..74936cc --- /dev/null +++ b/.github/workflows/gcc.yml @@ -0,0 +1,42 @@ +name: GCC + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ develop ] + workflow_dispatch: + schedule: + - cron: '30 15 * * 6' + +jobs: + gcc: + strategy: + fail-fast: false + matrix: + version: [15, 16] + + runs-on: ubuntu-latest + + container: + image: ghcr.io/mattkretz/cplusplus-ci/gcc${{ matrix.version }} + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: { python-version: "3.13" } + + - name: Setup Cpp + uses: aminya/setup-cpp@v1 + with: + cmake: 4.0.4 + ninja: 1.13.0 + gcovr: true + + - name: Run test suite + env: + CXX: g++-${{ matrix.version }} + run: | + export PATH=$HOME/.local/bin:$PATH + PRESET_NAME=gcc-release make test diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 70895b4..1665291 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,6 +7,7 @@ on: push: branches: - main + - develop jobs: pre-commit: From 2a4f008eabc55e2c67c13cd684fa68d5334b64b0 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 10:51:35 +0100 Subject: [PATCH 111/120] Prevent deprecated cmake warning --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b886b97..3ad68e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ set(BOOST_INCLUDE_LIBRARIES algorithm asio beast signals2) # see too: https://cmake.org/cmake/help/latest/module/FindBoost.html set(Boost_DEBUG ON) -find_package(Boost 1.87) # XXX COMPONENTS ${BOOST_INCLUDE_LIBRARIES} HINTS $ENV{HOME}/.local) +find_package(Boost CONFIG) # XXX COMPONENTS ${BOOST_INCLUDE_LIBRARIES} HINTS $ENV{HOME}/.local) if(Boost_FOUND) set(BOOST_LIBRARIES Boost::headers) else() From 8835d0af165b3da402e8fa5faec75f2265c955be Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 11:43:10 +0100 Subject: [PATCH 112/120] Add cmake multi platform workflow file Rebase and format workflow file --- .github/workflows/cmake-multi-platform.yml | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/cmake-multi-platform.yml diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml new file mode 100644 index 0000000..145e7f6 --- /dev/null +++ b/.github/workflows/cmake-multi-platform.yml @@ -0,0 +1,65 @@ +name: CMake on multiple platforms + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + build_type: [Release] + c_compiler: [gcc, clang, cl] + include: + # Windows - MSVC + - os: windows-latest + c_compiler: cl + cpp_compiler: cl + preset: msvc-release + + # Ubuntu - GCC + - os: ubuntu-latest + c_compiler: gcc + cpp_compiler: g++ + preset: gcc-release + + # Ubuntu - Clang + - os: ubuntu-latest + c_compiler: clang + cpp_compiler: clang++ + preset: llvm-release + + # macOS - Clang (default compiler) + - os: macos-latest + c_compiler: clang + cpp_compiler: clang++ + preset: appleclang-release + + exclude: + - os: windows-latest + c_compiler: gcc + - os: windows-latest + c_compiler: clang + - os: ubuntu-latest + c_compiler: cl + - os: macos-latest + c_compiler: gcc + - os: macos-latest + c_compiler: cl + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Configure and build using CMake preset + env: + CC: ${{ matrix.c_compiler }} + CXX: ${{ matrix.cpp_compiler }} + run: | + cmake --workflow --preset ${{ matrix.preset }} From 821312c618bf3f8a436548d34397378a4bf42f7b Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 12:14:18 +0100 Subject: [PATCH 113/120] Quickfixes for CI Setup MSVC envionment Check if CI is set at CMakeLists.txt --- .github/workflows/cmake-multi-platform.yml | 7 +++++ CMakeLists.txt | 32 +++++++++++++--------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 145e7f6..1cda389 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -57,6 +57,13 @@ jobs: - name: Checkout source uses: actions/checkout@v4 + # # see https://github.com/marketplace/actions/enable-developer-command-prompt + - uses: ilammy/msvc-dev-cmd@v1 + if: matrix.os == 'windows-latest' + with: + vsversion: 2022 + arch: x64 + - name: Configure and build using CMake preset env: CC: ${{ matrix.c_compiler }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ad68e5..7a18af5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,20 +49,26 @@ FetchContent_MakeAvailable(fmt) # ---- default settings ---- -if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - if(APPLE) - execute_process( - OUTPUT_VARIABLE LLVM_PREFIX - COMMAND brew --prefix llvm - COMMAND_ECHO STDOUT - ) - string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) - elseif(LINUX) - set(LLVM_PREFIX $ENV{LLVM_ROOT}) +if(DEFINED ENV{CI}) + message(STATUS "Running inside a CI environment") +else() + message(STATUS "Running locally") + + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if(APPLE) + execute_process( + OUTPUT_VARIABLE LLVM_PREFIX + COMMAND brew --prefix llvm + COMMAND_ECHO STDOUT + ) + string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) + elseif(LINUX) + set(LLVM_PREFIX $ENV{LLVM_ROOT}) + endif() + + add_compile_options(-fexperimental-library) + add_link_options(-L${LLVM_PREFIX}/lib/c++ -lc++experimental) endif() - - add_compile_options(-fexperimental-library) - add_link_options(-L${LLVM_PREFIX}/lib/c++ -lc++experimental) endif() set(CMAKE_CXX_EXTENSIONS OFF) From 7f5c2fb058ac8fed9ecb03cc48ff470f9bd8f5e9 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 13:04:22 +0100 Subject: [PATCH 114/120] Install libboost-all-dev with apt on Linux CI --- .github/workflows/clang.yml | 5 ++++- .github/workflows/gcc.yml | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index 1633095..65edfaa 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -27,11 +27,14 @@ jobs: - uses: actions/setup-python@v5 with: { python-version: "3.13" } + - name: Install Boost + run: sudo apt-get update && sudo apt-get install -y libboost-all-dev + - name: Setup Cpp uses: aminya/setup-cpp@v1 with: # compiler: llvm-${{ matrix.version }} - cmake: 4.0.4 + cmake: 4.1.2 ninja: 1.13.0 gcovr: true diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml index 74936cc..859f757 100644 --- a/.github/workflows/gcc.yml +++ b/.github/workflows/gcc.yml @@ -27,10 +27,14 @@ jobs: - uses: actions/setup-python@v5 with: { python-version: "3.13" } + - name: Install Boost + run: sudo apt-get update && sudo apt-get install -y libboost-all-dev + - name: Setup Cpp uses: aminya/setup-cpp@v1 with: - cmake: 4.0.4 + # compiler: gnu-${{ matrix.version }} + cmake: 4.1.2 ninja: 1.13.0 gcovr: true From e789e41fc35a490dfcd31075c67a95afaaebba8a Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 13:24:26 +0100 Subject: [PATCH 115/120] Workaround for missing killall --- .github/workflows/clang.yml | 4 ++-- .github/workflows/gcc.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index 65edfaa..d2edbc3 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -28,7 +28,7 @@ jobs: with: { python-version: "3.13" } - name: Install Boost - run: sudo apt-get update && sudo apt-get install -y libboost-all-dev + run: apt-get update && apt-get install -y libboost-all-dev || echo ignored - name: Setup Cpp uses: aminya/setup-cpp@v1 @@ -43,4 +43,4 @@ jobs: CXX: clang++-${{ matrix.version }} run: | export PATH=$HOME/.local/bin:$PATH - PRESET_NAME=llvm-release make test + PRESET_NAME=llvm-release make all diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml index 859f757..43036ac 100644 --- a/.github/workflows/gcc.yml +++ b/.github/workflows/gcc.yml @@ -28,7 +28,7 @@ jobs: with: { python-version: "3.13" } - name: Install Boost - run: sudo apt-get update && sudo apt-get install -y libboost-all-dev + run: apt-get update && apt-get install -y libboost-all-dev || echo ignored - name: Setup Cpp uses: aminya/setup-cpp@v1 @@ -43,4 +43,4 @@ jobs: CXX: g++-${{ matrix.version }} run: | export PATH=$HOME/.local/bin:$PATH - PRESET_NAME=gcc-release make test + PRESET_NAME=gcc-release make all From 03c4dc5b5f9b8f321939dd47a2e25aae931855ac Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 13:42:22 +0100 Subject: [PATCH 116/120] The final cut --- .github/workflows/clang.yml | 6 ++++-- .github/workflows/gcc.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index d2edbc3..6bc477f 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -28,7 +28,7 @@ jobs: with: { python-version: "3.13" } - name: Install Boost - run: apt-get update && apt-get install -y libboost-all-dev || echo ignored + run: apt-get update -qq && apt-get install -y -qq libboost-all-dev - name: Setup Cpp uses: aminya/setup-cpp@v1 @@ -43,4 +43,6 @@ jobs: CXX: clang++-${{ matrix.version }} run: | export PATH=$HOME/.local/bin:$PATH - PRESET_NAME=llvm-release make all + cmake --workflow --preset llvm-release + gcovr + # PRESET_NAME=llvm-release make all diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml index 43036ac..23c3173 100644 --- a/.github/workflows/gcc.yml +++ b/.github/workflows/gcc.yml @@ -28,7 +28,7 @@ jobs: with: { python-version: "3.13" } - name: Install Boost - run: apt-get update && apt-get install -y libboost-all-dev || echo ignored + run: apt-get update -qq && apt-get install -y -qq libboost-all-dev - name: Setup Cpp uses: aminya/setup-cpp@v1 @@ -43,4 +43,6 @@ jobs: CXX: g++-${{ matrix.version }} run: | export PATH=$HOME/.local/bin:$PATH - PRESET_NAME=gcc-release make all + cmake --workflow --preset gcc-release + gcovr + # PRESET_NAME=gcc-release make all From ca18a296bfb06a8ee1645c56f85406a8383fdfd6 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 21:12:41 +0100 Subject: [PATCH 117/120] Test Debug and Release on CI --- .github/workflows/clang.yml | 2 +- .github/workflows/cmake-multi-platform.yml | 21 +++++++------ .github/workflows/gcc.yml | 2 +- CMakeLists.txt | 28 ++++++++++++++++-- GNUmakefile | 15 +++------- run_test.py | 34 +++++++++++++--------- 6 files changed, 64 insertions(+), 38 deletions(-) diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index 6bc477f..d2c94ca 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -1,4 +1,4 @@ -name: Clang +name: Clang on Ubuntu on: push: diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 1cda389..80c7e22 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -2,9 +2,12 @@ name: CMake on multiple platforms on: push: - branches: [ "develop" ] + branches: [ main, develop ] pull_request: - branches: [ "develop" ] + branches: [ develop ] + workflow_dispatch: + schedule: + - cron: '30 15 * * 6' jobs: build: @@ -14,32 +17,32 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - build_type: [Release] + build_type: [debug, release] c_compiler: [gcc, clang, cl] include: # Windows - MSVC - os: windows-latest c_compiler: cl cpp_compiler: cl - preset: msvc-release + preset: msvc # Ubuntu - GCC - os: ubuntu-latest c_compiler: gcc cpp_compiler: g++ - preset: gcc-release + preset: gcc # Ubuntu - Clang - os: ubuntu-latest c_compiler: clang cpp_compiler: clang++ - preset: llvm-release + preset: llvm # macOS - Clang (default compiler) - os: macos-latest c_compiler: clang cpp_compiler: clang++ - preset: appleclang-release + preset: appleclang exclude: - os: windows-latest @@ -64,9 +67,9 @@ jobs: vsversion: 2022 arch: x64 - - name: Configure and build using CMake preset + - name: Workflow preset ${{ matrix.preset }}-${{ matrix.build_type }} env: CC: ${{ matrix.c_compiler }} CXX: ${{ matrix.cpp_compiler }} run: | - cmake --workflow --preset ${{ matrix.preset }} + cmake --workflow --preset ${{ matrix.preset }}-${{ matrix.build_type }} diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml index 23c3173..97c2a08 100644 --- a/.github/workflows/gcc.yml +++ b/.github/workflows/gcc.yml @@ -1,4 +1,4 @@ -name: GCC +name: GCC on Ubuntu on: push: diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a18af5..9922499 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,13 +146,21 @@ if(BUILD_EXAMPLES AND NOT ENABLE_TEST_COVERAGE) target_link_libraries(async_tcp_echo_client PRIVATE rrcp_helper) do_test(async_tcp_echo_client --help Usage) add_test( - NAME async_tcp_echo_client_test + NAME async_tcp_echo_client-test COMMAND ${PYTHON_EXECUTABLE} # ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # --client $ # --server $ ) + add_test( + NAME async_tcp_echo_client-test-no_server + COMMAND + ${PYTHON_EXECUTABLE} # + ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # + --client $ # + --timeout 7 + ) add_executable( blocking_tcp_echo_client @@ -180,7 +188,14 @@ add_executable(rrcp_async_tcp_client_threadsafe rrcp_async_tcp_client.cpp) target_link_libraries(rrcp_async_tcp_client_threadsafe PRIVATE rrcp_helper) do_test(rrcp_async_tcp_client_threadsafe --help Usage) add_test( - NAME async_tcp_echo_client_threadsafe_test + NAME rrcp_async_tcp_client_threadsafe-test-no_server + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # + --client $ # + --timeout 7 +) +add_test( + NAME rrcp_async_tcp_client_threadsafe-test COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # --client $ # @@ -193,7 +208,14 @@ target_link_libraries(rrcp_async_tcp_client PRIVATE rrcp_helper) target_compile_definitions(rrcp_async_tcp_client PRIVATE USE_SIMPLE_RRCP_CLIENT) do_test(rrcp_async_tcp_client --help Usage) add_test( - NAME async_tcp_echo_client_test + NAME rrcp_async_tcp_client-test-no_server + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # + --client $ # + --timeout 7 +) +add_test( + NAME rrcp_async_tcp_client-test COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # --client $ # diff --git a/GNUmakefile b/GNUmakefile index bdd5ff4..4739d12 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -80,19 +80,12 @@ readability-use-std-min-max,\ $(CPPFILES) test: all - -killall async_tcp_echo_server - -(echo Ping | $(BUILD_DIR)/rrcp_async_tcp_client_threadsafe localhost 8000) & - -(echo Ping | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000) & - $(BUILD_DIR)/async_tcp_echo_server 8000 & - -(echo | $(BUILD_DIR)/rrcp_async_tcp_client_threadsafe localhost 8000) & - cat rrcp.txt | $(BUILD_DIR)/rrcp_async_tcp_client_threadsafe localhost 8000 - -(echo | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000) & - cat rrcp.txt | $(BUILD_DIR)/rrcp_async_tcp_client localhost 8000 - # NOTE: simple example only! + # NOTE: simple examples only! + # $(BUILD_DIR)/async_tcp_echo_server 8000 # cat rrcp.txt | $(BUILD_DIR)/async_tcp_echo_client localhost 8000 - # -$(BUILD_DIR)/async_tcp_echo_client localhost + # -$(BUILD_DIR)/ async_tcp_echo_client localhost # -echo | $(BUILD_DIR)/async_tcp_echo_client localhost 8001 - -killall async_tcp_echo_server + # -killall async_tcp_echo_server ctest --test-dir $(BUILD_DIR) --rerun-failed --output-on-failure gcovr diff --git a/run_test.py b/run_test.py index eab7d44..577289d 100755 --- a/run_test.py +++ b/run_test.py @@ -4,12 +4,14 @@ Behavior: -Start the server first. If the server cannot be started (or exits immediately), +Start the server first if given. If the server cannot be started (or exits immediately), terminate and exit non-zero. + Then start the client. Let the client run for --timeout seconds. If the client times out: try to shut it down gracefully (SIGINT), wait a short grace period, then SIGKILL if necessary. + After the client has stopped, ask the server to exit (SIGINT) and wait a little. If the server does not exit, kill it hard. On any failure to start a subprocess, make sure the other process is terminated. @@ -24,9 +26,13 @@ import sys import time -from pathlib import Path from typing import List +# from pathlib import Path +# +# HERE = Path(__file__).resolve().parent +# PROJECT_DIR = HERE.parent.parent.parent + def send_and_wait(proc: subprocess.Popen, sig: int, wait: float) -> bool: """Send signal to proc and wait up to timeout seconds. Return True if exited.""" @@ -88,7 +94,7 @@ def start_process( except Exception as e: raise RuntimeError(f"Failed to start {role}: {e}") from e - # Allow short delay to detect immediate failure (e.g., missing binary) + # Allow short delay to detect immediate failure (resource missing or blocked: e.g. can't open port) time.sleep(check_delay) if proc.poll() is not None: raise RuntimeError( @@ -98,10 +104,6 @@ def start_process( return proc -HERE = Path(__file__).resolve().parent -PROJECT_DIR = HERE.parent.parent.parent - - def main(args: List[str]): parser = argparse.ArgumentParser() parser.add_argument( @@ -124,17 +126,23 @@ def main(args: List[str]): help="Number of seconds to run; defaults to %(default)s", ) args = parser.parse_args(args) + + if not args.client: + print("Missing path to client!") + return 1 + port = "8000" server = None client = None try: # Start server - try: - server = start_process([args.server, port], role="server") - except Exception as e: - print(e, file=sys.stderr) - return 2 + if args.server: + try: + server = start_process([args.server, port], role="server") + except Exception as e: + print(e, file=sys.stderr) + return 2 # Start client try: @@ -146,7 +154,7 @@ def main(args: List[str]): print(e, file=sys.stderr) if server and server.poll() is None: force_kill(server) - return 4 + return 3 # Wait for client to finish or timeout try: From 0e2c286b1aceb427553e72c4cb781f64b7ba63a0 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 21:22:44 +0100 Subject: [PATCH 118/120] Connection Timout needs to be 9 sec --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9922499..db374d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,7 +159,7 @@ if(BUILD_EXAMPLES AND NOT ENABLE_TEST_COVERAGE) ${PYTHON_EXECUTABLE} # ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # --client $ # - --timeout 7 + --timeout 9 ) add_executable( @@ -192,7 +192,7 @@ add_test( COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # --client $ # - --timeout 7 + --timeout 9 ) add_test( NAME rrcp_async_tcp_client_threadsafe-test @@ -212,7 +212,7 @@ add_test( COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_test.py # --client $ # - --timeout 7 + --timeout 9 ) add_test( NAME rrcp_async_tcp_client-test From 95aa22141a4312b22b5a2fead8296d51b54e25b2 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 21:49:32 +0100 Subject: [PATCH 119/120] Modernize .devcontainer --- .devcontainer/Dockerfile | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 93533e2..2e0db46 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,18 +1,34 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-22.04 +# use ubuntu-24.04 (noble) +FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04 USER vscode -# Install latest cmake -RUN wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null -RUN echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null -RUN sudo apt-get update && sudo apt-get install -y cmake libboost-dev +# ------------------------------- +# Install latest CMake and Boost +# ------------------------------- +RUN sudo apt-get update -qq \ + && sudo apt-get install -y -qq wget gpg software-properties-common \ + && wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc \ + | gpg --dearmor \ + | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null \ + && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ noble main" \ + | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null \ + && sudo apt-get update -qq \ + && sudo apt-get install -y -qq cmake libboost-all-dev \ + && sudo rm -rf /var/lib/apt/lists/* -# Install pre-commit -RUN sudo apt-get install -y python3-pip && pip3 install pre-commit gcovr ninja cmake - -# Avoid ASAN Stalling -# Alternative is to update to clang-18 and gcc-13.2 +# ------------------------------- +# Install pre-commit, gcovr, ninja +# ------------------------------- +RUN sudo apt-get update -qq \ + && sudo apt-get install -y -qq python3-pip \ + && pip3 install --no-cache-dir pre-commit gcovr ninja cmake \ + && sudo rm -rf /var/lib/apt/lists/* +# ------------------------------- +# Avoid ASAN stalling +# ------------------------------- +# Reduces mmap randomization slightly so AddressSanitizer works reliably RUN sudo sysctl -w vm.mmap_rnd_bits=28 From 672324aa69c36b43f60d0035254f0f3d2c24e736 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 31 Oct 2025 21:56:31 +0100 Subject: [PATCH 120/120] Optimize Dockerfile --- .devcontainer/Dockerfile | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2e0db46..1bb8612 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -# use ubuntu-24.04 (noble) +# We use ubuntu-24.04 (noble) FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04 USER vscode @@ -16,16 +16,13 @@ RUN sudo apt-get update -qq \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ noble main" \ | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && sudo apt-get update -qq \ - && sudo apt-get install -y -qq cmake libboost-all-dev \ + && sudo apt-get install -y -qq cmake libboost-all-dev python3-pip \ && sudo rm -rf /var/lib/apt/lists/* # ------------------------------- # Install pre-commit, gcovr, ninja # ------------------------------- -RUN sudo apt-get update -qq \ - && sudo apt-get install -y -qq python3-pip \ - && pip3 install --no-cache-dir pre-commit gcovr ninja cmake \ - && sudo rm -rf /var/lib/apt/lists/* +RUN pip3 install --no-cache-dir pre-commit gcovr ninja cmake # ------------------------------- # Avoid ASAN stalling