diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index 8c4b45587..466087758 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -10,7 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_cc//cc:defs.bzl", "cc_library") +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") cc_library( name = "return_types", @@ -109,6 +109,41 @@ cc_library( ], ) +cc_library( + name = "inotify_range", + hdrs = [ + "inotify_range.hpp", + ], + include_prefix = "score/mw/launch_manager/osal", + strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", + visibility = [ + "//score:__subpackages__", + "//tests:__subpackages__", + ], + deps = [ + "//score/launch_manager/src/daemon/src/common:log", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/language/safecpp/string_view:zstring_view", + "@score_baselibs//score/os/utils/inotify:inotify_event", + "@score_baselibs//score/os/utils/inotify:inotify_instance", + "@score_baselibs//score/os/utils/inotify:inotify_instance_impl", + "@score_baselibs//score/os/utils/inotify:inotify_watch_descriptor", + "@score_baselibs//score/result", + ], +) + +cc_test( + name = "inotify_range_UT", + srcs = [ + "inotify_range_UT.cpp", + ], + deps = [ + ":inotify_range", + "@googletest//:gtest_main", + "@score_baselibs//score/os/utils/inotify:inotify_instance_mock", + ], +) + cc_library( name = "osal", visibility = ["//score:__subpackages__"], diff --git a/score/launch_manager/src/daemon/src/osal/inotify_range.hpp b/score/launch_manager/src/daemon/src/osal/inotify_range.hpp new file mode 100644 index 000000000..e30cae8b5 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/inotify_range.hpp @@ -0,0 +1,168 @@ +/******************************************************************************** + * 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 INOTIFY_RANGE_HPP_ +#define INOTIFY_RANGE_HPP_ + +#include +#include +#include +#include + +#include "score/assert.hpp" +#include "score/os/utils/inotify/inotify_event.h" +#include "score/os/utils/inotify/inotify_instance.h" + +namespace score::mw::lifecycle +{ + +/// @brief A wrapper around score::os::InotiyInstance to get iterator syntax. +class INotifyRange +{ + public: + explicit INotifyRange(std::unique_ptr instance) noexcept + : instance_(std::move(instance)) + { + } + + ~INotifyRange() = default; + + INotifyRange(INotifyRange&& other) noexcept = default; + INotifyRange& operator=(INotifyRange&& other) noexcept = default; + + INotifyRange(const INotifyRange& other) = delete; + INotifyRange& operator=(const INotifyRange& other) = delete; + + /// @brief The iterator giving InotifyEvents. + /// @warning The iterator's lifetime must not exceed the `INotifyRange` it + /// was obtained from. + class iterator + { + public: + using iterator_category = std::input_iterator_tag; + using value_type = score::os::InotifyEvent; + using difference_type = std::ptrdiff_t; + using pointer = const score::os::InotifyEvent*; + using reference = const score::os::InotifyEvent&; + + explicit iterator(score::os::InotifyInstance* instance = nullptr) noexcept : instance_(instance) + { + if ((instance_ != nullptr) && !advance()) + { + instance_ = nullptr; + } + } + + [[nodiscard]] reference operator*() const + { + return events_[event_index_]; + } + + [[nodiscard]] pointer operator->() const + { + return &events_[event_index_]; + } + + iterator& operator++() + { + // we are end iterator + if (instance_ == nullptr) + { + return *this; + } + + // become end iterator + if (!advance()) + { + instance_ = nullptr; + } + + return *this; + } + + iterator operator++(int) + { + auto tmp = *this; + ++(*this); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const + { + return instance_ == other.instance_; + } + + [[nodiscard]] bool operator!=(const iterator& other) const + { + return !(*this == other); + } + + private: + [[nodiscard]] bool advance() + { + + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + instance_ != nullptr, "The parent INotifyRange must have been deleted, documented as UB!"); + + const bool events_in_internal_buffer = event_index_ + 1U < events_.size(); + if (events_in_internal_buffer) + { + ++event_index_; + return true; + } + + auto result = instance_->Read(); + if (!result.has_value()) + { + return false; + } + + events_ = std::move(result.value()); + + if (events_.empty()) + { + return false; + } + + event_index_ = 0U; + return true; + } + + public: + /// @brief The underlying `InotifyInstance` instance. + score::os::InotifyInstance* instance_{nullptr}; + + /// @brief Internal buffer of events. + score::cpp::static_vector events_{}; + + /// @brief Index to the next event to give from the internal buffer. + std::size_t event_index_{0U}; + }; + + [[nodiscard]] iterator begin() noexcept + { + return iterator{instance_.get()}; + } + + [[nodiscard]] iterator end() noexcept + { + return iterator{nullptr}; + } + + private: + std::unique_ptr instance_; +}; + +} // namespace score::mw::lifecycle + +#endif // INOTIFY_RANGE_HPP_ diff --git a/score/launch_manager/src/daemon/src/osal/inotify_range_UT.cpp b/score/launch_manager/src/daemon/src/osal/inotify_range_UT.cpp new file mode 100644 index 000000000..251526bba --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/inotify_range_UT.cpp @@ -0,0 +1,110 @@ +/******************************************************************************** + * 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/launch_manager/osal/inotify_range.hpp" +#include "score/os/utils/inotify/inotify_instance_mock.h" +#include +#include + +namespace score::mw::lifecycle::testing +{ + +using score::os::Error; +using score::os::MakeFakeEvent; +using ::testing::Return; + +using ReadRetT = + score::cpp::expected, + Error>; + +using namespace std::string_view_literals; + +class INotifyRangeUT : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + } +}; + +TEST_F(INotifyRangeUT, NormalUsage) +{ + /// Given we Read() one event + ReadRetT out{}; + out->push_back(MakeFakeEvent(1,1,1, "hello")); + ReadRetT empty{}; + auto mock_ptr = std::make_unique(); + EXPECT_CALL(*mock_ptr, Read()).WillOnce(Return(out)).WillOnce(Return(empty)); + + INotifyRange range{std::move(mock_ptr)}; + + /// Expect the first value will be given. + auto iterator = range.begin(); + auto event = *iterator; + EXPECT_EQ(event.GetCookie(), 1); + + /// Expect incrementing will give you an end iterator. + iterator++; + EXPECT_EQ(iterator, range.end()); +} + +TEST_F(INotifyRangeUT, AdvanceWithinBufferedEvents) +{ + /// Given we Read() 2 events, then 0 events + ReadRetT out{}; + out->push_back(MakeFakeEvent(1, 1, 10, "first")); + out->push_back(MakeFakeEvent(2, 1, 20, "second")); + ReadRetT empty{}; + auto mock_ptr = std::make_unique(); + EXPECT_CALL(*mock_ptr, Read()).WillOnce(Return(out)).WillOnce(Return(empty)); + + INotifyRange range{std::move(mock_ptr)}; + + /// Expect that the first value will equal to the first event. + auto iterator = range.begin(); + EXPECT_EQ(iterator->GetCookie(), 10); + + /// Expect that the second value will equal to the second event. + ++iterator; + EXPECT_EQ(iterator->GetCookie(), 20); + + /// Expect that incrementing will give you an end iterator. + ++iterator; + EXPECT_EQ(iterator, range.end()); +} + +TEST_F(INotifyRangeUT, AdvanceEndIterator) +{ + auto mock_ptr = std::make_unique(); + INotifyRange range{std::move(mock_ptr)}; + + /// Expect that incrementing end iterator still equals to an end iterator. + auto iterator = range.end(); + iterator++; + EXPECT_EQ(iterator, range.end()); +} + +TEST_F(INotifyRangeUT, AdvanceError) +{ + /// Given we get an Error from the Read(). + auto mock_ptr = std::make_unique(); + EXPECT_CALL(*mock_ptr, Read()).WillOnce(Return(score::cpp::unexpected(score::os::Error::createFromErrno(EINVAL)))); + + INotifyRange range{std::move(mock_ptr)}; + + /// Expect that the given iterator is empty. + EXPECT_EQ(range.begin(), range.end()); +} + +} // namespace score::mw::lifecycle::testing