diff --git a/score/mw/com/api_surface.lock.json b/score/mw/com/api_surface.lock.json index 3f640d303..8abf94832 100644 --- a/score/mw/com/api_surface.lock.json +++ b/score/mw/com/api_surface.lock.json @@ -38,16 +38,16 @@ "signature": "InitializeRuntime : void (const RuntimeConfiguration &)" }, { - "name": "InitializeRuntimeAddonConfiguration", - "qualified_name": "score::mw::com::runtime::InitializeRuntimeAddonConfiguration", + "name": "AddConfiguration", + "qualified_name": "score::mw::com::runtime::AddConfiguration", "kind": "function", - "signature": "InitializeRuntimeAddonConfiguration : Result (const RuntimeConfiguration &)" + "signature": "AddConfiguration : Result (const RuntimeConfiguration &)" }, { - "name": "InitializeRuntimeAddonConfiguration", - "qualified_name": "score::mw::com::runtime::InitializeRuntimeAddonConfiguration", + "name": "AddConfiguration", + "qualified_name": "score::mw::com::runtime::AddConfiguration", "kind": "function", - "signature": "InitializeRuntimeAddonConfiguration : Result (score::json::Any)" + "signature": "AddConfiguration : Result (score::json::Any)" }, { "name": "RuntimeConfiguration", diff --git a/score/mw/com/doc/user_facing_API_examples.md b/score/mw/com/doc/user_facing_API_examples.md index f72832b15..be5a4756e 100644 --- a/score/mw/com/doc/user_facing_API_examples.md +++ b/score/mw/com/doc/user_facing_API_examples.md @@ -16,7 +16,7 @@ This document contains examples of each mw::com user facing API. | [`RuntimeConfiguration(argc, argv)`](#example-3-using-runtimeconfiguration-for-configuration-management) | | [`RuntimeConfiguration(Path)`](#example-3-using-runtimeconfiguration-for-configuration-management) | | [`RuntimeConfiguration::GetConfigurationPath()`](#example-3-using-runtimeconfiguration-for-configuration-management) | -| [`RuntimeConfiguration::InitializeRuntimeAddonConfiguration()`](#example-4-using-initializeruntimeaddonconfiguration-to-load-additional-mwcom-configurations) | +| [`RuntimeConfiguration::AddConfiguration()`](#example-4-using-AddConfiguration-to-load-additional-mwcom-configurations) | | **Data Types** | | [`InstanceIdentifier::Create()`](#example-1-using-instanceidentifier-for-service-instance-management) | | [`InstanceIdentifier::ToString()`](#example-1-using-instanceidentifier-for-service-instance-management) | @@ -276,9 +276,9 @@ const auto& config_path = default_config.GetConfigurationPath(); --- -### Example 4: Using `InitializeRuntimeAddonConfiguration` to load additional `mw::com` configurations +### Example 4: Using `AddConfiguration` to load additional `mw::com` configurations -`Runtime` provides the APIs `InitializeRuntimeAddonConfiguration(RuntimeConfiguration&)` and `InitializeRuntimeAddonConfiguration(score::json::Any)` +`Runtime` provides the APIs `AddConfiguration(RuntimeConfiguration&)` and `AddConfiguration(score::json::Any)` to load additional configurations. For example, this can be used by libraries that also rely on mw::com to load their configuration in addition to the application's configuration. It is assumed that prior to that call a complete mw::com configuration has been loaded via `InitializeRuntime()`. If not this call will cause an application termination. diff --git a/score/mw/com/impl/runtime.cpp b/score/mw/com/impl/runtime.cpp index dbc831c80..d8493fdd8 100644 --- a/score/mw/com/impl/runtime.cpp +++ b/score/mw/com/impl/runtime.cpp @@ -133,14 +133,14 @@ void Runtime::Initialize(const runtime::RuntimeConfiguration& runtime_configurat score::cpp::ignore = initialization_config_.emplace(std::move(config)); } -Result Runtime::InitializeRuntimeAddonConfiguration(const runtime::RuntimeConfiguration& runtime_configuration) +Result Runtime::AddConfiguration(const runtime::RuntimeConfiguration& runtime_configuration) { auto config = configuration::Parse(runtime_configuration.GetConfigurationPath().Native()); return HandleAddonConfiguration(config); } -Result Runtime::InitializeRuntimeAddonConfiguration(score::json::Any json) +Result Runtime::AddConfiguration(score::json::Any json) { auto config = configuration::Parse(std::move(json)); diff --git a/score/mw/com/impl/runtime.h b/score/mw/com/impl/runtime.h index ed3b267ba..fca6af531 100644 --- a/score/mw/com/impl/runtime.h +++ b/score/mw/com/impl/runtime.h @@ -82,7 +82,7 @@ class Runtime final : public IRuntime /// \attention This function will call std::terminate() in case no initial configuration has been loaded yet, or /// that the configuration is incompatible to the previously loaded one. /// \param runtime_configuration object containing service definitions which should be added to existing set - static Result InitializeRuntimeAddonConfiguration(const runtime::RuntimeConfiguration& runtime_configuration); + static Result AddConfiguration(const runtime::RuntimeConfiguration& runtime_configuration); /// \brief Extends mw::com subsystem with the given add-on configuration provided as a JSON object. /// \details This call is optional and shall allow loading additional mw::com configuration as an in-memory JSON @@ -90,7 +90,7 @@ class Runtime final : public IRuntime /// \attention This function will call std::terminate() in case no initial configuration has been loaded yet, or /// that the configuration is incompatible to the previously loaded one. /// \param json object containing service definitions which should be added to existing set - static Result InitializeRuntimeAddonConfiguration(score::json::Any json); + static Result AddConfiguration(score::json::Any json); /// \brief get singleton. /// \details Might return either reference to a real Runtime instance or to a mock. diff --git a/score/mw/com/impl/runtime_single_exec_test.cpp b/score/mw/com/impl/runtime_single_exec_test.cpp index 6dfca3899..5a4957cbe 100644 --- a/score/mw/com/impl/runtime_single_exec_test.cpp +++ b/score/mw/com/impl/runtime_single_exec_test.cpp @@ -271,9 +271,8 @@ TEST_F(RuntimeInitializationTest, ConfigurationGetsMergedAndLoadedIfInitialConfi const auto configuration = runtime::RuntimeConfiguration{config_with_tire_pressure_port_other_}; Runtime::Initialize(configuration); - // When loading an add-on configuration with InitializeRuntimeAddonConfiguration - const auto addon_init_result = - Runtime::InitializeRuntimeAddonConfiguration(runtime::RuntimeConfiguration{config_to_merge_}); + // When loading an add-on configuration with AddConfiguration + const auto addon_init_result = Runtime::AddConfiguration(runtime::RuntimeConfiguration{config_to_merge_}); auto& updated_runtime = static_cast(Runtime::getInstance()); @@ -297,13 +296,11 @@ TEST_F(RuntimeInitializationTest, ConcurrentAddonConfigurationInitializationSucc std::optional> result_thread_2{}; std::thread thread_1{[&result_thread_1, this]() { - result_thread_1 = - Runtime::InitializeRuntimeAddonConfiguration(runtime::RuntimeConfiguration{config_to_merge_}); + result_thread_1 = Runtime::AddConfiguration(runtime::RuntimeConfiguration{config_to_merge_}); }}; std::thread thread_2{[&result_thread_2, this]() { - result_thread_2 = - Runtime::InitializeRuntimeAddonConfiguration(runtime::RuntimeConfiguration{config_to_merge_second_}); + result_thread_2 = Runtime::AddConfiguration(runtime::RuntimeConfiguration{config_to_merge_second_}); }}; thread_1.join(); @@ -333,8 +330,8 @@ TEST_F(RuntimeInitializationDeathTest, InitializationFailsIfNoAppConfigurationHa { // Given no configuration has been loaded const auto runtime_configuration = runtime::RuntimeConfiguration{config_with_tire_pressure_port_}; - // When loading an add-on configuration via InitializeRuntimeAddonConfiguration() - std::ignore = Runtime::InitializeRuntimeAddonConfiguration(runtime_configuration); + // When loading an add-on configuration via AddConfiguration() + std::ignore = Runtime::AddConfiguration(runtime_configuration); // Then the process terminates via std::terminate() }, ".*"); @@ -351,9 +348,9 @@ TEST_F(RuntimeInitializationDeathTest, AddOnConfigurationInitializationFailsIfMe const auto runtime_configuration = runtime::RuntimeConfiguration{config_with_tire_pressure_port_}; Runtime::Initialize(runtime_configuration); std::ignore = static_cast(Runtime::getInstance()); - // When loading the same configuration via InitializeRuntimeAddonConfiguration() + // When loading the same configuration via AddConfiguration() const auto add_on_configuration = runtime::RuntimeConfiguration{config_with_tire_pressure_port_}; - std::ignore = Runtime::InitializeRuntimeAddonConfiguration(add_on_configuration); + std::ignore = Runtime::AddConfiguration(add_on_configuration); // Then the process terminates via std::terminate() because there is a clash of service identifiers }, ".*"); diff --git a/score/mw/com/runtime.cpp b/score/mw/com/runtime.cpp index c528032d9..e53bfc812 100644 --- a/score/mw/com/runtime.cpp +++ b/score/mw/com/runtime.cpp @@ -86,14 +86,14 @@ void InitializeRuntime(const RuntimeConfiguration& runtime_configuration) impl::Runtime::Initialize(runtime_configuration); } -Result InitializeRuntimeAddonConfiguration(const RuntimeConfiguration& runtime_configuration) +Result AddConfiguration(const RuntimeConfiguration& runtime_configuration) { - return impl::Runtime::InitializeRuntimeAddonConfiguration(runtime_configuration); + return impl::Runtime::AddConfiguration(runtime_configuration); } -Result InitializeRuntimeAddonConfiguration(score::json::Any json) +Result AddConfiguration(score::json::Any json) { - return impl::Runtime::InitializeRuntimeAddonConfiguration(std::move(json)); + return impl::Runtime::AddConfiguration(std::move(json)); } } // namespace score::mw::com::runtime diff --git a/score/mw/com/runtime.h b/score/mw/com/runtime.h index 69eae7197..ab5a4d694 100644 --- a/score/mw/com/runtime.h +++ b/score/mw/com/runtime.h @@ -126,7 +126,7 @@ void InitializeRuntime(const RuntimeConfiguration& runtime_configuration); * \attention This function will call std::terminate() in case that the configuration is incompatible to the previously * loaded one or if no complete mw::com configuration has been loaded previously. **/ -Result InitializeRuntimeAddonConfiguration(const RuntimeConfiguration& runtime_configuration); +Result AddConfiguration(const RuntimeConfiguration& runtime_configuration); /** * \api @@ -137,7 +137,7 @@ Result InitializeRuntimeAddonConfiguration(const RuntimeConfiguration& run * loaded one or if no complete mw::com configuration has been loaded previously. * \param json The JSON object containing the add-on configuration. **/ -Result InitializeRuntimeAddonConfiguration(score::json::Any json); +Result AddConfiguration(score::json::Any json); } // namespace score::mw::com::runtime diff --git a/score/mw/com/test/loading_add_on_configuration/BUILD b/score/mw/com/test/loading_add_on_configuration/BUILD new file mode 100644 index 000000000..839639e6d --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/BUILD @@ -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 +# ******************************************************************************* + +load("@rules_cc//cc:defs.bzl", "cc_binary") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") +load("//score/mw/com/test:pkg_application.bzl", "pkg_application") + +exports_files( + ["config/logging.json"], + visibility = ["//score/mw/com/test:__subpackages__"], +) + +cc_library( + name = "common_resources", + srcs = ["common_resources.cpp"], + hdrs = ["common_resources.h"], + features = COMPILER_WARNING_FEATURES, + deps = [ + "//score/mw/com/test/common_test_resources:command_line_parser", + "//score/mw/com/test/common_test_resources:fail_test", + ], +) + +cc_library( + name = "test_constants", + srcs = ["test_constants.cpp"], + hdrs = ["test_constants.h"], + features = COMPILER_WARNING_FEATURES, + deps = [ + "//score/mw/com", + "//score/mw/com/test/loading_add_on_configuration/types:addon_interface", + ], +) + +cc_library( + name = "consumer_impl", + srcs = ["consumer.cpp"], + hdrs = ["consumer.h"], + features = COMPILER_WARNING_FEATURES, + deps = [ + ":common_resources", + ":test_constants", + "//score/mw/com", + "//score/mw/com/test/common_test_resources:fail_test", + "//score/mw/com/test/common_test_resources:process_synchronizer", + "//score/mw/com/test/common_test_resources:proxy_container", + "//score/mw/com/test/common_test_resources:proxy_event_receiver", + "//score/mw/com/test/common_test_resources:proxy_event_state_change_notifier", + "//score/mw/com/test/loading_add_on_configuration/types:addon_interface", + "//score/mw/com/test/loading_add_on_configuration/types:example_interface", + ], +) + +cc_library( + name = "provider_impl", + srcs = ["provider.cpp"], + hdrs = ["provider.h"], + features = COMPILER_WARNING_FEATURES, + deps = [ + ":common_resources", + ":test_constants", + "//score/mw/com", + "//score/mw/com/test/common_test_resources:fail_test", + "//score/mw/com/test/common_test_resources:process_synchronizer", + "//score/mw/com/test/common_test_resources:skeleton_container", + "//score/mw/com/test/loading_add_on_configuration/types:addon_interface", + "//score/mw/com/test/loading_add_on_configuration/types:example_interface", + ], +) + +cc_binary( + name = "consumer", + srcs = ["main_consumer.cpp"], + data = [ + "config/mw_com_add_on_config.json", + "config/mw_com_config.json", + "config/mw_com_invalid_add_on_config.json", + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + visibility = ["//score/mw/com/test/loading_add_on_configuration:__pkg__"], + deps = [ + ":common_resources", + ":consumer_impl", + "//score/mw/com", + "//score/mw/com/test/common_test_resources:fail_test", + "//score/mw/com/test/common_test_resources:process_synchronizer", + "//score/mw/com/test/common_test_resources:stop_token_sig_term_handler", + "@score_baselibs//score/mw/log", + ], +) + +cc_binary( + name = "provider", + srcs = ["main_provider.cpp"], + data = [ + "config/mw_com_add_on_config.json", + "config/mw_com_config.json", + "config/mw_com_invalid_add_on_config.json", + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + visibility = ["//score/mw/com/test/loading_add_on_configuration:__pkg__"], + deps = [ + ":common_resources", + ":provider_impl", + "//score/mw/com", + "//score/mw/com/test/common_test_resources:fail_test", + "//score/mw/com/test/common_test_resources:process_synchronizer", + "//score/mw/com/test/common_test_resources:stop_token_sig_term_handler", + "@score_baselibs//score/mw/log", + ], +) + +pkg_application( + name = "consumer-pkg", + app_name = "consumer", + bin = [":consumer"], + etc = [ + "config/logging.json", + "config/mw_com_config.json", + "config/mw_com_add_on_config.json", + "config/mw_com_invalid_add_on_config.json", + ], + visibility = [ + "//platform/aas/test/mw/com:__pkg__", + "//score/mw/com/test/loading_add_on_configuration:__subpackages__", + ], +) + +pkg_application( + name = "provider-pkg", + app_name = "provider", + bin = [":provider"], + etc = [ + "config/logging.json", + "config/mw_com_config.json", + "config/mw_com_invalid_add_on_config.json", + "config/mw_com_add_on_config.json", + ], + visibility = [ + "//platform/aas/test/mw/com:__pkg__", + "//score/mw/com/test/loading_add_on_configuration:__subpackages__", + ], +) diff --git a/score/mw/com/test/loading_add_on_configuration/common_resources.cpp b/score/mw/com/test/loading_add_on_configuration/common_resources.cpp new file mode 100644 index 000000000..6cc1ed8a0 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/common_resources.cpp @@ -0,0 +1,39 @@ +/******************************************************************************** + * 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/test/loading_add_on_configuration/common_resources.h" + +#include "score/mw/com/test/common_test_resources/command_line_parser.h" + +namespace score::mw::com::test +{ + +TestConfig ParseConfig(int argc, const char** argv) +{ + constexpr auto kServiceInstanceManifestArg = "service-instance-manifest"; + + const std::vector> parameter_description_pairs{ + {kServiceInstanceManifestArg, "Path to the service instance manifest"}, + }; + + const auto args = ParseCommandLineArguments(argc, argv, parameter_description_pairs); + + const auto manifest_result = GetValueIfProvided(args, kServiceInstanceManifestArg); + if (!manifest_result.has_value()) + { + FailTest("Missing or invalid --", kServiceInstanceManifestArg, " argument"); + } + + return TestConfig{manifest_result.value()}; +} + +} // namespace score::mw::com::test diff --git a/score/mw/com/test/loading_add_on_configuration/common_resources.h b/score/mw/com/test/loading_add_on_configuration/common_resources.h new file mode 100644 index 000000000..d0251b81d --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/common_resources.h @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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_TEST_LOADING_ADD_ON_CONFIGURATION_COMMON_RESOURCES_H +#define SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_COMMON_RESOURCES_H + +#include + +namespace score::mw::com::test +{ + +struct TestConfig +{ + std::string config_file_path; +}; + +/// \brief Parses the command line arguments for the test and returns a TestConfig object. +/// +/// Terminates if the argument is not provided. We use this function instead of providing argc / argv directly to +/// InitializeRuntime so that we detect if the config file was not provided instead of silently falling back to the +/// default config. It also makes it easier to extend the command line arguments in the future if needed. +TestConfig ParseConfig(int argc, const char** argv); + +} // namespace score::mw::com::test + +#endif // SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_COMMON_RESOURCES_H diff --git a/score/mw/com/test/loading_add_on_configuration/config/logging.json b/score/mw/com/test/loading_add_on_configuration/config/logging.json new file mode 100644 index 000000000..44554db82 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/config/logging.json @@ -0,0 +1,8 @@ +{ + "appId": "ADDONL", + "appDesc": "addonloader", + "logLevel": "kDebug", + "logLevelThresholdConsole": "kDebug", + "logMode": "kRemote|kConsole", + "dynamicDatarouterIdentifiers" : true +} diff --git a/score/mw/com/test/loading_add_on_configuration/config/mw_com_add_on_config.json b/score/mw/com/test/loading_add_on_configuration/config/mw_com_add_on_config.json new file mode 100644 index 000000000..6e529a0f7 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/config/mw_com_add_on_config.json @@ -0,0 +1,68 @@ +{ + "serviceTypes": [ + { + "serviceTypeName": "/score/example/AddOnService", + "version": { + "major": 1, + "minor": 0 + }, + "bindings": [ + { + "binding": "SHM", + "serviceId": 6532, + "events": [ + { + "eventName": "example_event", + "eventId": 1 + }, + { + "eventName": "active_event", + "eventId": 2 + } + ] + } + ] + } + ], + "serviceInstances": [ + { + "instanceSpecifier": "score/data/AddOnService", + "serviceTypeName": "/score/example/AddOnService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "allowedConsumer": { + "QM": [ + 4002, + 0 + ] + }, + "allowedProvider": { + "QM": [ + 4001, + 0 + ] + }, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "example_event", + "numberOfSampleSlots": 10, + "maxSubscribers": 3 + }, + { + "eventName": "active_event", + "numberOfSampleSlots": 5, + "maxSubscribers": 5 + } + ] + } + ] + } + ] +} diff --git a/score/mw/com/test/loading_add_on_configuration/config/mw_com_config.json b/score/mw/com/test/loading_add_on_configuration/config/mw_com_config.json new file mode 100644 index 000000000..1d0876a72 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/config/mw_com_config.json @@ -0,0 +1,59 @@ +{ + "serviceTypes": [ + { + "serviceTypeName": "/score/example/DataService", + "version": { + "major": 1, + "minor": 0 + }, + "bindings": [ + { + "binding": "SHM", + "serviceId": 6432, + "events": [ + { + "eventName": "example_event", + "eventId": 1 + } + ] + } + ] + } + ], + "serviceInstances": [ + { + "instanceSpecifier": "score/data/DataService", + "serviceTypeName": "/score/example/DataService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "allowedConsumer": { + "QM": [ + 4002, + 0 + ] + }, + "allowedProvider": { + "QM": [ + 4001, + 0 + ] + }, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "example_event", + "numberOfSampleSlots": 10, + "maxSubscribers": 3 + } + ] + } + ] + } + ] +} diff --git a/score/mw/com/test/loading_add_on_configuration/config/mw_com_invalid_add_on_config.json b/score/mw/com/test/loading_add_on_configuration/config/mw_com_invalid_add_on_config.json new file mode 100644 index 000000000..080ee5772 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/config/mw_com_invalid_add_on_config.json @@ -0,0 +1,68 @@ +{ + "serviceTypes": [ + { + "serviceTypeName": "/score/example/DataService", + "version": { + "major": 1, + "minor": 0 + }, + "bindings": [ + { + "binding": "SHM", + "serviceId": 6435, + "events": [ + { + "eventName": "example_event1", + "eventId": 1 + }, + { + "eventName": "example_event2", + "eventId": 2 + } + ] + } + ] + } + ], + "serviceInstances": [ + { + "instanceSpecifier": "score/data/DataService", + "serviceTypeName": "/score/example/DataService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "allowedConsumer": { + "QM": [ + 4002, + 0 + ] + }, + "allowedProvider": { + "QM": [ + 4001, + 0 + ] + }, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "example_event1", + "numberOfSampleSlots": 10, + "maxSubscribers": 3 + }, + { + "eventName": "example_event2", + "numberOfSampleSlots": 10, + "maxSubscribers": 3 + } + ] + } + ] + } + ] +} diff --git a/score/mw/com/test/loading_add_on_configuration/consumer.cpp b/score/mw/com/test/loading_add_on_configuration/consumer.cpp new file mode 100644 index 000000000..384d981f6 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/consumer.cpp @@ -0,0 +1,14 @@ +/******************************************************************************* + * 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/test/loading_add_on_configuration/consumer.h" diff --git a/score/mw/com/test/loading_add_on_configuration/consumer.h b/score/mw/com/test/loading_add_on_configuration/consumer.h new file mode 100644 index 000000000..6008ab181 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/consumer.h @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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_TEST_LOADING_ADD_ON_CONFIGURATION_CONSUMER_H +#define SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_CONSUMER_H + +#include "score/mw/com/test/loading_add_on_configuration/test_constants.h" + +#include "score/mw/com/test/common_test_resources/fail_test.h" +#include "score/mw/com/test/common_test_resources/process_synchronizer.h" +#include "score/mw/com/test/common_test_resources/proxy_container.h" +#include "score/mw/com/test/common_test_resources/proxy_event_receiver.h" +#include "score/mw/com/test/common_test_resources/proxy_event_state_change_notifier.h" + +#include "score/mw/com/types.h" + +#include + +namespace score::mw::com::test +{ +template +void run_consumer(const score::cpp::stop_token& stop_token, + const score::mw::com::InstanceSpecifier& instance_specifier, + ProcessSynchronizer& process_synchronizer, + ProcessSynchronizer& provider_ready_synchronizer, + const std::vector& samples) +{ + ExitFunctionGuard process_synchronizer_guard{[&process_synchronizer]() { + process_synchronizer.Notify(); + }}; + + std::cout << "\nConsumer: Step 1 - Waiting for provider" << std::endl; + + if (!provider_ready_synchronizer.WaitWithAbort(stop_token)) + { + FailTest("Consumer: WaitWithAbort (done) was stopped by stop_token instead of notification"); + } + // Reset for subsequent calls + provider_ready_synchronizer.Reset(); + + // Step 2. Find service and create proxy + std::cout << "\nConsumer: Step 2 - Find service and create proxy" << std::endl; + + ProxyContainer proxy_container{}; + proxy_container.CreateProxy(instance_specifier, "regular_service"); + auto& proxy = proxy_container.GetProxy(); + + ProxyEventReceiver event_receiver{proxy.example_event}; + ProxyEventStateChangeNotifier subscription_notifier{proxy.example_event}; + + // Step 3. Subscribe to event with enough buffer for all samples the provider will send + std::cout << "\nConsumer: Step 3 - Subscribe to event" << std::endl; + const auto subscribe_result = proxy.example_event.Subscribe(kTotalNumValuesToSend); + if (!subscribe_result.has_value()) + { + FailTest("Consumer: Subscribe failed for example_event: ", subscribe_result.error()); + } + + // Step 4. Wait for subscription + std::cout << "\nConsumer: Step 4 - Wait for subscription" << std::endl; + if (!subscription_notifier.WaitForStateChange(stop_token, SubscriptionState::kSubscribed)) + { + FailTest("Consumer: Subscription failed in event scenario"); + } + + // Step 5. Wait for all expected samples + std::cout << "\nConsumer: Step 5 - Wait for all expected samples" << std::endl; + if (!event_receiver.WaitForSamples(stop_token, samples)) + { + FailTest("Consumer: Did not receive all expected samples in event scenario"); + } + + // Step 6. Notify provider that data was received + std::cout << "\nConsumer: Step 6 - Notify provider that data was received" << std::endl; + + process_synchronizer.Notify(); +} + +} // namespace score::mw::com::test + +#endif // SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_CONSUMER_H diff --git a/score/mw/com/test/loading_add_on_configuration/integration_test/BUILD b/score/mw/com/test/loading_add_on_configuration/integration_test/BUILD new file mode 100644 index 000000000..8c9949130 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/integration_test/BUILD @@ -0,0 +1,31 @@ +# ******************************************************************************* +# 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_filegroup") +load("//quality/integration_testing:integration_testing.bzl", "integration_test") + +pkg_filegroup( + name = "filesystem", + srcs = [ + "//score/mw/com/test/loading_add_on_configuration:consumer-pkg", + "//score/mw/com/test/loading_add_on_configuration:provider-pkg", + ], +) + +integration_test( + name = "test_add_on_loading", + timeout = "moderate", + srcs = [ + "test_add_on_loading.py", + ], + filesystem = ":filesystem", +) diff --git a/score/mw/com/test/loading_add_on_configuration/integration_test/test_add_on_loading.py b/score/mw/com/test/loading_add_on_configuration/integration_test/test_add_on_loading.py new file mode 100644 index 000000000..20c0065b5 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/integration_test/test_add_on_loading.py @@ -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 +# ******************************************************************************* + + +import signal + +# 128 + SIGABRT: expected exit code when std::terminate() is invoked, e.g. because merging an invalid add-on +# configuration is rejected by the mw::com runtime. +SIGABRT_EXIT_CODE = 128 + signal.SIGABRT + + +def test_add_on_loading(target): + """Test loading an add-on application with communication between provider and consumer. Add-on configuration is + merged in between communication cycles.""" + with provider(target, "mw_com_config.json"): + with consumer(target, "mw_com_config.json"): + pass + + +def test_add_on_merge_during_active_communication(target): + """Test that merging the add-on configuration while the base service is already sending does not disrupt it. + + The provider merges the (valid) add-on configuration synchronously in the middle of its 1st publish loop. + The base service consumer is expected to receive its samples normally and the add-on service is expected to work + normally after the merge. + """ + with provider(target, "mw_com_config.json", merge_during_stream=True): + with consumer(target, "mw_com_config.json"): + pass + + +def test_invalid_add_on_loading(target): + """Test that loading an invalid add-on configuration (duplicate service identifier) is rejected. + + Merging the invalid add-on configuration is expected to make the mw::com runtime call std::terminate(), so the + provider/consumer processes are expected to be killed with SIGABRT rather than exit normally. + """ + with provider(target, "mw_com_config.json", invalid_addon_only=True, expected_exit_code=SIGABRT_EXIT_CODE): + pass + with consumer(target, "mw_com_config.json", invalid_addon_only=True, expected_exit_code=SIGABRT_EXIT_CODE): + pass + + +def test_asymmetric_add_on_merge(target): + """Test that a consumer which never merged the add-on configuration cannot discover the add-on service. + + The provider merges the add-on configuration and offers the add-on service, while on the consumer side it is never + merged and the consumer directly attempts to find/create a proxy for the add-on service instance. Since the add-on + instance specifier is unknown to the consumer's local configuration, service discovery is expected to fail and + the consumer is expected to exit gracefully and no crash is expected. + """ + with provider(target, "mw_com_config.json", offer_addon_only=True, wait_on_exit=True): + with consumer(target, "mw_com_config.json", addon_no_merge=True, expected_exit_code=1): + pass + + +def consumer(target, config, invalid_addon_only=False, addon_no_merge=False, **kwargs): + args = [ + "--service-instance-manifest", + f"./etc/{config}", + "--addon_manifest", + f"./etc/mw_com_add_on_config.json", + "--invalid_addon_manifest", + f"./etc/mw_com_invalid_add_on_config.json", + ] + if invalid_addon_only: + args += ["--invalid-addon-only", "true"] + if addon_no_merge: + args += ["--addon-no-merge", "true"] + return target.wrap_exec("bin/consumer", args, cwd="/opt/consumer", wait_on_exit=True, **kwargs) + + +def provider( + target, + config, + invalid_addon_only=False, + offer_addon_only=False, + merge_during_stream=False, + **kwargs, +): + args = [ + "--service-instance-manifest", + f"./etc/{config}", + "--addon_manifest", + f"./etc/mw_com_add_on_config.json", + "--invalid_addon_manifest", + f"./etc/mw_com_invalid_add_on_config.json", + ] + if invalid_addon_only: + args += ["--invalid-addon-only", "true"] + if offer_addon_only: + args += ["--offer-addon-only", "true"] + if merge_during_stream: + args += ["--merge-during-stream", "true"] + kwargs.setdefault("wait_on_exit", invalid_addon_only or offer_addon_only) + return target.wrap_exec("bin/provider", args, cwd="/opt/provider", **kwargs) diff --git a/score/mw/com/test/loading_add_on_configuration/main_consumer.cpp b/score/mw/com/test/loading_add_on_configuration/main_consumer.cpp new file mode 100644 index 000000000..e13cb0eac --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/main_consumer.cpp @@ -0,0 +1,190 @@ +/******************************************************************************* + * 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/test/loading_add_on_configuration/common_resources.h" +#include "score/mw/com/test/loading_add_on_configuration/consumer.h" +#include "score/mw/com/test/loading_add_on_configuration/test_constants.h" +#include "score/mw/com/test/loading_add_on_configuration/types/addon_interface.h" +#include "score/mw/com/test/loading_add_on_configuration/types/example_interface.h" + +#include "score/mw/com/test/common_test_resources/command_line_parser.h" +#include "score/mw/com/test/common_test_resources/fail_test.h" +#include "score/mw/com/test/common_test_resources/process_synchronizer.h" +#include "score/mw/com/test/common_test_resources/proxy_container.h" +#include "score/mw/com/test/common_test_resources/stop_token_sig_term_handler.h" + +#include "score/mw/com/runtime.h" + +#include +#include + +namespace +{ + +std::string ParseServiceInstanceManifest(int argc, const char** argv, std::string manifest_name) +{ + auto args = score::mw::com::test::ParseCommandLineArguments(argc, argv, {{manifest_name, ""}}); + return score::mw::com::test::GetValue(args, manifest_name); +} + +// \brief Checks whether the "invalid-addon-only" flag was passed on the command line. +// +// When set, the process only attempts to merge the (intentionally) invalid add-on configuration and then exits, +// instead of running the full consumer sequence. This is used by a dedicated, separate process invocation of this +// binary to test that loading an invalid add-on configuration is rejected: merging is expected to make the runtime +// call std::terminate(), so the process is expected to be killed with SIGABRT rather than exit normally. +bool ParseInvalidAddonOnlyFlag(int argc, const char** argv) +{ + auto args = score::mw::com::test::ParseCommandLineArguments(argc, argv, {{"invalid-addon-only", ""}}); + const auto flag_result = score::mw::com::test::GetValueIfProvided(args, "invalid-addon-only"); + return flag_result.has_value() && flag_result.value(); +} + +// \brief Checks whether the "addon-no-merge" flag was passed on the command line. +// +// When set, the process does *not* merge the add-on configuration at all, and instead directly attempts to create a +// proxy for the add-on service instance specifier. Since the add-on service is unknown to this process' local +// configuration, service discovery is expected to fail and the process is expected to exit gracefully with a +// controlled failure (via FailTest(), i.e. EXIT_FAILURE), not crash. This is used to test that a consumer which was +// never updated with the add-on configuration cannot accidentally "see" a service instance it doesn't know about, +// while also not misbehaving/crashing. +bool ParseAddonNoMergeFlag(int argc, const char** argv) +{ + auto args = score::mw::com::test::ParseCommandLineArguments(argc, argv, {{"addon-no-merge", ""}}); + const auto flag_result = score::mw::com::test::GetValueIfProvided(args, "addon-no-merge"); + return flag_result.has_value() && flag_result.value(); +} + +int RunInvalidAddOnConfigTestCase(int argc, const char** argv) +{ + // Try to merge an invalid add-on configuration which should fail, because its service identifier is already + // in use. The runtime is expected to call std::terminate(). + const auto invalid_service_instance_manifest_path = + ParseServiceInstanceManifest(argc, argv, "invalid_addon_manifest"); + const auto invalid_add_on_load_result = score::mw::com::runtime::AddConfiguration( + score::mw::com::runtime::RuntimeConfiguration{invalid_service_instance_manifest_path}); + + if (invalid_add_on_load_result.has_value()) + { + std::cerr << "Consumer: Could load invalid add-on configuration which should not be possible" << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +int RunAddOnServiceSearchWithoutConfigMergeTestCase() +{ + // Attempting to find/create a proxy for the add-on service instance specifier is expected to fail gracefully + // since this process' local configuration has no knowledge of that instance specifier, because the configuration + // was never merged + auto discovery_attempt_done_synchronizer_result = + score::mw::com::test::ProcessSynchronizer::Create(score::mw::com::test::kAddonDiscoveryAttemptDoneShmPath); + if (!discovery_attempt_done_synchronizer_result.has_value()) + { + score::mw::com::test::FailTest("Consumer: Could not create discovery attempt done ProcessSynchronizer"); + } + auto& discovery_attempt_done_synchronizer = *discovery_attempt_done_synchronizer_result; + + // Notify the peer provider that the discovery attempt has concluded (whether it fails as expected via + // FailTest(), or if it was successful), so add-on service does not have to be offered anymore. + score::mw::com::test::ExitFunctionGuard discovery_attempt_done_guard{[&discovery_attempt_done_synchronizer]() { + discovery_attempt_done_synchronizer.Notify(); + }}; + + std::cout << "\nConsumer: Attempting to use add-on service without having merged its configuration" << std::endl; + score::mw::com::test::ProxyContainer proxy_container{}; + proxy_container.CreateProxy(score::mw::com::test::kAddOnServiceInstanceSpecifier, + "Consumer: Unexpectedly able to find/create add-on proxy without merge:"); + // If we ever get here, discovery unexpectedly succeeded, which should not be possible. + std::cerr << "Consumer: Could create add-on proxy without merging its configuration, which should not be " + "possible" + << std::endl; + return EXIT_FAILURE; +} + +} // namespace + +int main(int argc, const char** argv) +{ + const auto config = score::mw::com::test::ParseConfig(argc, argv); + + score::mw::com::runtime::InitializeRuntime(score::mw::com::runtime::RuntimeConfiguration{config.config_file_path}); + + if (ParseInvalidAddonOnlyFlag(argc, argv)) + { + return RunInvalidAddOnConfigTestCase(argc, argv); + } + + if (ParseAddonNoMergeFlag(argc, argv)) + { + return RunAddOnServiceSearchWithoutConfigMergeTestCase(); + } + + score::cpp::stop_source stop_source{}; + const bool sig_term_handler_setup_success = score::mw::com::SetupStopTokenSigTermHandler(stop_source); + if (!sig_term_handler_setup_success) + { + std::cerr << "Unable to set signal handler for SIGINT and/or SIGTERM, cautiously continuing\n"; + } + + // Create the process synchronizers once so the same underlying shared memory object is reused across both + // rounds of run_consumer() + auto process_synchronizer_result = + score::mw::com::test::ProcessSynchronizer::Create(score::mw::com::test::kConsumerDoneShmPath); + if (!process_synchronizer_result.has_value()) + { + score::mw::com::test::FailTest("Consumer: Could not create ProcessSynchronizer"); + } + auto provider_ready_synchronizer_result = + score::mw::com::test::ProcessSynchronizer::Create(score::mw::com::test::kProviderReadyShmPath); + if (!provider_ready_synchronizer_result.has_value()) + { + score::mw::com::test::FailTest("Consumer: Could not create provider ready ProcessSynchronizer"); + } + + // 1st step: Run consumer with service defined in initial mw::com configuration + score::mw::com::test::run_consumer( + stop_source.get_token(), + score::mw::com::test::kRegularServiceInstanceSpecifier, + *process_synchronizer_result, + *provider_ready_synchronizer_result, + score::mw::com::test::kFirstServiceSamples); + + // 2nd step: Load add-on configuration and merge into existing configuration + const auto service_instance_manifest_path = ParseServiceInstanceManifest(argc, argv, "addon_manifest"); + const auto add_on_load_result = score::mw::com::runtime::AddConfiguration( + score::mw::com::runtime::RuntimeConfiguration{service_instance_manifest_path}); + + if (!add_on_load_result.has_value()) + { + std::cout << "Sender: Failed to load add-on configuration: " << add_on_load_result.error() << std::endl; + return EXIT_FAILURE; + } + // 3rd step: Rerun consumer with initial service as in previous consumer run + score::mw::com::test::run_consumer( + stop_source.get_token(), + score::mw::com::test::kRegularServiceInstanceSpecifier, + *process_synchronizer_result, + *provider_ready_synchronizer_result, + score::mw::com::test::kFirstServiceSamplesSecondCall); + + // 4th step: Run consumer with new service instance as defined in add-on config + score::mw::com::test::run_consumer( + stop_source.get_token(), + score::mw::com::test::kAddOnServiceInstanceSpecifier, + *process_synchronizer_result, + *provider_ready_synchronizer_result, + score::mw::com::test::kAddonServiceSamples); + + return EXIT_SUCCESS; +} diff --git a/score/mw/com/test/loading_add_on_configuration/main_provider.cpp b/score/mw/com/test/loading_add_on_configuration/main_provider.cpp new file mode 100644 index 000000000..2f1fa2640 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/main_provider.cpp @@ -0,0 +1,243 @@ +/******************************************************************************* + * 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/test/loading_add_on_configuration/common_resources.h" +#include "score/mw/com/test/loading_add_on_configuration/provider.h" +#include "score/mw/com/test/loading_add_on_configuration/test_constants.h" +#include "score/mw/com/test/loading_add_on_configuration/types/addon_interface.h" + +#include "score/mw/com/runtime.h" +#include "score/mw/com/test/common_test_resources/command_line_parser.h" +#include "score/mw/com/test/common_test_resources/fail_test.h" +#include "score/mw/com/test/common_test_resources/process_synchronizer.h" +#include "score/mw/com/test/common_test_resources/skeleton_container.h" +#include "score/mw/com/test/common_test_resources/stop_token_sig_term_handler.h" + +#include +#include +#include + +namespace +{ + +std::string ParseServiceInstanceManifest(int argc, const char** argv, std::string manifest_name) +{ + + auto args = score::mw::com::test::ParseCommandLineArguments(argc, argv, {{manifest_name, ""}}); + return score::mw::com::test::GetValue(args, manifest_name); +} + +// \brief Checks whether the "invalid-addon-only" flag was passed on the command line. +// +// When set, the process only attempts to merge the invalid add-on configuration and then exits, +// instead of running the full provider sequence. This is used by a dedicated, separate process invocation of this +// binary to test that loading an invalid add-on configuration is rejected. +bool ParseInvalidAddonOnlyFlag(int argc, const char** argv) +{ + auto args = score::mw::com::test::ParseCommandLineArguments(argc, argv, {{"invalid-addon-only", ""}}); + const auto flag_result = score::mw::com::test::GetValueIfProvided(args, "invalid-addon-only"); + return flag_result.has_value() && flag_result.value(); +} + +// \brief Checks whether the "offer-addon-only" flag was passed on the command line. +// +// When set, the process merges the add-on configuration, offers *only* the add-on service (not the base service), +// and keeps it offered until the peer consumer signals that it has concluded its discovery attempt, before exiting. +// This is used together with a consumer that never merges the add-on +// configuration, to prove that such a consumer cannot discover the add-on service (rather than that no one is +// offering it at all). +bool ParseOfferAddonOnlyFlag(int argc, const char** argv) +{ + auto args = score::mw::com::test::ParseCommandLineArguments(argc, argv, {{"offer-addon-only", ""}}); + const auto flag_result = score::mw::com::test::GetValueIfProvided(args, "offer-addon-only"); + return flag_result.has_value() && flag_result.value(); +} + +// \brief Checks whether the "merge-during-stream" flag was passed on the command line. +// +// When set, the add-on configuration is merged synchronously in the middle of the 1st publish loop (i.e. +// while the initial service is actively streaming samples to a consumer), instead of merging it in between +// communication. +bool ParseMergeDuringStreamFlag(int argc, const char** argv) +{ + auto args = score::mw::com::test::ParseCommandLineArguments(argc, argv, {{"merge-during-stream", ""}}); + const auto flag_result = score::mw::com::test::GetValueIfProvided(args, "merge-during-stream"); + return flag_result.has_value() && flag_result.value(); +} + +int RunAddOnServiceOnlyTestCase(int argc, const char** argv) +{ + // Merge the (valid) add-on configuration and offer only the add-on service, keeping it offered until the peer + // consumer (that never merged the add-on configuration) signals that it has concluded its (expected to fail) + // service discovery attempt. + const auto service_instance_manifest_path = ParseServiceInstanceManifest(argc, argv, "addon_manifest"); + const auto add_on_load_result = score::mw::com::runtime::AddConfiguration( + score::mw::com::runtime::RuntimeConfiguration{service_instance_manifest_path}); + + if (!add_on_load_result.has_value()) + { + std::cout << "Provider: Failed to load add-on configuration: " << add_on_load_result.error() << std::endl; + return EXIT_FAILURE; + } + + score::cpp::stop_source stop_source{}; + const bool sig_term_handler_setup_success = score::mw::com::SetupStopTokenSigTermHandler(stop_source); + if (!sig_term_handler_setup_success) + { + std::cerr << "Unable to set signal handler for SIGINT and/or SIGTERM, cautiously continuing\n"; + } + + auto discovery_attempt_done_synchronizer_result = + score::mw::com::test::ProcessSynchronizer::Create(score::mw::com::test::kAddonDiscoveryAttemptDoneShmPath); + if (!discovery_attempt_done_synchronizer_result.has_value()) + { + score::mw::com::test::FailTest("Provider: Could not create discovery attempt done ProcessSynchronizer"); + } + + std::cout << "\nProvider - AddOn Only: Step 1 - Create skeleton" << std::endl; + score::mw::com::test::SkeletonContainer skeleton_container{}; + skeleton_container.CreateSkeleton(score::mw::com::test::kAddOnServiceInstanceSpecifier, "provider"); + + std::cout << "\nProvider - AddOn Only: Step 2 - Offer add-on service" << std::endl; + skeleton_container.OfferService("provider"); + + // Keep the service offered until the consumer (which never merged the add-on configuration) signals that it + // has concluded its service discovery attempt (expected to fail). + std::cout << "\nProvider - AddOn Only: Step 3 - Wait for consumer's discovery attempt to conclude" << std::endl; + if (!discovery_attempt_done_synchronizer_result->WaitWithAbort(stop_source.get_token())) + { + score::mw::com::test::FailTest( + "Provider: WaitWithAbort (discovery attempt done) was stopped by stop_token instead of notification"); + } + + return EXIT_SUCCESS; +} + +int RunInvalidAddOnConfigTestCase(int argc, const char** argv) +{ + // Try to merge an invalid add-on configuration which should fail, because its service identifier is already + // in use. The runtime is expected to call std::terminate(). + const auto invalid_service_instance_manifest_path = + ParseServiceInstanceManifest(argc, argv, "invalid_addon_manifest"); + const auto invalid_add_on_load_result = score::mw::com::runtime::AddConfiguration( + score::mw::com::runtime::RuntimeConfiguration{invalid_service_instance_manifest_path}); + + if (invalid_add_on_load_result.has_value()) + { + std::cout << "Provider: Could load invalid add-on configuration which should not be possible" << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +} // namespace + +int main(int argc, const char** argv) +{ + + const auto config = score::mw::com::test::ParseConfig(argc, argv); + + score::mw::com::runtime::InitializeRuntime(score::mw::com::runtime::RuntimeConfiguration{config.config_file_path}); + + if (ParseInvalidAddonOnlyFlag(argc, argv)) + { + return RunInvalidAddOnConfigTestCase(argc, argv); + } + + if (ParseOfferAddonOnlyFlag(argc, argv)) + { + return RunAddOnServiceOnlyTestCase(argc, argv); + } + + score::cpp::stop_source stop_source{}; + const bool sig_term_handler_setup_success = score::mw::com::SetupStopTokenSigTermHandler(stop_source); + if (!sig_term_handler_setup_success) + { + std::cerr << "Unable to set signal handler for SIGINT and/or SIGTERM, cautiously continuing\n"; + } + + // Create the process synchronizers once so the same underlying shared memory object is reused across both + // rounds of run_provider() + auto done_synchronizer_result = + score::mw::com::test::ProcessSynchronizer::Create(score::mw::com::test::kConsumerDoneShmPath); + if (!done_synchronizer_result.has_value()) + { + score::mw::com::test::FailTest("Provider: Could not create done ProcessSynchronizer"); + } + auto provider_ready_synchronizer_result = + score::mw::com::test::ProcessSynchronizer::Create(score::mw::com::test::kProviderReadyShmPath); + if (!provider_ready_synchronizer_result.has_value()) + { + score::mw::com::test::FailTest("Provider: Could not create provider ready ProcessSynchronizer"); + } + + // 1st step: Run provider with service instance defined in initial mw::com config. If requested, merge the + // add-on configuration while this service is offered and data actively streamed to consumer. + const bool merge_during_stream = ParseMergeDuringStreamFlag(argc, argv); + + std::function mid_stream_callback{}; + if (merge_during_stream) + { + const auto addon_manifest_path = ParseServiceInstanceManifest(argc, argv, "addon_manifest"); + mid_stream_callback = [addon_manifest_path]() { + const auto result = score::mw::com::runtime::AddConfiguration( + score::mw::com::runtime::RuntimeConfiguration{addon_manifest_path}); + if (!result.has_value()) + { + std::cerr << "Provider: Failed to load add-on configuration mid-stream: " << result.error() + << std::endl; + } + }; + } + + score::mw::com::test::run_provider( + stop_source.get_token(), + score::mw::com::test::kRegularServiceInstanceSpecifier, + *done_synchronizer_result, + *provider_ready_synchronizer_result, + score::mw::com::test::kFirstServiceSamples, + mid_stream_callback); + + // 2nd step: Load add-on configuration and merge into existing configuration (skipped if it was already merged + // mid-stream in step 1 above). + if (!merge_during_stream) + { + const auto service_instance_manifest_path = ParseServiceInstanceManifest(argc, argv, "addon_manifest"); + const auto add_on_load_result = score::mw::com::runtime::AddConfiguration( + score::mw::com::runtime::RuntimeConfiguration{service_instance_manifest_path}); + + if (!add_on_load_result.has_value()) + { + std::cerr << "Provider: Failed to load add-on configuration: " << add_on_load_result.error() << std::endl; + return EXIT_FAILURE; + } + } + + // 3rd step: Rerun provider with initial service as in previous provider run + score::mw::com::test::run_provider( + stop_source.get_token(), + score::mw::com::test::kRegularServiceInstanceSpecifier, + *done_synchronizer_result, + *provider_ready_synchronizer_result, + score::mw::com::test::kFirstServiceSamplesSecondCall); + + // 4th step: Run provider with new service instance defined in add-on config + score::mw::com::test::run_provider( + stop_source.get_token(), + score::mw::com::test::kAddOnServiceInstanceSpecifier, + *done_synchronizer_result, + *provider_ready_synchronizer_result, + score::mw::com::test::kAddonServiceSamples); + + return EXIT_SUCCESS; +} diff --git a/score/mw/com/test/loading_add_on_configuration/provider.cpp b/score/mw/com/test/loading_add_on_configuration/provider.cpp new file mode 100644 index 000000000..8a32635f1 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/provider.cpp @@ -0,0 +1,14 @@ +/******************************************************************************* + * 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/test/loading_add_on_configuration/provider.h" diff --git a/score/mw/com/test/loading_add_on_configuration/provider.h b/score/mw/com/test/loading_add_on_configuration/provider.h new file mode 100644 index 000000000..b6e63360e --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/provider.h @@ -0,0 +1,101 @@ +/******************************************************************************* + * 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_TEST_LOADING_ADD_ON_CONFIGURATION_PROVIDER_H +#define SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_PROVIDER_H + +#include "score/mw/com/test/loading_add_on_configuration/test_constants.h" + +#include "score/mw/com/test/common_test_resources/process_synchronizer.h" +#include "score/mw/com/test/loading_add_on_configuration/common_resources.h" +#include "score/mw/com/types.h" +#include "types/example_interface.h" + +#include "score/mw/com/test/common_test_resources/fail_test.h" +#include "score/mw/com/test/common_test_resources/skeleton_container.h" + +#include + +#include +#include + +namespace score::mw::com::test +{ +template +void run_provider(const score::cpp::stop_token& stop_token, + const score::mw::com::InstanceSpecifier& instance_specifier, + ProcessSynchronizer& done_synchronizer, + ProcessSynchronizer& provider_ready_synchronizer, + const std::vector& samples, + std::function mid_stream_callback = {}) +{ + const auto cycle_time = std::chrono::milliseconds(score::mw::com::test::kCycleTimeMs); + + // Step 1. Create skeleton + std::cout << "\nProvider: Step 1 - Create skeleton" << std::endl; + SkeletonContainer skeleton_container{}; + skeleton_container.CreateSkeleton(instance_specifier, "provider"); + + auto& service = skeleton_container.GetSkeleton(); + + // Step 2. Offer service + std::cout << "\nProvider: Step 2 - Offer service" << std::endl; + skeleton_container.OfferService("provider"); + + // Step 3. Signal consumer that we are ready + std::cout << "\nProvider: Step 3 - Informing consumer that we are ready" << std::endl; + provider_ready_synchronizer.Notify(); + + // Step 4. Send data + int sample_counter = 0; + for (std::size_t cycle = 0U; + (cycle < score::mw::com::test::kTotalNumValuesToSend || score::mw::com::test::kTotalNumValuesToSend == 0U) && + !stop_token.stop_requested(); + ++cycle) + { + { + const auto send_result = service.example_event.Send(samples[sample_counter++]); + if (!send_result.has_value()) + { + FailTest("Unable to send data. Exiting."); + } + } + if (sample_counter >= score::mw::com::test::kTotalNumValuesToSend) + { + sample_counter = 0; + } + + // Invoke the callback (if provided) once, roughly halfway through sending the samples, so that + // callers can perform an action (e.g. merging an add-on configuration) while the service is actively + // streaming to a subscribed consumer. + if (mid_stream_callback && cycle == samples.size() / 2U) + { + std::invoke(mid_stream_callback); + } + + std::this_thread::sleep_for(cycle_time); + } + + // Step 5. Wait until consumer signals done + std::cout << "\nProvider: Step 5 - Wait for consumer done notification" << std::endl; + if (!done_synchronizer.WaitWithAbort(stop_token)) + { + FailTest("Provider: WaitWithAbort (done) was stopped by stop_token instead of notification"); + } + // Reset synchronizer for subsequent calls + done_synchronizer.Reset(); +} + +} // namespace score::mw::com::test + +#endif // SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_PROVIDER_H diff --git a/score/mw/com/test/loading_add_on_configuration/test_constants.cpp b/score/mw/com/test/loading_add_on_configuration/test_constants.cpp new file mode 100644 index 000000000..b1916468f --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/test_constants.cpp @@ -0,0 +1,14 @@ +/******************************************************************************* + * 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/test/loading_add_on_configuration/test_constants.h" diff --git a/score/mw/com/test/loading_add_on_configuration/test_constants.h b/score/mw/com/test/loading_add_on_configuration/test_constants.h new file mode 100644 index 000000000..bb4083e30 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/test_constants.h @@ -0,0 +1,57 @@ +/******************************************************************************* + * 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_TEST_LOADING_ADD_ON_CONFIGURATION_TEST_CONSTANTS_H +#define SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_TEST_CONSTANTS_H + +#include "score/mw/com/test/loading_add_on_configuration/types/addon_interface.h" + +#include "score/mw/com/types.h" + +#include +#include +#include +#include + +namespace score::mw::com::test +{ + +constexpr const char* const kRegularServiceInstanceSpecifierString = "score/data/DataService"; +constexpr const char* const kAddOnServiceInstanceSpecifierString = "score/data/AddOnService"; +const std::string kConsumerDoneShmPath{"/consumer_done"}; +const std::string kProviderReadyShmPath{"/provider_ready"}; +const std::string kAddonDiscoveryAttemptDoneShmPath{"/addon_discovery_attempt_done"}; +const auto kRegularServiceInstanceSpecifier = + InstanceSpecifier::Create(std::string{kRegularServiceInstanceSpecifierString}).value(); +const auto kAddOnServiceInstanceSpecifier = + InstanceSpecifier::Create(std::string{kAddOnServiceInstanceSpecifierString}).value(); + +constexpr std::size_t kTotalNumValuesToSend = 10U; +constexpr std::uint32_t kCycleTimeMs = 50; + +const std::vector kFirstServiceSamples = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; +const std::vector kFirstServiceSamplesSecondCall = {11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; +const std::vector kAddonServiceSamples = {{51, 52, true}, + {53, 54, false}, + {55, 56, true}, + {57, 58, false}, + {59, 60, true}, + {61, 62, false}, + {63, 64, true}, + {65, 66, false}, + {67, 68, true}, + {69, 70, false}}; + +} // namespace score::mw::com::test + +#endif // SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_TEST_CONSTANTS_H diff --git a/score/mw/com/test/loading_add_on_configuration/types/BUILD b/score/mw/com/test/loading_add_on_configuration/types/BUILD new file mode 100644 index 000000000..d4462dbae --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/types/BUILD @@ -0,0 +1,35 @@ +# ******************************************************************************* +# 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("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "example_interface", + srcs = ["example_interface.cpp"], + hdrs = ["example_interface.h"], + features = COMPILER_WARNING_FEATURES, + visibility = ["//score/mw/com/test/loading_add_on_configuration:__pkg__"], + deps = [ + "//score/mw/com", + ], +) + +cc_library( + name = "addon_interface", + srcs = ["addon_interface.cpp"], + hdrs = ["addon_interface.h"], + features = COMPILER_WARNING_FEATURES, + visibility = ["//score/mw/com/test/loading_add_on_configuration:__pkg__"], + deps = [ + "//score/mw/com", + ], +) diff --git a/score/mw/com/test/loading_add_on_configuration/types/addon_interface.cpp b/score/mw/com/test/loading_add_on_configuration/types/addon_interface.cpp new file mode 100644 index 000000000..c57108589 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/types/addon_interface.cpp @@ -0,0 +1,14 @@ +/******************************************************************************* + * 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/test/loading_add_on_configuration/types/addon_interface.h" diff --git a/score/mw/com/test/loading_add_on_configuration/types/addon_interface.h b/score/mw/com/test/loading_add_on_configuration/types/addon_interface.h new file mode 100644 index 000000000..e0b9e540e --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/types/addon_interface.h @@ -0,0 +1,62 @@ +/******************************************************************************* + * 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_TEST_LOADING_ADD_ON_CONFIGURATION_ADDON_INTERFACE_H +#define SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_ADDON_INTERFACE_H + +#include "score/mw/com/types.h" +#include +#include + +namespace score::mw::com::test +{ + +struct ExampleData +{ + std::uint16_t id; + std::uint32_t value; + bool valid; +}; + +inline bool operator==(const ExampleData& lhs, const ExampleData& rhs) noexcept +{ + return (lhs.id == rhs.id) && (lhs.value == rhs.value) && (lhs.valid == rhs.valid); +} + +inline bool operator!=(const ExampleData& lhs, const ExampleData& rhs) noexcept +{ + return !(lhs == rhs); +} + +inline std::ostream& operator<<(std::ostream& out, const ExampleData& data) +{ + out << "ExampleData{id: " << data.id << ", value: " << data.value << ", valid: " << data.valid << "}"; + return out; +} + +template +class AddonInterface : public T::Base +{ + public: + using T::Base::Base; + + typename T::template Event example_event{*this, "example_event"}; + typename T::template Event active_event{*this, "active_event"}; +}; + +using AddonInterfaceProxy = score::mw::com::AsProxy; +using AddonInterfaceSkeleton = score::mw::com::AsSkeleton; + +} // namespace score::mw::com::test + +#endif // SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_ADDON_INTERFACE_H diff --git a/score/mw/com/test/loading_add_on_configuration/types/example_interface.cpp b/score/mw/com/test/loading_add_on_configuration/types/example_interface.cpp new file mode 100644 index 000000000..7c8696c02 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/types/example_interface.cpp @@ -0,0 +1,14 @@ +/******************************************************************************* + * 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/test/loading_add_on_configuration/types/example_interface.h" diff --git a/score/mw/com/test/loading_add_on_configuration/types/example_interface.h b/score/mw/com/test/loading_add_on_configuration/types/example_interface.h new file mode 100644 index 000000000..5a9373ea4 --- /dev/null +++ b/score/mw/com/test/loading_add_on_configuration/types/example_interface.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_TEST_LOADING_ADD_ON_CONFIGURATION_EXAMPLE_INTERFACE_H +#define SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_EXAMPLE_INTERFACE_H + +#include "score/mw/com/types.h" +#include + +namespace score::mw::com::test +{ + +template +class ExampleInterface : public T::Base +{ + public: + using T::Base::Base; + + typename T::template Event example_event{*this, "example_event"}; +}; + +using ExampleInterfaceProxy = score::mw::com::AsProxy; +using ExampleInterfaceSkeleton = score::mw::com::AsSkeleton; + +} // namespace score::mw::com::test + +#endif // SCORE_MW_COM_TEST_LOADING_ADD_ON_CONFIGURATION_EXAMPLE_INTERFACE_H