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 29037f276..c0dfa10f6 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 9436dfbad..c91001b70 100644 --- a/score/mw/com/impl/bindings/lola/BUILD +++ b/score/mw/com/impl/bindings/lola/BUILD @@ -1114,6 +1114,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 9e3854be1..ae5be2fd2 100644 --- a/score/mw/com/impl/bindings/lola/generic_skeleton_event.h +++ b/score/mw/com/impl/bindings/lola/generic_skeleton_event.h @@ -61,6 +61,22 @@ 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)); + } + + /// \brief Unset the callback for ReceiveHandler registration change notifications. + 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 042ad3cc0..df4a9b058 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, @@ -113,6 +116,26 @@ 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 {}; + } + + /// \brief Unset the callback for Receive Handler registration change notifications + Result UnsetReceiveHandlerRegistrationChangedHandler() + { + receive_handler_registration_changed_callback_.reset(); + return {}; + } + private: Skeleton& parent_; std::string_view event_name_; @@ -146,6 +169,7 @@ class SkeletonEventCommon /// PrepareStopOfferCommon(). std::optional transaction_log_registration_guard_{}; std::optional type_erased_sample_ptrs_guard_{}; + std::optional receive_handler_registration_changed_callback_; void EmplaceTransactionLogRegistrationGuard(TransactionLogSet& transaction_log_set); void EmplaceTypeErasedSamplePtrsGuard(); @@ -218,6 +242,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) @@ -227,6 +256,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 e2f897564..d56b64703 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 @@ -39,6 +39,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 89d9b0120..1fb7ccbed 100644 --- a/score/mw/com/impl/generic_skeleton_event.cpp +++ b/score/mw/com/impl/generic_skeleton_event.cpp @@ -110,4 +110,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 8853f8b26..59433f7f1 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" @@ -40,6 +42,16 @@ class GenericSkeletonEvent : public SkeletonEventBase /// \note Caller must have already committed data to shared memory (gateway use). Result Notify() 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 fe811d8f1..6a5d53d29 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" @@ -35,6 +36,11 @@ class GenericSkeletonEventBinding : public SkeletonEventBindingBase virtual Result Notify() 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 e82366546..bde3932f8 100644 --- a/score/mw/com/impl/generic_skeleton_event_test.cpp +++ b/score/mw/com/impl/generic_skeleton_event_test.cpp @@ -92,6 +92,58 @@ class GenericSkeletonEventTest : public ::testing::Test GenericSkeletonEventBindingFactory::mock_ = nullptr; } + /// \brief Creates a GenericSkeleton with one event + GenericSkeletonEventTest& GivenAGenericSkeletonWithOneEvent( + 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); + + this->skeleton_ = std::make_unique(std::move(skeleton_result.value())); + this->event_ = event; + this->mock_event_binding_ptr_ = mock_event_binding_ptr; + + return *this; + } + + /// \brief Offers the skeleton service, setting up the required mock expectations for tests + /// that expect an offered service. + GenericSkeletonEventTest& OfferSkeletonService() + { + EXPECT_CALL(*skeleton_binding_mock_, VerifyAllMethodsRegistered()).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_event_binding_ptr_, PrepareOffer()).WillOnce(Return(score::Result{})); + const auto offer_result = this->skeleton_->OfferService(); + EXPECT_TRUE(offer_result.has_value()); + return *this; + } + + protected: + std::unique_ptr skeleton_; + GenericSkeletonEvent* event_; + mock_binding::GenericSkeletonEvent* mock_event_binding_ptr_; + // Mocks NiceMock generic_event_binding_factory_mock_; RuntimeMockGuard runtime_mock_guard_{}; @@ -111,31 +163,10 @@ 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); + this->GivenAGenericSkeletonWithOneEvent(); // When calling Allocate() before OfferService() - auto alloc_result = event->Allocate(); + auto alloc_result = event_->Allocate(); // Then it fails with kNotOffered ASSERT_FALSE(alloc_result.has_value()); @@ -148,29 +179,13 @@ 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); + this->GivenAGenericSkeletonWithOneEvent(); // And a valid sample to send mock_binding::SampleAllocateePtr dummy_sample{nullptr, [](void*) {}}; // When calling Send() before OfferService() - auto send_result = event->Send(MakeSampleAllocateePtr(std::move(dummy_sample))); + auto send_result = event_->Send(MakeSampleAllocateePtr(std::move(dummy_sample))); // Then it fails with kNotOffered ASSERT_FALSE(send_result.has_value()); @@ -182,43 +197,21 @@ 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 + this->GivenAGenericSkeletonWithOneEvent().OfferSkeletonService(); // When calling Allocate() mock_binding::SampleAllocateePtr dummy_alloc{nullptr, [](void*) {}}; - EXPECT_CALL(*mock_event_binding_ptr, Allocate()) + EXPECT_CALL(*mock_event_binding_ptr_, Allocate()) .WillOnce(Return(ByMove(MakeSampleAllocateePtr(std::move(dummy_alloc))))); - auto alloc_result = event->Allocate(); + auto alloc_result = event_->Allocate(); ASSERT_TRUE(alloc_result.has_value()); // And When calling Send() with the allocated sample - EXPECT_CALL(*mock_event_binding_ptr, Send(_)).WillOnce(Return(score::Result{})); + EXPECT_CALL(*mock_event_binding_ptr_, Send(_)).WillOnce(Return(score::Result{})); - auto send_result = event->Send(std::move(alloc_result.value())); + auto send_result = event_->Send(std::move(alloc_result.value())); // Then both operations succeed ASSERT_TRUE(send_result.has_value()); @@ -230,36 +223,15 @@ 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 + this->GivenAGenericSkeletonWithOneEvent().OfferSkeletonService(); // Expect the binding to fail allocation - EXPECT_CALL(*mock_event_binding_ptr, Allocate()) + EXPECT_CALL(*mock_event_binding_ptr_, Allocate()) .WillOnce(Return(ByMove(MakeUnexpected(ComErrc::kSampleAllocationFailure)))); // When calling Allocate() - auto alloc_result = event->Allocate(); + auto alloc_result = event_->Allocate(); // Then it fails with kSampleAllocationFailure ASSERT_FALSE(alloc_result.has_value()); @@ -271,36 +243,15 @@ 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 + this->GivenAGenericSkeletonWithOneEvent().OfferSkeletonService(); // Expect the binding to fail sending - EXPECT_CALL(*mock_event_binding_ptr, Send(_)).WillOnce(Return(MakeUnexpected(ComErrc::kBindingFailure))); + EXPECT_CALL(*mock_event_binding_ptr_, Send(_)).WillOnce(Return(MakeUnexpected(ComErrc::kBindingFailure))); // When calling Send() with a dummy sample mock_binding::SampleAllocateePtr dummy_alloc{nullptr, [](void*) {}}; - auto send_result = event->Send(MakeSampleAllocateePtr(std::move(dummy_alloc))); + auto send_result = event_->Send(MakeSampleAllocateePtr(std::move(dummy_alloc))); // Then it fails with kBindingFailure ASSERT_FALSE(send_result.has_value()); @@ -312,31 +263,15 @@ 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 + this->GivenAGenericSkeletonWithOneEvent(); // Expect the binding to return specific size info std::pair expected_size_info{32, 16}; - EXPECT_CALL(*mock_event_binding_ptr, GetSizeInfo()).WillOnce(Return(expected_size_info)); + EXPECT_CALL(*mock_event_binding_ptr_, GetSizeInfo()).WillOnce(Return(expected_size_info)); // When calling GetSizeInfo - auto result_info = event->GetSizeInfo(); + auto result_info = event_->GetSizeInfo(); // Then it matches the binding's return values EXPECT_EQ(result_info.size, expected_size_info.first); @@ -410,6 +345,22 @@ TEST_F(GenericSkeletonEventTest, NotifyDispatchesToBindingAfterOffer) // Then it succeeds ASSERT_TRUE(notify_result.has_value()); } +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 + this->GivenAGenericSkeletonWithOneEvent(); + + // 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..f1313dd3d --- /dev/null +++ b/score/mw/com/impl/receive_handler_registration_changed_handler.h @@ -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 + ********************************************************************************/ +#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. +/// \details The callback's boolean parameter will be true if the first handler has been registered and false if the +/// last handler has been withdrawn. +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