diff --git a/MODULE.bazel b/MODULE.bazel index 7f52d048e..8c66d35f1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -88,7 +88,7 @@ use_repo( # LLVM toolchain (used for clang-format and clang-tidy) bazel_dep(name = "toolchains_llvm", version = "1.4.0", dev_dependency = True) -llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm") +llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm", dev_dependency = True) llvm.toolchain( llvm_version = "15.0.2", stdlib = {"linux-x86_64": "stdc++"}, diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 78f87bf1b..db6a6a1d9 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1809,6 +1809,95 @@ "recordedRepoMappingEntries": [] } }, + "@@grpc+//bazel:grpc_deps.bzl%grpc_repo_deps_ext": { + "general": { + "bzlTransitiveDigest": "1AW88qzR36HAwI9e3wcCbNZ8wGITrBAs0zabntxP68c=", + "usagesDigest": "7dDSXJVRfj7BLfa5iUZ+8xRMy3KmxOEyK9R6rHpmU6M=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "google_cloud_cpp": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "e53ba3799c052d97acac9a6a6b27af24ce822dbde7bfde973bac9e5da714e6b2", + "strip_prefix": "google-cloud-cpp-2.33.0", + "urls": [ + "https://storage.googleapis.com/grpc-bazel-mirror/github.com/googleapis/google-cloud-cpp/archive/refs/tags/v2.33.0.tar.gz", + "https://github.com/googleapis/google-cloud-cpp/archive/refs/tags/v2.33.0.tar.gz" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "grpc+", + "bazel_tools", + "bazel_tools" + ], + [ + "grpc+", + "com_github_grpc_grpc", + "grpc+" + ] + ] + } + }, + "@@grpc-java+//:repositories.bzl%grpc_java_repositories_extension": { + "general": { + "bzlTransitiveDigest": "51Dhp1Vc9G+D2QGVxAQeKrH9kvMTNC8lhO0KHxq2sH8=", + "usagesDigest": "Sa5QQaLepBN1KrxpoHFrKfnZPhhFIY8XIS4JoUpKC2s=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "com_github_cncf_xds": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "strip_prefix": "xds-e9ce68804cb4e64cab5a52e3c8baf840d4ff87b7", + "sha256": "0d33b83f8c6368954e72e7785539f0d272a8aba2f6e2e336ed15fd1514bc9899", + "urls": [ + "https://github.com/cncf/xds/archive/e9ce68804cb4e64cab5a52e3c8baf840d4ff87b7.tar.gz" + ] + } + }, + "io_grpc_grpc_proto": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "729ac127a003836d539ed9da72a21e094aac4c4609e0481d6fc9e28a844e11af", + "strip_prefix": "grpc-proto-4f245d272a28a680606c0739753506880cf33b5f", + "urls": [ + "https://github.com/grpc/grpc-proto/archive/4f245d272a28a680606c0739753506880cf33b5f.zip" + ] + } + }, + "envoy_api": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "c4c9c43903e413924b0cb08e9747f3c3a0727ad221a3c446a326db32def18c60", + "strip_prefix": "data-plane-api-1611a7304794e13efe2d26f8480a2d2473a528c5", + "urls": [ + "https://storage.googleapis.com/grpc-bazel-mirror/github.com/envoyproxy/data-plane-api/archive/1611a7304794e13efe2d26f8480a2d2473a528c5.tar.gz", + "https://github.com/envoyproxy/data-plane-api/archive/1611a7304794e13efe2d26f8480a2d2473a528c5.tar.gz" + ], + "patch_args": [ + "-p1" + ], + "patches": [ + "@@grpc-java+//:buildscripts/data-plane-api-no-envoy.patch" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "grpc-java+", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "@@hedron_compile_commands+//:workspace_setup.bzl%hedron_compile_commands_extension": { "general": { "bzlTransitiveDigest": "Ojq9TbsjNl+y2stghx86ktVxgmENnzIi8xmtneTI+uU=", @@ -2031,6 +2120,89 @@ ] } }, + "@@rules_fuzzing+//fuzzing/private:extensions.bzl%non_module_dependencies": { + "general": { + "bzlTransitiveDigest": "4LouzhF/yT117s7peGnNs9ROomiJXC6Zl5R0oI21jho=", + "usagesDigest": "wy6ISK6UOcBEjj/mvJ/S3WeXoO67X+1llb9yPyFtPgc=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "platforms": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "urls": [ + "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz", + "https://github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz" + ], + "sha256": "8150406605389ececb6da07cbcb509d5637a3ab9a24bc69b1101531367d89d74" + } + }, + "rules_python": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "d70cd72a7a4880f0000a6346253414825c19cdd40a28289bdf67b8e6480edff8", + "strip_prefix": "rules_python-0.28.0", + "url": "https://github.com/bazelbuild/rules_python/releases/download/0.28.0/rules_python-0.28.0.tar.gz" + } + }, + "bazel_skylib": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "cd55a062e763b9349921f0f5db8c3933288dc8ba4f76dd9416aac68acee3cb94", + "urls": [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz" + ] + } + }, + "com_google_absl": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "urls": [ + "https://github.com/abseil/abseil-cpp/archive/refs/tags/20240116.1.zip" + ], + "strip_prefix": "abseil-cpp-20240116.1", + "integrity": "sha256-7capMWOvWyoYbUaHF/b+I2U6XLMaHmky8KugWvfXYuk=" + } + }, + "rules_fuzzing_oss_fuzz": { + "repoRuleId": "@@rules_fuzzing+//fuzzing/private/oss_fuzz:repository.bzl%oss_fuzz_repository", + "attributes": {} + }, + "honggfuzz": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file": "@@rules_fuzzing+//:honggfuzz.BUILD", + "sha256": "6b18ba13bc1f36b7b950c72d80f19ea67fbadc0ac0bb297ec89ad91f2eaa423e", + "url": "https://github.com/google/honggfuzz/archive/2.5.zip", + "strip_prefix": "honggfuzz-2.5" + } + }, + "rules_fuzzing_jazzer": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar", + "attributes": { + "sha256": "ee6feb569d88962d59cb59e8a31eb9d007c82683f3ebc64955fd5b96f277eec2", + "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer/0.20.1/jazzer-0.20.1.jar" + } + }, + "rules_fuzzing_jazzer_api": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar", + "attributes": { + "sha256": "f5a60242bc408f7fa20fccf10d6c5c5ea1fcb3c6f44642fec5af88373ae7aa1b", + "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer-api/0.20.1/jazzer-api-0.20.1.jar" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_fuzzing+", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { "general": { "bzlTransitiveDigest": "nvW/NrBXlAmiQw99EMGKkLaD2KbNp2mQDlxdfpr+0Ls=", @@ -9917,7 +10089,7 @@ "@@toolchains_llvm+//toolchain/extensions:llvm.bzl%llvm": { "general": { "bzlTransitiveDigest": "2nizkGuHTT5Rnm/m2ZKyN0gz6PG1uMRn/wIec1UMxaw=", - "usagesDigest": "Gtth39CdsnpUT5yLLTJT3RufqV5GtR3NGLNC0a6148E=", + "usagesDigest": "v3gqYxRqPrIpYPqVI+8nsnsq7WbfgFyTUiSPqxGd1Sg=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, diff --git a/score/launch_manager/BUILD b/score/launch_manager/BUILD index d6eda34ae..435174453 100644 --- a/score/launch_manager/BUILD +++ b/score/launch_manager/BUILD @@ -86,9 +86,13 @@ alias( actual = "//score/launch_manager/src/lifecycle_client:applicationcontext_mock", ) -alias( +cc_library( name = "lifecycle_mock_cc", - actual = "//score/launch_manager/src/lifecycle_client:lifecycle_mock", + testonly = True, + deps = [ + "//score/launch_manager/src/lifecycle_client:lifecycle_mock", + "//score/launch_manager/src/lifecycle_client:report_running_mock", + ], ) alias( diff --git a/score/launch_manager/src/lifecycle_client/BUILD b/score/launch_manager/src/lifecycle_client/BUILD index 72a9c9cc7..0600e5645 100644 --- a/score/launch_manager/src/lifecycle_client/BUILD +++ b/score/launch_manager/src/lifecycle_client/BUILD @@ -186,6 +186,7 @@ cc_library( visibility = ["//score/launch_manager:__subpackages__"], deps = [ ":applicationcontext_mock", + ":lifecyclemanager", "@googletest//:gtest", "@score_baselibs//score/os/utils:signal", "@score_baselibs//score/os/utils/mocklib:signal_mock", @@ -209,6 +210,16 @@ cc_library( ], ) +cc_test( + name = "lifecycle_mocks_UT", + srcs = ["src/lifecycle_mocks_UT.cpp"], + deps = [ + ":runapplication", + "//score/launch_manager:lifecycle_mock_cc", + "@googletest//:gtest_main", + ], +) + cc_test( name = "runapplication_UT", srcs = ["src/runapplication_UT.cpp"], diff --git a/score/launch_manager/src/lifecycle_client/src/applicationcontextmock.cpp b/score/launch_manager/src/lifecycle_client/src/applicationcontextmock.cpp index 193618318..c6e378535 100644 --- a/score/launch_manager/src/lifecycle_client/src/applicationcontextmock.cpp +++ b/score/launch_manager/src/lifecycle_client/src/applicationcontextmock.cpp @@ -60,19 +60,38 @@ score::mw::lifecycle::ApplicationContextMock::~ApplicationContextMock() } score::mw::lifecycle::ApplicationContext::ApplicationContext(const std::int32_t argc, const char* const argv[]) + : m_args(argv, argv + argc), m_app_path(argc > 0 ? std::string{argv[0]} : std::string{}) { auto& constructor_callback = GetConstructorCallback(); - constructor_callback(argc, argv); + if (constructor_callback) + { + constructor_callback(argc, argv); + } } const std::vector& score::mw::lifecycle::ApplicationContext::get_arguments() const noexcept { auto& get_arguments_callback = GetGetArgumentsCallback(); - return get_arguments_callback(); + if (get_arguments_callback) + { + return get_arguments_callback(); + } + return m_args; } std::string score::mw::lifecycle::ApplicationContext::get_argument(const std::string_view flag) const noexcept { auto& get_argument_callback = GetGetArgumentCallback(); - return get_argument_callback(flag); + if (get_argument_callback) + { + return get_argument_callback(flag); + } + for (auto it = m_args.cbegin(); it != m_args.cend(); ++it) + { + if (it->rfind(flag, 0) == 0) + { + return *it; + } + } + return {}; } diff --git a/score/launch_manager/src/lifecycle_client/src/lifecycle_mocks_UT.cpp b/score/launch_manager/src/lifecycle_client/src/lifecycle_mocks_UT.cpp new file mode 100644 index 000000000..64c232f17 --- /dev/null +++ b/score/launch_manager/src/lifecycle_client/src/lifecycle_mocks_UT.cpp @@ -0,0 +1,182 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +/// @file cpp_lifecycle_app_UT.cpp +/// @brief Unit tests for cpp_lifecycle_app that demonstrate how to use +/// LifeCycleManagerMock and ApplicationContextMock from external packages. +/// +/// These tests guard the mock API against accidental regressions and serve as +/// a reference for other components (e.g. config_management) that depend on the +/// lifecycle mocks in their own test suites. + +#include "score/mw/lifecycle/applicationcontextmock.h" +#include "score/mw/lifecycle/lifecyclemanagermock.h" +#include "score/mw/lifecycle/runapplication.h" + +#include +#include + +#include +#include + +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::Eq; +using ::testing::Return; +using ::testing::ReturnRef; + +namespace +{ + +/// Minimal Application that records which argument was retrieved during Initialize(). +class ArgCapturingApp final : public score::mw::lifecycle::Application +{ + public: + std::int32_t Initialize(const score::mw::lifecycle::ApplicationContext& ctx) override + { + captured_arg_ = ctx.get_argument("--config"); + return captured_arg_.empty() ? EXIT_FAILURE : EXIT_SUCCESS; + } + + std::int32_t Run(const score::cpp::stop_token&) override + { + return EXIT_SUCCESS; + } + + std::string captured_arg_; +}; + +/// Minimal Application that always initialises and runs successfully. +class AlwaysOkApp final : public score::mw::lifecycle::Application +{ + public: + std::int32_t Initialize(const score::mw::lifecycle::ApplicationContext&) override + { + return EXIT_SUCCESS; + } + + std::int32_t Run(const score::cpp::stop_token&) override + { + return EXIT_SUCCESS; + } +}; + +// --------------------------------------------------------------------------- +// Test fixture +// --------------------------------------------------------------------------- + +/// @brief Tests for lifecycle mock usage from an external-package perspective. +/// +/// The mocks must be constructed *before* any score::mw::lifecycle::Run<> object +/// so that the static link-time substitution callbacks are installed in time. +class LifecycleMockUsageTest : public ::testing::Test +{ + protected: + score::mw::lifecycle::ApplicationContextMock context_mock_; + score::mw::lifecycle::LifeCycleManagerMock lifecycle_mock_; + + static constexpr int kArgc = 1; + const char* kArgv[1] = {"test_app"}; + + void SetUp() override + { + EXPECT_CALL(context_mock_, ctor(_, _)).Times(AnyNumber()); + EXPECT_CALL(lifecycle_mock_, ctor()).Times(AnyNumber()); + EXPECT_CALL(lifecycle_mock_, dtor()).Times(AnyNumber()); + } +}; + +} // namespace + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// Verify that LifeCycleManagerMock::run() is called and its return value +/// is propagated through AsPosixProcess() — basic mock wiring smoke test. +TEST_F(LifecycleMockUsageTest, MockRunReturnValueIsForwardedToCaller) +{ + RecordProperty("Description", + "LifeCycleManagerMock::run() return value reaches AsPosixProcess()."); + + EXPECT_CALL(lifecycle_mock_, run(_, _)).WillOnce(Return(EXIT_SUCCESS)); + + score::mw::lifecycle::Run runner(kArgc, kArgv); + EXPECT_EQ(runner.AsPosixProcess(), EXIT_SUCCESS); +} + +/// Verify that a non-zero exit code coming from the mock is forwarded correctly, +/// matching the behaviour that external callers (e.g. config_management) rely on. +TEST_F(LifecycleMockUsageTest, MockRunNonZeroReturnValueIsForwarded) +{ + RecordProperty("Description", + "A non-zero return from LifeCycleManagerMock::run() is forwarded unchanged."); + + EXPECT_CALL(lifecycle_mock_, run(_, _)).WillOnce(Return(EXIT_FAILURE)); + + score::mw::lifecycle::Run runner(kArgc, kArgv); + EXPECT_EQ(runner.AsPosixProcess(), EXIT_FAILURE); +} + +/// Verify that ApplicationContextMock::get_argument() is called during Initialize() +/// and that the returned value is visible to the application logic. +/// This is the pattern used by config_management's unit tests to simulate +/// command-line arguments without spawning a real process. +TEST_F(LifecycleMockUsageTest, ApplicationReceivesArgumentFromContextMock) +{ + RecordProperty("Description", + "ApplicationContextMock::get_argument() result is visible inside Initialize()."); + + const std::string expected_config{"--config=/etc/app.cfg"}; + + EXPECT_CALL(lifecycle_mock_, run(_, _)) + .WillOnce([&](score::mw::lifecycle::Application& app, + const score::mw::lifecycle::ApplicationContext& ctx) { + EXPECT_CALL(context_mock_, get_argument(std::string_view{"--config"})) + .WillOnce(Return(expected_config)); + return app.Initialize(ctx); + }); + + score::mw::lifecycle::Run runner(kArgc, kArgv); + EXPECT_EQ(runner.AsPosixProcess(), EXIT_SUCCESS); +} + +/// Verify that ApplicationContextMock::get_arguments() can provide a full +/// argument list — mirroring how config_management passes a simulated argv. +TEST_F(LifecycleMockUsageTest, ContextMockProvidesArgumentList) +{ + RecordProperty("Description", + "ApplicationContextMock::get_arguments() returns the configured argument list."); + + const std::vector expected_args{"my_app", "--verbose", "--config=/tmp/cfg"}; + + EXPECT_CALL(context_mock_, get_arguments()).WillRepeatedly(ReturnRef(expected_args)); + + const auto& args = context_mock_.get_arguments(); + EXPECT_EQ(args, expected_args); +} + +/// Verify that exactly one LifeCycleManager is constructed and destroyed per +/// AsPosixProcess() call — important for callers that own RAII resources. +TEST_F(LifecycleMockUsageTest, ExactlyOneLifecycleManagerLifetimePerRun) +{ + RecordProperty("Description", + "Exactly one ctor/dtor pair of LifeCycleManager occurs per AsPosixProcess()."); + + EXPECT_CALL(lifecycle_mock_, ctor()).Times(1); + EXPECT_CALL(lifecycle_mock_, dtor()).Times(1); + EXPECT_CALL(lifecycle_mock_, run(_, _)).WillOnce(Return(EXIT_SUCCESS)); + + score::mw::lifecycle::Run runner(kArgc, kArgv); + runner.AsPosixProcess(); +}