From 0616704504a62a288dbf60fb0844879ee0b3b4f6 Mon Sep 17 00:00:00 2001 From: Sebastian Sperber Date: Thu, 7 May 2026 15:20:10 +0200 Subject: [PATCH] GenericSkeleton supports ReceiveHandler registration notification Extended GenericSkeleton and GenericSkeletonEvent so that users can register a callback that will be called when there are changes on the ReceiveHandler status of this event. This feature will be used in the LoLa gateway to keep track of the status across Gateway domains. Issue: https://github.com/eclipse-score/communication/issues/387 To be squashed (basically adding an Unset method) --- .../generic_skeleton_model.puml | 9 + score/mw/com/impl/BUILD | 13 ++ score/mw/com/impl/bindings/lola/BUILD | 10 + .../bindings/lola/generic_skeleton_event.h | 15 ++ .../bindings/lola/skeleton_event_common.h | 34 +++ .../lola/skeleton_event_common_test.cpp | 157 +++++++++++++ .../mock_binding/generic_skeleton_event.h | 5 + score/mw/com/impl/generic_skeleton_event.cpp | 22 ++ score/mw/com/impl/generic_skeleton_event.h | 12 + .../com/impl/generic_skeleton_event_binding.h | 6 + .../com/impl/generic_skeleton_event_test.cpp | 207 +++++++----------- ...e_handler_registration_changed_handler.cpp | 13 ++ ...ive_handler_registration_changed_handler.h | 27 +++ score/mw/com/types.h | 6 + 14 files changed, 409 insertions(+), 127 deletions(-) create mode 100644 score/mw/com/impl/bindings/lola/skeleton_event_common_test.cpp create mode 100644 score/mw/com/impl/receive_handler_registration_changed_handler.cpp create mode 100644 score/mw/com/impl/receive_handler_registration_changed_handler.h diff --git a/score/mw/com/design/skeleton_proxy/generic_skeleton/generic_skeleton_model.puml b/score/mw/com/design/skeleton_proxy/generic_skeleton/generic_skeleton_model.puml index 86d5ca4a3..fea0732a7 100644 --- a/score/mw/com/design/skeleton_proxy/generic_skeleton/generic_skeleton_model.puml +++ b/score/mw/com/design/skeleton_proxy/generic_skeleton/generic_skeleton_model.puml @@ -111,12 +111,16 @@ class "mw::com::impl::GenericSkeletonEvent" #yellow { +Send(SampleAllocateePtr sample): Result +Allocate(): Result> +GetSizeInfo() const : DataTypeMetaInfo + +SetReceiveHandlerRegistrationChangedHandler(score::cpp::callback callback): Result + +UnsetReceiveHandlerRegistrationChangedHandler(): Result } abstract class "GenericSkeletonEventBinding" #yellow { +{abstract} Send(SampleAllocateePtr sample) = 0: Result +{abstract} Allocate() = 0: Result> +{abstract} GetSizeInfo() const = 0: std::pair + +{abstract} SetReceiveHandlerRegistrationChangedHandler(score::cpp::callback callback) = 0: Result + +{abstract} UnsetReceiveHandlerRegistrationChangedHandler() = 0: Result } class "lola::SkeletonEventCommon" #yellow { @@ -124,6 +128,7 @@ class "lola::SkeletonEventCommon" #yellow { -element_fq_id_: ElementFqId -control_: score::cpp::optional& -current_timestamp_: EventSlotStatus::EventTimeStamp& + -receive_handler_registration_changed_callback_: std::optional +SkeletonEventCommon(lola::Skeleton&, const ElementFqId&, score::cpp::optional&, EventSlotStatus::EventTimeStamp&, impl::tracing::SkeletonEventTracingData) +PrepareOfferCommon(): void +PrepareStopOfferCommon(): void @@ -132,6 +137,8 @@ class "lola::SkeletonEventCommon" #yellow { +IsQmNotificationsRegistered(): bool +IsAsilBNotificationsRegistered(): bool +GetTracingData(): impl::tracing::SkeletonEventTracingData& + +SetReceiveHandlerRegistrationChangedHandler(score::cpp::callback): Result + +UnsetReceiveHandlerRegistrationChangedHandler(): Result } class "lola::GenericSkeletonEvent" #yellow { @@ -144,6 +151,8 @@ class "lola::GenericSkeletonEvent" #yellow { +GetBindingType(): BindingType +SetSkeletonEventTracingData(impl::tracing::SkeletonEventTracingData tracing_data): void +GetMaxSize() const : std::size_t + +SetReceiveHandlerNotificationCallback(score::cpp::callback): Result + +UnsetReceiveHandlerRegistrationChangedHandler(): Result -skeleton_event_common_ : lola::SkeletonEventCommon } diff --git a/score/mw/com/impl/BUILD b/score/mw/com/impl/BUILD index 50a22fd59..d3244ab6a 100644 --- a/score/mw/com/impl/BUILD +++ b/score/mw/com/impl/BUILD @@ -211,6 +211,7 @@ cc_library( "//score/mw/com/impl/plumbing:__pkg__", ], deps = [ + ":receive_handler_registration_changed_handler", ":skeleton_event_binding", "@score_baselibs//score/result", ], @@ -835,6 +836,18 @@ cc_library( deps = ["@score_baselibs//score/language/futurecpp"], ) +cc_library( + name = "receive_handler_registration_changed_handler", + srcs = ["receive_handler_registration_changed_handler.cpp"], + hdrs = ["receive_handler_registration_changed_handler.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//score/mw/com:__subpackages__", + ], + deps = ["@score_baselibs//score/language/futurecpp"], +) + cc_library( name = "subscription_state_change_handler", srcs = ["subscription_state_change_handler.cpp"], diff --git a/score/mw/com/impl/bindings/lola/BUILD b/score/mw/com/impl/bindings/lola/BUILD index 36e80889c..1a9b32ffe 100644 --- a/score/mw/com/impl/bindings/lola/BUILD +++ b/score/mw/com/impl/bindings/lola/BUILD @@ -1104,6 +1104,16 @@ cc_unit_test( ], ) +cc_unit_test( + name = "skeleton_event_common_test", + srcs = ["skeleton_event_common_test.cpp"], + features = COMPILER_WARNING_FEATURES, + deps = [ + ":skeleton", + "//score/mw/com/impl/bindings/lola/test:skeleton_event_test_resources", + ], +) + cc_unit_test( name = "proxy_test", srcs = ["proxy_test.cpp"], diff --git a/score/mw/com/impl/bindings/lola/generic_skeleton_event.h b/score/mw/com/impl/bindings/lola/generic_skeleton_event.h index 3dc63fa6c..35797bcc8 100644 --- a/score/mw/com/impl/bindings/lola/generic_skeleton_event.h +++ b/score/mw/com/impl/bindings/lola/generic_skeleton_event.h @@ -59,6 +59,21 @@ class GenericSkeletonEvent : public GenericSkeletonEventBinding return size_info_.size; } + /// \brief Set callback, to get notified, when either the 1st event-notification has been registered or the last + /// event-notification has been unregistered. + /// \detail This extension has been added to GenericSkeletonEvent, because we are only using it so far in the LoLa + /// gateway use case. + Result SetReceiveHandlerRegistrationChangedHandler( + ReceiveHandlerRegistrationChangedCallback callback) noexcept override + { + return skeleton_event_common_.SetReceiveHandlerRegistrationChangedHandler(std::move(callback)); + } + + Result UnsetReceiveHandlerRegistrationChangedHandler() noexcept override + { + return skeleton_event_common_.UnsetReceiveHandlerRegistrationChangedHandler(); + } + private: DataTypeMetaInfo size_info_; std::uint8_t* event_data_storage_; diff --git a/score/mw/com/impl/bindings/lola/skeleton_event_common.h b/score/mw/com/impl/bindings/lola/skeleton_event_common.h index d95f54f24..e506904e4 100644 --- a/score/mw/com/impl/bindings/lola/skeleton_event_common.h +++ b/score/mw/com/impl/bindings/lola/skeleton_event_common.h @@ -24,6 +24,7 @@ #include "score/mw/com/impl/bindings/lola/transaction_log_registration_guard.h" #include "score/mw/com/impl/bindings/lola/type_erased_sample_ptrs_guard.h" #include "score/mw/com/impl/configuration/quality_type.h" +#include "score/mw/com/impl/generic_skeleton_event_binding.h" #include "score/mw/com/impl/plumbing/sample_allocatee_ptr.h" #include "score/mw/com/impl/runtime.h" #include "score/mw/com/impl/skeleton_event_binding.h" @@ -56,6 +57,8 @@ class SkeletonEventCommon // private members and used for testing purposes only. friend class SkeletonEventAttorney; + using ReceiveHandlerRegistrationChangedCallback = lola::IMessagePassingService::HandlerStatusChangeCallback; + public: SkeletonEventCommon(Skeleton& parent, const std::string_view event_name, @@ -109,6 +112,25 @@ class SkeletonEventCommon return consumer_control_local_view_qm_.value(); } + /// \brief Set callback, to get notified, when either the 1st event-notification has been registered or the last + /// event-notification has been unregistered. + /// \detail This extension has been added to GenericSkeletonEvent, because we are only using it so far in the LoLa + /// gateway use case. + Result SetReceiveHandlerRegistrationChangedHandler( + ReceiveHandlerRegistrationChangedCallback callback) noexcept + { + static_assert(std::is_same_v, + "Callback type mismatch between GenericSkeletonEvent and lola::GenericSkeletonEvent"); + receive_handler_registration_changed_callback_ = std::move(callback); + return {}; + } + + Result UnsetReceiveHandlerRegistrationChangedHandler() + { + receive_handler_registration_changed_callback_.reset(); + return {}; + } + private: Skeleton& parent_; std::string_view event_name_; @@ -142,6 +164,8 @@ class SkeletonEventCommon /// PrepareStopOfferCommon(). std::optional transaction_log_registration_guard_{}; std::optional type_erased_sample_ptrs_guard_{}; + // ReceiveHandlerRegistrationChangedCallback + std::optional receive_handler_registration_changed_callback_; void EmplaceTransactionLogRegistrationGuard(TransactionLogSet& transaction_log_set); void EmplaceTypeErasedSamplePtrsGuard(); @@ -214,6 +238,11 @@ void SkeletonEventCommon::PrepareOfferCommon(EventControl& event_con .RegisterEventNotificationExistenceChangedCallback( QualityType::kASIL_QM, element_fq_id_, [this](const bool has_handlers) noexcept { SetQmNotificationsRegistered(has_handlers); + if (receive_handler_registration_changed_callback_.has_value()) + { + const bool qm_registered = qm_event_update_notifications_registered_.load(); + receive_handler_registration_changed_callback_.value()(qm_registered); + } }); if (parent_.GetInstanceQualityType() == QualityType::kASIL_B) @@ -223,6 +252,11 @@ void SkeletonEventCommon::PrepareOfferCommon(EventControl& event_con .RegisterEventNotificationExistenceChangedCallback( QualityType::kASIL_B, element_fq_id_, [this](const bool has_handlers) noexcept { SetAsilBNotificationsRegistered(has_handlers); + if (receive_handler_registration_changed_callback_.has_value()) + { + const bool asil_b_registered = asil_b_event_update_notifications_registered_.load(); + receive_handler_registration_changed_callback_.value()(asil_b_registered); + } }); } } diff --git a/score/mw/com/impl/bindings/lola/skeleton_event_common_test.cpp b/score/mw/com/impl/bindings/lola/skeleton_event_common_test.cpp new file mode 100644 index 000000000..63b4bc058 --- /dev/null +++ b/score/mw/com/impl/bindings/lola/skeleton_event_common_test.cpp @@ -0,0 +1,157 @@ +/******************************************************************************** + * 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/impl/bindings/lola/skeleton_event_common.h" +#include "score/mw/com/impl/bindings/lola/test/skeleton_event_test_resources.h" + +#include +#include + +namespace score::mw::com::impl::lola +{ +namespace +{ + +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::Return; +using ::testing::ReturnRef; + +class SkeletonEventCommonFixture : public SkeletonEventFixture +{ + public: + SkeletonEventCommonFixture() + { + ON_CALL(runtime_mock_, GetServiceDiscovery()).WillByDefault(ReturnRef(service_discovery_mock_)); + } + + void InitialiseSkeletonEventCommon(const ElementFqId element_fq_id, + const std::string& service_element_name, + const std::size_t max_samples, + const std::uint8_t max_subscribers, + const bool enforce_max_samples, + const QualityType quality_type, + impl::tracing::SkeletonEventTracingData skeleton_event_tracing_data) + { + // We defer initialisation of the Skeleton to InitialiseSkeletonEventCommon to allow test fixtures to set any + // mocked expectations before creating the skeleton. + InitialiseSkeleton(GetValidInstanceIdentifierForEventCommon(quality_type)); + + SkeletonBinding::SkeletonEventBindings events{}; + SkeletonBinding::SkeletonFieldBindings fields{}; + std::optional register_shm_object_trace_callback{}; + + std::ignore = skeleton_->PrepareOffer(events, fields, std::move(register_shm_object_trace_callback)); + + skeleton_event_ = std::make_unique>( + *skeleton_, + element_fq_id, + service_element_name, + SkeletonEventProperties{max_samples, max_subscribers, enforce_max_samples}, + skeleton_event_tracing_data); + + // Call PrepareOffer on the skeleton event to trigger Skeleton::Register, which populates + // the event_controls_ maps in ServiceDataControl for both QM and (if ASIL-B) ASIL-B. + std::ignore = skeleton_event_->PrepareOffer(); + + skeleton_event_common_ = + std::make_unique>( + *skeleton_, + event_name_, + SkeletonEventProperties{max_samples, max_subscribers, enforce_max_samples}, + element_fq_id, + skeleton_event_tracing_data); + } + + protected: + std::string_view event_name_{"test_event"}; + std::unique_ptr> skeleton_event_common_; + + InstanceIdentifier GetValidInstanceIdentifierForEventCommon(QualityType quality_type) const + { + if (quality_type == QualityType::kASIL_B) + { + return make_InstanceIdentifier(valid_asil_instance_deployment_, valid_type_deployment_); + } + else + { + return make_InstanceIdentifier(valid_qm_instance_deployment_, valid_type_deployment_); + } + } + + private: + ServiceInstanceDeployment valid_qm_instance_deployment_{make_ServiceIdentifierType(service_type_name_), + binding_info_, + QualityType::kASIL_QM, + instance_specifier_}; +}; + +TEST_F(SkeletonEventCommonFixture, RegisterEventNotificationCallbacksForAsilBTriggersMessagePassingRegistration) +{ + const bool enforce_max_samples{true}; + InitialiseSkeletonEventCommon(fake_element_fq_id_, + fake_event_name_, + max_samples_, + max_subscribers_, + enforce_max_samples, + QualityType::kASIL_B, + {}); + + auto* const event_control_qm = GetEventControl(fake_element_fq_id_, QualityType::kASIL_QM); + auto* const event_control_asil_b = GetEventControl(fake_element_fq_id_, QualityType::kASIL_B); + ASSERT_NE(event_control_qm, nullptr); + ASSERT_NE(event_control_asil_b, nullptr); + + // Expect that RegisterEventNotificationExistenceChangedCallback is called once per quality type with correct ASIL + // level and element ID + EXPECT_CALL(message_passing_mock_, + RegisterEventNotificationExistenceChangedCallback(impl::QualityType::kASIL_B, fake_element_fq_id_, _)) + .Times(1); + EXPECT_CALL(message_passing_mock_, + RegisterEventNotificationExistenceChangedCallback(impl::QualityType::kASIL_QM, fake_element_fq_id_, _)) + .Times(1); + // ... when PrepareOfferCommon is called + skeleton_event_common_->PrepareOfferCommon(*event_control_qm, event_control_asil_b); +} + +TEST_F(SkeletonEventCommonFixture, UnregisterEventNotificationCallbacksForAsilBTriggersMessagePassingUnregistration) +{ + const bool enforce_max_samples{true}; + InitialiseSkeletonEventCommon(fake_element_fq_id_, + fake_event_name_, + max_samples_, + max_subscribers_, + enforce_max_samples, + QualityType::kASIL_B, + {}); + + auto* const event_control_qm = GetEventControl(fake_element_fq_id_, QualityType::kASIL_QM); + auto* const event_control_asil_b = GetEventControl(fake_element_fq_id_, QualityType::kASIL_B); + ASSERT_NE(event_control_qm, nullptr); + ASSERT_NE(event_control_asil_b, nullptr); + + // Expect that RegisterEventNotificationExistenceChangedCallback is called once per quality type with correct ASIL + // level and element ID + EXPECT_CALL(message_passing_mock_, + UnregisterEventNotificationExistenceChangedCallback(impl::QualityType::kASIL_B, fake_element_fq_id_)) + .Times(1); + EXPECT_CALL(message_passing_mock_, + UnregisterEventNotificationExistenceChangedCallback(impl::QualityType::kASIL_QM, fake_element_fq_id_)) + .Times(1); + // ... when PrepareStopOfferCommon is called + skeleton_event_common_->PrepareStopOfferCommon(); +} + +} // namespace + +} // namespace score::mw::com::impl::lola diff --git a/score/mw/com/impl/bindings/mock_binding/generic_skeleton_event.h b/score/mw/com/impl/bindings/mock_binding/generic_skeleton_event.h index 253df6175..f3e8f9a42 100644 --- a/score/mw/com/impl/bindings/mock_binding/generic_skeleton_event.h +++ b/score/mw/com/impl/bindings/mock_binding/generic_skeleton_event.h @@ -37,6 +37,11 @@ class GenericSkeletonEvent : public GenericSkeletonEventBinding MOCK_METHOD(BindingType, GetBindingType, (), (const, noexcept, override)); MOCK_METHOD(void, SetSkeletonEventTracingData, (impl::tracing::SkeletonEventTracingData), (noexcept, override)); MOCK_METHOD(std::size_t, GetMaxSize, (), (const, noexcept, override)); + MOCK_METHOD(Result, + SetReceiveHandlerRegistrationChangedHandler, + (ReceiveHandlerRegistrationChangedCallback), + (noexcept, override)); + MOCK_METHOD(Result, UnsetReceiveHandlerRegistrationChangedHandler, (), (noexcept, override)); }; } // namespace score::mw::com::impl::mock_binding diff --git a/score/mw/com/impl/generic_skeleton_event.cpp b/score/mw/com/impl/generic_skeleton_event.cpp index eb5e049a7..2c2e4b1c4 100644 --- a/score/mw/com/impl/generic_skeleton_event.cpp +++ b/score/mw/com/impl/generic_skeleton_event.cpp @@ -97,4 +97,26 @@ DataTypeMetaInfo GenericSkeletonEvent::GetSizeInfo() const noexcept return {size_info_pair.first, size_info_pair.second}; } +Result GenericSkeletonEvent::SetReceiveHandlerRegistrationChangedHandler( + ReceiveHandlerRegistrationChangedCallback callback) noexcept +{ + auto* const binding = dynamic_cast(binding_.get()); + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + binding != nullptr, + "Cast to GenericSkeletonEventBinding failed, but ReceiveHandlerNotification is only supported for " + "GenericSkeletonEvents."); + + return binding->SetReceiveHandlerRegistrationChangedHandler(std::move(callback)); +} + +Result GenericSkeletonEvent::UnsetReceiveHandlerRegistrationChangedHandler() noexcept +{ + auto* const binding = dynamic_cast(binding_.get()); + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + binding != nullptr, + "Cast to GenericSkeletonEventBinding failed, but ReceiveHandlerNotification is only supported for " + "GenericSkeletonEvents."); + + return binding->UnsetReceiveHandlerRegistrationChangedHandler(); +} } // namespace score::mw::com::impl diff --git a/score/mw/com/impl/generic_skeleton_event.h b/score/mw/com/impl/generic_skeleton_event.h index 72c08534b..638398472 100644 --- a/score/mw/com/impl/generic_skeleton_event.h +++ b/score/mw/com/impl/generic_skeleton_event.h @@ -14,7 +14,9 @@ #define SCORE_MW_COM_IMPL_GENERIC_SKELETON_EVENT_H_ #include "score/mw/com/impl/data_type_meta_info.h" +#include "score/mw/com/impl/generic_skeleton_event_binding.h" #include "score/mw/com/impl/plumbing/sample_allocatee_ptr.h" +#include "score/mw/com/impl/receive_handler_registration_changed_handler.h" #include "score/mw/com/impl/skeleton_event_base.h" #include "score/result/result.h" @@ -37,6 +39,16 @@ class GenericSkeletonEvent : public SkeletonEventBase Result> Allocate() noexcept; DataTypeMetaInfo GetSizeInfo() const noexcept; + + /// \brief Set callback, to get notified, when either the 1st event-notification has been registered or the last + /// event-notification has been unregistered. + /// \detail This extension has been added to GenericSkeletonEvent, because we are only using it so far in the LoLa + /// gateway use case. + Result SetReceiveHandlerRegistrationChangedHandler( + ReceiveHandlerRegistrationChangedCallback callback) noexcept; + + /// \brief Unset the callback for receive handler registration change notifications. + Result UnsetReceiveHandlerRegistrationChangedHandler() noexcept; }; } // namespace score::mw::com::impl diff --git a/score/mw/com/impl/generic_skeleton_event_binding.h b/score/mw/com/impl/generic_skeleton_event_binding.h index 1c22856e0..0489a03f7 100644 --- a/score/mw/com/impl/generic_skeleton_event_binding.h +++ b/score/mw/com/impl/generic_skeleton_event_binding.h @@ -13,6 +13,7 @@ #ifndef SCORE_MW_COM_IMPL_GENERIC_SKELETON_EVENT_BINDING_H_ #define SCORE_MW_COM_IMPL_GENERIC_SKELETON_EVENT_BINDING_H_ +#include "score/mw/com/impl/receive_handler_registration_changed_handler.h" #include "score/mw/com/impl/skeleton_event_binding.h" #include "score/mw/com/impl/plumbing/sample_allocatee_ptr.h" @@ -32,6 +33,11 @@ class GenericSkeletonEventBinding : public SkeletonEventBindingBase virtual Result> Allocate() noexcept = 0; virtual std::pair GetSizeInfo() const noexcept = 0; + + virtual Result SetReceiveHandlerRegistrationChangedHandler( + ReceiveHandlerRegistrationChangedCallback callback) noexcept = 0; + + virtual Result UnsetReceiveHandlerRegistrationChangedHandler() noexcept = 0; }; } // namespace score::mw::com::impl diff --git a/score/mw/com/impl/generic_skeleton_event_test.cpp b/score/mw/com/impl/generic_skeleton_event_test.cpp index df08ed9b9..ce57cc10d 100644 --- a/score/mw/com/impl/generic_skeleton_event_test.cpp +++ b/score/mw/com/impl/generic_skeleton_event_test.cpp @@ -92,6 +92,56 @@ class GenericSkeletonEventTest : public ::testing::Test GenericSkeletonEventBindingFactory::mock_ = nullptr; } + /// \brief Creates a GenericSkeleton with a single event and a event binding mock. Used as return value for + /// CreateSkeletonWithEvent() setup method. + struct SkeletonWithEvent + { + GenericSkeleton skeleton; + GenericSkeletonEvent* event; + mock_binding::GenericSkeletonEvent* mock_event_binding; + }; + + /// \brief Helper method to create the common setup code for the following tests. + SkeletonWithEvent CreateSkeletonWithEvent( + const std::string& event_name = "test_event", + DataTypeMetaInfo size_info = {16, 8}, + std::unique_ptr> mock_event_binding = nullptr) + { + if (!mock_event_binding) + { + mock_event_binding = std::make_unique>(); + } + auto* mock_event_binding_ptr = mock_event_binding.get(); + + EXPECT_CALL(generic_event_binding_factory_mock_, Create(_, event_name, _)) + .WillOnce(Return(ByMove(std::move(mock_event_binding)))); + + GenericSkeletonServiceElementInfo create_params; + std::vector events; + events.push_back({event_name, size_info}); + create_params.events = events; + + auto skeleton_result = GenericSkeleton::Create( + dummy_instance_identifier_builder_.CreateValidLolaInstanceIdentifierWithEvent(), create_params); + EXPECT_TRUE(skeleton_result.has_value()); + + auto& skeleton = skeleton_result.value(); + auto it = skeleton.GetEvents().find(event_name); + EXPECT_NE(it, skeleton.GetEvents().cend()); + auto* event = const_cast(&it->second); + + return {std::move(skeleton_result.value()), event, mock_event_binding_ptr}; + } + + /// \brief Helper method: Offers the skeleton service, setting up the required mock expectations for tests + /// that expect an offered service. + void OfferSkeletonService(GenericSkeleton& skeleton, mock_binding::GenericSkeletonEvent* mock_event_binding_ptr) + { + EXPECT_CALL(*skeleton_binding_mock_, VerifyAllMethodsRegistered()).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_event_binding_ptr, PrepareOffer()).WillOnce(Return(score::Result{})); + ASSERT_TRUE(skeleton.OfferService().has_value()); + } + // Mocks NiceMock generic_event_binding_factory_mock_; RuntimeMockGuard runtime_mock_guard_{}; @@ -111,28 +161,7 @@ TEST_F(GenericSkeletonEventTest, AllocateBeforeOfferReturnsError) RecordProperty("TestType", "Requirements-based test"); // Given a skeleton created with one event "test_event" - const DataTypeMetaInfo size_info{16, 8}; - const std::string event_name = "test_event"; - - GenericSkeletonServiceElementInfo create_params; - std::vector events; - events.push_back({event_name, size_info}); - create_params.events = events; - - EXPECT_CALL(generic_event_binding_factory_mock_, Create(_, event_name, _)) - .WillOnce(Return(ByMove(std::make_unique>()))); - - auto skeleton_result = GenericSkeleton::Create( - dummy_instance_identifier_builder_.CreateValidLolaInstanceIdentifierWithEvent(), create_params); - ASSERT_TRUE(skeleton_result.has_value()); - - // And given the event instance - auto& skeleton = skeleton_result.value(); - const auto& events_map = skeleton.GetEvents(); - auto it = events_map.find(event_name); - ASSERT_NE(it, events_map.cend()); - - auto* event = const_cast(&it->second); + auto [skeleton, event, mock_event_binding_ptr] = CreateSkeletonWithEvent(); // When calling Allocate() before OfferService() auto alloc_result = event->Allocate(); @@ -148,23 +177,7 @@ TEST_F(GenericSkeletonEventTest, SendBeforeOfferReturnsError) RecordProperty("TestType", "Requirements-based test"); // Given a skeleton created with one event "test_event" - const std::string event_name = "test_event"; - - GenericSkeletonServiceElementInfo create_params; - std::vector events; - events.push_back({event_name, {16, 8}}); - create_params.events = events; - - EXPECT_CALL(generic_event_binding_factory_mock_, Create(_, event_name, _)) - .WillOnce(Return(ByMove(std::make_unique>()))); - - auto skeleton_result = GenericSkeleton::Create( - dummy_instance_identifier_builder_.CreateValidLolaInstanceIdentifierWithEvent(), create_params); - ASSERT_TRUE(skeleton_result.has_value()); - - auto& skeleton = skeleton_result.value(); - - auto* event = const_cast(&skeleton.GetEvents().find(event_name)->second); + auto [skeleton, event, mock_event_binding_ptr] = CreateSkeletonWithEvent(); // And a valid sample to send mock_binding::SampleAllocateePtr dummy_sample{nullptr, [](void*) {}}; @@ -182,30 +195,9 @@ TEST_F(GenericSkeletonEventTest, AllocateAndSendDispatchesToBindingAfterOffer) RecordProperty("Description", "Checks that Allocate and Send dispatch to the binding when the service is offered."); RecordProperty("TestType", "Requirements-based test"); - // Given a skeleton configured with an event binding mock - const std::string event_name = "test_event"; - auto mock_event_binding = std::make_unique>(); - auto* mock_event_binding_ptr = mock_event_binding.get(); - - EXPECT_CALL(generic_event_binding_factory_mock_, Create(_, event_name, _)) - .WillOnce(Return(ByMove(std::move(mock_event_binding)))); - - GenericSkeletonServiceElementInfo create_params; - std::vector events; - events.push_back({event_name, {16, 8}}); - create_params.events = events; - - auto skeleton_result = GenericSkeleton::Create( - dummy_instance_identifier_builder_.CreateValidLolaInstanceIdentifierWithEvent(), create_params); - ASSERT_TRUE(skeleton_result.has_value()); - auto& skeleton = skeleton_result.value(); - - auto* event = const_cast(&skeleton.GetEvents().find(event_name)->second); - - // And Given the service is Offered - EXPECT_CALL(*skeleton_binding_mock_, VerifyAllMethodsRegistered()).WillRepeatedly(Return(true)); - EXPECT_CALL(*mock_event_binding_ptr, PrepareOffer()).WillOnce(Return(score::Result{})); - ASSERT_TRUE(skeleton.OfferService().has_value()); + // Given a skeleton with a mock event binding, and the service is offered + auto [skeleton, event, mock_event_binding_ptr] = CreateSkeletonWithEvent(); + OfferSkeletonService(skeleton, mock_event_binding_ptr); // When calling Allocate() mock_binding::SampleAllocateePtr dummy_alloc{nullptr, [](void*) {}}; @@ -230,29 +222,9 @@ TEST_F(GenericSkeletonEventTest, AllocateReturnsErrorWhenBindingFails) "Checks that Allocate returns kSampleAllocationFailure if the binding allocation fails."); RecordProperty("TestType", "Requirements-based test"); - // Given a skeleton configured with an event binding mock - const std::string event_name = "test_event"; - auto mock_event_binding = std::make_unique>(); - auto* mock_event_binding_ptr = mock_event_binding.get(); - - EXPECT_CALL(generic_event_binding_factory_mock_, Create(_, event_name, _)) - .WillOnce(Return(ByMove(std::move(mock_event_binding)))); - - GenericSkeletonServiceElementInfo create_params; - std::vector events; - events.push_back({event_name, {16, 8}}); - create_params.events = events; - - auto skeleton_result = GenericSkeleton::Create( - dummy_instance_identifier_builder_.CreateValidLolaInstanceIdentifierWithEvent(), create_params); - ASSERT_TRUE(skeleton_result.has_value()); - auto& skeleton = skeleton_result.value(); - auto* event = const_cast(&skeleton.GetEvents().find(event_name)->second); - - // And Given the service is Offered - EXPECT_CALL(*skeleton_binding_mock_, VerifyAllMethodsRegistered()).WillRepeatedly(Return(true)); - EXPECT_CALL(*mock_event_binding_ptr, PrepareOffer()).WillOnce(Return(score::Result{})); - ASSERT_TRUE(skeleton.OfferService().has_value()); + // Given a skeleton with a mock event binding, and the service is offered + auto [skeleton, event, mock_event_binding_ptr] = CreateSkeletonWithEvent(); + OfferSkeletonService(skeleton, mock_event_binding_ptr); // Expect the binding to fail allocation EXPECT_CALL(*mock_event_binding_ptr, Allocate()) @@ -271,29 +243,9 @@ TEST_F(GenericSkeletonEventTest, SendReturnsErrorWhenBindingFails) RecordProperty("Description", "Checks that Send returns kBindingFailure if the binding send fails."); RecordProperty("TestType", "Requirements-based test"); - // Given a skeleton configured with an event binding mock - const std::string event_name = "test_event"; - auto mock_event_binding = std::make_unique>(); - auto* mock_event_binding_ptr = mock_event_binding.get(); - - EXPECT_CALL(generic_event_binding_factory_mock_, Create(_, event_name, _)) - .WillOnce(Return(ByMove(std::move(mock_event_binding)))); - - GenericSkeletonServiceElementInfo create_params; - std::vector events; - events.push_back({event_name, {16, 8}}); - create_params.events = events; - - auto skeleton_result = GenericSkeleton::Create( - dummy_instance_identifier_builder_.CreateValidLolaInstanceIdentifierWithEvent(), create_params); - ASSERT_TRUE(skeleton_result.has_value()); - auto& skeleton = skeleton_result.value(); - auto* event = const_cast(&skeleton.GetEvents().find(event_name)->second); - - // And Given the service is Offered - EXPECT_CALL(*skeleton_binding_mock_, VerifyAllMethodsRegistered()).WillRepeatedly(Return(true)); - EXPECT_CALL(*mock_event_binding_ptr, PrepareOffer()).WillOnce(Return(score::Result{})); - ASSERT_TRUE(skeleton.OfferService().has_value()); + // Given a skeleton with a mock event binding, and the service is offered + auto [skeleton, event, mock_event_binding_ptr] = CreateSkeletonWithEvent(); + OfferSkeletonService(skeleton, mock_event_binding_ptr); // Expect the binding to fail sending EXPECT_CALL(*mock_event_binding_ptr, Send(_)).WillOnce(Return(MakeUnexpected(ComErrc::kBindingFailure))); @@ -312,24 +264,8 @@ TEST_F(GenericSkeletonEventTest, GetSizeInfoDispatchesToBinding) RecordProperty("Description", "Checks that GetSizeInfo returns the correct DataTypeMetaInfo from the binding."); RecordProperty("TestType", "Requirements-based test"); - // Given a skeleton configured with an event binding mock - const std::string event_name = "test_event"; - auto mock_event_binding = std::make_unique>(); - auto* mock_event_binding_ptr = mock_event_binding.get(); - - EXPECT_CALL(generic_event_binding_factory_mock_, Create(_, event_name, _)) - .WillOnce(Return(ByMove(std::move(mock_event_binding)))); - - GenericSkeletonServiceElementInfo create_params; - std::vector events; - events.push_back({event_name, {16, 8}}); // Original creation info - create_params.events = events; - - auto skeleton_result = GenericSkeleton::Create( - dummy_instance_identifier_builder_.CreateValidLolaInstanceIdentifierWithEvent(), create_params); - ASSERT_TRUE(skeleton_result.has_value()); - auto& skeleton = skeleton_result.value(); - auto* event = const_cast(&skeleton.GetEvents().find(event_name)->second); + // Given a skeleton with a mock event binding + auto [skeleton, event, mock_event_binding_ptr] = CreateSkeletonWithEvent(); // Expect the binding to return specific size info std::pair expected_size_info{32, 16}; @@ -343,5 +279,22 @@ TEST_F(GenericSkeletonEventTest, GetSizeInfoDispatchesToBinding) EXPECT_EQ(result_info.alignment, expected_size_info.second); } +TEST_F(GenericSkeletonEventTest, SetReceiveHandlerNotificationCallbackDispatchesToBinding) +{ + RecordProperty("Description", + "Checks that SetReceiveHandlerNotificationCallback sets the callback in the binding."); + RecordProperty("TestType", "Requirements-based test"); + + // Given a skeleton with a mock event binding + auto [skeleton, event, mock_event_binding_ptr] = CreateSkeletonWithEvent(); + + // Expect the binding to receive the callback + ReceiveHandlerRegistrationChangedCallback expected_callback = [](bool) {}; + + EXPECT_CALL(*mock_event_binding_ptr, SetReceiveHandlerRegistrationChangedHandler(_)); + + event->SetReceiveHandlerRegistrationChangedHandler(std::move(expected_callback)); +} + } // namespace } // namespace score::mw::com::impl diff --git a/score/mw/com/impl/receive_handler_registration_changed_handler.cpp b/score/mw/com/impl/receive_handler_registration_changed_handler.cpp new file mode 100644 index 000000000..e49fb9372 --- /dev/null +++ b/score/mw/com/impl/receive_handler_registration_changed_handler.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/impl/receive_handler_registration_changed_handler.h" diff --git a/score/mw/com/impl/receive_handler_registration_changed_handler.h b/score/mw/com/impl/receive_handler_registration_changed_handler.h new file mode 100644 index 000000000..291848386 --- /dev/null +++ b/score/mw/com/impl/receive_handler_registration_changed_handler.h @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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_IMPL_REGISTRATION_CHANGE_HANDLER_H +#define SCORE_MW_COM_IMPL_REGISTRATION_CHANGE_HANDLER_H + +#include + +namespace score::mw::com::impl +{ + +/// \brief Callback that will be called if the first ReceiveHandler of a GenericEvent will get registered or the last +/// ReceiveHandler will be removed +using ReceiveHandlerRegistrationChangedCallback = score::cpp::callback; + +} // namespace score::mw::com::impl + +#endif // SCORE_MW_COM_IMPL_REGISTRATION_CHANGE_HANDLER_H diff --git a/score/mw/com/types.h b/score/mw/com/types.h index c29f16ab9..6051ee861 100644 --- a/score/mw/com/types.h +++ b/score/mw/com/types.h @@ -27,6 +27,7 @@ #include "score/mw/com/impl/handle_type.h" #include "score/mw/com/impl/instance_identifier.h" #include "score/mw/com/impl/instance_specifier.h" +#include "score/mw/com/impl/receive_handler_registration_changed_handler.h" #include "score/mw/com/impl/skeleton_base.h" #include "score/mw/com/impl/subscription_state.h" #include "score/mw/com/impl/traits.h" @@ -142,6 +143,11 @@ using GenericSkeletonServiceElementInfo = impl::GenericSkeletonServiceElementInf /// \brief A type erased skeleton event that can be used to send data without knowing the SampleType. using GenericSkeletonEvent = impl::GenericSkeletonEvent; +/// \api +/// \brief A callback that can be registered on a GenericSkeletonEvent to be notified about changes in the +/// availability of receive-handlers for this event. +using ReceiveHandlerRegistrationChangedCallback = impl::ReceiveHandlerRegistrationChangedCallback; + } // namespace score::mw::com #endif // SCORE_MW_COM_TYPES_H