diff --git a/CMakeLists.txt b/CMakeLists.txt index fdb111c..8409162 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,15 +2,8 @@ cmake_minimum_required(VERSION 3.25) # Platform-specific compiler detection (must be before project()) -if(APPLE) - # macOS: Use Homebrew LLVM if not specified - if(NOT DEFINED CMAKE_CXX_COMPILER) - set(CMAKE_CXX_COMPILER /opt/homebrew/opt/llvm/bin/clang++) - endif() - if(NOT DEFINED CMAKE_C_COMPILER) - set(CMAKE_C_COMPILER /opt/homebrew/opt/llvm/bin/clang) - endif() -elseif(UNIX) +# By default, use system compiler. Override with -DCMAKE_CXX_COMPILER=... +if(UNIX AND NOT APPLE) # Linux: Prefer clang-18 if not specified if(NOT DEFINED CMAKE_CXX_COMPILER) find_program(CLANG_CXX NAMES clang++-18 clang++) @@ -37,15 +30,17 @@ set(CMAKE_C_STANDARD_REQUIRED ON) # Platform-specific flags if(APPLE) - # macOS: Use Homebrew LLVM libc++ + # macOS: Use system libc++ (works with both Apple Clang and MacPorts) add_compile_options($<$:-stdlib=libc++>) add_link_options($<$:-stdlib=libc++>) - add_link_options($<$:-L/opt/homebrew/opt/llvm/lib/c++>) - add_link_options($<$:-Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++>) - set(CMAKE_PREFIX_PATH "/opt/homebrew;/opt/homebrew/opt/abseil") - set(Protobuf_INCLUDE_DIR "/opt/homebrew/include") - set(Protobuf_PROTOC_EXECUTABLE "/opt/homebrew/bin/protoc") + # Support MacPorts (/opt/local) or Homebrew (/opt/homebrew) + if(EXISTS "/opt/local") + list(APPEND CMAKE_PREFIX_PATH "/opt/local") + endif() + if(EXISTS "/opt/homebrew") + list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew" "/opt/homebrew/opt/abseil") + endif() elseif(UNIX) # Linux: No special flags needed (use distribution's default stdlib) endif() @@ -80,6 +75,25 @@ else() find_package(OpenSSL REQUIRED) endif() +# fmt library for std::format compatibility on older platforms +# (macOS < 13.3 lacks to_chars for floating point required by std::format) +if(APPLE) + # Check if we need fmt (macOS < 13.3) + # Try to find fmt in MacPorts location + find_library(FMT_LIBRARY NAMES fmt PATHS /opt/local/lib/libfmt11 /opt/local/lib) + find_path(FMT_INCLUDE_DIR fmt/format.h PATHS /opt/local/include/libfmt11 /opt/local/include) + if(FMT_LIBRARY AND FMT_INCLUDE_DIR) + message(STATUS "Found fmt: ${FMT_LIBRARY}") + set(SPARKPLUG_USE_FMT ON CACHE BOOL "Use fmt library for format" FORCE) + add_compile_definitions(SPARKPLUG_USE_FMT) + # Make fmt available globally for examples and tests + include_directories(${FMT_INCLUDE_DIR}) + link_libraries(${FMT_LIBRARY}) + else() + message(STATUS "fmt library not found, using std::format (requires macOS 13.3+)") + endif() +endif() + add_subdirectory(proto) target_compile_options(sparkplug_proto PRIVATE -w) add_subdirectory(src) diff --git a/examples/tck_edge_node.cpp b/examples/tck_edge_node.cpp index b43b604..36ee4db 100644 --- a/examples/tck_edge_node.cpp +++ b/examples/tck_edge_node.cpp @@ -5,6 +5,8 @@ #include #include +#include + namespace sparkplug::tck { TCKEdgeNode::TCKEdgeNode(TCKEdgeNodeConfig config) @@ -93,7 +95,7 @@ void TCKEdgeNode::run_session_establishment_test(const std::vector& try { log("INFO", - std::format( + fmt::format( "Starting SessionEstablishmentTest: host={}, group={}, node={}, devices={}", host_id, group_id, edge_node_id, params.size() > 3 ? params[3] : "none")); @@ -109,7 +111,7 @@ void TCKEdgeNode::run_session_establishment_test(const std::vector& log("INFO", "NBIRTH published with bdSeq and metrics"); if (!device_ids.empty()) { - log("INFO", std::format("Published DBIRTH for {} device(s)", device_ids.size())); + log("INFO", fmt::format("Published DBIRTH for {} device(s)", device_ids.size())); } } catch (const std::exception& e) { @@ -136,7 +138,7 @@ void TCKEdgeNode::run_session_termination_test(const std::vector& p try { log("INFO", - std::format( + fmt::format( "Starting SessionTerminationTest: host={}, group={}, node={}, devices={}", host_id, group_id, edge_node_id, params.size() > 3 ? params[3] : "none")); @@ -155,10 +157,10 @@ void TCKEdgeNode::run_session_termination_test(const std::vector& p for (const auto& device_id : device_ids_) { auto result = edge_node_->publish_device_death(device_id); if (!result) { - log("WARN", std::format("Failed to publish DDEATH for {}: {}", device_id, + log("WARN", fmt::format("Failed to publish DDEATH for {}: {}", device_id, result.error())); } else { - log("INFO", std::format("DDEATH published for device: {}", device_id)); + log("INFO", fmt::format("DDEATH published for device: {}", device_id)); } } @@ -203,7 +205,7 @@ void TCKEdgeNode::run_send_data_test(const std::vector& params) { try { log("INFO", - std::format("Starting SendDataTest: host={}, group={}, node={}, devices={}", + fmt::format("Starting SendDataTest: host={}, group={}, node={}, devices={}", host_id, group_id, edge_node_id, params.size() > 3 ? params[3] : "none")); @@ -234,11 +236,11 @@ void TCKEdgeNode::run_send_data_test(const std::vector& params) { return; } - log("INFO", std::format("NDATA message {} published with aliases", i + 1)); + log("INFO", fmt::format("NDATA message {} published with aliases", i + 1)); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } - log("INFO", std::format("Successfully sent {} NDATA messages", num_ndata_messages)); + log("INFO", fmt::format("Successfully sent {} NDATA messages", num_ndata_messages)); if (!device_ids.empty()) { log("INFO", "Sending DDATA messages from devices"); @@ -254,18 +256,18 @@ void TCKEdgeNode::run_send_data_test(const std::vector& params) { auto ddata_result = edge_node_->publish_device_data(device_id, ddata); if (!ddata_result) { - log("ERROR", std::format("Failed to publish DDATA for {}: {}", device_id, + log("ERROR", fmt::format("Failed to publish DDATA for {}: {}", device_id, ddata_result.error())); publish_result("OVERALL: FAIL"); return; } - log("INFO", std::format("DDATA message {} published for device {} with aliases", + log("INFO", fmt::format("DDATA message {} published for device {} with aliases", i + 1, device_id)); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } - log("INFO", std::format("Successfully sent {} DDATA messages for device {}", + log("INFO", fmt::format("Successfully sent {} DDATA messages for device {}", num_ddata_messages, device_id)); } } @@ -310,7 +312,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id, return stdx::unexpected("Edge Node already exists"); } - log("INFO", std::format("Creating Edge Node group_id={}, edge_node_id={}, devices={}", + log("INFO", fmt::format("Creating Edge Node group_id={}, edge_node_id={}, devices={}", group_id, edge_node_id, device_ids.size())); current_group_id_ = group_id; @@ -328,7 +330,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id, } edge_config.command_callback = [this](const Topic& topic, const auto& /*payload*/) { - log("INFO", std::format("Received command on topic: {}", topic.to_string())); + log("INFO", fmt::format("Received command on topic: {}", topic.to_string())); }; edge_config.primary_host_id = host_id; @@ -342,7 +344,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id, } if (!host_id.empty()) { - log("INFO", std::format("Waiting for primary host '{}' to be online", host_id)); + log("INFO", fmt::format("Waiting for primary host '{}' to be online", host_id)); constexpr int max_wait_ms = 10000; constexpr int poll_interval_ms = 100; int waited_ms = 0; @@ -359,7 +361,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id, if (!edge_node_->is_primary_host_online()) { return stdx::unexpected( - std::format("Timeout waiting for primary host '{}' to be online", host_id)); + fmt::format("Timeout waiting for primary host '{}' to be online", host_id)); } } @@ -380,7 +382,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id, log("INFO", "NBIRTH published successfully"); for (const auto& device_id : device_ids) { - log("INFO", std::format("Publishing DBIRTH for device: {}", device_id)); + log("INFO", fmt::format("Publishing DBIRTH for device: {}", device_id)); PayloadBuilder dbirth; auto device_timestamp = get_timestamp(); @@ -393,10 +395,10 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id, auto device_result = edge_node_->publish_device_birth(device_id, dbirth); if (!device_result) { - log("WARN", std::format("Failed to publish DBIRTH for {}: {}", device_id, + log("WARN", fmt::format("Failed to publish DBIRTH for {}: {}", device_id, device_result.error())); } else { - log("INFO", std::format("DBIRTH published for device: {}", device_id)); + log("INFO", fmt::format("DBIRTH published for device: {}", device_id)); } } diff --git a/include/sparkplug/detail/compat.hpp b/include/sparkplug/detail/compat.hpp index c79994f..f7a60b3 100644 --- a/include/sparkplug/detail/compat.hpp +++ b/include/sparkplug/detail/compat.hpp @@ -20,3 +20,37 @@ using tl::expected; using tl::unexpected; } // namespace sparkplug::stdx #endif + +// std::format compatibility +// Use fmt library on platforms where std::format is unavailable or broken +// (e.g., macOS < 13.3 lacks to_chars for floating point required by std::format) +#if defined(SPARKPLUG_USE_FMT) || \ + (defined(__APPLE__) && __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ < 130300) +# include +namespace sparkplug::stdx { +using fmt::format; +} // namespace sparkplug::stdx +#else +# include +namespace sparkplug::stdx { +using std::format; +} // namespace sparkplug::stdx +#endif + +// std::unreachable compatibility +#if __cpp_lib_unreachable >= 202202L +# include +namespace sparkplug::stdx { +using std::unreachable; +} // namespace sparkplug::stdx +#else +namespace sparkplug::stdx { +[[noreturn]] inline void unreachable() { +# if defined(__GNUC__) || defined(__clang__) + __builtin_unreachable(); +# elif defined(_MSC_VER) + __assume(false); +# endif +} +} // namespace sparkplug::stdx +#endif diff --git a/scripts/format.sh b/scripts/format.sh index 2166bef..9a8bee7 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -9,6 +9,11 @@ if [ -n "$CLANG_FORMAT" ]; then : elif command -v clang-format-18 &> /dev/null; then CLANG_FORMAT=clang-format-18 +elif command -v clang-format-mp-18 &> /dev/null; then + # MacPorts clang-18 + CLANG_FORMAT=clang-format-mp-18 +elif [ -f /opt/local/bin/clang-format-mp-18 ]; then + CLANG_FORMAT=/opt/local/bin/clang-format-mp-18 elif [ -f /opt/homebrew/opt/llvm@18/bin/clang-format ]; then CLANG_FORMAT=/opt/homebrew/opt/llvm@18/bin/clang-format elif command -v /opt/homebrew/bin/clang-format &> /dev/null; then @@ -17,7 +22,10 @@ elif command -v clang-format &> /dev/null; then CLANG_FORMAT=clang-format else echo "Error: clang-format not found!" - echo "Install it with: brew install clang-format (macOS) or apt install clang-format-18 (Linux)" + echo "Install it with:" + echo " macOS (MacPorts): sudo port install clang-18" + echo " macOS (Homebrew): brew install llvm@18" + echo " Linux (Ubuntu): apt install clang-format-18" exit 1 fi diff --git a/scripts/tidy.sh b/scripts/tidy.sh index c91a606..10fc7aa 100755 --- a/scripts/tidy.sh +++ b/scripts/tidy.sh @@ -3,16 +3,27 @@ set -e -# Detect clang-tidy binary -if [ -f /opt/homebrew/opt/llvm/bin/clang-tidy ]; then - CLANG_TIDY=/opt/homebrew/opt/llvm/bin/clang-tidy +# Detect clang-tidy binary (prefer clang-tidy-18 to match CI) +if [ -n "$CLANG_TIDY" ]; then + # Use environment variable if set + : elif command -v clang-tidy-18 &> /dev/null; then CLANG_TIDY=clang-tidy-18 +elif command -v clang-tidy-mp-18 &> /dev/null; then + # MacPorts clang-18 + CLANG_TIDY=clang-tidy-mp-18 +elif [ -f /opt/local/bin/clang-tidy-mp-18 ]; then + CLANG_TIDY=/opt/local/bin/clang-tidy-mp-18 +elif [ -f /opt/homebrew/opt/llvm/bin/clang-tidy ]; then + CLANG_TIDY=/opt/homebrew/opt/llvm/bin/clang-tidy elif command -v clang-tidy &> /dev/null; then CLANG_TIDY=clang-tidy else echo "Error: clang-tidy not found!" - echo "Install it with: brew install llvm (macOS) or apt install clang-tidy-18 (Linux)" + echo "Install it with:" + echo " macOS (MacPorts): sudo port install clang-18" + echo " macOS (Homebrew): brew install llvm" + echo " Linux (Ubuntu): apt install clang-tidy-18" exit 1 fi @@ -64,7 +75,10 @@ done # Find all C++ source files if none specified # Only check .cpp files - headers will be analyzed when included if [ ${#FILES_TO_CHECK[@]} -eq 0 ]; then - mapfile -t FILES_TO_CHECK < <(find src tests examples \ + # Use while loop for portability (mapfile requires bash 4+) + while IFS= read -r file; do + FILES_TO_CHECK+=("$file") + done < <(find src tests examples \ -name "*.cpp" \ -not -path "*/build/*" \ -not -path "*/build-*/*") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7497d04..a8f87f4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,12 @@ target_include_directories(sparkplug_cpp $ ) +# Add fmt include directory and link library if using fmt fallback +if(SPARKPLUG_USE_FMT) + target_include_directories(sparkplug_cpp PUBLIC ${FMT_INCLUDE_DIR}) + target_link_libraries(sparkplug_cpp PRIVATE ${FMT_LIBRARY}) +endif() + if(BUILD_STATIC_BUNDLE) target_link_libraries(sparkplug_cpp PUBLIC diff --git a/src/c_bindings.cpp b/src/c_bindings.cpp index 7efc04d..801e6a6 100644 --- a/src/c_bindings.cpp +++ b/src/c_bindings.cpp @@ -1,10 +1,10 @@ +#include "sparkplug/detail/compat.hpp" #include "sparkplug/edge_node.hpp" #include "sparkplug/host_application.hpp" #include "sparkplug/payload_builder.hpp" #include "sparkplug/sparkplug_c.h" #include -#include #include struct sparkplug_publisher { @@ -220,7 +220,8 @@ int sparkplug_publisher_connect(sparkplug_publisher_t* pub) { auto result = pub->impl.connect(); if (!result.has_value()) { - auto error_msg = std::format("EdgeNode MQTT connection failed: {}", result.error()); + auto error_msg = + sparkplug::stdx::format("EdgeNode MQTT connection failed: {}", result.error()); pub->impl.log(sparkplug::LogLevel::ERROR, error_msg); return -1; } @@ -894,8 +895,8 @@ int sparkplug_host_application_connect(sparkplug_host_application_t* host) { auto result = host->impl.connect(); if (!result.has_value()) { - auto error_msg = - std::format("HostApplication MQTT connection failed: {}", result.error()); + auto error_msg = sparkplug::stdx::format("HostApplication MQTT connection failed: {}", + result.error()); host->impl.log(sparkplug::LogLevel::ERROR, error_msg); return -1; } @@ -951,8 +952,9 @@ int sparkplug_host_application_publish_node_command(sparkplug_host_application_t auto result = host->impl.publish_node_command(group_id, target_edge_node_id, builder); if (!result.has_value()) { - auto error_msg = std::format("publish_node_command failed for {}/{}: {}", group_id, - target_edge_node_id, result.error()); + auto error_msg = + sparkplug::stdx::format("publish_node_command failed for {}/{}: {}", group_id, + target_edge_node_id, result.error()); host->impl.log(sparkplug::LogLevel::ERROR, error_msg); } return result.has_value() ? 0 : -1; diff --git a/src/edge_node.cpp b/src/edge_node.cpp index 65f5657..cab6061 100644 --- a/src/edge_node.cpp +++ b/src/edge_node.cpp @@ -1,8 +1,9 @@ // src/edge_node.cpp #include "sparkplug/edge_node.hpp" +#include "sparkplug/detail/compat.hpp" + #include -#include #include #include #include @@ -25,7 +26,7 @@ void on_connect_success(void* context, MQTTAsync_successData* response) { void on_connect_failure(void* context, MQTTAsync_failureData* response) { auto* promise = static_cast*>(context); - auto error = std::format("Connection failed: code={}", response ? response->code : -1); + auto error = stdx::format("Connection failed: code={}", response ? response->code : -1); promise->set_exception(std::make_exception_ptr(std::runtime_error(error))); } @@ -37,7 +38,7 @@ void on_disconnect_success(void* context, MQTTAsync_successData* response) { void on_disconnect_failure(void* context, MQTTAsync_failureData* response) { auto* promise = static_cast*>(context); - auto error = std::format("Disconnect failed: code={}", response ? response->code : -1); + auto error = stdx::format("Disconnect failed: code={}", response ? response->code : -1); promise->set_exception(std::make_exception_ptr(std::runtime_error(error))); } @@ -49,7 +50,7 @@ void on_subscribe_success(void* context, MQTTAsync_successData* response) { void on_subscribe_failure(void* context, MQTTAsync_failureData* response) { auto* promise = static_cast*>(context); - auto error = std::format("Subscribe failed: code={}", response ? response->code : -1); + auto error = stdx::format("Subscribe failed: code={}", response ? response->code : -1); promise->set_exception(std::make_exception_ptr(std::runtime_error(error))); } @@ -201,7 +202,7 @@ stdx::expected EdgeNode::connect() { MQTTAsync_create(&raw_client, config_.broker_url.c_str(), config_.client_id.c_str(), MQTTCLIENT_PERSISTENCE_NONE, nullptr); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to create client: {}", rc)); + return stdx::unexpected(stdx::format("Failed to create client: {}", rc)); } client_ = MQTTAsyncHandle(raw_client); @@ -210,7 +211,7 @@ stdx::expected EdgeNode::connect() { rc = MQTTAsync_setCallbacks(client_.get(), this, on_connection_lost, on_message_arrived, nullptr); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to set callbacks: {}", rc)); + return stdx::unexpected(stdx::format("Failed to set callbacks: {}", rc)); } // Increment bdSeq for this session (Sparkplug spec requires bdSeq to start at 1) @@ -276,7 +277,7 @@ stdx::expected EdgeNode::connect() { rc = MQTTAsync_connect(client_.get(), &conn_opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to connect: {}", rc)); + return stdx::unexpected(stdx::format("Failed to connect: {}", rc)); } auto status = connect_future.wait_for(std::chrono::milliseconds(CONNECTION_TIMEOUT_MS)); @@ -313,7 +314,7 @@ stdx::expected EdgeNode::connect() { rc = MQTTAsync_subscribe(client_.get(), ncmd_topic_str.c_str(), 1, &sub_opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to subscribe to NCMD: {}", rc)); + return stdx::unexpected(stdx::format("Failed to subscribe to NCMD: {}", rc)); } auto sub_status = @@ -325,7 +326,7 @@ stdx::expected EdgeNode::connect() { try { subscribe_future.get(); } catch (const std::exception& e) { - return stdx::unexpected(std::format("NCMD subscription failed: {}", e.what())); + return stdx::unexpected(stdx::format("NCMD subscription failed: {}", e.what())); } if (config_.primary_host_id.has_value()) { @@ -341,7 +342,7 @@ stdx::expected EdgeNode::connect() { rc = MQTTAsync_subscribe(client_.get(), state_topic.c_str(), 1, &state_sub_opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to subscribe to STATE: {}", rc)); + return stdx::unexpected(stdx::format("Failed to subscribe to STATE: {}", rc)); } auto state_sub_status = @@ -353,7 +354,7 @@ stdx::expected EdgeNode::connect() { try { state_subscribe_future.get(); } catch (const std::exception& e) { - return stdx::unexpected(std::format("STATE subscription failed: {}", e.what())); + return stdx::unexpected(stdx::format("STATE subscription failed: {}", e.what())); } } @@ -378,7 +379,7 @@ stdx::expected EdgeNode::disconnect() { int rc = MQTTAsync_disconnect(client_.get(), &opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to disconnect: {}", rc)); + return stdx::unexpected(stdx::format("Failed to disconnect: {}", rc)); } auto status = @@ -417,7 +418,7 @@ EdgeNode::publish_message(MQTTAsync client, int rc = MQTTAsync_sendMessage(client, topic_str.c_str(), &msg, &opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to publish: {}", rc)); + return stdx::unexpected(stdx::format("Failed to publish: {}", rc)); } return {}; @@ -692,7 +693,7 @@ EdgeNode::publish_device_birth(std::string_view device_id, PayloadBuilder& paylo int rc = MQTTAsync_subscribe(client, dcmd_topic_str.c_str(), 1, &sub_opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to subscribe to DCMD: {}", rc)); + return stdx::unexpected(stdx::format("Failed to subscribe to DCMD: {}", rc)); } auto sub_status = @@ -704,7 +705,7 @@ EdgeNode::publish_device_birth(std::string_view device_id, PayloadBuilder& paylo try { subscribe_future.get(); } catch (const std::exception& e) { - return stdx::unexpected(std::format("DCMD subscription failed: {}", e.what())); + return stdx::unexpected(stdx::format("DCMD subscription failed: {}", e.what())); } auto result = publish_message(client, topic_str, payload_data, qos, false); @@ -739,7 +740,7 @@ EdgeNode::publish_device_data(std::string_view device_id, PayloadBuilder& payloa auto it = device_states_.find(device_id); if (it == device_states_.end() || !it->second.is_online) { return stdx::unexpected( - std::format("Must publish DBIRTH for device '{}' before DDATA", device_id)); + stdx::format("Must publish DBIRTH for device '{}' before DDATA", device_id)); } seq_num_ = (seq_num_ + 1) % SEQ_NUMBER_MAX; @@ -778,7 +779,7 @@ EdgeNode::publish_device_death(std::string_view device_id) { auto it = device_states_.find(device_id); if (it == device_states_.end()) { - return stdx::unexpected(std::format("Unknown device: '{}'", device_id)); + return stdx::unexpected(stdx::format("Unknown device: '{}'", device_id)); } seq_num_ = (seq_num_ + 1) % 256; diff --git a/src/host_application.cpp b/src/host_application.cpp index 85cff63..149bab7 100644 --- a/src/host_application.cpp +++ b/src/host_application.cpp @@ -1,10 +1,10 @@ // src/host_application.cpp #include "sparkplug/host_application.hpp" +#include "sparkplug/detail/compat.hpp" #include "sparkplug/topic.hpp" #include -#include #include #include #include @@ -28,8 +28,8 @@ void on_connect_failure(void* context, MQTTAsync_failureData* response) { auto* promise = static_cast*>(context); std::string error; if (response) { - error = std::format("Connection failed: code={}, message={}", response->code, - response->message ? response->message : "none"); + error = stdx::format("Connection failed: code={}, message={}", response->code, + response->message ? response->message : "none"); } else { error = "Connection failed: no response data"; } @@ -44,7 +44,7 @@ void on_disconnect_success(void* context, MQTTAsync_successData* response) { void on_disconnect_failure(void* context, MQTTAsync_failureData* response) { auto* promise = static_cast*>(context); - auto error = std::format("Disconnect failed: code={}", response ? response->code : -1); + auto error = stdx::format("Disconnect failed: code={}", response ? response->code : -1); promise->set_exception(std::make_exception_ptr(std::runtime_error(error))); } @@ -112,14 +112,14 @@ stdx::expected HostApplication::connect() { MQTTAsync_create(&raw_client, config_.broker_url.c_str(), config_.client_id.c_str(), MQTTCLIENT_PERSISTENCE_NONE, nullptr); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to create client: {}", rc)); + return stdx::unexpected(stdx::format("Failed to create client: {}", rc)); } client_ = MQTTAsyncHandle(raw_client); rc = MQTTAsync_setCallbacks(client_.get(), this, on_connection_lost, on_message_arrived, nullptr); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to set callbacks: {}", rc)); + return stdx::unexpected(stdx::format("Failed to set callbacks: {}", rc)); } MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer; @@ -158,7 +158,7 @@ stdx::expected HostApplication::connect() { rc = MQTTAsync_connect(client_.get(), &conn_opts); if (rc != MQTTASYNC_SUCCESS) { MQTTAsync_setCallbacks(client_.get(), nullptr, nullptr, nullptr, nullptr); - return stdx::unexpected(std::format("Failed to connect: {}", rc)); + return stdx::unexpected(stdx::format("Failed to connect: {}", rc)); } auto status = connect_future.wait_for(std::chrono::milliseconds(CONNECTION_TIMEOUT_MS)); @@ -199,7 +199,7 @@ stdx::expected HostApplication::disconnect() { int rc = MQTTAsync_disconnect(client_.get(), &opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to disconnect: {}", rc)); + return stdx::unexpected(stdx::format("Failed to disconnect: {}", rc)); } auto status = @@ -227,9 +227,9 @@ HostApplication::publish_state_birth(uint64_t timestamp) { } std::string json_payload = - std::format("{{\"online\":true,\"timestamp\":{}}}", timestamp); + stdx::format("{{\"online\":true,\"timestamp\":{}}}", timestamp); - std::string topic = std::format("{}/STATE/{}", NAMESPACE, config_.host_id); + std::string topic = stdx::format("{}/STATE/{}", NAMESPACE, config_.host_id); std::vector payload_data(json_payload.begin(), json_payload.end()); @@ -245,9 +245,9 @@ HostApplication::publish_state_death(uint64_t timestamp) { } std::string json_payload = - std::format("{{\"online\":false,\"timestamp\":{}}}", timestamp); + stdx::format("{{\"online\":false,\"timestamp\":{}}}", timestamp); - std::string topic = std::format("{}/STATE/{}", NAMESPACE, config_.host_id); + std::string topic = stdx::format("{}/STATE/{}", NAMESPACE, config_.host_id); std::vector payload_data(json_payload.begin(), json_payload.end()); @@ -332,13 +332,13 @@ HostApplication::publish_raw_message(std::string_view topic, opts.onFailure = [](void* context, MQTTAsync_failureData* response) { auto* promise = static_cast*>(context); std::string error = - std::format("Publish failed: code={}", response ? response->code : -1); + stdx::format("Publish failed: code={}", response ? response->code : -1); promise->set_exception(std::make_exception_ptr(std::runtime_error(error))); }; int rc = MQTTAsync_sendMessage(client_.get(), std::string(topic).c_str(), &msg, &opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to publish: {}", rc)); + return stdx::unexpected(stdx::format("Failed to publish: {}", rc)); } auto status = send_future.wait_for(std::chrono::milliseconds(5000)); @@ -372,7 +372,7 @@ HostApplication::publish_command_message(std::string_view topic, int rc = MQTTAsync_sendMessage(client_.get(), std::string(topic).c_str(), &msg, &opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to publish: {}", rc)); + return stdx::unexpected(stdx::format("Failed to publish: {}", rc)); } return {}; @@ -385,13 +385,13 @@ stdx::expected HostApplication::subscribe_all_groups() { return stdx::unexpected("Not connected"); } - std::string topic = std::format("{}/#", NAMESPACE); + std::string topic = stdx::format("{}/#", NAMESPACE); MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; int rc = MQTTAsync_subscribe(client_.get(), topic.c_str(), config_.qos, &opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to subscribe: {}", rc)); + return stdx::unexpected(stdx::format("Failed to subscribe: {}", rc)); } return {}; @@ -405,13 +405,13 @@ HostApplication::subscribe_group(std::string_view group_id) { return stdx::unexpected("Not connected"); } - std::string topic = std::format("{}/{}/#", NAMESPACE, group_id); + std::string topic = stdx::format("{}/{}/#", NAMESPACE, group_id); MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; int rc = MQTTAsync_subscribe(client_.get(), topic.c_str(), config_.qos, &opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to subscribe: {}", rc)); + return stdx::unexpected(stdx::format("Failed to subscribe: {}", rc)); } return {}; @@ -426,13 +426,13 @@ HostApplication::subscribe_node(std::string_view group_id, return stdx::unexpected("Not connected"); } - std::string topic = std::format("{}/{}/+/{}/#", NAMESPACE, group_id, edge_node_id); + std::string topic = stdx::format("{}/{}/+/{}/#", NAMESPACE, group_id, edge_node_id); MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; int rc = MQTTAsync_subscribe(client_.get(), topic.c_str(), config_.qos, &opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to subscribe: {}", rc)); + return stdx::unexpected(stdx::format("Failed to subscribe: {}", rc)); } return {}; @@ -446,13 +446,13 @@ HostApplication::subscribe_state(std::string_view host_id) { return stdx::unexpected("Not connected"); } - std::string topic = std::format("{}/STATE/{}", NAMESPACE, host_id); + std::string topic = stdx::format("{}/STATE/{}", NAMESPACE, host_id); MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; int rc = MQTTAsync_subscribe(client_.get(), topic.c_str(), config_.qos, &opts); if (rc != MQTTASYNC_SUCCESS) { - return stdx::unexpected(std::format("Failed to subscribe: {}", rc)); + return stdx::unexpected(stdx::format("Failed to subscribe: {}", rc)); } return {}; @@ -525,8 +525,8 @@ bool HostApplication::validate_message( switch (topic.message_type) { case MessageType::NBIRTH: { if (payload.has_seq() && payload.seq() != 0) { - log(LogLevel::WARN, std::format("NBIRTH for {} has invalid seq: {} (expected 0)", - node_id, payload.seq())); + log(LogLevel::WARN, stdx::format("NBIRTH for {} has invalid seq: {} (expected 0)", + node_id, payload.seq())); return false; } @@ -542,7 +542,7 @@ bool HostApplication::validate_message( if (!has_bdseq) { log(LogLevel::WARN, - std::format("NBIRTH for {} missing required bdSeq metric", node_id)); + stdx::format("NBIRTH for {} missing required bdSeq metric", node_id)); return false; } @@ -573,8 +573,8 @@ bool HostApplication::validate_message( if (state.birth_received && bd_seq != state.bd_seq) { log(LogLevel::WARN, - std::format("NDEATH bdSeq mismatch for {} (NDEATH: {}, NBIRTH: {})", node_id, - bd_seq, state.bd_seq)); + stdx::format("NDEATH bdSeq mismatch for {} (NDEATH: {}, NBIRTH: {})", node_id, + bd_seq, state.bd_seq)); } state.is_online = false; @@ -583,7 +583,7 @@ bool HostApplication::validate_message( case MessageType::NDATA: { if (!state.birth_received) { - log(LogLevel::WARN, std::format("Received NDATA for {} before NBIRTH", node_id)); + log(LogLevel::WARN, stdx::format("Received NDATA for {} before NBIRTH", node_id)); return false; } @@ -593,8 +593,8 @@ bool HostApplication::validate_message( if (seq != expected_seq) { log(LogLevel::WARN, - std::format("Sequence number gap for {} (got {}, expected {})", node_id, seq, - expected_seq)); + stdx::format("Sequence number gap for {} (got {}, expected {})", node_id, seq, + expected_seq)); } state.last_seq = seq; @@ -606,7 +606,7 @@ bool HostApplication::validate_message( case MessageType::DBIRTH: { if (!state.birth_received) { log(LogLevel::WARN, - std::format("Received DBIRTH for device on {} before node NBIRTH", node_id)); + stdx::format("Received DBIRTH for device on {} before node NBIRTH", node_id)); return false; } @@ -616,7 +616,7 @@ bool HostApplication::validate_message( if (seq != expected_seq) { log(LogLevel::WARN, - std::format( + stdx::format( "Sequence number gap for DBIRTH device '{}' on {} (got {}, expected {})", topic.device_id, node_id, seq, expected_seq)); } @@ -643,16 +643,16 @@ bool HostApplication::validate_message( case MessageType::DDATA: { if (!state.birth_received) { log(LogLevel::WARN, - std::format("Received DDATA for device '{}' on {} before node NBIRTH", - topic.device_id, node_id)); + stdx::format("Received DDATA for device '{}' on {} before node NBIRTH", + topic.device_id, node_id)); return false; } auto device_it = state.devices.find(topic.device_id); if (device_it == state.devices.end() || !device_it->second.birth_received) { log(LogLevel::WARN, - std::format("Received DDATA for device '{}' on {} before DBIRTH", - topic.device_id, node_id)); + stdx::format("Received DDATA for device '{}' on {} before DBIRTH", + topic.device_id, node_id)); return false; } @@ -662,8 +662,9 @@ bool HostApplication::validate_message( if (seq != expected_seq) { log(LogLevel::WARN, - std::format("Sequence number gap for device '{}' on {} (got {}, expected {})", - topic.device_id, node_id, seq, expected_seq)); + stdx::format( + "Sequence number gap for device '{}' on {} (got {}, expected {})", + topic.device_id, node_id, seq, expected_seq)); } state.last_seq = seq; @@ -680,11 +681,11 @@ bool HostApplication::validate_message( device_it->second.offline_timestamp = payload.timestamp(); } device_it->second.metrics_stale = true; - log(LogLevel::DEBUG, std::format("Device {} offline, metrics stale on {}", - topic.device_id, node_id)); + log(LogLevel::DEBUG, stdx::format("Device {} offline, metrics stale on {}", + topic.device_id, node_id)); } else { - log(LogLevel::WARN, std::format("Received DDEATH for unknown device {} on {}", - topic.device_id, node_id)); + log(LogLevel::WARN, stdx::format("Received DDEATH for unknown device {} on {}", + topic.device_id, node_id)); } return true; } @@ -694,7 +695,7 @@ bool HostApplication::validate_message( case MessageType::STATE: return true; } - std::unreachable(); + stdx::unreachable(); } int HostApplication::on_message_arrived(void* context, @@ -713,7 +714,7 @@ int HostApplication::on_message_arrived(void* context, std::string topic_str(topicName, topicLen > 0 ? topicLen : strlen(topicName)); - std::string state_prefix = std::format("{}/STATE/", NAMESPACE); + std::string state_prefix = stdx::format("{}/STATE/", NAMESPACE); if (topic_str.starts_with(state_prefix)) { std::string state_value(static_cast(message->payload), message->payloadlen); @@ -740,7 +741,7 @@ int HostApplication::on_message_arrived(void* context, if (!topic_result) { host_app->log(LogLevel::DEBUG, - std::format("Ignoring non-Sparkplug topic: {}", topic_str)); + stdx::format("Ignoring non-Sparkplug topic: {}", topic_str)); MQTTAsync_freeMessage(&message); MQTTAsync_free(topicName); return 1; @@ -783,7 +784,7 @@ void HostApplication::on_connection_lost(void* context, char* cause) { } if (cause) { - host_app->log(LogLevel::WARN, std::format("Connection lost: {}", cause)); + host_app->log(LogLevel::WARN, stdx::format("Connection lost: {}", cause)); } else { host_app->log(LogLevel::WARN, "Connection lost"); } diff --git a/src/topic.cpp b/src/topic.cpp index 37762c8..090fe1f 100644 --- a/src/topic.cpp +++ b/src/topic.cpp @@ -1,10 +1,10 @@ // src/topic.cpp #include "sparkplug/topic.hpp" +#include "sparkplug/detail/compat.hpp" + #include -#include -#include -#include +#include #include namespace sparkplug { @@ -34,7 +34,7 @@ constexpr std::string_view message_type_to_string(MessageType type) noexcept { case MessageType::STATE: return "STATE"; } - std::unreachable(); + stdx::unreachable(); } stdx::expected parse_message_type(std::string_view str) { @@ -56,43 +56,50 @@ stdx::expected parse_message_type(std::string_view str return MessageType::DCMD; if (str == "STATE") return MessageType::STATE; - return stdx::unexpected(std::format("Unknown message type: {}", str)); + return stdx::unexpected(stdx::format("Unknown message type: {}", str)); +} + +// Simple string_view split without C++20 ranges +std::vector split_string_view(std::string_view str, char delim) { + std::vector parts; + size_t start = 0; + while (start < str.size()) { + size_t end = str.find(delim, start); + if (end == std::string_view::npos) { + parts.push_back(str.substr(start)); + break; + } + parts.push_back(str.substr(start, end - start)); + start = end + 1; + } + return parts; } + } // namespace std::string Topic::to_string() const { if (message_type == MessageType::STATE) { - return std::format("{}/STATE/{}", NAMESPACE, edge_node_id); + return stdx::format("{}/STATE/{}", NAMESPACE, edge_node_id); } - auto base = std::format("{}/{}/{}/{}", NAMESPACE, group_id, - message_type_to_string(message_type), edge_node_id); + auto base = stdx::format("{}/{}/{}/{}", NAMESPACE, group_id, + message_type_to_string(message_type), edge_node_id); if (!device_id.empty()) { - return std::format("{}/{}", base, device_id); + return stdx::format("{}/{}", base, device_id); } return base; } stdx::expected Topic::parse(std::string_view topic_str) { - // Parse without allocating vector - use iterators directly - auto parts = topic_str | std::views::split('/') | std::views::transform([](auto&& rng) { - return std::string_view(rng.begin(), - std::ranges::distance(rng.begin(), rng.end())); - }); + auto parts = split_string_view(topic_str, '/'); - auto it = parts.begin(); - auto end = parts.end(); - - if (it == end) { + if (parts.size() < 2) { return stdx::unexpected("Invalid topic format"); } - std::string_view part0 = *it++; - if (it == end) { - return stdx::unexpected("Invalid topic format"); - } - std::string_view part1 = *it++; + std::string_view part0 = parts[0]; + std::string_view part1 = parts[1]; // Sparkplug B topic: spBv1.0/{group_id}/{message_type}/{edge_node_id}[/{device_id}] // or STATE message: spBv1.0/STATE/{host_id} @@ -102,25 +109,22 @@ stdx::expected Topic::parse(std::string_view topic_str) { // Check for STATE message: spBv1.0/STATE/{host_id} if (part1 == "STATE") { - if (it == end) { + if (parts.size() < 3) { return stdx::unexpected("STATE topic requires host_id"); } - std::string_view host_id = *it++; + std::string_view host_id = parts[2]; return Topic{.group_id = "", .message_type = MessageType::STATE, .edge_node_id = std::string(host_id), .device_id = ""}; } - if (it == end) { + if (parts.size() < 4) { return stdx::unexpected("Invalid Sparkplug B topic"); } - std::string_view part2 = *it++; - if (it == end) { - return stdx::unexpected("Invalid Sparkplug B topic"); - } - std::string_view part3 = *it++; + std::string_view part2 = parts[2]; + std::string_view part3 = parts[3]; auto msg_type = parse_message_type(part2); if (!msg_type) { @@ -128,8 +132,8 @@ stdx::expected Topic::parse(std::string_view topic_str) { } std::string device_id; - if (it != end) { - device_id = std::string(*it); + if (parts.size() > 4) { + device_id = std::string(parts[4]); } return Topic{.group_id = std::string(part1), @@ -138,4 +142,4 @@ stdx::expected Topic::parse(std::string_view topic_str) { .device_id = std::move(device_id)}; } -} // namespace sparkplug \ No newline at end of file +} // namespace sparkplug diff --git a/tests/test_command_handling.cpp b/tests/test_command_handling.cpp index 40e878a..f4014e9 100644 --- a/tests/test_command_handling.cpp +++ b/tests/test_command_handling.cpp @@ -1,4 +1,5 @@ // tests/test_command_handling.cpp +#include // Tests for command handling (NCMD/DCMD) #include #include @@ -106,7 +107,7 @@ void test_ncmd_callback_invoked() { bool passed = command_received && rebirth_command; report_test("NCMD callback invoked", passed, passed ? "" - : std::format("Received: {}, Rebirth: {}", command_received.load(), + : fmt::format("Received: {}, Rebirth: {}", command_received.load(), rebirth_command.load())); (void)pub.disconnect(); @@ -193,7 +194,7 @@ void test_dcmd_callback_invoked() { (received_metric == "SetPoint"); report_test("DCMD callback invoked", passed, passed ? "" - : std::format("Received: {}, Device: {}, Metric: {}", + : fmt::format("Received: {}, Device: {}, Metric: {}", command_received.load(), received_device, received_metric)); @@ -283,7 +284,7 @@ void test_multiple_commands() { report_test("Multiple commands handled", passed, passed ? "" - : std::format("Count: {}, Rebirth: {}, Scan: {}", command_count.load(), + : fmt::format("Count: {}, Rebirth: {}, Scan: {}", command_count.load(), rebirth_cmd.load(), scan_rate.load())); (void)pub.disconnect(); @@ -355,7 +356,7 @@ void test_both_callbacks_invoked() { bool passed = regular_callback_invoked; report_test("Both callbacks invoked", passed, passed ? "" - : std::format("Regular CB: {}", regular_callback_invoked.load())); + : fmt::format("Regular CB: {}", regular_callback_invoked.load())); (void)pub.disconnect(); (void)sub.disconnect(); diff --git a/tests/test_compliance.cpp b/tests/test_compliance.cpp index e50ea6e..0170a1d 100644 --- a/tests/test_compliance.cpp +++ b/tests/test_compliance.cpp @@ -1,4 +1,5 @@ // tests/test_compliance.cpp +#include // Sparkplug 2.2 Compliance Tests #include #include @@ -52,7 +53,7 @@ void test_nbirth_sequence_zero() { bool passed = (pub.get_seq() == 0); report_test("NBIRTH sequence zero", passed, - passed ? "" : std::format("Got seq={}", pub.get_seq())); + passed ? "" : fmt::format("Got seq={}", pub.get_seq())); (void)pub.disconnect(); } @@ -85,7 +86,7 @@ void test_sequence_wraps() { data.add_metric("test", i); if (!pub.publish_data(data)) { report_test("Sequence wraps at 256", false, - std::format("Failed at iteration {}", i)); + fmt::format("Failed at iteration {}", i)); (void)pub.disconnect(); return; } @@ -93,7 +94,7 @@ void test_sequence_wraps() { bool passed = (pub.get_seq() == 0); report_test("Sequence wraps at 256", passed, - passed ? "" : std::format("Got seq={}", pub.get_seq())); + passed ? "" : fmt::format("Got seq={}", pub.get_seq())); (void)pub.disconnect(); } @@ -133,7 +134,7 @@ void test_bdseq_increment() { report_test("bdSeq increments on rebirth", passed, passed ? "" - : std::format("First={}, Second={}", first_bdseq, second_bdseq)); + : fmt::format("First={}, Second={}", first_bdseq, second_bdseq)); (void)pub.disconnect(); } @@ -383,7 +384,7 @@ void test_auto_sequence() { bool passed = (new_seq == 1 && prev_seq == 0); report_test("Auto sequence management", passed, - passed ? "" : std::format("Expected 0->1, got {}->{}", prev_seq, new_seq)); + passed ? "" : fmt::format("Expected 0->1, got {}->{}", prev_seq, new_seq)); (void)pub.disconnect(); } @@ -461,7 +462,7 @@ void test_dbirth_sequence_zero() { bool passed = got_dbirth && dbirth_seq == 1; report_test("DBIRTH sequence zero", passed, !got_dbirth ? "No DBIRTH received" - : std::format("Expected seq=1 (after NBIRTH seq=0), got seq={}", + : fmt::format("Expected seq=1 (after NBIRTH seq=0), got seq={}", dbirth_seq.load())); (void)pub.disconnect(); @@ -592,7 +593,7 @@ void test_device_sequence_shared() { report_test("Device and node share sequence", passed, !got_ddata ? "No DDATA received" - : std::format("Expected seq=3 for both, got DDATA seq={}, node seq={}", + : fmt::format("Expected seq=3 for both, got DDATA seq={}, node seq={}", ddata_seq.load(), pub.get_seq())); (void)pub.disconnect(); diff --git a/tests/test_device_apis.cpp b/tests/test_device_apis.cpp index 6285fad..828c17d 100644 --- a/tests/test_device_apis.cpp +++ b/tests/test_device_apis.cpp @@ -1,4 +1,5 @@ // tests/test_device_apis.cpp +#include // Tests for device-level Sparkplug B APIs (DBIRTH/DDATA/DDEATH) #include #include @@ -133,7 +134,7 @@ void test_dbirth_sequence_zero() { bool passed = found_dbirth && (dbirth_seq == 1); report_test("DBIRTH sequence zero", passed, passed ? "" - : std::format("Found: {}, Seq: {} (expected 1 after NBIRTH seq=0)", + : fmt::format("Found: {}, Seq: {} (expected 1 after NBIRTH seq=0)", found_dbirth.load(), dbirth_seq.load())); (void)pub.disconnect(); @@ -279,7 +280,7 @@ void test_device_sequence_shared() { report_test( "Device and node share sequence", passed, passed ? "" - : std::format("NDATA count={} seq={} (expected 5/10), DDATA count={} seq={} " + : fmt::format("NDATA count={} seq={} (expected 5/10), DDATA count={} seq={} " "(expected 5/11)", ndata_count.load(), last_ndata_seq.load(), ddata_count.load(), last_ddata_seq.load())); @@ -371,7 +372,7 @@ void test_ddata_sequence_increments() { ddata.add_metric_by_alias(1, 20.5 + i); if (!pub.publish_device_data("Device01", ddata)) { report_test("DDATA sequence increments (TCK)", false, - std::format("DDATA #{} failed", i + 1)); + fmt::format("DDATA #{} failed", i + 1)); (void)pub.disconnect(); (void)sub.disconnect(); return; @@ -392,18 +393,18 @@ void test_ddata_sequence_increments() { if (dbirth_seq != 1) { passed = false; error_msg = - std::format("DBIRTH seq={}, expected 1 (after NBIRTH seq=0)", dbirth_seq.load()); + fmt::format("DBIRTH seq={}, expected 1 (after NBIRTH seq=0)", dbirth_seq.load()); } else if (ddata_sequences.size() != 10) { passed = false; error_msg = - std::format("Received {} DDATA messages, expected 10", ddata_sequences.size()); + fmt::format("Received {} DDATA messages, expected 10", ddata_sequences.size()); } else { // Check each DDATA sequence increments correctly for (size_t i = 0; i < ddata_sequences.size(); i++) { uint64_t expected_seq = i + 2; // First DDATA should be 2 (after DBIRTH=1) if (ddata_sequences[i] != expected_seq) { passed = false; - error_msg = std::format("DDATA #{} has seq={}, expected {}", i + 1, + error_msg = fmt::format("DDATA #{} has seq={}, expected {}", i + 1, ddata_sequences[i], expected_seq); break; }