diff --git a/README.md b/README.md index 4259e096d..ffa495045 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ + + # Communication Module (LoLa) [![Eclipse Score](https://img.shields.io/badge/Eclipse-Score-orange.svg)](https://eclipse-score.github.io/score/main/modules/communication/index.html) diff --git a/score/mw/com/README.md b/score/mw/com/README.md index e16ae063f..3ae235f0e 100644 --- a/score/mw/com/README.md +++ b/score/mw/com/README.md @@ -1,3 +1,17 @@ + + # Communication Middleware (mw::com) ## Overview diff --git a/score/mw/com/gateway/gateway_application/BUILD b/score/mw/com/gateway/gateway_application/BUILD index 900974b99..904c7f845 100644 --- a/score/mw/com/gateway/gateway_application/BUILD +++ b/score/mw/com/gateway/gateway_application/BUILD @@ -97,3 +97,16 @@ cc_unit_test( "@score_baselibs//score/result", ], ) + +cc_library( + name = "gateway_core_mock", + hdrs = ["gateway_core_mock.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + ":gateway_core", + "@googletest//:gtest", + ], +) diff --git a/score/mw/com/gateway/gateway_application/gateway_core_mock.h b/score/mw/com/gateway/gateway_application/gateway_core_mock.h new file mode 100644 index 000000000..0f8080fc7 --- /dev/null +++ b/score/mw/com/gateway/gateway_application/gateway_core_mock.h @@ -0,0 +1,49 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_MW_COM_GATEWAY_CORE_MOCK_H +#define SCORE_MW_COM_GATEWAY_CORE_MOCK_H + +#include "score/mw/com/gateway/gateway_application/gateway_core.h" + +#include + +namespace score::mw::com::gateway +{ + +class GatewayCoreMock : public GatewayCore +{ + public: + MOCK_METHOD((score::Result), + ProvideService, + (impl::InstanceSpecifier, std::vector), + (override)); + MOCK_METHOD((score::Result), OfferService, (impl::InstanceSpecifier), (override)); + MOCK_METHOD(void, StopOfferService, (impl::InstanceSpecifier), (override)); + MOCK_METHOD((score::Result), + NotifyUpdate, + (impl::InstanceSpecifier, impl::ServiceElementType, std::string), + (override)); + MOCK_METHOD((score::Result), + RegisterUpdateNotification, + (impl::InstanceSpecifier, impl::ServiceElementType, std::string), + (override)); + MOCK_METHOD((score::Result), + UnregisterUpdateNotification, + (impl::InstanceSpecifier, impl::ServiceElementType, std::string), + (override)); +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_CORE_MOCK_H diff --git a/score/mw/com/gateway/transport_layer/BUILD b/score/mw/com/gateway/transport_layer/BUILD index 72eccd0ec..61f780b94 100644 --- a/score/mw/com/gateway/transport_layer/BUILD +++ b/score/mw/com/gateway/transport_layer/BUILD @@ -57,6 +57,9 @@ cc_library( deps = [ ":transport", "//score/mw/com/gateway/gateway_application:gateway_core", + "//score/mw/com/gateway/transport_layer/sample:bidirectional_transport", + "//score/mw/com/gateway/transport_layer/sample:sample_hypervisor_transport", + "//score/mw/com/gateway/transport_layer/sample/configuration:hypervisor_socket_configuration", "//score/mw/com/gateway/transport_layer/sample/configuration:sample_transport_config_parser", ], ) diff --git a/score/mw/com/gateway/transport_layer/sample/BUILD b/score/mw/com/gateway/transport_layer/sample/BUILD new file mode 100644 index 000000000..1e5855ff7 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/BUILD @@ -0,0 +1,154 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_library") +load("//quality/unit_testing:unit_testing.bzl", "cc_unit_test") +load("//score/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "i_bidirectional_transport", + hdrs = ["i_bidirectional_transport.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + "//score/mw/com/gateway/transport_layer/sample/messages:gateway_messages", + "@score_baselibs//score/result", + ], +) + +cc_library( + name = "bidirectional_transport", + srcs = ["bidirectional_transport.cpp"], + hdrs = [ + "bidirectional_transport.h", + ], + features = COMPILER_WARNING_FEATURES, + implementation_deps = [ + "@score_baselibs//score/mw/log", + "@score_baselibs//score/os:socket", + ], + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + ":i_bidirectional_transport", + "//score/mw/com/gateway/transport_layer:transport_error", + "//score/mw/com/gateway/transport_layer/sample:unique_socket", + "//score/mw/com/gateway/transport_layer/sample/configuration:hypervisor_socket_configuration", + "//score/mw/com/gateway/transport_layer/sample/messages:gateway_messages", + "@score_baselibs//score/concurrency:long_running_threads_container", + "@score_baselibs//score/os:unistd", + ], +) + +cc_library( + name = "sample_hypervisor_transport", + srcs = ["sample_hypervisor_transport.cpp"], + hdrs = [ + "sample_hypervisor_transport.h", + ], + features = COMPILER_WARNING_FEATURES, + implementation_deps = [ + "//score/mw/com/gateway/transport_layer/sample/messages:gateway_messages", + "//score/mw/com/impl:runtime", + "@score_baselibs//score/mw/log", + ], + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + "//score/mw/com/gateway/gateway_application:gateway_core", + "//score/mw/com/gateway/transport_layer:transport", + "//score/mw/com/gateway/transport_layer/sample:i_bidirectional_transport", + ], +) + +cc_library( + name = "unique_socket", + srcs = ["unique_socket.cpp"], + hdrs = [ + "unique_socket.h", + ], + features = COMPILER_WARNING_FEATURES, + implementation_deps = [ + "@score_baselibs//score/mw/log", + ], + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + "@score_baselibs//score/os:unistd", + ], +) + +cc_unit_test( + name = "unique_socket_test", + srcs = ["unique_socket_test.cpp"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + ":unique_socket", + "@score_baselibs//score/os/mocklib:unistd_mock", + ], +) + +cc_unit_test( + name = "bidirectional_transport_test", + srcs = [ + "bidirectional_transport_test.cpp", + "sample_transport_test_resources.h", + ], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + ":bidirectional_transport", + "@score_baselibs//score/os/mocklib:socket_mock", + ], +) + +cc_unit_test( + name = "sample_hypervisor_transport_test", + srcs = ["sample_hypervisor_transport_test.cpp"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + ":bidirectional_transport_mock", + ":sample_hypervisor_transport", + "//score/mw/com/gateway/gateway_application:gateway_core_mock", + "//score/mw/com/gateway/transport_layer:transport_error", + "//score/mw/com/impl/test:dummy_instance_identifier_builder", + "//score/mw/com/impl/test:runtime_mock_guard", + "@score_baselibs//score/mw/log", + "@score_baselibs//score/mw/log:recorder_mock", + ], +) + +cc_library( + name = "bidirectional_transport_mock", + hdrs = ["bidirectional_transport_mock.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + ":i_bidirectional_transport", + "@googletest//:gtest", + ], +) diff --git a/score/mw/com/gateway/transport_layer/sample/README.md b/score/mw/com/gateway/transport_layer/sample/README.md new file mode 100644 index 000000000..cb4155d29 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/README.md @@ -0,0 +1,32 @@ + + +# Gateway Transport Layer Sample Implementation + +This module provides a sample implementation of a mw::com gateway's transport layer based on regular POSIX network sockets +for communication. Since this communication mechanism should be available in most HyperVisor environments, we decided to +add this as a sample implementation. + +## Vendor-specific implementation details + +Parts that interact with the underlying shared memory have not been implemented as part of this sample implementation, +as these parts are vendor-specific and depend on the used HyperVisor and its shared memory abilities. +This includes the following methods: + +- `ShmPaths score::mw::com::gateway::ResolveShmPaths(const impl::InstanceSpecifier&)` where a path to access the HV-shared memory region has to be created. +- `ShmSizes score::mw::com::gateway::GetShmSizes(const impl::InstanceSpecifier&)` where the size of the shared memory region has to be determined. +- `void SampleHyperVisorTransport::PreCreateInterVmSharedMemory(const impl::InstanceSpecifier& specifier, + std::uint32_t shm_control_size, + std::uint32_t shm_data_size)` where the shared memory region needs to be opened diff --git a/score/mw/com/gateway/transport_layer/sample/bidirectional_transport.cpp b/score/mw/com/gateway/transport_layer/sample/bidirectional_transport.cpp new file mode 100644 index 000000000..311322862 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/bidirectional_transport.cpp @@ -0,0 +1,668 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h" + +#include "score/concurrency/simple_task.h" +#include "score/mw/log/logging.h" +#include "score/os/socket.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace score::mw::com::gateway +{ + +namespace +{ + +using score::os::Socket; + +std::unique_ptr CreateMessageForType(MessageType type) +{ + switch (type) + { + case MessageType::kProvideServiceRequest: + return std::make_unique(); + case MessageType::kOfferServiceRequest: + return std::make_unique(); + case MessageType::kStopOfferServiceRequest: + return std::make_unique(); + case MessageType::kRegisterNotificationRequest: + return std::make_unique(); + case MessageType::kUnregisterNotificationRequest: + return std::make_unique(); + case MessageType::kUpdateNotification: + return std::make_unique(); + case MessageType::kAckResponse: + return std::make_unique(); + case MessageType::kInvalid: + default: + return nullptr; + } +} + +void SetTcpNoDelay(std::int32_t socket_fd) +{ + std::int32_t flag = 1; + const auto socket_result = Socket::instance().setsockopt(socket_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)); + if (!socket_result.has_value()) + { + ::score::mw::log::LogError() << "Failed to set TCP_NODELAY: " << socket_result.error().ToString(); + return; + } +} + +int SendAll(std::int32_t socket_fd, const void* data, std::size_t length) +{ + auto* ptr = static_cast(data); + while (length > 0) + { + auto result = Socket::instance().sendto(socket_fd, ptr, length, Socket::MessageFlag::kNone, nullptr, 0); + if (!result.has_value()) + { + return -1; + } + const auto sent = static_cast(result.value()); + ptr += sent; + length -= sent; + } + return 0; +} + +ssize_t ReceiveAll(std::int32_t socket_fd, void* buffer, std::size_t length) +{ + auto* ptr = static_cast(buffer); + const auto original_length = length; + const score::os::Socket::MessageFlag flags{}; + while (length > 0) + { + auto result = Socket::instance().recv(socket_fd, ptr, length, flags); + if (!result.has_value()) + { + ::score::mw::log::LogError() << "ReceiveAll: recv error: " << result.error().ToString() + << " fd=" << socket_fd << " remaining=" << length; + return -1; + } + if (result.value() == 0) + { + ::score::mw::log::LogWarn() << "ReceiveAll: peer closed connection, fd=" << socket_fd + << " remaining=" << length << " of " << original_length; + return 0; + } + const auto received = static_cast(result.value()); + ptr += received; + length -= received; + } + return static_cast(original_length); +} + +} // namespace + +BidirectionalTransport::BidirectionalTransport(HyperVisorSocketConfiguration socket_config) noexcept + : socket_config_(std::move(socket_config)) +{ +} + +BidirectionalTransport::~BidirectionalTransport() +{ + Shutdown(); +} + +score::ResultBlank BidirectionalTransport::Setup() +{ + auto listen_result = SetupListenSocket(); + if (!listen_result.has_value()) + { + score::mw::log::LogError() << "BidirectionalTransport: failed to set up listen socket"; + return listen_result; + } + threads_.Enqueue(score::concurrency::SimpleTaskFactory::Make(score::cpp::pmr::get_default_resource(), + [this](const score::cpp::stop_token stop_token) { + this->ConnectionLoop(stop_token); + })); + threads_.Enqueue(score::concurrency::SimpleTaskFactory::Make(score::cpp::pmr::get_default_resource(), + [this](const score::cpp::stop_token stop_token) { + this->DispatchLoop(stop_token); + })); + + // we block the connection loop until the first connection is established to ensure that Setup() only returns once + // the transport is actually ready to send and receive messages + while (!is_connected_ && !threads_.ShutdownRequested()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + if (!is_connected_) + { + Shutdown(); + return score::MakeUnexpected(TransportErrorc::kConnectionFailure); + } + + return {}; +} + +score::ResultBlank BidirectionalTransport::SetupSendSocket(score::cpp::stop_token stop_token) +{ + auto socket_result = Socket::instance().socket(Socket::Domain::kIPv4, SOCK_STREAM, 0); + if (!socket_result.has_value()) + { + return score::MakeUnexpected(TransportErrorc::kConnectionFailure); + } + UniqueSocket socket(socket_result.value()); + + SetTcpNoDelay(socket.Get()); + + // Setup remote address structure from configuration + sockaddr_in remote_addr{}; + remote_addr.sin_family = AF_INET; + remote_addr.sin_port = htons(socket_config_.remote_port_); + const auto ip_bytes = socket_config_.remote_ip_.ToIpv4Bytes(); + std::memcpy(&remote_addr.sin_addr, ip_bytes.data(), sizeof(remote_addr.sin_addr)); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) required by POSIX sockaddr API + const auto* remote_addr_ptr = reinterpret_cast(&remote_addr); + + constexpr auto kRetryInterval = std::chrono::milliseconds(50); + + while (!stop_token.stop_requested()) + { + auto connect_result = Socket::instance().connect(socket.Get(), remote_addr_ptr, sizeof(remote_addr)); + if (connect_result.has_value()) + { + send_socket_ = std::move(socket); + return {}; + } + // retry indefinitely until stop is requested for any errors, could potentially hang here if the remote side is + // never available + score::mw::log::LogDebug() << "BidirectionalTransport: connect failed, retrying in " << kRetryInterval.count() + << " ms: " << connect_result.error().ToString(); + std::this_thread::sleep_for(kRetryInterval); + + // Creating a new socket for each retry since the previous connect attempt might have put the socket into an + // unusable state. This is especially important if the remote side is not up yet, since in that case we expect + // multiple retries and want to avoid issues with reusing a socket that has been put into an error state by the + // failed connect attempt. + auto new_sock_result = Socket::instance().socket(Socket::Domain::kIPv4, SOCK_STREAM, 0); + if (!new_sock_result.has_value()) + { + ::score::mw::log::LogError() << "BidirectionalTransport: failed to recreate socket for retry"; + return score::MakeUnexpected(TransportErrorc::kConnectionFailure); + } + socket.Reset(new_sock_result.value()); + SetTcpNoDelay(socket.Get()); + } + + ::score::mw::log::LogError() << "BidirectionalTransport: connect aborted"; + return score::MakeUnexpected(TransportErrorc::kConnectionFailure); +} + +score::ResultBlank BidirectionalTransport::SetupListenSocket() +{ + auto socket_result = Socket::instance().socket(Socket::Domain::kIPv4, SOCK_STREAM | SOCK_NONBLOCK, 0); + if (!socket_result.has_value()) + { + return score::MakeUnexpected(TransportErrorc::kConnectionFailure); + } + + UniqueSocket socket(socket_result.value()); + + constexpr std::int32_t opt = 1; + std::ignore = Socket::instance().setsockopt(socket.Get(), SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in local_addr{}; + local_addr.sin_family = AF_INET; + local_addr.sin_addr.s_addr = INADDR_ANY; + local_addr.sin_port = htons(socket_config_.local_port_); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) required by POSIX sockaddr API + const auto* local_addr_ptr = reinterpret_cast(&local_addr); + + auto bind_result = Socket::instance().bind(socket.Get(), local_addr_ptr, sizeof(local_addr)); + if (!bind_result.has_value()) + { + ::score::mw::log::LogError() << "BidirectionalTransport: bind failed on port " << socket_config_.local_port_; + return score::MakeUnexpected(TransportErrorc::kConnectionFailure); + } + constexpr std::int32_t backlog = 1; + auto listen_result = Socket::instance().listen(socket.Get(), backlog); + if (!listen_result.has_value()) + { + return score::MakeUnexpected(TransportErrorc::kConnectionFailure); + } + listen_socket_ = std::move(socket); + return {}; +} + +void BidirectionalTransport::ConnectionLoop(score::cpp::stop_token stop_token) +{ + while (!stop_token.stop_requested()) + { + // This is used to handle the case where the connection is lost and we need to re-establish it. In that case we + // need to set up a new listen socket since the previous one would have been used to accept the connection that + // got lost and is now in an unusable state. By setting up a new listen socket we can ensure that we are able to + // accept a new connection when the remote side comes back up. + if (!listen_socket_.IsValid()) + { + auto result = SetupListenSocket(); + if (!result.has_value()) + { + ::score::mw::log::LogError() << "BidirectionalTransport: failed to re-establish listen socket"; + return; + } + } + + auto send_task_result = threads_.Submit([this](score::cpp::stop_token st) { + return SetupSendSocket(st); + }); + + const bool receive_connected = WaitForConnection(stop_token); + + const auto send_result = send_task_result.Get(); + + if (!receive_connected || !send_result.has_value()) + { + CleanupSocketsForReconnection(); + break; // Shutdown has been requested or setup failed. + } + + is_connected_ = true; + ReceiveUntilDisconnect(stop_token); + + CleanupSocketsForReconnection(); + if (!stop_token.stop_requested()) + { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } +} + +bool BidirectionalTransport::WaitForConnection(score::cpp::stop_token stop_token) +{ + struct sockaddr_in client_addr{}; + socklen_t client_len = sizeof(client_addr); + + while (!stop_token.stop_requested()) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) required by POSIX sockaddr API + auto accept_result = Socket::instance().accept( + listen_socket_.Get(), reinterpret_cast(&client_addr), &client_len); + + if (!accept_result.has_value()) + { + // EAGAIN and EWOULDBLOCK are expected since the listen socket is non-blocking, so we just continue waiting + // in that case. For other errors we still continue trying to accept new connections since + // transient errors can occur and we want to be resilient against them. + if (accept_result.error() == score::os::Error::createFromErrno(EAGAIN) || + accept_result.error() == score::os::Error::createFromErrno(EWOULDBLOCK)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + if (!stop_token.stop_requested()) + { + ::score::mw::log::LogError() << "BidirectionalTransport: accept failed"; + } + continue; + } + + UniqueSocket client_sock(accept_result.value()); + SetTcpNoDelay(client_sock.Get()); + + // On QNX, accepted sockets inherit SOCK_NONBLOCK from the listen socket. + // Clear it so recv() blocks properly instead of returning EAGAIN immediately. + int flags = fcntl(client_sock.Get(), F_GETFL, 0); + if (flags != -1) + { + fcntl(client_sock.Get(), F_SETFL, flags & ~O_NONBLOCK); + } + + receive_socket_ = std::move(client_sock); + listen_socket_.Reset(); + return true; + } + return false; +} + +void BidirectionalTransport::ReceiveUntilDisconnect(score::cpp::stop_token stop_token) +{ + while (!stop_token.stop_requested() && is_connected_) + { + auto message = ReceiveMessageFromSocket(receive_socket_); + if (message == nullptr) + { + if (!stop_token.stop_requested()) + { + is_connected_ = false; + ::score::mw::log::LogWarn() << "BidirectionalTransport: disconnected, will attempt to reconnect"; + } + return; + } + HandleIncomingMessage(std::move(message)); + } +} + +void BidirectionalTransport::HandleIncomingMessage(std::unique_ptr message) +{ + if (IsResponse(message->GetType())) + { + HandleResponse(std::move(message)); + return; + } + + if (!has_message_handler_) + { + ::score::mw::log::LogWarn() << "BidirectionalTransport: received message but no handler registered to handle " + "it: messages are dropped. Message type: " + << static_cast(message->GetType()); + return; + } + + if (RequiresResponse(message->GetType())) + { + SendAck(message->GetSequenceNumber()); + } + + // Post to dispatch queue rather than calling the handler inline. This keeps the + // receive loop free to process incoming ACKs while the handler (which may itself + // call SendRequest) runs on the dedicated dispatch thread. + { + std::lock_guard lock(dispatch_mutex_); + dispatch_queue_.push(std::move(message)); + } + dispatch_cv_.notify_one(); +} + +void BidirectionalTransport::CleanupSocketsForReconnection() +{ + is_connected_ = false; + { + std::lock_guard lock(send_mutex_); + send_socket_.ShutdownAndReset(); + } + receive_socket_.ShutdownAndReset(); + pending_cv_.notify_all(); +} + +void BidirectionalTransport::DispatchLoop(score::cpp::stop_token /*stop_token*/) +{ + while (true) + { + std::unique_ptr message; + { + std::unique_lock lock(dispatch_mutex_); + dispatch_cv_.wait(lock, [this] { + return !dispatch_queue_.empty() || dispatch_shutdown_.load(); + }); + + if (dispatch_shutdown_.load() && dispatch_queue_.empty()) + { + break; + } + + message = std::move(dispatch_queue_.front()); + dispatch_queue_.pop(); + } + + message_handler_(std::move(message)); + } +} + +void BidirectionalTransport::HandleResponse(std::unique_ptr response) +{ + if (response->GetType() != MessageType::kAckResponse) + { + // Just a sanity check, we should only receive AckResponses as responses since that's the only response type we + // have. + return; + } + + const auto* ack = static_cast(response.get()); + + std::lock_guard lock(pending_mutex_); + auto it = pending_requests_.find(ack->GetAckedSequence()); + if (it != pending_requests_.end()) + { + it->second.acknowledged = true; + pending_cv_.notify_all(); + } +} + +score::ResultBlank BidirectionalTransport::SendAck(const std::uint32_t sequence) +{ + AckResponse ack_response(sequence); + std::lock_guard lock(send_mutex_); + return SendMessageOnSocket(send_socket_, ack_response); +} + +std::uint32_t BidirectionalTransport::GetNextSequenceNumber() +{ + return next_sequence_.fetch_add(1U, std::memory_order_relaxed); +} + +score::ResultBlank BidirectionalTransport::TrySendAndWaitForAck(TransportMessage& message, const std::uint32_t sequence) +{ + { + std::lock_guard lock(send_mutex_); + auto send_result = SendMessageOnSocket(send_socket_, message); + if (!send_result.has_value()) + { + score::mw::log::LogError() << "BidirectionalTransport: failed to send message of type " + << static_cast(message.GetType()); + return send_result; + } + } + + std::unique_lock lock(pending_mutex_); + auto it = pending_requests_.find(sequence); + if (it == pending_requests_.end()) + { + score::mw::log::LogError() << "BidirectionalTransport: internal error: pending request not found for sequence " + << sequence; + return score::MakeUnexpected(TransportErrorc::kSendFailure); + } + + pending_cv_.wait_for(lock, std::chrono::milliseconds(socket_config_.request_timeout_ms_), [&it, this] { + return it->second.acknowledged || !is_connected_.load(); + }); + + if (it->second.acknowledged) + { + score::mw::log::LogDebug() << "BidirectionalTransport: received ack for sequence " << sequence; + return {}; + } + + if (!is_connected_.load()) + { + ::score::mw::log::LogError() << "BidirectionalTransport: connection lost while waiting for ack for sequence " + << sequence; + return score::MakeUnexpected(TransportErrorc::kNotConnected); + } + + ::score::mw::log::LogError() << "BidirectionalTransport: timeout waiting for ack for sequence " << sequence; + return score::MakeUnexpected(TransportErrorc::kTimeout, + "timeout waiting for ack for sequence " + std::to_string(sequence)); +} + +score::ResultBlank BidirectionalTransport::SendRequest(TransportMessage& message) +{ + if (!is_connected_) + { + return score::MakeUnexpected(TransportErrorc::kNotConnected, "BidirectionalTransport: not connected"); + } + + const std::uint32_t sequence = GetNextSequenceNumber(); + message.SetSequenceNumber(sequence); + + { + std::lock_guard lock(pending_mutex_); + pending_requests_[sequence] = PendingRequest{}; + } + + score::ResultBlank last_result = score::MakeUnexpected(TransportErrorc::kSendFailure); + for (std::size_t attempt = 0U; attempt < kMaxRetries; ++attempt) + { + if (!is_connected_) + { + last_result = score::MakeUnexpected(TransportErrorc::kNotConnected); + break; + } + + if (attempt > 0U) + { + ::score::mw::log::LogWarn() << "BidirectionalTransport: request timeout or failure, retrying (" << attempt + << "/" << kMaxRetries << ")"; + std::lock_guard lock(pending_mutex_); + pending_requests_[sequence].acknowledged = false; + } + + last_result = TrySendAndWaitForAck(message, sequence); + if (last_result.has_value()) + { + std::lock_guard lock(pending_mutex_); + // erase the pending request since we got the ack successfully. + pending_requests_.erase(sequence); + return {}; + } + + // Don't retry on disconnect — the remote has no knowledge of this pending request + if (!is_connected_) + { + break; + } + } + + std::lock_guard lock(pending_mutex_); + pending_requests_.erase(sequence); // erase the pending request since we are giving up after max retries. + return last_result; +} + +score::ResultBlank BidirectionalTransport::SendNotification(TransportMessage& message) +{ + if (!is_connected_) + { + return score::MakeUnexpected(TransportErrorc::kNotConnected); + } + + std::lock_guard lock(send_mutex_); + return SendMessageOnSocket(send_socket_, message); +} + +void BidirectionalTransport::SetMessageHandler(MessageHandler handler) +{ + message_handler_ = std::move(handler); + has_message_handler_ = true; +} + +bool BidirectionalTransport::IsConnected() const +{ + return is_connected_.load(); +} + +void BidirectionalTransport::Shutdown() +{ + is_connected_ = false; + + pending_cv_.notify_all(); + + send_socket_.ShutdownFd(); + receive_socket_.ShutdownFd(); + listen_socket_.ShutdownFd(); + + dispatch_shutdown_ = true; + dispatch_cv_.notify_all(); + + threads_.Shutdown(); +} + +score::ResultBlank BidirectionalTransport::SendMessageOnSocket(const UniqueSocket& socket, + const TransportMessage& message) +{ + const auto payload_size = message.Serialize(send_buffer_); + if (payload_size == 0U) + { + return score::MakeUnexpected(TransportErrorc::kSendFailure); + } + + MessageHeader header = message.GetHeader(); + header.payload_size = static_cast(payload_size); + + std::uint8_t header_buf[MessageHeader::kWireSize]{}; + header.SerializeToBuffer(header_buf); + + if (SendAll(socket.Get(), header_buf, MessageHeader::kWireSize) != 0) + { + return score::MakeUnexpected(TransportErrorc::kSendFailure); + } + + if (payload_size > 0U && SendAll(socket.Get(), send_buffer_.data(), payload_size) != 0) + { + return score::MakeUnexpected(TransportErrorc::kSendFailure); + } + + return {}; +} + +std::unique_ptr BidirectionalTransport::ReceiveMessageFromSocket(const UniqueSocket& socket) +{ + std::uint8_t header_buf[MessageHeader::kWireSize]{}; + if (ReceiveAll(socket.Get(), header_buf, MessageHeader::kWireSize) <= 0) + { + return nullptr; + } + + MessageHeader header{}; + header.DeserializeFromBuffer(header_buf); + + if (header.payload_size > kMaxPayloadSize) + { + ::score::mw::log::LogError() << "BidirectionalTransport: payload size " << header.payload_size + << " exceeds maximum " << kMaxPayloadSize; + return nullptr; + } + + if (header.payload_size > 0U) + { + const auto bytes_received = ReceiveAll(socket.Get(), receive_buffer_.data(), header.payload_size); + if (bytes_received <= 0) + { + return nullptr; + } + } + + auto message = CreateMessageForType(header.type); + if (message == nullptr) + { + ::score::mw::log::LogError() << "BidirectionalTransport: unknown message type " + << static_cast(header.type); + return nullptr; + } + + message->SetSequenceNumber(header.sequence); + + if (header.payload_size > 0U && + !message->Deserialize(score::cpp::span(receive_buffer_.data(), header.payload_size))) + { + ::score::mw::log::LogError() << "BidirectionalTransport: deserialization failed"; + return nullptr; + } + + return message; +} + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h b/score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h new file mode 100644 index 000000000..210bfcdee --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h @@ -0,0 +1,126 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_BIDIRECTIONAL_TRANSPORT_H_ +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_BIDIRECTIONAL_TRANSPORT_H_ + +#include "score/concurrency/long_running_threads_container.h" +#include "score/mw/com/gateway/transport_layer/sample/configuration/hypervisor_socket_configuration.h" +#include "score/mw/com/gateway/transport_layer/sample/i_bidirectional_transport.h" +#include "score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h" +#include "score/mw/com/gateway/transport_layer/transport_error.h" + +#include "score/mw/com/gateway/transport_layer/sample/unique_socket.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace score::mw::com::gateway +{ + +class BidirectionalTransport : public IBidirectionalTransport +{ + friend class BidirectionalTransportAttorney; // For testing purposes only + + public: + explicit BidirectionalTransport(HyperVisorSocketConfiguration socket_config) noexcept; + ~BidirectionalTransport() override; + + BidirectionalTransport(const BidirectionalTransport&) = delete; + BidirectionalTransport& operator=(const BidirectionalTransport&) = delete; + BidirectionalTransport(BidirectionalTransport&&) = delete; + BidirectionalTransport& operator=(BidirectionalTransport&&) = delete; + + score::ResultBlank Setup() override; + void Shutdown() override; + + bool IsConnected() const override; + + score::ResultBlank SendRequest(TransportMessage& message) override; + score::ResultBlank SendNotification(TransportMessage& message) override; + + void SetMessageHandler(MessageHandler handler) override; + + private: + struct PendingRequest + { + bool acknowledged{false}; + }; + + score::ResultBlank TrySendAndWaitForAck(TransportMessage& message, const std::uint32_t sequence); + + score::ResultBlank SetupSendSocket(score::cpp::stop_token stop_token); + score::ResultBlank SetupListenSocket(); + + void ConnectionLoop(score::cpp::stop_token stop_token); + bool WaitForConnection(score::cpp::stop_token stop_token); + void ReceiveUntilDisconnect(score::cpp::stop_token stop_token); + void CleanupSocketsForReconnection(); + void DispatchLoop(score::cpp::stop_token stop_token); + + void HandleIncomingMessage(std::unique_ptr message); + void HandleResponse(std::unique_ptr response); + + score::ResultBlank SendAck(const std::uint32_t sequence); + + std::uint32_t GetNextSequenceNumber(); + + score::ResultBlank SendMessageOnSocket(const UniqueSocket& socket, const TransportMessage& message); + std::unique_ptr ReceiveMessageFromSocket(const UniqueSocket& socket); + + HyperVisorSocketConfiguration socket_config_; + + UniqueSocket send_socket_; + std::mutex send_mutex_; + + UniqueSocket listen_socket_; + UniqueSocket receive_socket_; + + std::atomic is_connected_{false}; + + MessageHandler message_handler_; + bool has_message_handler_{false}; + + // Dispatch queue: incoming non-ACK messages are pushed here by the receive loop and + // processed by a dedicated dispatch thread. This decouples the receive loop from the + // message handler, so the handler can call SendRequest() without blocking ACK reception. + std::queue> dispatch_queue_; + std::mutex dispatch_mutex_; + std::condition_variable dispatch_cv_; + std::atomic dispatch_shutdown_{false}; + + std::mutex pending_mutex_; + std::condition_variable pending_cv_; + std::atomic next_sequence_{1U}; + std::map pending_requests_; + + static constexpr int kMaxRetries = 5U; + static constexpr std::size_t kMaxPayloadSize = 1024U; + + std::array send_buffer_{}; + std::array receive_buffer_{}; + + // Intentionally keeping at the end to ensure that it's destroyed first during shutdown. + score::concurrency::LongRunningThreadsContainer threads_; +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_BIDIRECTIONAL_TRANSPORT_H_ diff --git a/score/mw/com/gateway/transport_layer/sample/bidirectional_transport_mock.h b/score/mw/com/gateway/transport_layer/sample/bidirectional_transport_mock.h new file mode 100644 index 000000000..216a20d61 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/bidirectional_transport_mock.h @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_MW_COM_GATEWAY_BIDIRECTIONAL_TRANSPORT_MOCK_H +#define SCORE_MW_COM_GATEWAY_BIDIRECTIONAL_TRANSPORT_MOCK_H + +#include "score/mw/com/gateway/transport_layer/sample/i_bidirectional_transport.h" + +#include + +namespace score::mw::com::gateway +{ + +class BidirectionalTransportMock : public IBidirectionalTransport +{ + public: + MOCK_METHOD((score::ResultBlank), Setup, (), (override)); + MOCK_METHOD((void), Shutdown, (), (override)); + MOCK_METHOD((bool), IsConnected, (), (const, override)); + MOCK_METHOD((score::ResultBlank), SendRequest, (TransportMessage&), (override)); + MOCK_METHOD((score::ResultBlank), SendNotification, (TransportMessage&), (override)); + MOCK_METHOD((void), SetMessageHandler, (MessageHandler), (override)); +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_BIDIRECTIONAL_TRANSPORT_MOCK_H diff --git a/score/mw/com/gateway/transport_layer/sample/bidirectional_transport_test.cpp b/score/mw/com/gateway/transport_layer/sample/bidirectional_transport_test.cpp new file mode 100644 index 000000000..712872998 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/bidirectional_transport_test.cpp @@ -0,0 +1,1113 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h" +#include "score/mw/com/gateway/transport_layer/sample/sample_transport_test_resources.h" + +#include "score/os/mocklib/socketmock.h" +#include "score/os/socket.h" + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace score::mw::com::gateway +{ + +using ::testing::_; +using ::testing::Invoke; +using ::testing::Return; + +namespace +{ + +// Test message that intentionally fails to serialize and deserialize +class ZeroSerializeMessage : public TransportMessage +{ + public: + ZeroSerializeMessage() : TransportMessage(MessageType::kStopOfferServiceRequest) {} + std::size_t Serialize(score::cpp::span) const override + { + return 0U; + } + bool Deserialize(score::cpp::span) override + { + return false; + } +}; + +} // namespace + +class BidirectionalTransportFixture : public ::testing::Test +{ + public: + void SetUp() override + { + score::os::Socket::set_testing_instance(socket_mock_); + } + + void TearDown() override + { + if (transport_ != nullptr) + { + transport_->Shutdown(); + } + score::os::Socket::restore_instance(); + } + + BidirectionalTransportFixture& CreateTransport(std::uint32_t timeout_ms = 50U) + { + auto config = HyperVisorSocketConfiguration{score::os::Ipv4Address{"127.0.0.1"}}; + config.request_timeout_ms_ = timeout_ms; + transport_ = std::make_shared(std::move(config)); + return *this; + } + + BidirectionalTransportFixture& ExpectSendtoWritesAllBytes(std::int32_t fd) + { + EXPECT_CALL(socket_mock_, sendto(fd, _, _, _, _, _)) + .WillRepeatedly(Invoke([](auto, auto, const std::size_t len, auto, auto, auto) + -> score::cpp::expected { + return static_cast(len); + })); + return *this; + } + + BidirectionalTransportFixture& ExpectSendtoNeverCalled(std::int32_t fd) + { + EXPECT_CALL(socket_mock_, sendto(fd, _, _, _, _, _)).Times(0); + return *this; + } + + BidirectionalTransportFixture& ExpectSendtoReturnsError(std::int32_t fd, int errno_val) + { + EXPECT_CALL(socket_mock_, sendto(fd, _, _, _, _, _)) + .WillOnce(Return(score::cpp::expected{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(errno_val))})); + return *this; + } + + BidirectionalTransportFixture& ExpectSendtoCountingCalls(std::int32_t fd, std::atomic& sendto_count) + { + EXPECT_CALL(socket_mock_, sendto(fd, _, _, _, _, _)) + .WillRepeatedly(Invoke([&sendto_count](auto, auto, const std::size_t len, auto, auto, auto) + -> score::cpp::expected { + sendto_count++; + return static_cast(len); + })); + return *this; + } + + BidirectionalTransportFixture& ExpectSendtoSuccessThenError(std::int32_t fd, int errno_val) + { + EXPECT_CALL(socket_mock_, sendto(fd, _, _, _, _, _)) + .WillOnce(Invoke([](auto, auto, const std::size_t len, auto, auto, auto) + -> score::cpp::expected { + return static_cast(len); + })) + .WillOnce(Return(score::cpp::expected{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(errno_val))})); + return *this; + } + + BidirectionalTransportFixture& ExpectSendtoDisconnectsOnCall(std::int32_t fd, + BidirectionalTransportAttorney& attorney) + { + EXPECT_CALL(socket_mock_, sendto(fd, _, _, _, _, _)) + .Times(1) + .WillOnce(Invoke([&attorney](auto, auto, const std::size_t, auto, auto, auto) + -> score::cpp::expected { + attorney.SetIsConnected(false); + return score::cpp::make_unexpected(score::os::Error::createFromErrno(EPIPE)); + })); + return *this; + } + + BidirectionalTransportFixture& ExpectSocketCreatedAndSockOptSet(std::int32_t listen_fd) + { + EXPECT_CALL(socket_mock_, socket(score::os::Socket::Domain::kIPv4, SOCK_STREAM | SOCK_NONBLOCK, 0)) + .WillOnce(Return(listen_fd)); + EXPECT_CALL(socket_mock_, setsockopt(_, _, _, _, _)) + .WillRepeatedly(Return(score::cpp::expected_blank{})); + return *this; + } + + BidirectionalTransportFixture& ExpectBasicSocketSetup(std::int32_t listen_fd) + { + ExpectSocketCreatedAndSockOptSet(listen_fd); + EXPECT_CALL(socket_mock_, bind(_, _, _)).WillOnce(Return(score::cpp::expected_blank{})); + EXPECT_CALL(socket_mock_, listen(_, _)).WillOnce(Return(score::cpp::expected_blank{})); + return *this; + } + + BidirectionalTransportFixture& ExpectFullSocketSetup(std::int32_t listen_fd, + std::int32_t send_fd, + std::int32_t receive_fd) + { + ExpectBasicSocketSetup(listen_fd); + EXPECT_CALL(socket_mock_, socket(score::os::Socket::Domain::kIPv4, SOCK_STREAM, 0)).WillOnce(Return(send_fd)); + EXPECT_CALL(socket_mock_, connect(send_fd, _, _)) + .WillOnce(Return(score::cpp::expected_blank{})); + EXPECT_CALL(socket_mock_, accept(listen_fd, _, _)).WillOnce(Return(receive_fd)); + return *this; + } + + score::os::SocketMock socket_mock_; + std::shared_ptr transport_; +}; + +TEST_F(BidirectionalTransportFixture, MessageHandlerCanBeRegistered) +{ + // Given a transport instance + CreateTransport(); + BidirectionalTransportAttorney attorney{transport_}; + + // When a message handler is registered + transport_->SetMessageHandler([](std::unique_ptr) {}); + + // Then the message handler is set + EXPECT_TRUE(attorney.IsMessageHandlerSet()); +} + +TEST_F(BidirectionalTransportFixture, SendNotificationReturnsNotConnectedWhenDisconnected) +{ + // Given a transport that is not connected. + CreateTransport(); + + StopOfferServiceRequest message{}; + // When SendNotification is called + const auto result = transport_->SendNotification(message); + + // Then the call fails with kNotConnected + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kNotConnected); +} + +TEST_F(BidirectionalTransportFixture, SendNotificationFailsWhenSerializeReturnsZero) +{ + // Given a connected transport and a message that serializes to zero bytes + CreateTransport(); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(2001); + + ZeroSerializeMessage message{}; + // When SendNotification is called + const auto result = transport_->SendNotification(message); + + // Then the call fails with kSendFailure + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kSendFailure); +} + +TEST_F(BidirectionalTransportFixture, SendNotificationSucceedsWhenConnected) +{ + // Given a connected transport, that is setup so that data can be sent + const int socket_number = 2002; + CreateTransport().ExpectSendtoWritesAllBytes(socket_number); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(socket_number); + + StopOfferServiceRequest message{}; + // When SendNotification is called + const auto result = transport_->SendNotification(message); + + // Then the call succeeds + EXPECT_TRUE(result.has_value()); +} + +TEST_F(BidirectionalTransportFixture, SendRequestSucceedsWhenAckArrives) +{ + // Given a connected transport where a pending request gets acknowledged. + const uint32_t timeout = 40U; + const int socket_number = 3001; + const uint32_t sequence_to_ack = 1U; + CreateTransport(timeout).ExpectSendtoWritesAllBytes(socket_number); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(socket_number); + + // Acknowledge the pending request in a separate thread before SendRequest times out + std::thread ack_thread([&attorney]() { + if (attorney.WaitForPendingRequest(sequence_to_ack, std::chrono::milliseconds(100))) + { + attorney.AcknowledgePendingRequest(sequence_to_ack); + } + }); + + StopOfferServiceRequest message{}; + // When SendRequest is called and the ack is set before the timeout expires + const auto result = transport_->SendRequest(message); + ack_thread.join(); + + // Then the request completes successfully. + EXPECT_TRUE(result.has_value()); +} + +TEST_F(BidirectionalTransportFixture, SendRequestReturnsNotConnectedIfDisconnectedBeforeFirstAttempt) +{ + const uint32_t timeout = 20U; + const int socket_number = 3002; + // Given transport in state 'disconnected' and a request blocked before first send + CreateTransport(timeout).ExpectSendtoNeverCalled(socket_number); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(socket_number); + + attorney.LockPendingMutex(); + + score::ResultBlank result = score::MakeUnexpected(TransportErrorc::kSendFailure); + std::thread request_thread([this, &result]() { + StopOfferServiceRequest request{}; + // When SendRequest resumes + result = transport_->SendRequest(request); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + attorney.SetIsConnected(false); + attorney.UnlockPendingMutex(); + + request_thread.join(); + + // Then the request fails with kNotConnected + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kNotConnected); +} + +TEST_F(BidirectionalTransportFixture, SetupFailsWhenListenSocketCreationFails) +{ + // Given a transport for which no listening socket can be created + CreateTransport(); + + EXPECT_CALL(socket_mock_, socket(score::os::Socket::Domain::kIPv4, SOCK_STREAM | SOCK_NONBLOCK, 0)) + .WillOnce(Return(score::cpp::expected{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(EACCES))})); + + // When Setup() is called + const auto result = transport_->Setup(); + + // Then Setup() returns kConnectionFailure + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kConnectionFailure); +} + +TEST_F(BidirectionalTransportFixture, SetupFailsWhenBindFails) +{ + // Given a transport with a listening socket but for which bind can not be called + CreateTransport().ExpectSocketCreatedAndSockOptSet(4001); + + EXPECT_CALL(socket_mock_, bind(_, _, _)) + .WillOnce(Return(score::cpp::expected_blank{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(EADDRINUSE))})); + + // When Setup is called. + const auto result = transport_->Setup(); + + // Then Setup returns kConnectionFailure + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kConnectionFailure); +} + +TEST_F(BidirectionalTransportFixture, SetupReturnsSuccessWhenAlreadyConnected) +{ + // Given a transport already marked as connected. + CreateTransport().ExpectBasicSocketSetup(5001); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + + // When Setup() is called. + const auto result = transport_->Setup(); + + // Then Setup() returns success. + EXPECT_TRUE(result.has_value()); +} + +TEST_F(BidirectionalTransportFixture, SetupDispatchThreadDeliversQueuedMessageToHandler) +{ + // Given setup succeeds and a message handler is registered + CreateTransport().ExpectBasicSocketSetup(6001); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + + EXPECT_CALL(socket_mock_, socket(score::os::Socket::Domain::kIPv4, SOCK_STREAM, 0)) + .WillRepeatedly(Return(score::cpp::expected{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(EACCES))})); + EXPECT_CALL(socket_mock_, accept(_, _, _)) + .WillRepeatedly(Return(score::cpp::expected{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(EAGAIN))})); + + std::atomic handler_called{false}; + transport_->SetMessageHandler([&handler_called](std::unique_ptr message) { + if (message != nullptr && message->GetType() == MessageType::kStopOfferServiceRequest) + { + handler_called = true; + } + }); + + // When a message is queued for dispatch. + const auto setup_result = transport_->Setup(); + ASSERT_TRUE(setup_result.has_value()); + + auto queued = std::make_unique(); + queued->SetSequenceNumber(42U); + attorney.EnqueueForDispatch(std::move(queued)); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + while (!handler_called.load() && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + // Then the dispatch thread invokes the handler + EXPECT_TRUE(handler_called.load()); +} + +TEST_F(BidirectionalTransportFixture, SetupReceivesIncomingRequestDispatchesItAndSendsAck) +{ + // Given connected sockets and an incoming request header. + constexpr std::int32_t kListenFd = 6101; + constexpr std::int32_t kSendFd = 6102; + constexpr std::int32_t kReceiveFd = 6103; + constexpr std::uint32_t kIncomingSequence = 77U; + + CreateTransport().ExpectFullSocketSetup(kListenFd, kSendFd, kReceiveFd).ExpectSendtoWritesAllBytes(kSendFd); + + MessageHeader inbound_header{}; + inbound_header.type = MessageType::kStopOfferServiceRequest; + inbound_header.sequence = kIncomingSequence; + inbound_header.payload_size = 0U; + + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, MessageHeader::kWireSize, _)) + .WillOnce( + Invoke([&inbound_header]( + auto, void* buffer, const std::size_t, auto) -> score::cpp::expected { + inbound_header.SerializeToBuffer(static_cast(buffer)); + return static_cast(MessageHeader::kWireSize); + })) + .WillOnce( + Invoke([this](auto, void*, const std::size_t, auto) -> score::cpp::expected { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(250); + while (transport_->IsConnected() && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return static_cast(0); + })); + + std::atomic handler_called{false}; + std::atomic received_sequence{0U}; + transport_->SetMessageHandler([&handler_called, &received_sequence](std::unique_ptr message) { + if (message != nullptr) + { + received_sequence = message->GetSequenceNumber(); + handler_called = true; + } + }); + + // When Setup starts receive processing. + const auto setup_result = transport_->Setup(); + ASSERT_TRUE(setup_result.has_value()); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(250); + while (!handler_called.load() && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + // Then the request is dispatched and processed with an ACK send. + EXPECT_TRUE(handler_called.load()); + EXPECT_EQ(received_sequence.load(), kIncomingSequence); + + transport_->Shutdown(); +} + +TEST_F(BidirectionalTransportFixture, SetupReceivesIncomingAckResponseAndCompletesPendingRequest) +{ + // Given setup receive path returns an ACK payload for a pending request. + constexpr std::int32_t kListenFd = 6201; + constexpr std::int32_t kSendFd = 6202; + constexpr std::int32_t kReceiveFd = 6203; + constexpr std::uint32_t kRequestSequence = 1U; + + CreateTransport(40U).ExpectFullSocketSetup(kListenFd, kSendFd, kReceiveFd).ExpectSendtoWritesAllBytes(kSendFd); + BidirectionalTransportAttorney attorney{transport_}; + + MessageHeader ack_header{}; + ack_header.type = MessageType::kAckResponse; + ack_header.sequence = 999U; + ack_header.payload_size = sizeof(std::uint32_t); + + { + ::testing::InSequence in_sequence; + + // Then the pending request is completed successfully. + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, MessageHeader::kWireSize, _)) + .WillOnce(Invoke([&attorney, &ack_header](auto, void* buffer, const std::size_t, auto) + -> score::cpp::expected { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(150); + while (!attorney.HasPendingRequest(kRequestSequence) && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + ack_header.SerializeToBuffer(static_cast(buffer)); + return static_cast(MessageHeader::kWireSize); + })); + + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, sizeof(std::uint32_t), _)) + .WillOnce(Invoke( + [](auto, void* buffer, const std::size_t, auto) -> score::cpp::expected { + const std::uint32_t acked_sequence = kRequestSequence; + std::memcpy(buffer, &acked_sequence, sizeof(acked_sequence)); + return static_cast(sizeof(acked_sequence)); + })); + + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, MessageHeader::kWireSize, _)) + .WillOnce( + Invoke([this](auto, void*, const std::size_t, auto) -> score::cpp::expected { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(250); + while (transport_->IsConnected() && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return static_cast(0); + })); + } + + const auto setup_result = transport_->Setup(); + ASSERT_TRUE(setup_result.has_value()); + + StopOfferServiceRequest message{}; + // When SendRequest is called after Setup. + const auto result = transport_->SendRequest(message); + + EXPECT_TRUE(result.has_value()); + + transport_->Shutdown(); +} + +TEST_F(BidirectionalTransportFixture, SetupDisconnectsOnOversizedIncomingPayload) +{ + // Given Setup reads an incoming header with an oversized payload. + constexpr std::int32_t kListenFd = 6301; + constexpr std::int32_t kSendFd = 6302; + constexpr std::int32_t kReceiveFd = 6303; + + CreateTransport().ExpectFullSocketSetup(kListenFd, kSendFd, kReceiveFd); + BidirectionalTransportAttorney attorney{transport_}; + + MessageHeader inbound_header{}; + inbound_header.type = MessageType::kAckResponse; + inbound_header.sequence = 11U; + inbound_header.payload_size = 1025U; + std::atomic allow_header_read{false}; + + // Then Setup fails and the transport disconnects. + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, MessageHeader::kWireSize, _)) + .WillOnce( + Invoke([&inbound_header, &allow_header_read]( + auto, void* buffer, const std::size_t, auto) -> score::cpp::expected { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + while (!allow_header_read.load() && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + inbound_header.SerializeToBuffer(static_cast(buffer)); + return static_cast(MessageHeader::kWireSize); + })); + + score::ResultBlank setup_result = score::MakeUnexpected(TransportErrorc::kConnectionFailure); + std::atomic setup_finished{false}; + std::thread setup_thread([this, &setup_result, &setup_finished]() { + // When the header is processed. + setup_result = transport_->Setup(); + setup_finished = true; + }); + + const bool connected_seen = attorney.WaitForConnectedState(true, std::chrono::milliseconds(200)); + allow_header_read = true; + + if (!connected_seen) + { + transport_->Shutdown(); + setup_thread.join(); + FAIL() << "Setup() did not reach connected state before oversized payload"; + } + + const auto setup_deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(300); + while (!setup_finished.load() && std::chrono::steady_clock::now() < setup_deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + if (!setup_finished.load()) + { + transport_->Shutdown(); + } + + setup_thread.join(); + + ASSERT_FALSE(setup_result.has_value()); + EXPECT_EQ(setup_result.error(), TransportErrorc::kConnectionFailure); + EXPECT_TRUE(attorney.WaitForConnectedState(false, std::chrono::milliseconds(100))); + + transport_->Shutdown(); +} + +TEST_F(BidirectionalTransportFixture, SetupDisconnectsWhenIncomingPayloadReadFails) +{ + // Given Setup reads a valid header but payload read returns EOF. + constexpr std::int32_t kListenFd = 6401; + constexpr std::int32_t kSendFd = 6402; + constexpr std::int32_t kReceiveFd = 6403; + + CreateTransport().ExpectFullSocketSetup(kListenFd, kSendFd, kReceiveFd); + BidirectionalTransportAttorney attorney{transport_}; + + MessageHeader inbound_header{}; + inbound_header.type = MessageType::kAckResponse; + inbound_header.sequence = 12U; + inbound_header.payload_size = sizeof(std::uint32_t); + std::atomic allow_payload_read{false}; + + { + ::testing::InSequence in_sequence; + + // First receive call returns size of header bytes + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, MessageHeader::kWireSize, _)) + .WillOnce(Invoke([&inbound_header](auto, void* buffer, const std::size_t, auto) + -> score::cpp::expected { + inbound_header.SerializeToBuffer(static_cast(buffer)); + return static_cast(MessageHeader::kWireSize); + })); + // Follow-up receive call, to read payload, returns 0 bytes + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, sizeof(std::uint32_t), _)) + .WillOnce( + Invoke([&allow_payload_read]( + auto, void*, const std::size_t, auto) -> score::cpp::expected { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + while (!allow_payload_read.load() && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return score::cpp::expected{0}; + })); + } + + score::ResultBlank setup_result = score::MakeUnexpected(TransportErrorc::kConnectionFailure); + std::atomic setup_finished{false}; + std::thread setup_thread([this, &setup_result, &setup_finished]() { + // When receive processing continues + setup_result = transport_->Setup(); + setup_finished = true; + }); + + const bool connected_seen = attorney.WaitForConnectedState(true, std::chrono::milliseconds(200)); + allow_payload_read = true; + + if (!connected_seen) + { + transport_->Shutdown(); + setup_thread.join(); + FAIL() << "Setup() did not reach connected state before payload read failure"; + } + + const auto setup_deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(300); + while (!setup_finished.load() && std::chrono::steady_clock::now() < setup_deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + if (!setup_finished.load()) + { + transport_->Shutdown(); + } + + setup_thread.join(); + + // Then Setup fails and the transport disconnects + ASSERT_FALSE(setup_result.has_value()); + EXPECT_EQ(setup_result.error(), TransportErrorc::kConnectionFailure); + EXPECT_TRUE(attorney.WaitForConnectedState(false, std::chrono::milliseconds(100))); + + transport_->Shutdown(); +} + +TEST_F(BidirectionalTransportFixture, SetupDisconnectsOnUnknownIncomingMessageType) +{ + // Given Setup reads an incoming header with an invalid message type. + constexpr std::int32_t kListenFd = 6501; + constexpr std::int32_t kSendFd = 6502; + constexpr std::int32_t kReceiveFd = 6503; + + CreateTransport().ExpectFullSocketSetup(kListenFd, kSendFd, kReceiveFd); + BidirectionalTransportAttorney attorney{transport_}; + + MessageHeader inbound_header{}; + inbound_header.type = MessageType::kInvalid; + inbound_header.sequence = 13U; + inbound_header.payload_size = 0U; + std::atomic allow_header_read{false}; + + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, MessageHeader::kWireSize, _)) + .WillOnce( + Invoke([&inbound_header, &allow_header_read]( + auto, void* buffer, const std::size_t, auto) -> score::cpp::expected { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + while (!allow_header_read.load() && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + inbound_header.SerializeToBuffer(static_cast(buffer)); + return static_cast(MessageHeader::kWireSize); + })); + + score::ResultBlank setup_result = score::MakeUnexpected(TransportErrorc::kConnectionFailure); + std::atomic setup_finished{false}; + std::thread setup_thread([this, &setup_result, &setup_finished]() { + // When the header is handled. + setup_result = transport_->Setup(); + setup_finished = true; + }); + + const bool connected_seen = attorney.WaitForConnectedState(true, std::chrono::milliseconds(200)); + allow_header_read = true; + + if (!connected_seen) + { + transport_->Shutdown(); + setup_thread.join(); + FAIL() << "Setup() did not reach connected state before unknown message type"; + } + + const auto setup_deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(300); + while (!setup_finished.load() && std::chrono::steady_clock::now() < setup_deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + if (!setup_finished.load()) + { + transport_->Shutdown(); + } + + setup_thread.join(); + + // Then Setup fails and the transport disconnects. + ASSERT_FALSE(setup_result.has_value()); + EXPECT_EQ(setup_result.error(), TransportErrorc::kConnectionFailure); + EXPECT_TRUE(attorney.WaitForConnectedState(false, std::chrono::milliseconds(100))); + + transport_->Shutdown(); +} + +TEST_F(BidirectionalTransportFixture, SetupDisconnectsOnIncomingDeserializationFailure) +{ + // Given Setup receives a request payload that cannot be deserialized. + constexpr std::int32_t kListenFd = 6601; + constexpr std::int32_t kSendFd = 6602; + constexpr std::int32_t kReceiveFd = 6603; + + CreateTransport().ExpectFullSocketSetup(kListenFd, kSendFd, kReceiveFd); + BidirectionalTransportAttorney attorney{transport_}; + + MessageHeader inbound_header{}; + inbound_header.type = MessageType::kStopOfferServiceRequest; + inbound_header.sequence = 14U; + inbound_header.payload_size = 1U; + std::atomic allow_payload_read{false}; + + { + ::testing::InSequence in_sequence; + + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, MessageHeader::kWireSize, _)) + .WillOnce(Invoke([&inbound_header](auto, void* buffer, const std::size_t, auto) + -> score::cpp::expected { + inbound_header.SerializeToBuffer(static_cast(buffer)); + return static_cast(MessageHeader::kWireSize); + })); + EXPECT_CALL(socket_mock_, recv(kReceiveFd, _, 1U, _)) + .WillOnce(Invoke([&allow_payload_read](auto, void* buffer, const std::size_t, auto) + -> score::cpp::expected { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + while (!allow_payload_read.load() && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + static_cast(buffer)[0] = 0U; + return static_cast(1); + })); + } + + score::ResultBlank setup_result = score::MakeUnexpected(TransportErrorc::kConnectionFailure); + std::atomic setup_finished{false}; + std::thread setup_thread([this, &setup_result, &setup_finished]() { + // When receive processing handles that message. + setup_result = transport_->Setup(); + setup_finished = true; + }); + + const bool connected_seen = attorney.WaitForConnectedState(true, std::chrono::milliseconds(200)); + allow_payload_read = true; + + if (!connected_seen) + { + transport_->Shutdown(); + setup_thread.join(); + FAIL() << "Setup() did not reach connected state before deserialization failure"; + } + + const auto setup_deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(300); + while (!setup_finished.load() && std::chrono::steady_clock::now() < setup_deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + if (!setup_finished.load()) + { + transport_->Shutdown(); + } + + setup_thread.join(); + + // Then Setup fails and the transport disconnects + ASSERT_FALSE(setup_result.has_value()); + EXPECT_EQ(setup_result.error(), TransportErrorc::kConnectionFailure); + EXPECT_TRUE(attorney.WaitForConnectedState(false, std::chrono::milliseconds(100))); + + transport_->Shutdown(); +} + +TEST_F(BidirectionalTransportFixture, IsConnectedReturnsFalseAfterConstruction) +{ + // Given a freshly constructed transport + CreateTransport(); + // When IsConnected is queried + // Then it reports false + EXPECT_FALSE(transport_->IsConnected()); +} + +TEST_F(BidirectionalTransportFixture, IsConnectedReturnsTrueWhenConnected) +{ + // Given a transport marked as connected + CreateTransport(); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + // When IsConnected is queried + // Then it reports true + EXPECT_TRUE(transport_->IsConnected()); +} + +TEST_F(BidirectionalTransportFixture, SendRequestReturnsNotConnectedWhenDisconnected) +{ + // Given a transport that is not connected + CreateTransport(); + StopOfferServiceRequest message{}; + // When SendRequest is called + const auto result = transport_->SendRequest(message); + + // Then the call fails with kNotConnected + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kNotConnected); +} + +TEST_F(BidirectionalTransportFixture, SendRequestReturnsNotConnectedIfDisconnectedDuringRetry) +{ + // Given a connected transport instance where sendto is instrumented to count the number of calls + std::atomic sendto_count{0}; + CreateTransport(20U).ExpectSendtoCountingCalls(3003, sendto_count); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(3003); + + // When the transport disconnects while trying to send the request + score::ResultBlank result = score::MakeUnexpected(TransportErrorc::kSendFailure); + std::thread request_thread([this, &result]() { + StopOfferServiceRequest request{}; + result = transport_->SendRequest(request); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + attorney.SetIsConnected(false); + + request_thread.join(); + + // Then SendRequest fails with kNotConnected + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kNotConnected); +} + +TEST_F(BidirectionalTransportFixture, SendRequestTimeoutReturnsTimeoutError) +{ + const int socket_number = 3004; + // Given a connected transport that never receives an ACK + CreateTransport(10U).ExpectSendtoWritesAllBytes(socket_number); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(socket_number); + + StopOfferServiceRequest message{}; + // When SendRequest is called + const auto result = transport_->SendRequest(message); + + // Then it fails with kTimeout + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kTimeout); +} + +TEST_F(BidirectionalTransportFixture, SendRequestRetriesOnTimeout) +{ + const int socket_number = 3005; + // Given a connected transport where sendto is instrumented to count the number of calls and ACKs never arrive. + std::atomic sendto_count{0}; + CreateTransport(15U).ExpectSendtoCountingCalls(socket_number, sendto_count); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(socket_number); + + // When SendRequest is called + StopOfferServiceRequest message{}; + const auto result = transport_->SendRequest(message); + + // Then sendto is called multiple times in order to retry + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kTimeout); + EXPECT_GT(sendto_count, 1); +} + +TEST_F(BidirectionalTransportFixture, SendRequestFailsWhenSerializeReturnsZero) +{ + // Given a connected transport + CreateTransport(); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(3006); + + // When SendRequest is called with a message that serializes to zero bytes. + ZeroSerializeMessage message{}; + const auto result = transport_->SendRequest(message); + + // Then it fails with kSendFailure. + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kSendFailure); +} + +TEST_F(BidirectionalTransportFixture, ShutdownStopsTransport) +{ + // Given a transport in state 'connected' + CreateTransport(); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(4001); + attorney.SetReceiveSocket(4002); + attorney.SetListenSocket(4003); + + // When Shutdown is called + transport_->Shutdown(); + + // Then the transport is in state 'Disconnected' + EXPECT_FALSE(transport_->IsConnected()); +} + +TEST_F(BidirectionalTransportFixture, SetupFailsWhenListenSocketCreationFailsWithDifferentError) +{ + // Given a transport where creating the listening socket fails with ENOMEM + CreateTransport(); + EXPECT_CALL(socket_mock_, socket(score::os::Socket::Domain::kIPv4, SOCK_STREAM | SOCK_NONBLOCK, 0)) + .WillOnce(Return(score::cpp::expected{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOMEM))})); + + // When Setup is called + const auto result = transport_->Setup(); + + // Then Setup returns kConnectionFailure + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kConnectionFailure); +} + +TEST_F(BidirectionalTransportFixture, SetupFailsWhenListenFails) +{ + // Given a transport where calling listen on the listening socket fails + CreateTransport().ExpectSocketCreatedAndSockOptSet(5002); + + EXPECT_CALL(socket_mock_, bind(_, _, _)).WillOnce(Return(score::cpp::expected_blank{})); + EXPECT_CALL(socket_mock_, listen(_, _)) + .WillOnce(Return(score::cpp::expected_blank{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(EADDRINUSE))})); + + // When Setup is called + const auto result = transport_->Setup(); + + // Then Setup returns kConnectionFailure + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kConnectionFailure); +} + +TEST_F(BidirectionalTransportFixture, SendNotificationReturnsSendFailureWhenHeaderSendFails) +{ + const int socket_number = 5001; + // Given a connected transport, where calling sendto on the socket always returns an error + CreateTransport().ExpectSendtoReturnsError(socket_number, EPIPE); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(socket_number); + + StopOfferServiceRequest message{}; + // When SendNotification is called + const auto result = transport_->SendNotification(message); + + // Then it fails with kSendFailure + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kSendFailure); +} + +TEST_F(BidirectionalTransportFixture, SendNotificationReturnsSendFailureWhenPayloadSendFails) +{ + const int socket_number = 5002; + // Given a connected transport where payload send fails, after header has been sent successfully + CreateTransport().ExpectSendtoSuccessThenError(socket_number, EPIPE); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(socket_number); + + // When SendNotification is called + StopOfferServiceRequest message{}; + const auto result = transport_->SendNotification(message); + + // Then it fails with kSendFailure + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kSendFailure); +} + +TEST_F(BidirectionalTransportFixture, SendRequestDisconnectsDuringWaitForAck) +{ + const int socket_number = 3007; + // Given SendRequest starts while the transport is connected + CreateTransport(30U).ExpectSendtoWritesAllBytes(socket_number); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(socket_number); + + std::atomic send_started{false}; + std::thread disconnect_thread([&attorney, &send_started]() { + while (!send_started) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + attorney.SetIsConnected(false); + }); + + score::ResultBlank result = score::MakeUnexpected(TransportErrorc::kSendFailure); + std::thread request_thread([this, &result, &send_started]() { + send_started = true; + StopOfferServiceRequest request{}; + // When the transport disconnects during ACK wait. + result = transport_->SendRequest(request); + }); + + disconnect_thread.join(); + request_thread.join(); + + // Then SendRequest fails with kNotConnected + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kNotConnected); +} + +TEST_F(BidirectionalTransportFixture, TrySendAndWaitForAckReturnsErrorWhenSendFails) +{ + const int socket_number = 3008; + // Given a connected transport where calling sendto on the socket returns an error + CreateTransport(); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(socket_number); + + ExpectSendtoDisconnectsOnCall(socket_number, attorney); + + StopOfferServiceRequest message{}; + // When SendRequest is called + const auto result = transport_->SendRequest(message); + + // Then the request fails with kSendFailure + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kSendFailure); +} + +TEST_F(BidirectionalTransportFixture, SetupReturnsConnectionFailureWhenShutdownInterruptsConnectionAttempt) +{ + // Given during Setup() the calls to create/connect sockets return retryable errors + CreateTransport(50U).ExpectBasicSocketSetup(7001); + + EXPECT_CALL(socket_mock_, socket(score::os::Socket::Domain::kIPv4, SOCK_STREAM, 0)).WillRepeatedly(Return(7002)); + EXPECT_CALL(socket_mock_, connect(_, _, _)) + .WillRepeatedly(Return(score::cpp::expected_blank{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(ECONNREFUSED))})); + EXPECT_CALL(socket_mock_, accept(_, _, _)) + .WillRepeatedly(Return(score::cpp::expected{ + score::cpp::make_unexpected(score::os::Error::createFromErrno(EAGAIN))})); + + // When Shutdown() is called while Setup is retrying + score::ResultBlank result = {}; + std::thread setup_thread([this, &result]() { + result = transport_->Setup(); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + transport_->Shutdown(); + setup_thread.join(); + + // Then Setup returns kConnectionFailure and transport is disconnected + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), TransportErrorc::kConnectionFailure); + EXPECT_FALSE(transport_->IsConnected()); +} + +TEST_F(BidirectionalTransportFixture, ConstructorAndDestructorWork) +{ + // Given a valid socket configuration + HyperVisorSocketConfiguration config{score::os::Ipv4Address{"127.0.0.1"}}; + config.local_port_ = 9000; + config.remote_port_ = 9001; + + // When a transport object is constructed and destroyed + BidirectionalTransport transport(config); + // Then construction succeeds and initial state is disconnected + EXPECT_FALSE(transport.IsConnected()); +} + +TEST_F(BidirectionalTransportFixture, MultipleShutdownsAreIdempotent) +{ + // Given a transport in state connected + CreateTransport(); + BidirectionalTransportAttorney attorney{transport_}; + attorney.SetIsConnected(true); + attorney.SetSendSocket(8001); + + // When Shutdown is called multiple times + transport_->Shutdown(); + + EXPECT_FALSE(transport_->IsConnected()); + // Then transport should still be in state 'disconnected' + transport_->Shutdown(); + EXPECT_FALSE(transport_->IsConnected()); +} + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/i_bidirectional_transport.h b/score/mw/com/gateway/transport_layer/sample/i_bidirectional_transport.h new file mode 100644 index 000000000..b5a3e8e91 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/i_bidirectional_transport.h @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_I_BIDIRECTIONAL_TRANSPORT_H_ +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_I_BIDIRECTIONAL_TRANSPORT_H_ + +#include "score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h" +#include "score/result/result.h" + +#include + +#include + +namespace score::mw::com::gateway +{ + +/// \brief Interface of BidirectionalTransport, introduced only for testing/mocking purposes +class IBidirectionalTransport +{ + public: + using MessageHandler = score::cpp::callback), 64>; + + virtual ~IBidirectionalTransport() = default; + + virtual score::ResultBlank Setup() = 0; + virtual void Shutdown() = 0; + + virtual bool IsConnected() const = 0; + + virtual score::ResultBlank SendRequest(TransportMessage& message) = 0; + virtual score::ResultBlank SendNotification(TransportMessage& message) = 0; + + virtual void SetMessageHandler(MessageHandler handler) = 0; +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_I_BIDIRECTIONAL_TRANSPORT_H_ diff --git a/score/mw/com/gateway/transport_layer/sample/messages/BUILD b/score/mw/com/gateway/transport_layer/sample/messages/BUILD new file mode 100644 index 000000000..e3b8aef1d --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/BUILD @@ -0,0 +1,99 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("//quality/unit_testing:unit_testing.bzl", "cc_unit_test") +load("//score/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "message_error", + srcs = ["message_error.cpp"], + hdrs = ["message_error.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + "@score_baselibs//score/result", + ], +) + +cc_library( + name = "serialization", + hdrs = ["serialization.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + "//score/mw/com/gateway/transport_layer/sample/messages:message_error", + "@score_baselibs//score/language/futurecpp", + ], +) + +cc_library( + name = "gateway_messages", + srcs = ["gateway_messages.cpp"], + hdrs = ["gateway_messages.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + "//score/mw/com/gateway/transport_layer:transport", + "//score/mw/com/gateway/transport_layer/sample/messages:serialization", + "//score/mw/com/gateway/transport_layer/sample/messages:transport_message", + "//score/mw/com/impl:instance_specifier", + ], +) + +cc_library( + name = "transport_message", + srcs = ["transport_message.cpp"], + hdrs = ["transport_message.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//score/mw/com/gateway:__subpackages__", + ], + deps = [ + "@score_baselibs//score/language/futurecpp", + ], +) + +cc_unit_test( + name = "serialization_test", + srcs = ["serialization_test.cpp"], + features = COMPILER_WARNING_FEATURES, + deps = [ + "//score/mw/com/gateway/transport_layer:transport", + "//score/mw/com/gateway/transport_layer/sample/messages:serialization", + ], +) + +cc_unit_test( + name = "gateway_messages_test", + srcs = ["gateway_messages_test.cpp"], + features = COMPILER_WARNING_FEATURES, + deps = [ + "//score/mw/com/gateway/transport_layer/sample/messages:gateway_messages", + "//score/mw/com/gateway/transport_layer/sample/messages:message_error", + "//score/mw/com/impl:instance_specifier", + ], +) + +cc_unit_test( + name = "message_error_test", + srcs = ["message_error_test.cpp"], + features = COMPILER_WARNING_FEATURES, + deps = [ + "//score/mw/com/gateway/transport_layer/sample/messages:message_error", + ], +) diff --git a/score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.cpp b/score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.cpp new file mode 100644 index 000000000..6d9dec7cc --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.cpp @@ -0,0 +1,106 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h" + +#include "score/mw/com/gateway/transport_layer/sample/messages/serialization.h" + +namespace score::mw::com::gateway +{ + +namespace +{ + +template +std::size_t SerializeWithTemplate(const T& message, score::cpp::span buffer) +{ + const auto size = gateway::ComputeSerializedSize(message); + if (size > static_cast(buffer.size())) + { + score::mw::log::LogError() << "Serialize buffer too small: need " << size << " have " << buffer.size(); + return 0U; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) uint8_t and std::byte have same representation + score::cpp::span span(reinterpret_cast(buffer.data()), size); + auto result = gateway::Serialize(message, span); + if (!result.has_value()) + { + score::mw::log::LogError() << "Failed to serialize message of type " << static_cast(message.GetType()) + << ": " << (result.error()); + return 0U; + } + return size; +} + +template +bool DeserializeWithTemplate(T& message, score::cpp::span data) +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) uint8_t and std::byte have same representation + score::cpp::span span(reinterpret_cast(data.data()), data.size()); + auto result = gateway::Deserialize(message, span); + return result.has_value(); +} + +} // namespace + +std::size_t ProvideServiceRequest::Serialize(score::cpp::span buffer) const +{ + return SerializeWithTemplate(*this, buffer); +} + +bool ProvideServiceRequest::Deserialize(score::cpp::span data) +{ + return DeserializeWithTemplate(*this, data); +} + +std::size_t AckResponse::Serialize(score::cpp::span buffer) const +{ + return SerializeWithTemplate(*this, buffer); +} + +bool AckResponse::Deserialize(score::cpp::span data) +{ + return DeserializeWithTemplate(*this, data); +} + +std::size_t OfferServiceRequest::Serialize(score::cpp::span buffer) const +{ + return SerializeWithTemplate(*this, buffer); +} + +bool OfferServiceRequest::Deserialize(score::cpp::span data) +{ + return DeserializeWithTemplate(*this, data); +} + +std::size_t StopOfferServiceRequest::Serialize(score::cpp::span buffer) const +{ + return SerializeWithTemplate(*this, buffer); +} + +bool StopOfferServiceRequest::Deserialize(score::cpp::span data) +{ + return DeserializeWithTemplate(*this, data); +} + +std::size_t ServiceElementMessage::Serialize(score::cpp::span buffer) const +{ + return SerializeWithTemplate(*this, buffer); +} + +bool ServiceElementMessage::Deserialize(score::cpp::span data) +{ + return DeserializeWithTemplate(*this, data); +} + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h b/score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h new file mode 100644 index 000000000..61e3c2d7d --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h @@ -0,0 +1,320 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_GATEWAY_MESSAGES_H_ +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_GATEWAY_MESSAGES_H_ + +#include "score/mw/com/gateway/transport_layer/sample/messages/transport_message.h" +#include "score/mw/com/gateway/transport_layer/transport.h" +#include "score/mw/com/impl/instance_specifier.h" +#include "score/mw/com/impl/service_element_type.h" + +#include +#include +#include +#include + +namespace score::mw::com::gateway +{ + +/// \brief Message to propagate service instance metadata (instance specifier + service element configurations) +/// from the source gateway to the destination gateway. The destination gateway uses this information to create a +/// (generic) skeleton for the given service instance. +class ProvideServiceRequest : public TransportMessage +{ + template + static auto GetSerializeMembersImpl(Self& self) + { + using SelfNoRef = std::remove_reference_t; + using StringType = std::conditional_t, const std::string, std::string>; + using VectorType = std::conditional_t, + const std::vector, + std::vector>; + using Uint32Type = std::conditional_t, const std::uint32_t, std::uint32_t>; + return std::tuple( + self.instance_specifier_, self.elements_, self.shm_control_size_, self.shm_data_size_); + } + + public: + ProvideServiceRequest() : TransportMessage(MessageType::kProvideServiceRequest) {} + + ProvideServiceRequest(impl::InstanceSpecifier service_instance_specifier, + std::vector service_elements, + std::uint32_t shm_control_size = 0U, + std::uint32_t shm_data_size = 0U) + : TransportMessage(MessageType::kProvideServiceRequest), + instance_specifier_(std::string{service_instance_specifier.ToString()}), + elements_(std::move(service_elements)), + shm_control_size_(shm_control_size), + shm_data_size_(shm_data_size) + { + } + + std::size_t Serialize(score::cpp::span buffer) const override; + bool Deserialize(score::cpp::span data) override; + + const std::string& GetInstanceSpecifier() const + { + return instance_specifier_; + } + + const std::vector& GetServiceElements() const + { + return elements_; + } + + std::uint32_t GetShmControlSize() const + { + return shm_control_size_; + } + + std::uint32_t GetShmDataSize() const + { + return shm_data_size_; + } + + auto GetSerializeMembers() const + { + return GetSerializeMembersImpl(*this); + } + auto GetSerializeMembers() + { + return GetSerializeMembersImpl(*this); + } + + private: + std::string instance_specifier_; + std::vector elements_; + std::uint32_t shm_control_size_{0U}; + std::uint32_t shm_data_size_{0U}; +}; + +/// \brief Acknowledgement response message, sent to confirm receipt of a request with a given sequence number. +class AckResponse : public TransportMessage +{ + public: + AckResponse() : TransportMessage(MessageType::kAckResponse), acked_sequence_(0) {} + + explicit AckResponse(const std::uint32_t acked_sequence) + : TransportMessage(MessageType::kAckResponse), acked_sequence_(acked_sequence) + { + } + + std::size_t Serialize(score::cpp::span buffer) const override; + bool Deserialize(score::cpp::span data) override; + + std::uint32_t GetAckedSequence() const + { + return acked_sequence_; + } + + void SetAckedSequence(std::uint32_t sequence) + { + acked_sequence_ = sequence; + } + + std::tuple GetSerializeMembers() const + { + return {acked_sequence_}; + } + std::tuple GetSerializeMembers() + { + return {acked_sequence_}; + } + + private: + std::uint32_t acked_sequence_; +}; + +/// \brief Message to trigger service-instance offering at the destination gateway side. +/// The service instance is expected to be already created at the destination gateway side, +/// e.g. by a previous ProvideServiceRequest. +class OfferServiceRequest : public TransportMessage +{ + public: + OfferServiceRequest() : TransportMessage(MessageType::kOfferServiceRequest) {} + + explicit OfferServiceRequest(impl::InstanceSpecifier service_instance_specifier) + : TransportMessage(MessageType::kOfferServiceRequest), + instance_specifier_(std::string{service_instance_specifier.ToString()}) + { + } + + std::size_t Serialize(score::cpp::span buffer) const override; + bool Deserialize(score::cpp::span data) override; + + const std::string& GetInstanceSpecifier() const + { + return instance_specifier_; + } + + std::tuple GetSerializeMembers() const + { + return {instance_specifier_}; + } + std::tuple GetSerializeMembers() + { + return {instance_specifier_}; + } + + private: + std::string instance_specifier_; +}; + +/// \brief Message to trigger service-instance stop-offer at the destination gateway side. +class StopOfferServiceRequest : public TransportMessage +{ + public: + StopOfferServiceRequest() : TransportMessage(MessageType::kStopOfferServiceRequest) {} + + explicit StopOfferServiceRequest(impl::InstanceSpecifier service_instance_specifier) + : TransportMessage(MessageType::kStopOfferServiceRequest), + instance_specifier_(std::string{service_instance_specifier.ToString()}) + { + } + + std::size_t Serialize(score::cpp::span buffer) const override; + bool Deserialize(score::cpp::span data) override; + + const std::string& GetInstanceSpecifier() const + { + return instance_specifier_; + } + + std::tuple GetSerializeMembers() const + { + return {instance_specifier_}; + } + std::tuple GetSerializeMembers() + { + return {instance_specifier_}; + } + + private: + std::string instance_specifier_; +}; + +/// \brief Base class for messages that identify a service element by instance specifier, type and name. +/// \details Used as a common base for RegisterNotificationRequest, UnregisterNotificationRequest, +/// SubscribeRequest, UnsubscribeRequest, and UpdateNotification messages. +class ServiceElementMessage : public TransportMessage +{ + template + static auto GetSerializeMembersImpl(Self& self) + { + using SelfNoRef = std::remove_reference_t; + using StringType = std::conditional_t, const std::string, std::string>; + using TypeType = + std::conditional_t, const impl::ServiceElementType, impl::ServiceElementType>; + return std::tuple( + self.instance_specifier_, self.element_type_, self.element_name_); + } + + public: + using TransportMessage::TransportMessage; + + ServiceElementMessage(MessageType message_type, + impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) + : TransportMessage(message_type), + instance_specifier_(std::string{service_instance_specifier.ToString()}), + element_type_(element_type), + element_name_(std::move(element_name)) + { + } + + std::size_t Serialize(score::cpp::span buffer) const override; + bool Deserialize(score::cpp::span data) override; + + const std::string& GetInstanceSpecifier() const + { + return instance_specifier_; + } + impl::ServiceElementType GetElementType() const + { + return element_type_; + } + const std::string& GetElementName() const + { + return element_name_; + } + + auto GetSerializeMembers() const + { + return GetSerializeMembersImpl(*this); + } + auto GetSerializeMembers() + { + return GetSerializeMembersImpl(*this); + } + + private: + std::string instance_specifier_; + impl::ServiceElementType element_type_{impl::ServiceElementType::INVALID}; + std::string element_name_; +}; + +/// \brief Message to register for update notifications of a service element at the source gateway side. +/// \details The source gateway is expected to subscribe to the service element and forward update notifications +/// via UpdateNotification messages. +class RegisterNotificationRequest final : public ServiceElementMessage +{ + public: + RegisterNotificationRequest() : ServiceElementMessage(MessageType::kRegisterNotificationRequest) {} + RegisterNotificationRequest(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) + : ServiceElementMessage(MessageType::kRegisterNotificationRequest, + std::move(service_instance_specifier), + element_type, + std::move(element_name)) + { + } +}; + +/// \brief Message to unregister from update notifications of a service element at the source gateway side. +class UnregisterNotificationRequest final : public ServiceElementMessage +{ + public: + UnregisterNotificationRequest() : ServiceElementMessage(MessageType::kUnregisterNotificationRequest) {} + UnregisterNotificationRequest(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) + : ServiceElementMessage(MessageType::kUnregisterNotificationRequest, + std::move(service_instance_specifier), + element_type, + std::move(element_name)) + { + } +}; + +/// \brief Message used to notify about updates of service elements (e.g. new event samples, field value changes). +/// \details Sent from the source gateway to the destination gateway when a subscribed service element has new data. +class UpdateNotification final : public ServiceElementMessage +{ + public: + UpdateNotification() : ServiceElementMessage(MessageType::kUpdateNotification) {} + UpdateNotification(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) + : ServiceElementMessage(MessageType::kUpdateNotification, + std::move(service_instance_specifier), + element_type, + std::move(element_name)) + { + } +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_GATEWAY_MESSAGES_H_ diff --git a/score/mw/com/gateway/transport_layer/sample/messages/gateway_messages_test.cpp b/score/mw/com/gateway/transport_layer/sample/messages/gateway_messages_test.cpp new file mode 100644 index 000000000..e5028486c --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/gateway_messages_test.cpp @@ -0,0 +1,377 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h" +#include "score/mw/com/gateway/transport_layer/sample/messages/message_error.h" + +#include "score/mw/com/impl/instance_specifier.h" + +#include + +#include +#include +#include +#include +#include + +namespace score::mw::com::gateway +{ +namespace +{ + +constexpr std::size_t kTestBufferSize = 1024U; + +TEST(GatewayMessagesTest, ProvideServiceRequestRoundTrip) +{ + auto specifier = impl::InstanceSpecifier::Create(std::string{"SpeedService/Instance42"}); + ASSERT_TRUE(specifier.has_value()); + + std::vector elements{ + {"SpeedEvent", DataTypeSizeInfo{64U, 8U}}, + {"SpeedField", DataTypeSizeInfo{32U, 4U}}, + }; + + ProvideServiceRequest original{std::move(specifier).value(), elements, 4U, 8U}; + + std::array buffer{}; + const auto size = original.Serialize(buffer); + ASSERT_GT(size, 0U); + + ProvideServiceRequest deserialized; + ASSERT_TRUE(deserialized.Deserialize(score::cpp::span(buffer.data(), size))); + + EXPECT_EQ(deserialized.GetInstanceSpecifier(), "SpeedService/Instance42"); + ASSERT_EQ(deserialized.GetServiceElements().size(), 2U); + EXPECT_EQ(deserialized.GetServiceElements()[0].element_name, "SpeedEvent"); + EXPECT_EQ(deserialized.GetServiceElements()[0].size_info.Size(), 64U); + EXPECT_EQ(deserialized.GetServiceElements()[0].size_info.Alignment(), 8U); + EXPECT_EQ(deserialized.GetServiceElements()[1].element_name, "SpeedField"); + EXPECT_EQ(deserialized.GetServiceElements()[1].size_info.Size(), 32U); + EXPECT_EQ(deserialized.GetServiceElements()[1].size_info.Alignment(), 4U); + EXPECT_EQ(deserialized.GetShmControlSize(), 4U); + EXPECT_EQ(deserialized.GetShmDataSize(), 8U); +} + +TEST(GatewayMessagesTest, ProvideServiceRequestEmptyElements) +{ + auto specifier = impl::InstanceSpecifier::Create(std::string{"EmptyService/Inst1"}); + ASSERT_TRUE(specifier.has_value()); + + ProvideServiceRequest original{std::move(specifier).value(), {}}; + + std::array buffer{}; + const auto size = original.Serialize(buffer); + ASSERT_GT(size, 0U); + + ProvideServiceRequest deserialized; + ASSERT_TRUE(deserialized.Deserialize(score::cpp::span(buffer.data(), size))); + + EXPECT_EQ(deserialized.GetInstanceSpecifier(), "EmptyService/Inst1"); + EXPECT_TRUE(deserialized.GetServiceElements().empty()); +} + +TEST(GatewayMessagesTest, OfferServiceRequestRoundTrip) +{ + auto specifier = impl::InstanceSpecifier::Create(std::string{"SpeedService/Instance42"}); + ASSERT_TRUE(specifier.has_value()); + + OfferServiceRequest original{std::move(specifier).value()}; + + std::array buffer{}; + const auto size = original.Serialize(buffer); + ASSERT_GT(size, 0U); + + OfferServiceRequest deserialized; + ASSERT_TRUE(deserialized.Deserialize(score::cpp::span(buffer.data(), size))); + + EXPECT_EQ(deserialized.GetInstanceSpecifier(), "SpeedService/Instance42"); +} + +TEST(GatewayMessagesTest, StopOfferServiceRequestRoundTrip) +{ + auto specifier = impl::InstanceSpecifier::Create(std::string{"SpeedService/Instance42"}); + ASSERT_TRUE(specifier.has_value()); + + StopOfferServiceRequest original{std::move(specifier).value()}; + + std::array buffer{}; + const auto size = original.Serialize(buffer); + ASSERT_GT(size, 0U); + + StopOfferServiceRequest deserialized; + ASSERT_TRUE(deserialized.Deserialize(score::cpp::span(buffer.data(), size))); + + EXPECT_EQ(deserialized.GetInstanceSpecifier(), "SpeedService/Instance42"); +} + +TEST(GatewayMessagesTest, UpdateNotificationRoundTrip) +{ + auto specifier = impl::InstanceSpecifier::Create(std::string{"SpeedService/Instance1"}); + ASSERT_TRUE(specifier.has_value()); + + UpdateNotification original{std::move(specifier).value(), impl::ServiceElementType::EVENT, "SpeedEvent"}; + + std::array buffer{}; + const auto size = original.Serialize(buffer); + ASSERT_GT(size, 0U); + + UpdateNotification deserialized; + ASSERT_TRUE(deserialized.Deserialize(score::cpp::span(buffer.data(), size))); + + EXPECT_EQ(deserialized.GetInstanceSpecifier(), "SpeedService/Instance1"); + EXPECT_EQ(deserialized.GetElementType(), impl::ServiceElementType::EVENT); + EXPECT_EQ(deserialized.GetElementName(), "SpeedEvent"); +} + +TEST(GatewayMessagesTest, RegisterNotificationRequestRoundTrip) +{ + auto specifier = impl::InstanceSpecifier::Create(std::string{"SpeedService/Instance1"}); + ASSERT_TRUE(specifier.has_value()); + + RegisterNotificationRequest original{std::move(specifier).value(), impl::ServiceElementType::EVENT, "SpeedEvent"}; + + std::array buffer{}; + const auto size = original.Serialize(buffer); + ASSERT_GT(size, 0U); + + RegisterNotificationRequest deserialized; + ASSERT_TRUE(deserialized.Deserialize(score::cpp::span(buffer.data(), size))); + + EXPECT_EQ(deserialized.GetInstanceSpecifier(), "SpeedService/Instance1"); + EXPECT_EQ(deserialized.GetElementType(), impl::ServiceElementType::EVENT); + EXPECT_EQ(deserialized.GetElementName(), "SpeedEvent"); +} + +TEST(GatewayMessagesTest, AckResponseRoundTrip) +{ + AckResponse original{42U}; + + std::array buffer{}; + const auto size = original.Serialize(buffer); + ASSERT_GT(size, 0U); + + AckResponse deserialized; + ASSERT_TRUE(deserialized.Deserialize(score::cpp::span(buffer.data(), size))); + + EXPECT_EQ(deserialized.GetAckedSequence(), 42U); +} + +TEST(GatewayMessagesTest, MessageErrorDomainMessages) +{ + MessageErrorDomain domain; + EXPECT_EQ(domain.MessageFor(static_cast(MessageErrorc::kBufferTooSmall)), + "Serialization buffer too small"); + EXPECT_EQ(domain.MessageFor(static_cast(MessageErrorc::kPayloadInvalid)), + "Format/layout of the message payload is invalid"); + EXPECT_EQ(domain.MessageFor(9999), "unknown message error"); +} + +TEST(GatewayMessagesTest, MakeErrorConstructsError) +{ + auto err = MakeError(MessageErrorc::kBufferTooSmall, "test"); + EXPECT_EQ(*err, static_cast(MessageErrorc::kBufferTooSmall)); + EXPECT_EQ(err.UserMessage(), "test"); +} + +TEST(GatewayMessagesTest, SequenceNumberIsSetAndRetrievedCorrectly) +{ + const auto sequence_number = 123U; + // Given a ProvideServiceRequest with a specific instance specifier and empty service elements + ProvideServiceRequest request{impl::InstanceSpecifier::Create("TestService/Instance1").value(), {}}; + EXPECT_EQ(request.GetSequenceNumber(), 0U); + // when setting the sequence number + request.SetSequenceNumber(sequence_number); + // then the sequence number can be retrieved correctly + EXPECT_EQ(request.GetSequenceNumber(), sequence_number); +} + +TEST(GatewayMessagesTest, MessageTypeCanBeRetrievedCorrectly) +{ + // Given a ProvideServiceRequest + ProvideServiceRequest request{impl::InstanceSpecifier::Create("TestService/Instance1").value(), {}}; + // when retrieving the message type + auto type = request.GetType(); + // then the message type is correct + EXPECT_EQ(type, MessageType::kProvideServiceRequest); +} + +TEST(GatewayMessagesTest, MessageHeaderCanBeRetrievedCorrectly) +{ + // Given a ProvideServiceRequest with a specific instance specifier and empty service elements + ProvideServiceRequest request{impl::InstanceSpecifier::Create("TestService/Instance1").value(), {}}; + // when retrieving the message header + const auto& header = request.GetHeader(); + // then the message header contains the correct type and default sequence and payload size + EXPECT_EQ(header.type, MessageType::kProvideServiceRequest); + EXPECT_EQ(header.sequence, 0U); + EXPECT_EQ(header.payload_size, 0U); +} + +TEST(GatewayMessagesTest, SerializeWithTooSmallBufferReturnsSizeZero) +{ + // Given a ProvideServiceRequest with a specific instance specifier and empty service elements + auto specifier = impl::InstanceSpecifier::Create(std::string{"SpeedService/Instance42"}); + ASSERT_TRUE(specifier.has_value()); + + ProvideServiceRequest service_request{std::move(specifier).value(), {}}; + // when serializing with an intentionally too small buffer + std::array buffer{}; + const auto size = service_request.Serialize(buffer); + // then the serialization fails and returns size 0 + EXPECT_EQ(size, 0U); +} + +TEST(GatewayMessagesTest, SerializeWithVectorExceedingUint16MaxReturnsZero) +{ + // Given a ProvideServiceRequest with more than 65535 ServiceElementConfiguration elements + auto specifier = impl::InstanceSpecifier::Create(std::string{"SpeedService/Instance42"}); + ASSERT_TRUE(specifier.has_value()); + + constexpr std::size_t kTooManyElements = static_cast(std::numeric_limits::max()) + 1U; + std::vector elements(kTooManyElements, + ServiceElementConfiguration{"A", DataTypeSizeInfo{8U, 8U}}); + + ProvideServiceRequest request{std::move(specifier).value(), std::move(elements)}; + + // when serializing with a large buffer so that the initial size check will pass + constexpr std::size_t kLargeBufferSize = 4U * 1024U * 1024U; + std::vector buffer(kLargeBufferSize, 0U); + const auto size = request.Serialize(score::cpp::span(buffer.data(), buffer.size())); + + // then the serialization fails (Serialize of vector checks for max. size of elements) and returns size 0 + EXPECT_EQ(size, 0U); +} + +TEST(GatewayMessageTest, ConstructUnregisterNotificationRequestWithoutConstructorArguments) +{ + // When constructing an UnregisterNotificationRequest without constructor arguments + UnregisterNotificationRequest request; + + // then the message header type should be set to kUnregisterNotificationRequest + EXPECT_EQ(request.GetHeader().type, MessageType::kUnregisterNotificationRequest); +} + +TEST(GatewayMessageTest, AckedSequenceNumberCanBeSetAtAckResponse) +{ + // Given an AckResponse message + AckResponse ack_response; + + // when setting the sequence number + const auto sequence_number = 99U; + ack_response.SetAckedSequence(sequence_number); + + // then this sequence number will be returned when calling the matching getter + EXPECT_EQ(ack_response.GetAckedSequence(), sequence_number); +} + +TEST(GatewayMessageTest, MessageHeaderCanBeSerializedAndDeserialized) +{ + // Given a MessageHeader with specific values + MessageHeader header; + header.type = MessageType::kOfferServiceRequest; + header.sequence = 42U; + header.payload_size = 128U; + + // when serializing to a buffer and deserializing it + std::array buffer{}; + header.SerializeToBuffer(buffer.data()); + + MessageHeader deserialized_header; + deserialized_header.DeserializeFromBuffer(buffer.data()); + + // then the deserialized header should have the same values as the original + EXPECT_EQ(deserialized_header.type, header.type); + EXPECT_EQ(deserialized_header.sequence, header.sequence); + EXPECT_EQ(deserialized_header.payload_size, header.payload_size); +} + +TEST(GatewayMessageTest, RequestTypeCanBeConvertedToMessageType) +{ + // Given a MessageType that is a request type + RequestType request_type = RequestType::kProvideService; + + // when converting it to a message type + const auto message_type = ToMessageType(request_type); + + // then it should be of the expected message type + EXPECT_EQ(message_type, MessageType::kProvideServiceRequest); +} + +TEST(GatewayMessageTest, ResponseTypeCanBeConvertedToMessageType) +{ + // Given a MessageType that is a response type + ResponseType response_type = ResponseType::kAck; + + // when converting it to a message type + const auto message_type = ToMessageType(response_type); + + // then it should be of the expected message type + EXPECT_EQ(message_type, MessageType::kAckResponse); +} + +TEST(GatewayMessageTest, NotificationTypeCanBeConvertedToMessageType) +{ + // Given a MessageType that is a notification type + NotificationType notification_type = NotificationType::kUpdate; + + // when converting it to a message type + const auto message_type = ToMessageType(notification_type); + + // then it should be of the expected message type + EXPECT_EQ(message_type, MessageType::kUpdateNotification); +} + +TEST(GatewayMessageTest, IsRequestIdentifiesRequestTypes) +{ + EXPECT_TRUE(IsRequest(MessageType::kProvideServiceRequest)); + EXPECT_TRUE(IsRequest(MessageType::kOfferServiceRequest)); + EXPECT_TRUE(IsRequest(MessageType::kStopOfferServiceRequest)); + EXPECT_TRUE(IsRequest(MessageType::kRegisterNotificationRequest)); + EXPECT_TRUE(IsRequest(MessageType::kUnregisterNotificationRequest)); + EXPECT_FALSE(IsRequest(MessageType::kUpdateNotification)); + EXPECT_FALSE(IsRequest(MessageType::kAckResponse)); +} + +TEST(GatewayMessageTest, IsNotificationIdentifiesNotificationTypes) +{ + EXPECT_FALSE(IsNotification(MessageType::kProvideServiceRequest)); + EXPECT_FALSE(IsNotification(MessageType::kOfferServiceRequest)); + EXPECT_FALSE(IsNotification(MessageType::kStopOfferServiceRequest)); + EXPECT_FALSE(IsNotification(MessageType::kRegisterNotificationRequest)); + EXPECT_FALSE(IsNotification(MessageType::kUnregisterNotificationRequest)); + EXPECT_TRUE(IsNotification(MessageType::kUpdateNotification)); + EXPECT_FALSE(IsNotification(MessageType::kAckResponse)); +} + +TEST(GatewayMessageTest, IsResponseIdentifiesResponseTypes) +{ + EXPECT_FALSE(IsResponse(MessageType::kProvideServiceRequest)); + EXPECT_FALSE(IsResponse(MessageType::kOfferServiceRequest)); + EXPECT_FALSE(IsResponse(MessageType::kStopOfferServiceRequest)); + EXPECT_FALSE(IsResponse(MessageType::kRegisterNotificationRequest)); + EXPECT_FALSE(IsResponse(MessageType::kUnregisterNotificationRequest)); + EXPECT_FALSE(IsResponse(MessageType::kUpdateNotification)); + EXPECT_TRUE(IsResponse(MessageType::kAckResponse)); +} + +TEST(GatewayMessageTest, RequiresResponseIdentifiesRequestTypes) +{ + EXPECT_TRUE(RequiresResponse(MessageType::kProvideServiceRequest)); + EXPECT_TRUE(RequiresResponse(MessageType::kOfferServiceRequest)); + EXPECT_TRUE(RequiresResponse(MessageType::kStopOfferServiceRequest)); + EXPECT_TRUE(RequiresResponse(MessageType::kRegisterNotificationRequest)); + EXPECT_TRUE(RequiresResponse(MessageType::kUnregisterNotificationRequest)); + EXPECT_FALSE(RequiresResponse(MessageType::kUpdateNotification)); + EXPECT_FALSE(RequiresResponse(MessageType::kAckResponse)); +} +} // namespace +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/messages/message_error.cpp b/score/mw/com/gateway/transport_layer/sample/messages/message_error.cpp new file mode 100644 index 000000000..6bb883288 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/message_error.cpp @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/mw/com/gateway/transport_layer/sample/messages/message_error.h" + +namespace score::mw::com::gateway +{ + +namespace +{ +constexpr MessageErrorDomain g_MessageErrorDomain; +} // namespace + +score::result::Error MakeError(const MessageErrorc code, const std::string_view message) +{ + return {static_cast(code), g_MessageErrorDomain, message}; +} + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/messages/message_error.h b/score/mw/com/gateway/transport_layer/sample/messages/message_error.h new file mode 100644 index 000000000..68ec53b66 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/message_error.h @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_MESSAGES_MESSAGE_ERROR_H_ +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_MESSAGES_MESSAGE_ERROR_H_ + +#include "score/result/result.h" + +#include + +#include + +namespace score::mw::com::gateway +{ + +enum class MessageErrorc : score::result::ErrorCode +{ + kBufferTooSmall = 1, + kPayloadInvalid = 2, +}; + +score::result::Error MakeError(const MessageErrorc code, const std::string_view message = ""); + +class MessageErrorDomain final : public score::result::ErrorDomain +{ + public: + std::string_view MessageFor(const score::result::ErrorCode& code) const noexcept override final + { + switch (code) + { + case static_cast(MessageErrorc::kBufferTooSmall): + return "Serialization buffer too small"; + case static_cast(MessageErrorc::kPayloadInvalid): + return "Format/layout of the message payload is invalid"; + default: + return "unknown message error"; + } + } +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_MESSAGES_MESSAGE_ERROR_H_ diff --git a/score/mw/com/gateway/transport_layer/sample/messages/message_error_test.cpp b/score/mw/com/gateway/transport_layer/sample/messages/message_error_test.cpp new file mode 100644 index 000000000..16aeb2512 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/message_error_test.cpp @@ -0,0 +1,61 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/messages/message_error.h" + +#include + +#include + +namespace score::mw::com::gateway +{ +namespace +{ + +class GatewayErrorMessageFixture : public ::testing::Test +{ + protected: + void testErrorMessage(MessageErrorc errorCode, std::string_view expectedErrorOutput) + { + const auto errorCodeTest = ComErrorDomainDummy.MessageFor(static_cast(errorCode)); + ASSERT_EQ(errorCodeTest, expectedErrorOutput); + } + + MessageErrorDomain ComErrorDomainDummy{}; +}; + +TEST_F(GatewayErrorMessageFixture, MessageForBufferTooSmall) +{ + testErrorMessage(MessageErrorc::kBufferTooSmall, "Serialization buffer too small"); +} + +TEST_F(GatewayErrorMessageFixture, MessageForPayloadInvalid) +{ + testErrorMessage(MessageErrorc::kPayloadInvalid, "Format/layout of the message payload is invalid"); +} + +TEST_F(GatewayErrorMessageFixture, MessageForDefaultError) +{ + auto one_past_the_last_lable = static_cast(MessageErrorc::kPayloadInvalid) + 1; + testErrorMessage(static_cast(one_past_the_last_lable), "unknown message error"); +} + +TEST_F(GatewayErrorMessageFixture, MakeErrorConstructsError) +{ + auto err = MakeError(MessageErrorc::kPayloadInvalid, "test"); + EXPECT_EQ(*err, static_cast(MessageErrorc::kPayloadInvalid)); + EXPECT_EQ(err.UserMessage(), "test"); +} + +} // namespace + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/messages/serialization.h b/score/mw/com/gateway/transport_layer/sample/messages/serialization.h new file mode 100644 index 000000000..8a79ff879 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/serialization.h @@ -0,0 +1,399 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_MESSAGES_SERIALIZATION_H_ +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_MESSAGES_SERIALIZATION_H_ + +#include "score/mw/com/gateway/transport_layer/sample/messages/message_error.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace score::mw::com::gateway +{ + +// Helper to detect std::vector +template +struct is_std_vector : std::false_type +{ +}; + +template +struct is_std_vector> : std::true_type +{ +}; + +// Helper to detect std::string +template +struct is_std_string : std::false_type +{ +}; + +template <> +struct is_std_string : std::true_type +{ +}; + +// Tag types for dispatch +struct trivially_copyable_tag +{ +}; +struct std_vector_tag +{ +}; +struct std_string_tag +{ +}; +struct non_trivial_tag +{ +}; + +// Tag selector for Serialize/Deserialize +template +constexpr auto serialization_tag() +{ + if constexpr (is_std_string>::value) + { + return std_string_tag{}; + } + else if constexpr (std::is_trivially_copyable_v) + { + return trivially_copyable_tag{}; + } + else if constexpr (is_std_vector>::value) + { + return std_vector_tag{}; + } + else + { + return non_trivial_tag{}; + } +} + +// Main entry point for Serialize +/** + * @brief Serialize a value of any type into a byte buffer. This function will dispatch to the correct implementation + * based on the type of the value. + * + * The supported types are: + * - std::string: serialized as a null-terminated string (i.e. the string data followed by a null terminator). The + * required buffer size is the string size + 1 (for the null terminator). + * - Trivially copyable types: serialized as a binary copy of the value. The required buffer size is sizeof the type. + * - std::vector: serialized as a 16-bit unsigned integer for the number of elements, followed by the serialized + * elements. The required buffer size is 2 (for the number of elements) + the sum of the required buffer sizes of the + * elements. + * - Non-trivial, non-vector, non-string types: serialized by calling GetSerializeMembers() to get a tuple of + * references to the members that need to be serialized, and then serializing each member in order. The required + * buffer size is the sum of the required buffer sizes of the members. If any member fails to serialize (e.g. due to + * insufficient buffer size), the entire serialization fails with a buffer too small error. + * + * @tparam T type to be serialized + * @param value value to be serialized + * @param target_buffer buffer to serialize into + * @return number of bytes written to the buffer, or an error if the buffer is too small + */ +template +score::Result Serialize(const T& value, score::cpp::span target_buffer); + +// Main entry point for Deserialize +/** + * @brief Deserialize a value of any type from a byte buffer. This function will dispatch to the correct implementation + * based on the type of the value. + * + * The supported types are: + * - std::string: deserialized from a null-terminated string (i.e. the string data followed by a null terminator). + * - Trivially copyable types: deserialized as a binary copy of the value. + * - std::vector: deserialized as a 16-bit unsigned integer for the number of elements, followed by the deserialized + * elements. + * - Non-trivial, non-vector, non-string types: deserialized by calling GetSerializeMembers() to get a tuple of + * references to the members that need to be deserialized, and then deserializing each member in order. + * + * @tparam T type to be deserialized + * @param value value to be deserialized + * @param source_buffer buffer to deserialize from + * @return number of bytes read from the buffer, or an error if the buffer is too small or invalid + */ +template +score::Result Deserialize(T& value, score::cpp::span source_buffer); + +/** + * @brief Serialize implementation for std::string. + * @details See above in the main Serialize function + */ +template +score::Result Serialize(const T& string, score::cpp::span target_buffer, std_string_tag) +{ + const auto required_size = string.size() + 1U; + if (target_buffer.size() < required_size) + { + return score::MakeUnexpected(score::mw::com::gateway::MessageErrorc::kBufferTooSmall); + } + + // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage): This is no string_view. + std::memcpy(target_buffer.data(), string.data(), string.size()); + std::memset(&target_buffer[string.size()], 0, 1U); + return static_cast(required_size); +} + +/** + * @brief Serialize implementation for trivially copyable types. + * @details See above in the main Serialize function + */ +template +score::Result Serialize(const T& trivial_copyable, + score::cpp::span target_buffer, + trivially_copyable_tag) +{ + constexpr auto required_size = sizeof(T); + if (target_buffer.size() < required_size) + { + return score::MakeUnexpected(score::mw::com::gateway::MessageErrorc::kBufferTooSmall); + } + std::memcpy(static_cast(target_buffer.data()), static_cast(&trivial_copyable), sizeof(T)); + return static_cast(required_size); +} + +/** + * @brief Serialize implementation for std::vector. + * @details See above in the main Serialize function + */ +template +score::Result Serialize(const T& vector, score::cpp::span target_buffer, std_vector_tag) +{ + std::uint32_t copied_size{0}; + if (vector.size() > std::numeric_limits::max()) + { + return score::MakeUnexpected(score::mw::com::gateway::MessageErrorc::kBufferTooSmall); + } + std::uint16_t num_elements = static_cast(vector.size()); + if (target_buffer.size() < sizeof(num_elements)) + { + return score::MakeUnexpected(score::mw::com::gateway::MessageErrorc::kBufferTooSmall); + } + std::memcpy(&target_buffer[copied_size], &num_elements, sizeof(num_elements)); + copied_size += static_cast(sizeof(num_elements)); + for (const auto& element : vector) + { + auto element_result = Serialize(element, target_buffer.subspan(copied_size)); + if (!element_result) + { + return element_result; + } + copied_size += element_result.value(); + } + return copied_size; +} + +/** + * @brief Serialize implementation for non-trivial, non-vector, non-string types. + * @details See above in the main Serialize function + */ +template +score::Result Serialize(const T& non_trivial_copyable, + score::cpp::span target_buffer, + non_trivial_tag) +{ + auto serializable_members = non_trivial_copyable.GetSerializeMembers(); + std::uint32_t total_written_bytes{0}; + bool error_found = false; + std::apply( + [&](auto&... member) { + (([&] { + if (error_found) + return; + auto bytes_written = Serialize(member, target_buffer); + if (!bytes_written) + { + error_found = true; + total_written_bytes = 0; + return; + } + target_buffer = target_buffer.subspan(bytes_written.value()); + total_written_bytes += bytes_written.value(); + }()), + ...); + }, + serializable_members); + if (error_found) + { + return score::MakeUnexpected(score::mw::com::gateway::MessageErrorc::kBufferTooSmall); + } + return total_written_bytes; +} + +template +score::Result Serialize(const T& value, score::cpp::span target_buffer) +{ + return Serialize(value, target_buffer, serialization_tag()); +} + +template +std::size_t ComputeSerializedSize(const T& value); + +template +std::size_t ComputeSerializedSize(const T& value, std_string_tag) +{ + return value.size() + 1U; +} + +template +std::size_t ComputeSerializedSize(const T&, trivially_copyable_tag) +{ + return sizeof(T); +} + +template +std::size_t ComputeSerializedSize(const T& vector, std_vector_tag) +{ + std::size_t size = sizeof(std::uint16_t); + for (const auto& element : vector) + { + size += ComputeSerializedSize(element); + } + return size; +} + +template +std::size_t ComputeSerializedSize(const T& non_trivial, non_trivial_tag) +{ + auto members = non_trivial.GetSerializeMembers(); + std::size_t size = 0U; + std::apply( + [&](const auto&... member) { + ((size += ComputeSerializedSize(member)), ...); + }, + members); + return size; +} + +template +std::size_t ComputeSerializedSize(const T& value) +{ + return ComputeSerializedSize(value, serialization_tag()); +} + +/** + * @brief Deserialize implementation for std::string. + * @details See above in the main Deserialize function + */ +template +score::Result Deserialize(T& target_string, score::cpp::span source_buffer, std_string_tag) +{ + auto string_start = reinterpret_cast(source_buffer.data()); + const auto string_len = strnlen(string_start, source_buffer.size()); + auto remaining_size = source_buffer.size(); + if (string_len == remaining_size || string_len == 0) + { + return score::MakeUnexpected(score::mw::com::gateway::MessageErrorc::kPayloadInvalid); + } + target_string.assign(string_start, string_len); + return static_cast(string_len + 1U); +} + +/** + * @brief Deserialize implementation for trivially copyable types. + * @details See above in the main Deserialize function + */ +template +score::Result Deserialize(T& target, score::cpp::span source_buffer, trivially_copyable_tag) +{ + if (source_buffer.size() < sizeof(T)) + { + return score::MakeUnexpected(score::mw::com::gateway::MessageErrorc::kPayloadInvalid); + } + std::memcpy(static_cast(&target), static_cast(source_buffer.data()), sizeof(T)); + return static_cast(sizeof(T)); +} + +/** + * @brief Deserialize implementation for std::vector. + * @details See above in the main Deserialize function + */ +template +score::Result Deserialize(T& vector, score::cpp::span source_buffer, std_vector_tag) +{ + if (source_buffer.size() < sizeof(std::uint16_t)) + { + return score::MakeUnexpected(score::mw::com::gateway::MessageErrorc::kPayloadInvalid); + } + uint32_t bytes_consumed{0}; + std::uint16_t num_elements{}; + std::memcpy(&num_elements, source_buffer.data(), sizeof(std::uint16_t)); + bytes_consumed += static_cast(sizeof(std::uint16_t)); + vector.clear(); + vector.reserve(num_elements); + for (std::uint16_t i = 0; i < num_elements; i++) + { + typename T::value_type element{}; + auto element_result = Deserialize(element, source_buffer.subspan(bytes_consumed)); + if (!element_result) + { + return element_result; + } + bytes_consumed += element_result.value(); + vector.emplace_back(std::move(element)); + } + return bytes_consumed; +} + +/** + * @brief Deserialize implementation for non-trivial, non-vector, non-string types. + * @details See above in the main Deserialize function + */ +template +score::Result Deserialize(T& non_trivial_copyable, + score::cpp::span source_buffer, + non_trivial_tag) +{ + auto deserializable_members = non_trivial_copyable.GetSerializeMembers(); + std::uint32_t total_consumed_bytes{0}; + bool error_found = false; + std::apply( + [&](auto&... member) { + (([&] { + if (error_found) + return; + auto bytes_consumed = Deserialize(member, source_buffer); + if (!bytes_consumed) + { + error_found = true; + total_consumed_bytes = 0; + return; + } + source_buffer = source_buffer.subspan(bytes_consumed.value()); + total_consumed_bytes += bytes_consumed.value(); + }()), + ...); + }, + deserializable_members); + if (error_found) + { + return score::MakeUnexpected(score::mw::com::gateway::MessageErrorc::kPayloadInvalid); + } + return total_consumed_bytes; +} + +template +score::Result Deserialize(T& value, score::cpp::span source_buffer) +{ + return Deserialize(value, source_buffer, serialization_tag()); +} + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_MESSAGES_SERIALIZATION_H_ diff --git a/score/mw/com/gateway/transport_layer/sample/messages/serialization_test.cpp b/score/mw/com/gateway/transport_layer/sample/messages/serialization_test.cpp new file mode 100644 index 000000000..ac49e09c5 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/serialization_test.cpp @@ -0,0 +1,344 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/messages/serialization.h" + +#include "score/mw/com/gateway/transport_layer/transport.h" + +#include + +#include +#include +#include +#include +#include + +namespace score::mw::com::gateway +{ +namespace +{ + +TEST(SerializationTest, SerializeAndDeserializeTrivialTypeProducesOriginalValue) +{ + const std::uint32_t original = 0xDEADBEEFU; + std::array buf{}; + + auto written = Serialize(original, score::cpp::span(buf.data(), buf.size())); + ASSERT_TRUE(written.has_value()); + EXPECT_EQ(written.value(), sizeof(original)); + + std::uint32_t out{}; + auto consumed = Deserialize(out, score::cpp::span(buf.data(), buf.size())); + ASSERT_TRUE(consumed.has_value()); + EXPECT_EQ(out, 0xDEADBEEFU); +} + +TEST(SerializationTest, TrivialTypeSerializeAndDeserializeFailOnTooSmallBuffer) +{ + const std::uint32_t value = 42U; + std::array buf{}; + + EXPECT_FALSE(Serialize(value, score::cpp::span(buf.data(), buf.size())).has_value()); + + std::uint32_t out{}; + EXPECT_FALSE(Deserialize(out, score::cpp::span(buf.data(), buf.size())).has_value()); +} + +TEST(SerializationTest, SerializeAndDeserializeStringProducesOriginalValue) +{ + const std::string original = "hello"; + constexpr std::size_t kExpectedSize = 5U + 1U; // string length + null terminator + std::array buf{}; + + auto written = Serialize(original, score::cpp::span(buf.data(), buf.size())); + ASSERT_TRUE(written.has_value()); + EXPECT_EQ(written.value(), kExpectedSize); + EXPECT_EQ(buf[original.size()], std::byte{0}); + + std::string out; + auto consumed = Deserialize(out, score::cpp::span(buf.data(), written.value())); + ASSERT_TRUE(consumed.has_value()); + EXPECT_EQ(out, "hello"); +} + +TEST(SerializationTest, StringSerializeFailsOnTooSmallBuffer) +{ + const std::string original = "hello"; + std::array buf{}; + + EXPECT_FALSE(Serialize(original, score::cpp::span(buf.data(), buf.size())).has_value()); +} + +TEST(SerializationTest, StringDeserializeFailsWithoutNullTerminator) +{ + std::array buf{}; + std::memset(buf.data(), 'A', buf.size()); + + std::string out; + EXPECT_FALSE(Deserialize(out, score::cpp::span(buf.data(), buf.size())).has_value()); +} + +TEST(SerializationTest, SerializeAndDeserializeVectorOfTrivialsProducesOriginalValues) +{ + const std::vector original = {10U, 20U, 30U}; + std::array buf{}; + + constexpr std::size_t kNumElements = 3U; + constexpr std::size_t kVectorHeaderSize = sizeof(std::uint16_t); + constexpr std::size_t kExpectedSize = kVectorHeaderSize + kNumElements * sizeof(std::uint16_t); + + auto written = Serialize(original, score::cpp::span(buf.data(), buf.size())); + ASSERT_TRUE(written.has_value()); + EXPECT_EQ(written.value(), kExpectedSize); + + std::vector out; + auto consumed = Deserialize(out, score::cpp::span(buf.data(), written.value())); + ASSERT_TRUE(consumed.has_value()); + EXPECT_EQ(out, original); +} + +TEST(SerializationTest, EmptyVectorSerializesToJustTheElementCountHeader) +{ + const std::vector original; + std::array buf{}; + + auto written = Serialize(original, score::cpp::span(buf.data(), buf.size())); + ASSERT_TRUE(written.has_value()); + EXPECT_EQ(written.value(), sizeof(std::uint16_t)); + + std::vector out; + auto consumed = Deserialize(out, score::cpp::span(buf.data(), written.value())); + ASSERT_TRUE(consumed.has_value()); + EXPECT_TRUE(out.empty()); +} + +TEST(SerializationTest, SerializeAndDeserializeVectorOfStringsProducesOriginalValues) +{ + const std::vector original = {"foo", "bar", "baz"}; + std::array buf{}; + + auto written = Serialize(original, score::cpp::span(buf.data(), buf.size())); + ASSERT_TRUE(written.has_value()); + + std::vector out; + auto consumed = Deserialize(out, score::cpp::span(buf.data(), written.value())); + ASSERT_TRUE(consumed.has_value()); + EXPECT_EQ(out, original); +} + +TEST(SerializationTest, VectorSerializeAndDeserializeFailWhenBufferTooSmallForHeader) +{ + const std::vector original = {1U}; + std::array buf{}; + + EXPECT_FALSE(Serialize(original, score::cpp::span(buf.data(), buf.size())).has_value()); + + std::vector out; + EXPECT_FALSE(Deserialize(out, score::cpp::span(buf.data(), buf.size())).has_value()); +} + +TEST(SerializationTest, SerializeAndDeserializeNonTrivialStructProducesOriginalMembers) +{ + ServiceElementConfiguration original{"SpeedEvent", DataTypeSizeInfo{64U, 8U}}; + std::array buf{}; + + auto written = Serialize(original, score::cpp::span(buf.data(), buf.size())); + ASSERT_TRUE(written.has_value()); + + ServiceElementConfiguration out; + auto consumed = Deserialize(out, score::cpp::span(buf.data(), written.value())); + ASSERT_TRUE(consumed.has_value()); + EXPECT_EQ(out.element_name, "SpeedEvent"); + EXPECT_EQ(out.size_info, original.size_info); +} + +TEST(SerializationTest, ComputeSerializedSizeReturnsCorrectValuesPerTypeCategory) +{ + constexpr std::size_t kVectorHeaderSize = sizeof(std::uint16_t); + constexpr std::size_t kNullTerminatorSize = 1U; + + EXPECT_EQ(ComputeSerializedSize(std::uint32_t{0}), sizeof(std::uint32_t)); + EXPECT_EQ(ComputeSerializedSize(std::uint8_t{0}), sizeof(std::uint8_t)); + + const std::string test_str = "test"; + EXPECT_EQ(ComputeSerializedSize(test_str), test_str.size() + kNullTerminatorSize); + EXPECT_EQ(ComputeSerializedSize(std::string{}), kNullTerminatorSize); + + const std::vector vec = {1U, 2U, 3U}; + EXPECT_EQ(ComputeSerializedSize(vec), kVectorHeaderSize + vec.size() * sizeof(std::uint32_t)); + + const std::string ab = "ab"; + const std::string cde = "cde"; + const std::vector str_vec = {ab, cde}; + const std::size_t kExpectedStrVecSize = + kVectorHeaderSize + (ab.size() + kNullTerminatorSize) + (cde.size() + kNullTerminatorSize); + EXPECT_EQ(ComputeSerializedSize(str_vec), kExpectedStrVecSize); + + EXPECT_EQ(ComputeSerializedSize(std::vector{}), kVectorHeaderSize); +} + +TEST(SerializationTest, ComputeSerializedSizeMatchesBytesWrittenBySerialize) +{ + std::array buf{}; + + const std::uint32_t trivial = 42U; + const std::string str = "gateway_test"; + const std::vector vec = {1U, 2U, 3U, 4U, 5U}; + ServiceElementConfiguration config{"SpeedEvent", DataTypeSizeInfo{64U, 8U}}; + std::vector configs = { + {"SpeedEvent", DataTypeSizeInfo{64U, 8U}}, + {"BrakeField", DataTypeSizeInfo{32U, 4U}}, + }; + + auto w1 = Serialize(trivial, score::cpp::span(buf.data(), buf.size())); + EXPECT_EQ(ComputeSerializedSize(trivial), w1.value()); + + auto w2 = Serialize(str, score::cpp::span(buf.data(), buf.size())); + EXPECT_EQ(ComputeSerializedSize(str), w2.value()); + + auto w3 = Serialize(vec, score::cpp::span(buf.data(), buf.size())); + EXPECT_EQ(ComputeSerializedSize(vec), w3.value()); + + auto w4 = Serialize(config, score::cpp::span(buf.data(), buf.size())); + EXPECT_EQ(ComputeSerializedSize(config), w4.value()); + + auto w5 = Serialize(configs, score::cpp::span(buf.data(), buf.size())); + EXPECT_EQ(ComputeSerializedSize(configs), w5.value()); +} + +TEST(SerializationTest, SerializeSucceedsIntoExactSizeBufferAndFailsWithOneByteLess) +{ + const std::string value = "exact"; + const auto size = ComputeSerializedSize(value); + + std::vector exact_buf(size); + EXPECT_TRUE(Serialize(value, score::cpp::span(exact_buf.data(), exact_buf.size())).has_value()); + + std::vector small_buf(size - 1U); + EXPECT_FALSE(Serialize(value, score::cpp::span(small_buf.data(), small_buf.size())).has_value()); +} + +TEST(SerializationTest, SerializeStringFailsIfTargetBufferTooSmall) +{ + // Given a string and a buffer that is smaller than the string's size + const std::string value = "HelloWorld"; + const auto size = ComputeSerializedSize(value); + + std::vector small_buf(size - 1U); + // when trying to serialize the string + const auto result = Serialize(value, score::cpp::span(small_buf.data(), small_buf.size())); + // then the result should not have a value and the error code should be kBufferTooSmall + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MessageErrorc::kBufferTooSmall); +} + +TEST(SerializationTest, DeserializeStringFailsIfSourceBufferIsNotNullTerminated) +{ + // Given a buffer that is smaller than the expected string size and the string is copied into that buffer + const std::string expected = "HelloWorld"; + const auto size = ComputeSerializedSize(expected); + + std::vector small_buf(size - 1U); + std::memcpy(small_buf.data(), expected.data(), small_buf.size()); + + // when trying to deserialize into a string, which is not null-terminated due to the small buffer + std::string out; + const auto result = Deserialize(out, score::cpp::span(small_buf.data(), small_buf.size())); + // then the result should not have a value and the error code should be kPayloadInvalid + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MessageErrorc::kPayloadInvalid); +} + +TEST(SerializationTest, DeserializeStringFailsIfSourceBufferIsTooSmall) +{ + // Given a buffer that is smaller than the expected string size and the string is copied into that buffer + const std::string expected = "HelloWorld"; + const auto size = ComputeSerializedSize(expected); + std::vector small_buf(size - 1U); + + // when trying to deserialize into a string + std::string out; + const auto result = Deserialize(out, score::cpp::span(small_buf.data(), small_buf.size())); + // then the result should not have a value and the error code should be kPayloadInvalid + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MessageErrorc::kPayloadInvalid); +} + +TEST(SerializationTest, SerializeVectorExceedingUint16MaxSizeReturnsError) +{ + // Given a vector with a size that exceeds the maximum representable by uint16_t + const std::vector original(1U + std::numeric_limits::max(), 0U); + std::array buf{}; + // when trying to serialize the vector + const auto serialize_result = Serialize(original, score::cpp::span(buf.data(), buf.size())); + // then the result should have the error code kBufferTooSmall due to the size limit of input vectors + EXPECT_FALSE(serialize_result.has_value()); + EXPECT_EQ(serialize_result.error(), MessageErrorc::kBufferTooSmall); +} + +TEST(SerializationTest, SerializeVectorFailsIfNotEnoughSpaceForSubElement) +{ + // Given a vector with one element and a buffer that is too small to hold the element + const std::vector original = {42U}; + std::array buf{}; + + // when trying to serialize the vector + const auto serialize_result = Serialize(original, score::cpp::span(buf.data(), buf.size())); + // then the result should have the error code kBufferTooSmall due to insufficient space for the element + EXPECT_FALSE(serialize_result.has_value()); + EXPECT_EQ(serialize_result.error(), MessageErrorc::kBufferTooSmall); +} + +TEST(SerializationTest, DeserializeVectorFailsIfNotEnoughSpaceForSubElement) +{ + // Given a buffer that encodes a vector of 1 uint32_t element, but does not have enough bytes for the element itself + constexpr std::uint16_t num_elements = 1U; + // Buffer holds the 2-byte element count header, but only 3 bytes for the element + std::array buf{}; + std::memcpy(buf.data(), &num_elements, sizeof(num_elements)); + + // when trying to deserialize the vector + std::vector out; + const auto result = Deserialize(out, score::cpp::span(buf.data(), buf.size())); + + // then the result should not have a value and the error code should be kPayloadInvalid + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MessageErrorc::kPayloadInvalid); +} + +TEST(SerializationTest, SerializeNonTrivialCopyableTypeFailsIfBufferTooSmall) +{ + // Given a non-trivial copyable type and a buffer that is too small to hold its serialized form + ServiceElementConfiguration config{"SpeedEvent", DataTypeSizeInfo{64U, 8U}}; + std::array buf{}; + + // when trying to serialize the non-trivial type + const auto serialize_result = Serialize(config, score::cpp::span(buf.data(), buf.size())); + // then the result should have the error code kBufferTooSmall due to insufficient space for the serialized struct + EXPECT_FALSE(serialize_result.has_value()); + EXPECT_EQ(serialize_result.error(), MessageErrorc::kBufferTooSmall); +} + +TEST(SerializationTest, DeserializeNonTrivialCopyableTypeFailsIfBufferTooSmall) +{ + // Given a buffer that is too small to hold the serialized form of a non-trivial copyable type + std::array buf{}; + + // when trying to deserialize into a non-trivial type + ServiceElementConfiguration out; + const auto deserialize_result = Deserialize(out, score::cpp::span(buf.data(), buf.size())); + // then the result should have the error code kPayloadInvalid due to insufficient bytes for deserialization + EXPECT_FALSE(deserialize_result.has_value()); + EXPECT_EQ(deserialize_result.error(), MessageErrorc::kPayloadInvalid); +} +} // namespace +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/messages/transport_message.cpp b/score/mw/com/gateway/transport_layer/sample/messages/transport_message.cpp new file mode 100644 index 000000000..e32bea227 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/transport_message.cpp @@ -0,0 +1,13 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/messages/transport_message.h" diff --git a/score/mw/com/gateway/transport_layer/sample/messages/transport_message.h b/score/mw/com/gateway/transport_layer/sample/messages/transport_message.h new file mode 100644 index 000000000..ae470a19f --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/messages/transport_message.h @@ -0,0 +1,171 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_TRANSPORT_MESSAGE_H_ +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_TRANSPORT_MESSAGE_H_ + +#include + +#include +#include + +namespace score::mw::com::gateway +{ + +enum class RequestType : std::uint8_t +{ + kProvideService = 0x01, + kOfferService = 0x02, + kStopOfferService = 0x03, + kRegisterNotification = 0x04, + kUnregisterNotification = 0x05 +}; + +enum class NotificationType : std::uint8_t +{ + kUpdate = 0x40, +}; + +enum class ResponseType : std::uint8_t +{ + kAck = 0x80 +}; + +enum class MessageType : std::uint8_t +{ + // Requests + kProvideServiceRequest = static_cast(RequestType::kProvideService), + kOfferServiceRequest = static_cast(RequestType::kOfferService), + kStopOfferServiceRequest = static_cast(RequestType::kStopOfferService), + kRegisterNotificationRequest = static_cast(RequestType::kRegisterNotification), + kUnregisterNotificationRequest = static_cast(RequestType::kUnregisterNotification), + + // Responses + kAckResponse = static_cast(ResponseType::kAck), + + // Notifications + kUpdateNotification = static_cast(NotificationType::kUpdate), + // Invalid message type, e.g. for uninitialized messages or in case of deserialization failures. + kInvalid = 0xFFU +}; + +inline constexpr MessageType ToMessageType(RequestType type) +{ + return static_cast(type); +} + +inline constexpr MessageType ToMessageType(NotificationType type) +{ + return static_cast(type); +} + +inline constexpr MessageType ToMessageType(ResponseType type) +{ + return static_cast(type); +} + +inline bool IsRequest(MessageType type) +{ + return type == MessageType::kProvideServiceRequest || type == MessageType::kOfferServiceRequest || + type == MessageType::kStopOfferServiceRequest || type == MessageType::kRegisterNotificationRequest || + type == MessageType::kUnregisterNotificationRequest; +} + +inline bool IsNotification(MessageType type) +{ + return (type == MessageType::kUpdateNotification); +} + +inline bool IsResponse(MessageType type) +{ + return type == MessageType::kAckResponse; +} + +inline bool RequiresResponse(MessageType type) +{ + return IsRequest(type); +} + +struct MessageHeader +{ + static constexpr std::size_t kWireSize = sizeof(MessageType) + sizeof(std::uint32_t) + sizeof(std::uint32_t); + + MessageType type{MessageType::kInvalid}; + std::uint32_t sequence{0U}; + std::uint32_t payload_size{0U}; + + void SerializeToBuffer(std::uint8_t* buffer) const noexcept + { + std::size_t offset = 0U; + std::memcpy(buffer + offset, &type, sizeof(type)); + offset += sizeof(type); + std::memcpy(buffer + offset, &sequence, sizeof(sequence)); + offset += sizeof(sequence); + std::memcpy(buffer + offset, &payload_size, sizeof(payload_size)); + } + + void DeserializeFromBuffer(const std::uint8_t* buffer) noexcept + { + std::size_t offset = 0U; + std::memcpy(&type, buffer + offset, sizeof(type)); + offset += sizeof(type); + std::memcpy(&sequence, buffer + offset, sizeof(sequence)); + offset += sizeof(sequence); + std::memcpy(&payload_size, buffer + offset, sizeof(payload_size)); + } +}; + +class TransportMessage +{ + public: + explicit TransportMessage(MessageType message_type) noexcept + { + header_.type = message_type; + } + + virtual ~TransportMessage() = default; + + TransportMessage(const TransportMessage&) = delete; + TransportMessage& operator=(const TransportMessage&) = delete; + TransportMessage(TransportMessage&&) = default; + TransportMessage& operator=(TransportMessage&&) = default; + + virtual std::size_t Serialize(score::cpp::span buffer) const = 0; + virtual bool Deserialize(score::cpp::span data) = 0; + + const MessageHeader& GetHeader() const + { + return header_; + } + + MessageType GetType() const + { + return header_.type; + } + + std::uint32_t GetSequenceNumber() const + { + return header_.sequence; + } + + void SetSequenceNumber(std::uint32_t sequence) + { + header_.sequence = sequence; + } + + protected: + MessageHeader header_; +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_TRANSPORT_MESSAGE_H_ diff --git a/score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.cpp b/score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.cpp new file mode 100644 index 000000000..12dd798cb --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.cpp @@ -0,0 +1,255 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.h" + +#include "score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h" +#include "score/mw/com/impl/configuration/lola_service_instance_deployment.h" +#include "score/mw/com/impl/configuration/lola_service_type_deployment.h" +#include "score/mw/com/impl/instance_identifier.h" +#include "score/mw/com/impl/runtime.h" +#include "score/mw/log/logging.h" + +#include +#include + +namespace score::mw::com::gateway +{ + +ShmPaths ResolveShmPaths(const impl::InstanceSpecifier& specifier) +{ + auto& runtime = impl::Runtime::getInstance(); + auto identifiers = runtime.resolve(specifier); + if (identifiers.empty()) + { + return {}; + } + impl::InstanceIdentifierView view{identifiers.front()}; + const auto* lola_type = + std::get_if(&view.GetServiceTypeDeployment().binding_info_); + const auto* lola_instance = + std::get_if(&view.GetServiceInstanceDeployment().bindingInfo_); + + if (lola_type == nullptr || lola_instance == nullptr || !lola_instance->instance_id_.has_value()) + { + return {}; + } + // TODO Implement a ShmPathBuilder for your specific hypervisor shared memory technology that provides the according + // path names. + // LCOV_EXCL_START This code is not yet implemented so its not yet covered by tests + SCORE_LANGUAGE_FUTURECPP_PRECONDITION_PRD_MESSAGE( + false, "Missing implementation of ShmPathBuilder for LoLa Hypervisor gateway."); + // LCOV_EXCL_STOP +} + +ShmSizes GetShmSizes(const impl::InstanceSpecifier& specifier) +{ + auto paths = ResolveShmPaths(specifier); + if (paths.control.empty()) + { + ::score::mw::log::LogError() << "GetShmSizes: failed to resolve SHM paths for " << specifier.ToString(); + return {0U, 0U}; + } + + // LCOV_EXCL_START This code is not yet implemented so its not yet covered by tests + SCORE_LANGUAGE_FUTURECPP_PRECONDITION_PRD_MESSAGE( + false, "Missing specific implementation of LoLa hypervisor gateway SHM size calculation."); + // TODO Add an implementation that is opening the SHM paths and calculates the size of those segments. This sample + // implementation does not include this implementation, as it is highly specific to the used hypervisor shared + // memory technology. + // LCOV_EXCL_STOP +} + +SampleHyperVisorTransport::SampleHyperVisorTransport(GatewayCore& gateway_app, + std::unique_ptr transport) noexcept + : gateway_app_{gateway_app}, transport_{std::move(transport)} +{ +} + +SampleHyperVisorTransport::~SampleHyperVisorTransport() +{ + Shutdown(); +} + +bool SampleHyperVisorTransport::IsMemorySharingSupported() const +{ + return true; +} + +score::ResultBlank SampleHyperVisorTransport::Setup() +{ + transport_->SetMessageHandler([this](std::unique_ptr message) { + OnMessageReceived(std::move(message)); + }); + return transport_->Setup(); +} + +void SampleHyperVisorTransport::OnMessageReceived(std::unique_ptr message) +{ + if (!message) + { + log::LogError("LoLa") << "SampleTransport: Invalid message received: nullptr"; + return; + } + + if (message->GetType() == MessageType::kProvideServiceRequest) + { + auto& request = dynamic_cast(*message); + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "SampleTransport: Invalid instance specifier in ProvideServiceRequest!"; + return; + } + PreCreateInterVmSharedMemory(specifier_result.value(), request.GetShmControlSize(), request.GetShmDataSize()); + gateway_app_.ProvideService(specifier_result.value(), request.GetServiceElements()); + } + else if (message->GetType() == MessageType::kStopOfferServiceRequest) + { + auto& request = dynamic_cast(*message); + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "SampleTransport: Invalid instance specifier in StopOfferServiceRequest!"; + return; + } + gateway_app_.StopOfferService(specifier_result.value()); + } + else if (message->GetType() == MessageType::kOfferServiceRequest) + { + auto& request = dynamic_cast(*message); + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "SampleTransport: Invalid instance specifier in OfferServiceRequest!"; + return; + } + gateway_app_.OfferService(specifier_result.value()); + } + else if (message->GetType() == MessageType::kRegisterNotificationRequest) + { + auto& request = dynamic_cast(*message); + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "SampleTransport: Invalid instance specifier in RegisterNotificationRequest!"; + return; + } + gateway_app_.RegisterUpdateNotification( + specifier_result.value(), request.GetElementType(), request.GetElementName()); + } + else if (message->GetType() == MessageType::kUnregisterNotificationRequest) + { + auto& request = dynamic_cast(*message); + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "SampleTransport: Invalid instance specifier in UnregisterNotificationRequest!"; + return; + } + gateway_app_.UnregisterUpdateNotification( + specifier_result.value(), request.GetElementType(), request.GetElementName()); + } + else if (message->GetType() == MessageType::kUpdateNotification) + { + auto& notification = dynamic_cast(*message); + auto specifier_result = impl::InstanceSpecifier::Create(std::string{notification.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "SampleTransport: Invalid instance specifier in UpdateNotification!"; + return; + } + gateway_app_.NotifyUpdate( + specifier_result.value(), notification.GetElementType(), notification.GetElementName()); + } + else + { + log::LogError("LoLa") << "SampleTransport: Unexpected TransportMessage received: " + << static_cast(message->GetType()); + } +} + +void SampleHyperVisorTransport::Shutdown() +{ + transport_->Shutdown(); +} + +score::ResultBlank SampleHyperVisorTransport::ProvideService(impl::InstanceSpecifier service_instance_specifier, + std::vector service_elements) +{ + const auto shm_sizes = GetShmSizes(service_instance_specifier); + + ProvideServiceRequest request{ + std::move(service_instance_specifier), std::move(service_elements), shm_sizes.control, shm_sizes.data}; + return transport_->SendRequest(request); +} + +score::ResultBlank SampleHyperVisorTransport::OfferService(impl::InstanceSpecifier service_instance_specifier) +{ + OfferServiceRequest request{std::move(service_instance_specifier)}; + return transport_->SendRequest(request); +} + +score::ResultBlank SampleHyperVisorTransport::StopOfferService(impl::InstanceSpecifier service_instance_specifier) +{ + StopOfferServiceRequest request{std::move(service_instance_specifier)}; + return transport_->SendRequest(request); +} + +score::ResultBlank SampleHyperVisorTransport::NotifyUpdate(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType updated_element_type, + std::string updated_element_name) +{ + UpdateNotification notification{ + std::move(service_instance_specifier), updated_element_type, std::move(updated_element_name)}; + return transport_->SendNotification(notification); +} + +score::ResultBlank SampleHyperVisorTransport::RegisterUpdateNotification( + impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) +{ + RegisterNotificationRequest request{std::move(service_instance_specifier), element_type, std::move(element_name)}; + return transport_->SendRequest(request); +} + +score::ResultBlank SampleHyperVisorTransport::UnregisterUpdateNotification( + impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) +{ + UnregisterNotificationRequest request{std::move(service_instance_specifier), element_type, std::move(element_name)}; + return transport_->SendRequest(request); +} + +void SampleHyperVisorTransport::PreCreateInterVmSharedMemory(const impl::InstanceSpecifier& specifier, + std::uint32_t shm_control_size, + std::uint32_t shm_data_size) +{ + auto paths = ResolveShmPaths(specifier); + if (paths.control.empty()) + { + ::score::mw::log::LogError() << "PreCreateInterVmSharedMemory: failed to resolve SHM paths for " + << specifier.ToString(); + return; + } + // LCOV_EXCL_START This code is not yet implemented so its not yet covered by tests + SCORE_LANGUAGE_FUTURECPP_PRECONDITION_PRD_MESSAGE( + false, "Missing specific implementation of hypervisor SHM memory access."); + // TODO Add an implementation that is opening the SHM paths in a hypervisor gateway compatible way. This sample + // implementation does not include this implementation, as it is highly specific to the used hypervisor shared + // memory technology. + // LCOV_EXCL_STOP +} + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.h b/score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.h new file mode 100644 index 000000000..4d896a324 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.h @@ -0,0 +1,90 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_HYPERVISOR_TRANSPORT_H_ +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_HYPERVISOR_TRANSPORT_H_ + +#include "score/mw/com/gateway/gateway_application/gateway_core.h" +#include "score/mw/com/gateway/transport_layer/sample/i_bidirectional_transport.h" +#include "score/mw/com/gateway/transport_layer/transport.h" + +#include + +namespace score::mw::com::gateway +{ + +struct ShmPaths +{ + std::string control; + std::string data; +}; + +struct ShmSizes +{ + std::uint32_t control; + std::uint32_t data; +}; + +ShmPaths ResolveShmPaths(const impl::InstanceSpecifier& specifier); + +ShmSizes GetShmSizes(const impl::InstanceSpecifier& specifier); + +class SampleHyperVisorTransport : public Transport +{ + public: + SampleHyperVisorTransport(GatewayCore& gateway_app, std::unique_ptr transport) noexcept; + ~SampleHyperVisorTransport() override; + + SampleHyperVisorTransport(const SampleHyperVisorTransport&) = delete; + SampleHyperVisorTransport& operator=(const SampleHyperVisorTransport&) = delete; + SampleHyperVisorTransport(SampleHyperVisorTransport&&) = delete; + SampleHyperVisorTransport& operator=(SampleHyperVisorTransport&&) = delete; + + bool IsMemorySharingSupported() const override; + + Result Setup() override; + void Shutdown() override; + + Result ProvideService(impl::InstanceSpecifier service_instance_specifier, + std::vector service_elements) override; + Result OfferService(impl::InstanceSpecifier service_instance_specifier) override; + Result StopOfferService(impl::InstanceSpecifier service_instance_specifier) override; + + Result NotifyUpdate(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType updated_element_type, + std::string updated_element_name) override; + Result RegisterUpdateNotification(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) override; + Result UnregisterUpdateNotification(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) override; + + private: + /// \brief Internal callback for incoming messages from the transport. + /// \details inspects the received message and dispatches it to related API calls of the gateway application + /// (e.g. ProvideService, NotifyUpdate, etc.). In some cases it does a message specific processing before calling + /// the gateway application API (e.g. for ProvideServiceRequest, it creates the shared memory segments for the + /// service instance before calling ProvideService). + /// @param message received message from the BidirectionalTransport (transport_ member variable) + void OnMessageReceived(std::unique_ptr message); + void PreCreateInterVmSharedMemory(const impl::InstanceSpecifier& specifier, + std::uint32_t shm_control_size, + std::uint32_t shm_data_size); + + GatewayCore& gateway_app_; + std::unique_ptr transport_; +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_HYPERVISOR_TRANSPORT_H_ diff --git a/score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport_test.cpp b/score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport_test.cpp new file mode 100644 index 000000000..d96e3579d --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport_test.cpp @@ -0,0 +1,631 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.h" + +#include "score/mw/com/gateway/gateway_application/gateway_core_mock.h" +#include "score/mw/com/gateway/transport_layer/sample/bidirectional_transport_mock.h" +#include "score/mw/com/gateway/transport_layer/transport_error.h" + +#include "score/mw/com/impl/runtime_mock.h" +#include "score/mw/com/impl/test/dummy_instance_identifier_builder.h" +#include "score/mw/com/impl/test/runtime_mock_guard.h" +#include "score/mw/log/recorder_mock.h" + +#include +#include + +namespace score::mw::com::gateway +{ + +namespace +{ +class SampleHyperVisorTransportTest : public ::testing::Test +{ + public: + SampleHyperVisorTransportTest() {} + + ~SampleHyperVisorTransportTest() override {} + + SampleHyperVisorTransportTest& WithASampleHyperVisorTransport() + { + transport_ = std::make_unique(gateway_core_mock_, std::move(mock_owner_)); + return *this; + } + + SampleHyperVisorTransportTest& WithARegisteredOnSetupCallback() + { + // Capture the message handler callback set during Setup() + + EXPECT_CALL(*bi_directional_transport_mock_, SetMessageHandler(::testing::_)) + .WillOnce([this](IBidirectionalTransport::MessageHandler handler) { + captured_handler_ = std::move(handler); + }); + EXPECT_CALL(*bi_directional_transport_mock_, Setup()).WillOnce(::testing::Return(score::ResultBlank{})); + const auto setup_result = transport_->Setup(); + EXPECT_TRUE(setup_result.has_value()); + + return *this; + } + + impl::InstanceSpecifier CreateValidInstanceSpecifier() + { + const auto specifier_result = impl::InstanceSpecifier::Create("SpeedService/Instance42"); + EXPECT_TRUE(specifier_result.has_value()); + return specifier_result.value(); + } + + std::unique_ptr CreateMessageOfType(MessageType type, bool valid_instance_specifier = true) + { + if (!valid_instance_specifier) + { + switch (type) + { + case MessageType::kProvideServiceRequest: + return std::make_unique(); + case MessageType::kStopOfferServiceRequest: + return std::make_unique(); + case MessageType::kOfferServiceRequest: + return std::make_unique(); + case MessageType::kRegisterNotificationRequest: + return std::make_unique(); + case MessageType::kUnregisterNotificationRequest: + return std::make_unique(); + case MessageType::kUpdateNotification: + return std::make_unique(); + case MessageType::kAckResponse: + return std::make_unique(); + case MessageType::kInvalid: + default: + return nullptr; + } + } + impl::InstanceSpecifier specifier = CreateValidInstanceSpecifier(); + + switch (type) + { + case MessageType::kProvideServiceRequest: + { + std::vector elements{}; + constexpr std::uint32_t kShmControlSize = 1024U; + constexpr std::uint32_t kShmDataSize = 4096U; + return std::make_unique(specifier, elements, kShmControlSize, kShmDataSize); + } + case MessageType::kStopOfferServiceRequest: + { + return std::make_unique(specifier); + } + case MessageType::kOfferServiceRequest: + { + return std::make_unique(specifier); + } + case MessageType::kRegisterNotificationRequest: + { + return std::make_unique( + specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); + } + case MessageType::kUnregisterNotificationRequest: + { + return std::make_unique( + specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); + } + case MessageType::kUpdateNotification: + { + return std::make_unique(specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); + } + case MessageType::kAckResponse: + return std::make_unique(); + case MessageType::kInvalid: + default: + return nullptr; + } + } + + protected: + void SetUp() override + { + mock_owner_ = std::make_unique(); + bi_directional_transport_mock_ = mock_owner_.get(); + } + + void TearDown() override {} + + std::unique_ptr transport_; + // Raw pointer to the mock of BidirectionalTransport, either owned by mock_owner_ or by transport_ + BidirectionalTransportMock* bi_directional_transport_mock_{nullptr}; + // Owns the mock until it is moved into transport_ via WithASampleHyperVisorTransport() + std::unique_ptr mock_owner_; + GatewayCoreMock gateway_core_mock_; + IBidirectionalTransport::MessageHandler captured_handler_; +}; + +TEST_F(SampleHyperVisorTransportTest, CanBeConstructedWithValidConfiguration) +{ + EXPECT_NO_THROW(SampleHyperVisorTransport transport(gateway_core_mock_, std::move(mock_owner_))); +} + +TEST_F(SampleHyperVisorTransportTest, IsMemorySharingSupportedReturnsTrue) +{ + SampleHyperVisorTransport transport(gateway_core_mock_, std::move(mock_owner_)); + EXPECT_TRUE(transport.IsMemorySharingSupported()); +} + +TEST_F(SampleHyperVisorTransportTest, SetupCallsSetMessageHandlerAndSetupOnTransport) +{ + EXPECT_CALL(*bi_directional_transport_mock_, SetMessageHandler(::testing::_)).Times(1); + EXPECT_CALL(*bi_directional_transport_mock_, Setup()).WillOnce(::testing::Return(score::ResultBlank{})); + + SampleHyperVisorTransport transport(gateway_core_mock_, std::move(mock_owner_)); + const auto result = transport.Setup(); + EXPECT_TRUE(result.has_value()); +} + +TEST_F(SampleHyperVisorTransportTest, SetupReturnsErrorWhenTransportSetupFails) +{ + // Given a SampleHyperVisorTransport with a mocked BidirectionalTransport + this->WithASampleHyperVisorTransport(); + // when the BidirectionalTransport's Setup method returns a connection failure error + + EXPECT_CALL(*bi_directional_transport_mock_, SetMessageHandler(::testing::_)).Times(1); + EXPECT_CALL(*bi_directional_transport_mock_, Setup()) + .WillOnce(::testing::Return(score::MakeUnexpected(TransportErrorc::kConnectionFailure))); + // then calling Setup on SampleHyperVisorTransport should return an error + const auto result = transport_->Setup(); + EXPECT_FALSE(result.has_value()); +} + +TEST_F(SampleHyperVisorTransportTest, ShutdownCallsShutdownOnTransport) +{ + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(2); + + SampleHyperVisorTransport transport(gateway_core_mock_, std::move(mock_owner_)); + transport.Shutdown(); +} + +TEST_F(SampleHyperVisorTransportTest, OfferServiceRequestWithCorrectType) +{ + // Given a SampleHyperVisorTransport with a mocked BidirectionalTransport and a valid instance specifier + this->WithASampleHyperVisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + // then the BidirectionalTransport's SendRequest method should be called with a TransportMessage of type + // kOfferServiceRequest + EXPECT_CALL(*bi_directional_transport_mock_, + SendRequest(::testing::Property(&TransportMessage::GetType, MessageType::kOfferServiceRequest))) + .WillOnce(::testing::Return(score::ResultBlank{})); + // when calling OfferService on SampleHyperVisorTransport + transport_->OfferService(specifier); +} + +TEST_F(SampleHyperVisorTransportTest, StopOfferServiceRequestWithCorrectType) +{ + // Given a SampleHyperVisorTransport with a mocked BidirectionalTransport and a valid instance specifier + this->WithASampleHyperVisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + // then the BidirectionalTransport's SendRequest method should be called with a TransportMessage of type + // kStopOfferServiceRequest + EXPECT_CALL(*bi_directional_transport_mock_, + SendRequest(::testing::Property(&TransportMessage::GetType, MessageType::kStopOfferServiceRequest))) + .WillOnce(::testing::Return(score::ResultBlank{})); + // when calling StopOfferService on SampleHyperVisorTransport + transport_->StopOfferService(specifier); +} + +TEST_F(SampleHyperVisorTransportTest, NotifyUpdateWithCorrectType) +{ + // Given a SampleHyperVisorTransport with a mocked BidirectionalTransport and a valid instance specifier + this->WithASampleHyperVisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + // then the BidirectionalTransport's SendNotification method should be called with a TransportMessage of type + // kUpdateNotification + EXPECT_CALL(*bi_directional_transport_mock_, + SendNotification(::testing::Property(&TransportMessage::GetType, MessageType::kUpdateNotification))) + .WillOnce(::testing::Return(score::ResultBlank{})); + // when calling NotifyUpdate on SampleHyperVisorTransport + transport_->NotifyUpdate(specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); +} + +TEST_F(SampleHyperVisorTransportTest, RegisterUpdateNotificationWithCorrectType) +{ + // Given a SampleHyperVisorTransport with a mocked BidirectionalTransport and a valid instance specifier + this->WithASampleHyperVisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + // then the BidirectionalTransport's SendRequest method should be called with a TransportMessage of type + // kRegisterNotificationRequest + EXPECT_CALL(*bi_directional_transport_mock_, + SendRequest(::testing::Property(&TransportMessage::GetType, MessageType::kRegisterNotificationRequest))) + .WillOnce(::testing::Return(score::ResultBlank{})); + // when calling RegisterUpdateNotification on SampleHyperVisorTransport + transport_->RegisterUpdateNotification(specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); +} + +TEST_F(SampleHyperVisorTransportTest, UnregisterUpdateNotificationWithCorrectType) +{ + // Given a SampleHyperVisorTransport with a mocked BidirectionalTransport and a valid instance specifier + this->WithASampleHyperVisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + // then the BidirectionalTransport's SendRequest method should be called with a TransportMessage of type + // kUnregisterNotificationRequest + EXPECT_CALL( + *bi_directional_transport_mock_, + SendRequest(::testing::Property(&TransportMessage::GetType, MessageType::kUnregisterNotificationRequest))) + .WillOnce(::testing::Return(score::ResultBlank{})); + // when calling UnregisterUpdateNotification on SampleHyperVisorTransport + transport_->UnregisterUpdateNotification(specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); +} + +TEST_F(SampleHyperVisorTransportTest, ResolveShmPathReturnsEmptyObjectIfSpecifierCanNotBeResolved) +{ + // Given a valid instance specifier and a mocked runtime + const auto specifier = CreateValidInstanceSpecifier(); + impl::RuntimeMockGuard runtime_mock_guard_{}; + + // When the runtime can not resolve the instance specifier + EXPECT_CALL(runtime_mock_guard_.runtime_mock_, resolve(::testing::_)) + .WillOnce(::testing::Return(std::vector{})); + + // Then the returned SHM paths for control and data should both be empty + const auto resolved_shm_paths = ResolveShmPaths(specifier); + EXPECT_TRUE(resolved_shm_paths.control.empty()); + EXPECT_TRUE(resolved_shm_paths.data.empty()); +} + +TEST_F(SampleHyperVisorTransportTest, ResolveShmPathReturnsEmptyObjectIfServiceInstanceDeploymentIsInvalid) +{ + // Given a valid instance specifier and a mocked runtime + const auto specifier = CreateValidInstanceSpecifier(); + impl::RuntimeMockGuard runtime_mock_guard_{}; + + // When providing an (invalid) instance identifier without a instance deployment that will be used to resolve the + // SHM paths + score::mw::com::impl::DummyInstanceIdentifierBuilder builder{}; + auto instance_identifier = builder.CreateBlankBindingInstanceIdentifier(); + + EXPECT_CALL(runtime_mock_guard_.runtime_mock_, resolve(::testing::_)) + .WillOnce(::testing::Return(std::vector{instance_identifier})); + + // Then the returned SHM paths for control and data should both be empty + const auto resolved_shm_paths = ResolveShmPaths(specifier); + EXPECT_TRUE(resolved_shm_paths.control.empty()); + EXPECT_TRUE(resolved_shm_paths.data.empty()); +} + +TEST_F(SampleHyperVisorTransportTest, ResolveShmPathReturnsEmptyObjectIfServiceTypeDeploymentIsInvalid) +{ + // Given a valid instance specifier and a mocked runtime + const auto specifier = CreateValidInstanceSpecifier(); + impl::RuntimeMockGuard runtime_mock_guard_{}; + + // When providing an (invalid) instance identifier without a type deployment that will be used to resolve the SHM + // paths + score::mw::com::impl::DummyInstanceIdentifierBuilder builder{}; + auto instance_identifier = builder.CreateLolaInstanceIdentifierWithoutTypeDeployment(); + + EXPECT_CALL(runtime_mock_guard_.runtime_mock_, resolve(::testing::_)) + .WillOnce(::testing::Return(std::vector{instance_identifier})); + + // Then the returned SHM paths for control and data should both be empty + const auto resolved_shm_paths = ResolveShmPaths(specifier); + EXPECT_TRUE(resolved_shm_paths.control.empty()); + EXPECT_TRUE(resolved_shm_paths.data.empty()); +} + +TEST_F(SampleHyperVisorTransportTest, GetShmSizesReturnsZeroSizesIfResolveShmPathsReturnsEmptyPaths) +{ + // Given a valid instance specifier and a mocked runtime + const auto specifier = CreateValidInstanceSpecifier(); + impl::RuntimeMockGuard runtime_mock_guard_{}; + + // When providing an (invalid) instance identifier without a type deployment that will be used to resolve the SHM + // paths + score::mw::com::impl::DummyInstanceIdentifierBuilder builder{}; + auto instance_identifier = builder.CreateLolaInstanceIdentifierWithoutTypeDeployment(); + + EXPECT_CALL(runtime_mock_guard_.runtime_mock_, resolve(::testing::_)) + .WillOnce(::testing::Return(std::vector{instance_identifier})); + + // Then the returned SHM sizes for control and data should both be 0 + const auto shm_sizes = GetShmSizes(specifier); + EXPECT_TRUE(shm_sizes.control == 0U); + EXPECT_TRUE(shm_sizes.data == 0U); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedProvideServiceRequestWithValidInstanceSpecifierCallsProvideServiceOnGatewayCore) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + impl::RuntimeMockGuard runtime_mock_guard_{}; + // PreCreateInterVmSharedMemory calls ResolveShmPaths, which needs the runtime mock. + // Return empty identifiers so that ResolveShmPaths returns empty paths and PreCreateInterVmSharedMemory returns + // early. + EXPECT_CALL(runtime_mock_guard_.runtime_mock_, resolve(::testing::_)) + .WillOnce(::testing::Return(std::vector{})); + + // When a ProvideServiceRequest message with a valid instance specifier is received + auto request = CreateMessageOfType(MessageType::kProvideServiceRequest); + + // Then ProvideService should be called on the gateway core with the matching instance specifier and service + // elements + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedProvideServiceRequestWithInvalidInstanceSpecifierReturnsWithoutCoreCall) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + impl::RuntimeMockGuard runtime_mock_guard_{}; + + // When a ProvideServiceRequest message with an invalid instance specifier is received + auto request = CreateMessageOfType(MessageType::kProvideServiceRequest, false); + + // Then ProvideService should not be called + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)).Times(0); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedStopOfferServiceRequestWithValidInstanceSpecifierCallsStopOfferServiceOnGatewayCore) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When a StopOfferServiceRequest message with a valid instance specifier is received + auto request = CreateMessageOfType(MessageType::kStopOfferServiceRequest); + + // Then StopOfferService should be called on the gateway core with the matching instance specifier + EXPECT_CALL(gateway_core_mock_, StopOfferService(::testing::_)).WillOnce(::testing::Return()); + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedStopOfferServiceRequestWithInvalidInstanceSpecifierReturnsWithoutCoreCall) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When a StopOfferServiceRequest message with an invalid instance specifier is received + auto request = CreateMessageOfType(MessageType::kStopOfferServiceRequest, false); + + // Then StopOfferService should be not be called on the gateway core because it will return early + EXPECT_CALL(gateway_core_mock_, StopOfferService(::testing::_)).Times(0); + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedOfferServiceRequestWithValidInstanceSpecifierCallsOfferServiceOnGatewayCore) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When an OfferServiceRequest message with a valid instance specifier is received + auto request = CreateMessageOfType(MessageType::kOfferServiceRequest); + + // Then OfferService should be called on the gateway core with the matching instance specifier + EXPECT_CALL(gateway_core_mock_, OfferService(::testing::_)).WillOnce(::testing::Return(score::ResultBlank{})); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedOfferServiceRequestWithValidInstanceSpecifierReturnsWithoutCoreCall) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When an OfferServiceRequest message with an invalid instance specifier is received + auto request = CreateMessageOfType(MessageType::kOfferServiceRequest, false); + + // Then OfferService should not be called on the gateway core because it will return early due to invalid instance + // specifier + EXPECT_CALL(gateway_core_mock_, OfferService(::testing::_)).Times(0); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F( + SampleHyperVisorTransportTest, + OnMessageReceivedRegisterNotificationRequestWithValidInstanceSpecifierCallsRegisterNotificationRequestOnGatewayCore) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When an RegisterNotificationRequest message with a valid instance specifier is received + auto request = CreateMessageOfType(MessageType::kRegisterNotificationRequest); + + // Then RegisterNotification should be called on the gateway core with the matching instance specifier + EXPECT_CALL(gateway_core_mock_, RegisterUpdateNotification(::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedRegisterNotificationRequestWithInvalidInstanceSpecifierReturnsWithoutCoreCall) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When an RegisterNotificationRequest message with an invalid instance specifier is received + auto request = CreateMessageOfType(MessageType::kRegisterNotificationRequest, false); + + // Then RegisterNotification should not be called on the gateway core because it wil return early + EXPECT_CALL(gateway_core_mock_, RegisterUpdateNotification(::testing::_, ::testing::_, ::testing::_)).Times(0); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F( + SampleHyperVisorTransportTest, + OnMessageReceivedUnregisterNotificationRequestWithValidInstanceSpecifierCallsUnregisterNotificationRequestOnGatewayCore) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When an RegisterNotificationRequest message with a valid instance specifier is received + auto request = CreateMessageOfType(MessageType::kUnregisterNotificationRequest); + + // Then UnregisterNotification should be called on the gateway core with the matching instance specifier + EXPECT_CALL(gateway_core_mock_, UnregisterUpdateNotification(::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedUnregisterNotificationRequestWithInvalidInstanceSpecifierReturnsWithoutCoreCall) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When an RegisterNotificationRequest message with an invalid instance specifier is received + auto request = CreateMessageOfType(MessageType::kUnregisterNotificationRequest, false); + + // Then UnregisterNotification should not be called on the gateway core because it will return early + EXPECT_CALL(gateway_core_mock_, UnregisterUpdateNotification(::testing::_, ::testing::_, ::testing::_)).Times(0); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F( + SampleHyperVisorTransportTest, + OnMessageReceivedUnregisterNotificationRequestWithValidInstanceSpecifierCallsUnregisterUpdateNotificationOnGatewayCore) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When an RegisterNotificationRequest message with a valid instance specifier is received + auto request = CreateMessageOfType(MessageType::kUnregisterNotificationRequest); + + // Then UnregisterNotification should be called on the gateway core with the matching instance specifier + EXPECT_CALL(gateway_core_mock_, UnregisterUpdateNotification(::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedUpdateNotificationWithValidInstanceSpecifierCallsNotifyUpdateOnGatewayCore) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When an UpdateNotification message with a valid instance specifier is received + auto request = CreateMessageOfType(MessageType::kUpdateNotification); + + // Then NotifyUpdate should be called on the gateway core with the matching instance specifier + EXPECT_CALL(gateway_core_mock_, NotifyUpdate(::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, + OnMessageReceivedUpdateNotificationWithInvalidInstanceSpecifierReturnsWithoutCoreCall) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + + // When an UpdateNotification message with an invalid instance specifier is received + auto request = CreateMessageOfType(MessageType::kUpdateNotification, false); + + // Then NotifyUpdate should not be called on the gateway core + EXPECT_CALL(gateway_core_mock_, NotifyUpdate(::testing::_, ::testing::_, ::testing::_)).Times(0); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, UnsupportedMessageWillBeLogged) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + // and a mocked LogRecorder + score::mw::log::RecorderMock recorder_mock{}; + score::mw::log::SetLogRecorder(&recorder_mock); + + // When a message with an unsupported type is received, i.e. not considered by the Sample transport + auto request = CreateMessageOfType(MessageType::kAckResponse); + + // Then an error should be logged + EXPECT_CALL(recorder_mock, StartRecord(::testing::_, mw::log::LogLevel::kError)) + .WillOnce(::testing::Return(mw::log::SlotHandle{})); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, InvalidMessageWillBeLogged) +{ + // Given a SampleHyperVisorTransport, a mocked runtime and a message handler callback has been set for the + // underlying BiDirectionalTransport + this->WithASampleHyperVisorTransport().WithARegisteredOnSetupCallback(); + // and a mocked LogRecorder + score::mw::log::RecorderMock recorder_mock{}; + score::mw::log::SetLogRecorder(&recorder_mock); + + // When an invalid message is received (MessageType::kInvalid will cause CreateMessageOfType to return a nullptr) + auto request = CreateMessageOfType(MessageType::kInvalid); + + // Then an error should be logged + EXPECT_CALL(recorder_mock, StartRecord(::testing::_, mw::log::LogLevel::kError)) + .WillOnce(::testing::Return(mw::log::SlotHandle{})); + + // Invoke the captured handler to trigger OnMessageReceived + captured_handler_(std::move(request)); +} + +TEST_F(SampleHyperVisorTransportTest, ProvideServiceDeathTest) +{ + // Given a SampleHyperVisorTransport + this->WithASampleHyperVisorTransport(); + + // When calling ProvideService with a valid instance specifier, then it is expected to terminate. + // TODO This test needs to be adapted when implementing ResolveShmPaths() and GetShmSizes() based on the actual + // HyperVisor SHM technology. + EXPECT_DEATH(transport_->ProvideService(CreateValidInstanceSpecifier(), std::vector{}), + ".*"); +} + +} // namespace + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/sample_transport_test_resources.h b/score/mw/com/gateway/transport_layer/sample/sample_transport_test_resources.h new file mode 100644 index 000000000..106102cf2 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/sample_transport_test_resources.h @@ -0,0 +1,144 @@ +#ifndef COMMUNICATION_SAMPLE_TRANSPORT_TEST_RESOURCES_H +#define COMMUNICATION_SAMPLE_TRANSPORT_TEST_RESOURCES_H + +#include "score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h" + +#include +#include +#include + +namespace score::mw::com::gateway +{ + +/// \brief Test attorney for BidirectionalTransport. +/// +/// This class provides access to BidirectionalTransport for testing purposes. +class BidirectionalTransportAttorney +{ + public: + explicit BidirectionalTransportAttorney(std::shared_ptr transport) noexcept + : transport_{transport} + { + } + + bool IsMessageHandlerSet() const noexcept + { + return transport_->has_message_handler_; + } + + bool IsListenSocketValid() const noexcept + { + return transport_->listen_socket_.IsValid(); + } + + bool IsSendSocketValid() const noexcept + { + return transport_->send_socket_.IsValid(); + } + + bool IsReceiveSocketValid() const noexcept + { + return transport_->receive_socket_.IsValid(); + } + + bool HasPendingRequest(std::uint32_t sequence) const + { + std::lock_guard lock(transport_->pending_mutex_); + return transport_->pending_requests_.find(sequence) != transport_->pending_requests_.end(); + } + + bool WaitForPendingRequest(std::uint32_t sequence, std::chrono::milliseconds timeout) const + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) + { + if (HasPendingRequest(sequence)) + { + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return HasPendingRequest(sequence); + } + + bool WaitForConnectedState(bool expected, std::chrono::milliseconds timeout) const + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) + { + if (transport_->is_connected_.load() == expected) + { + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return transport_->is_connected_.load() == expected; + } + + void SetIsConnected(bool is_connected) noexcept + { + transport_->is_connected_ = is_connected; + } + + void SetSendSocket(std::int32_t fd) noexcept + { + transport_->send_socket_.Reset(fd); + } + + void SetReceiveSocket(std::int32_t fd) noexcept + { + transport_->receive_socket_.Reset(fd); + } + + void SetListenSocket(std::int32_t fd) noexcept + { + transport_->listen_socket_.Reset(fd); + } + + void SetRequestTimeoutMs(std::uint32_t timeout_ms) noexcept + { + transport_->socket_config_.request_timeout_ms_ = timeout_ms; + } + + void SetDispatchShutdown(bool value) noexcept + { + transport_->dispatch_shutdown_ = value; + } + + void EnqueueForDispatch(std::unique_ptr message) + { + { + std::lock_guard lock(transport_->dispatch_mutex_); + transport_->dispatch_queue_.push(std::move(message)); + } + transport_->dispatch_cv_.notify_one(); + } + + void LockPendingMutex() + { + transport_->pending_mutex_.lock(); + } + + void UnlockPendingMutex() + { + transport_->pending_mutex_.unlock(); + } + + void AcknowledgePendingRequest(std::uint32_t sequence) + { + std::lock_guard lock(transport_->pending_mutex_); + auto it = transport_->pending_requests_.find(sequence); + if (it != transport_->pending_requests_.end()) + { + it->second.acknowledged = true; + } + transport_->pending_cv_.notify_all(); + } + + private: + std::shared_ptr transport_; +}; + +} // namespace score::mw::com::gateway + +#endif // COMMUNICATION_SAMPLE_TRANSPORT_TEST_RESOURCES_H diff --git a/score/mw/com/gateway/transport_layer/sample/test/BUILD b/score/mw/com/gateway/transport_layer/sample/test/BUILD new file mode 100644 index 000000000..43a7db2f2 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/test/BUILD @@ -0,0 +1,52 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("//quality/integration_testing:integration_testing.bzl", "integration_test") + +cc_binary( + name = "app2", + srcs = ["app2_main.cpp"], + deps = [ + "//score/mw/com/gateway/transport_layer/sample:bidirectional_transport", + ], +) + +cc_binary( + name = "app1", + srcs = ["app1_main.cpp"], + deps = [ + "//score/mw/com/gateway/transport_layer/sample:bidirectional_transport", + ], +) + +pkg_files( + name = "example-app-pkg", + srcs = [ + ":app1", + ":app2", + ], + attributes = pkg_attributes( + mode = "0777", + ), + prefix = "/", +) + +integration_test( + name = "test_bidirectional_transport_communication", + timeout = "moderate", + srcs = [ + "test_bidirectional_transport_communication.py", + ], + filesystem = ":example-app-pkg", +) diff --git a/score/mw/com/gateway/transport_layer/sample/test/app1_main.cpp b/score/mw/com/gateway/transport_layer/sample/test/app1_main.cpp new file mode 100644 index 000000000..49ae9639e --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/test/app1_main.cpp @@ -0,0 +1,78 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h" +#include "score/mw/com/gateway/transport_layer/sample/configuration/hypervisor_socket_configuration.h" + +#include +#include +#include + +using score::mw::com::gateway::BidirectionalTransport; +using score::mw::com::gateway::HyperVisorSocketConfiguration; + +HyperVisorSocketConfiguration CreateConfiguration() +{ + HyperVisorSocketConfiguration config{}; + config.remote_ip_ = score::os::Ipv4Address{"127.0.0.1"}; + config.local_port_ = 9001; + config.remote_port_ = 9000; + return config; +} + +int main() +{ + // App app1 is expected to receive 2 messages from sender and will in return send 1 message back to app2. + int expected_amount_received_messages = 2; + + auto config = CreateConfiguration(); + + BidirectionalTransport transport{std::move(config)}; + const auto setup_result = transport.Setup(); + if (!setup_result) + { + std::cerr << "Transport setup failed: " << setup_result.error() << std::endl; + return 1; + } + + int received_message_count = 0; + + transport.SetMessageHandler([&](std::unique_ptr message) { + std::cout << "Received message of type: " << static_cast(message->GetHeader().type) << "\n"; + ++received_message_count; + if (received_message_count == expected_amount_received_messages) + { + auto ack_response = score::mw::com::gateway::UpdateNotification{ + score::mw::com::impl::InstanceSpecifier::Create("TestService/Instance1").value(), + score::mw::com::impl::ServiceElementType::EVENT, + "TestEvent"}; + transport.SendNotification(ack_response); + } + }); + + int sleep_counter = 0; + // Sleep until all expected messages are received or a timeout occurs (whichever comes first) + while (received_message_count < expected_amount_received_messages || sleep_counter < 200) + { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + sleep_counter++; + } + + if (received_message_count != expected_amount_received_messages) + { + std::cerr << "Amount of expected messages have not been received. Received " << received_message_count + << " messages, expected " << expected_amount_received_messages << " messages." << std::endl; + return 1; + } + + return 0; +} diff --git a/score/mw/com/gateway/transport_layer/sample/test/app2_main.cpp b/score/mw/com/gateway/transport_layer/sample/test/app2_main.cpp new file mode 100644 index 000000000..28e2bb663 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/test/app2_main.cpp @@ -0,0 +1,87 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h" +#include "score/mw/com/gateway/transport_layer/sample/configuration/hypervisor_socket_configuration.h" + +#include +#include +#include + +using score::mw::com::gateway::BidirectionalTransport; +using score::mw::com::gateway::HyperVisorSocketConfiguration; + +HyperVisorSocketConfiguration CreateConfiguration() +{ + HyperVisorSocketConfiguration config{}; + config.remote_ip_ = score::os::Ipv4Address{"127.0.0.1"}; + config.local_port_ = 9000; + config.remote_port_ = 9001; + return config; +} + +void SendMessages(BidirectionalTransport& transport) +{ + auto service_request = score::mw::com::gateway::ProvideServiceRequest{ + score::mw::com::impl::InstanceSpecifier::Create("TestService/Instance1").value(), + {{"TestEvent", {64U, 8U}}, {"TestField", {32U, 4U}}}, + 4U, + 8U}; + const auto send_result = transport.SendRequest(service_request); + + auto notification = score::mw::com::gateway::RegisterNotificationRequest{ + score::mw::com::impl::InstanceSpecifier::Create("TestService/Instance1").value(), + score::mw::com::impl::ServiceElementType::EVENT, + "TestEvent"}; + const auto notification_result = transport.SendNotification(notification); +} + +int main() +{ + // App app2 is expected to send 2 messages to receiver and will in return receive 1 message back from app1. + auto config = CreateConfiguration(); + + BidirectionalTransport transport{std::move(config)}; + const auto setup_result = transport.Setup(); + if (!setup_result) + { + std::cerr << "Transport setup failed: " << setup_result.error() << std::endl; + return 1; + } + + int received_message_count = 0; + + transport.SetMessageHandler([&](std::unique_ptr message) { + std::cout << "Sender App: Received message of type " << static_cast(message->GetHeader().type) + << std::endl; + received_message_count++; + }); + + SendMessages(transport); + + int expected_amount_received_messages = 1; + int sleep_counter = 0; + // Sleep until all expected messages are received or a timeout occurs (whichever comes first) + while (received_message_count < expected_amount_received_messages || sleep_counter < 100) + { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + sleep_counter++; + } + + if (expected_amount_received_messages != received_message_count) + { + std::cerr << "Amount of expected messages have not been received. Received " << received_message_count + << " messages, expected " << expected_amount_received_messages << " messages." << std::endl; + return 1; + } + return 0; +} diff --git a/score/mw/com/gateway/transport_layer/sample/test/test_bidirectional_transport_communication.py b/score/mw/com/gateway/transport_layer/sample/test/test_bidirectional_transport_communication.py new file mode 100644 index 000000000..110d78a28 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/test/test_bidirectional_transport_communication.py @@ -0,0 +1,18 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Simple integration test that will send messages from one app to another using BidirectionalTransport. +def test_bidirectional_transport_communication(target): + with target.wrap_exec("/app1", wait_on_exit=True): + with target.wrap_exec("/app2", wait_on_exit=True): + pass \ No newline at end of file diff --git a/score/mw/com/gateway/transport_layer/sample/unique_socket.cpp b/score/mw/com/gateway/transport_layer/sample/unique_socket.cpp new file mode 100644 index 000000000..5cafb94bd --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/unique_socket.cpp @@ -0,0 +1,92 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/sample/unique_socket.h" + +#include "score/os/unistd.h" + +#include + +#include + +namespace score::mw::com::gateway +{ + +UniqueSocket::UniqueSocket(std::int32_t fd) noexcept : fd_(fd) {} + +UniqueSocket::~UniqueSocket() noexcept +{ + CloseIfValid(); +} + +UniqueSocket::UniqueSocket(UniqueSocket&& other) noexcept : fd_(other.fd_.exchange(kInvalidFd)) {} + +UniqueSocket& UniqueSocket::operator=(UniqueSocket&& other) noexcept +{ + if (this != &other) + { + CloseIfValid(); + fd_.store(other.fd_.exchange(kInvalidFd)); + } + return *this; +} + +std::int32_t UniqueSocket::Get() const noexcept +{ + return fd_.load(); +} + +bool UniqueSocket::IsValid() const noexcept +{ + return fd_.load() >= 0; +} + +UniqueSocket::operator bool() const noexcept +{ + return IsValid(); +} + +void UniqueSocket::Reset(std::int32_t new_fd) noexcept +{ + CloseIfValid(); + fd_.store(new_fd); +} + +void UniqueSocket::ShutdownAndReset() noexcept +{ + const auto fd = fd_.load(); + if (fd >= 0) + { + ::shutdown(fd, SHUT_RDWR); + } + Reset(); +} + +void UniqueSocket::ShutdownFd() noexcept +{ + const auto fd = fd_.load(); + if (fd >= 0) + { + ::shutdown(fd, SHUT_RDWR); + } +} + +void UniqueSocket::CloseIfValid() noexcept +{ + const auto fd = fd_.exchange(kInvalidFd); + if (fd >= 0) + { + std::ignore = score::os::Unistd::instance().close(fd); + } +} + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/sample/unique_socket.h b/score/mw/com/gateway/transport_layer/sample/unique_socket.h new file mode 100644 index 000000000..a5e260d0d --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/unique_socket.h @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_UNIQUE_SOCKET_H_ +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_UNIQUE_SOCKET_H_ + +#include +#include + +namespace score::mw::com::gateway +{ + +/// \brief Thread-safe RAII wrapper for POSIX socket file descriptors. +class UniqueSocket +{ + public: + UniqueSocket() noexcept = default; + + explicit UniqueSocket(std::int32_t fd) noexcept; + + ~UniqueSocket() noexcept; + + UniqueSocket(UniqueSocket&& other) noexcept; + UniqueSocket& operator=(UniqueSocket&& other) noexcept; + + UniqueSocket(const UniqueSocket&) = delete; + UniqueSocket& operator=(const UniqueSocket&) = delete; + + std::int32_t Get() const noexcept; + bool IsValid() const noexcept; + explicit operator bool() const noexcept; + + void Reset(std::int32_t new_fd = kInvalidFd) noexcept; + void ShutdownAndReset() noexcept; + + /// Use this when another thread might still be inside recv() or send() on the same fd, since close() would race + /// with those calls. + void ShutdownFd() noexcept; + + private: + static constexpr std::int32_t kInvalidFd = -1; + std::atomic fd_{kInvalidFd}; + + void CloseIfValid() noexcept; +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_SAMPLE_UNIQUE_SOCKET_H_ diff --git a/score/mw/com/gateway/transport_layer/sample/unique_socket_test.cpp b/score/mw/com/gateway/transport_layer/sample/unique_socket_test.cpp new file mode 100644 index 000000000..c233307ea --- /dev/null +++ b/score/mw/com/gateway/transport_layer/sample/unique_socket_test.cpp @@ -0,0 +1,109 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/mw/com/gateway/transport_layer/sample/unique_socket.h" + +#include "score/os/mocklib/unistdmock.h" + +#include +#include + +namespace score::mw::com::gateway +{ + +namespace +{ + +TEST(UniqueSocketTest, DefaultConstructorCreatesInvalidSocket) +{ + UniqueSocket socket; + EXPECT_FALSE(socket.IsValid()); + EXPECT_EQ(socket.Get(), -1); +} + +TEST(UniqueSocketTest, ConstructorWithFdCreatesValidSocket) +{ + UniqueSocket socket(42); + EXPECT_TRUE(socket.IsValid()); + EXPECT_EQ(socket.Get(), 42); +} + +TEST(UniqueSocketTest, MoveConstructorTransfersOwnership) +{ + UniqueSocket original(42); + UniqueSocket moved(std::move(original)); + EXPECT_FALSE(original.IsValid()); + EXPECT_TRUE(moved.IsValid()); + EXPECT_EQ(moved.Get(), 42); +} + +TEST(UniqueSocketTest, MoveAssignmentOperatorTransfersOwnership) +{ + UniqueSocket original(42); + UniqueSocket moved; + moved = std::move(original); + EXPECT_FALSE(original.IsValid()); + EXPECT_TRUE(moved.IsValid()); + EXPECT_EQ(moved.Get(), 42); +} + +TEST(UniqueSocketTest, OperatorBoolReturnsTrueForValidSocket) +{ + UniqueSocket socket(42); + EXPECT_TRUE(socket); +} + +TEST(UniqueSocketTest, ResetReplacesFileDescriptor) +{ + UniqueSocket socket(42); + socket.Reset(84); + EXPECT_TRUE(socket.IsValid()); + EXPECT_EQ(socket.Get(), 84); +} + +TEST(UniqueSocketTest, ResetWithoutArgumentInvalidatesSocket) +{ + UniqueSocket socket(42); + socket.Reset(); + + EXPECT_FALSE(socket.IsValid()); + EXPECT_EQ(socket.Get(), -1); +} + +TEST(UniqueSocketTest, ShutdownFdCallsShutdownIfValidFd) +{ + // Given a valid socket + UniqueSocket socket(42); + // ... when calling ShutdownFd() + socket.ShutdownFd(); + // ... then the socket should still be valid (only the POSIX shutdown() syscall is called) + EXPECT_TRUE(socket.IsValid()); + EXPECT_EQ(socket.Get(), 42); +} + +TEST(UniqueSocketTest, ShutdownAndResetCallsResetForValidFd) +{ + // Given a valid socket and the underlying close() syscall is expected to succeed + UniqueSocket socket(42); + score::os::MockGuard unistd_mock; + EXPECT_CALL(*unistd_mock, close(42)).WillOnce(::testing::Return(score::cpp::expected_blank{})); + // ... when calling ShutdownAndReset() + socket.ShutdownAndReset(); + // ... then the socket should be invalid + EXPECT_FALSE(socket.IsValid()); + EXPECT_EQ(socket.Get(), -1); +} + +} // namespace + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/transport.h b/score/mw/com/gateway/transport_layer/transport.h index 518252b91..d4bda1ad6 100644 --- a/score/mw/com/gateway/transport_layer/transport.h +++ b/score/mw/com/gateway/transport_layer/transport.h @@ -35,7 +35,7 @@ static_assert(std::is_trivially_copyable_v, "DataTypeSizeInfo struct ServiceElementConfiguration { std::string element_name; - DataTypeSizeInfo size_info; + DataTypeSizeInfo size_info{1U, 1U}; std::tuple GetSerializeMembers() const { @@ -57,7 +57,7 @@ class Transport /// \brief Returns whether this transport implementation supports memory sharing between source and destination. /// \return true if shared memory transport is supported, false otherwise. - virtual bool IsMemorySharingSupported() = 0; + virtual bool IsMemorySharingSupported() const = 0; /// \brief Sets up the transport layer (e.g. establishes connections or allocates resources). /// \return result indicating success or failure. diff --git a/score/mw/com/gateway/transport_layer/transport_factory.cpp b/score/mw/com/gateway/transport_layer/transport_factory.cpp index 565312057..83037f159 100644 --- a/score/mw/com/gateway/transport_layer/transport_factory.cpp +++ b/score/mw/com/gateway/transport_layer/transport_factory.cpp @@ -12,7 +12,9 @@ *******************************************************************************/ #include "score/mw/com/gateway/transport_layer/transport_factory.h" +#include "score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h" #include "score/mw/com/gateway/transport_layer/sample/configuration/sample_transport_config_parser.h" +#include "score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.h" #include #include @@ -20,7 +22,7 @@ namespace score::mw::com::gateway { -std::unique_ptr TransportFactory::Create(GatewayCore& /*gateway_core*/, +std::unique_ptr TransportFactory::Create(GatewayCore& gateway_core, const std::string& transport_layer_id, const std::string& transport_config_path) { @@ -30,10 +32,8 @@ std::unique_ptr TransportFactory::Create(GatewayCore& /*gateway_core* const HyperVisorSocketConfiguration socket_cfg = ParseSampleTransportConfig(transport_config_path); (void)socket_cfg; - // TODO (sync with Sebastian PR #508): instantiate sample transport once available: - // auto bidirectional = std::make_unique(socket_cfg); - // return std::make_unique(gateway_core, std::move(bidirectional)); - std::terminate(); + auto bidirectional = std::make_unique(socket_cfg); + return std::make_unique(gateway_core, std::move(bidirectional)); } // Unknown transport layer id — no implementation registered. diff --git a/score/mw/com/gateway/transport_layer/transport_mock.h b/score/mw/com/gateway/transport_layer/transport_mock.h index adbe248dc..af9bb0bde 100644 --- a/score/mw/com/gateway/transport_layer/transport_mock.h +++ b/score/mw/com/gateway/transport_layer/transport_mock.h @@ -23,7 +23,7 @@ namespace score::mw::com::gateway class TransportMock : public Transport { public: - MOCK_METHOD(bool, IsMemorySharingSupported, (), (override)); + MOCK_METHOD(bool, IsMemorySharingSupported, (), (const, override)); MOCK_METHOD(score::Result, Setup, (), (override)); MOCK_METHOD(void, Shutdown, (), (override)); MOCK_METHOD(score::Result, diff --git a/score/mw/com/impl/test/BUILD b/score/mw/com/impl/test/BUILD index 9206f01d2..05ea2a05a 100644 --- a/score/mw/com/impl/test/BUILD +++ b/score/mw/com/impl/test/BUILD @@ -57,7 +57,10 @@ cc_library( hdrs = ["dummy_instance_identifier_builder.h"], features = COMPILER_WARNING_FEATURES, implementation_deps = ["@score_baselibs//score/language/futurecpp"], - visibility = ["//score/mw/com/impl:__subpackages__"], + visibility = [ + "//score/mw/com/gateway:__subpackages__", + "//score/mw/com/impl:__subpackages__", + ], deps = ["//score/mw/com/impl"], )